@space3-npm/cybersoul-client 1.2.6 → 1.2.8
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 +9 -1
- package/dist/client.js +167 -15
- package/dist/types.d.ts +17 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CyberSoulClientConfig, InteractParams, OndemandEventParams, OndemandEventResponse, DispatcherIntent, InteractResponse, CharacterState, CoreMemory, UserCodex } from "./types.js";
|
|
1
|
+
import { CyberSoulClientConfig, InteractParams, ProactiveParams, ProactiveResponse, OndemandEventParams, OndemandEventResponse, DispatcherIntent, InteractResponse, CharacterState, CoreMemory, UserCodex } from "./types.js";
|
|
2
2
|
export declare class CyberSoulClient {
|
|
3
3
|
private config;
|
|
4
4
|
private llm;
|
|
@@ -19,6 +19,9 @@ export declare class CyberSoulClient {
|
|
|
19
19
|
private buildStateContextPrompt;
|
|
20
20
|
private normalizeOngoingSceneState;
|
|
21
21
|
private getImageSchemaParams;
|
|
22
|
+
private getOutfitSelectionPrompt;
|
|
23
|
+
private getTriggerEventPolicyPrompt;
|
|
24
|
+
private getOutfitAcquisitionPolicyPrompt;
|
|
22
25
|
private getEventSchemaParams;
|
|
23
26
|
private getVoiceSchemaParams;
|
|
24
27
|
private buildVoiceSchemaFromDynamicParams;
|
|
@@ -44,6 +47,11 @@ export declare class CyberSoulClient {
|
|
|
44
47
|
* Evaluates and triggers an on-demand event, intelligently deciding if an outfit change is needed.
|
|
45
48
|
*/
|
|
46
49
|
ondemandEvent(params: OndemandEventParams): Promise<OndemandEventResponse>;
|
|
50
|
+
/**
|
|
51
|
+
* Generates a proactive message when the user hasn't responded.
|
|
52
|
+
* Safely prevents spamming, and adjusts its approach based on relationship dynamics.
|
|
53
|
+
*/
|
|
54
|
+
proactiveInteract(params: ProactiveParams): Promise<ProactiveResponse>;
|
|
47
55
|
/**
|
|
48
56
|
* Manually generate an image of the character outside of chat flow.
|
|
49
57
|
*/
|
package/dist/client.js
CHANGED
|
@@ -159,7 +159,7 @@ export class CyberSoulClient {
|
|
|
159
159
|
}
|
|
160
160
|
return normalized;
|
|
161
161
|
}
|
|
162
|
-
buildStateContextPrompt(state, localContext) {
|
|
162
|
+
buildStateContextPrompt(state, localContext, isProactive = false) {
|
|
163
163
|
const dyn = state.dynamic_context || {};
|
|
164
164
|
const stage = state.relationship_stage || "NEUTRAL";
|
|
165
165
|
const temperature = dyn.temperature ?? 50;
|
|
@@ -197,10 +197,10 @@ Current time: ${new Date(currentTimeMs).toLocaleString("zh-CN", { timeZone: "Asi
|
|
|
197
197
|
if (state.active_event) {
|
|
198
198
|
contextParts.push(`Active Event: ${state.active_event.title} (${state.active_event.narrative_context})`);
|
|
199
199
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
200
|
+
if (localContext) {
|
|
201
|
+
contextParts.push(`Additional Context: ${localContext}`);
|
|
202
|
+
}
|
|
203
|
+
if (state.next_event) {
|
|
204
204
|
contextParts.push(`Next Event: ${state.next_event.title} at ${state.next_event.start_time} (in ${state.next_event.time_until_mins} mins)`);
|
|
205
205
|
}
|
|
206
206
|
if (state.active_wardrobe) {
|
|
@@ -269,7 +269,9 @@ ${scenarioContext}
|
|
|
269
269
|
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.
|
|
270
270
|
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.
|
|
271
271
|
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).
|
|
272
|
-
|
|
272
|
+
${isProactive
|
|
273
|
+
? "5. REAL-TIME PACING: You are initiating the conversation because the user hasn't replied recently. Transition naturally from your last message or start a new topic seamlessly. Ensure everything happens in a single real-time moment."
|
|
274
|
+
: "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."}`;
|
|
273
275
|
}
|
|
274
276
|
normalizeOngoingSceneState(raw, fallbackOutfit) {
|
|
275
277
|
if (raw === null || raw === undefined)
|
|
@@ -316,6 +318,15 @@ ${scenarioContext}
|
|
|
316
318
|
"style": "e.g., photorealistic (ENGLISH ONLY)"
|
|
317
319
|
}`;
|
|
318
320
|
}
|
|
321
|
+
getOutfitSelectionPrompt() {
|
|
322
|
+
return `When generating a triggerEvent, you MUST provide a suitable 'triggerEvent.outfitId' if the VERY LAST USER MESSAGE explicitly asks for an outfit change, OR if the new activity implies a context/location shift that conflicts with the current outfit (e.g., currently in SLEEPWEAR at home but going outside). Otherwise, keep it null. When changing outfits, match it to the event's activity, environment, and relationship stage (e.g., DAILY, INTIMATE, SLEEPWEAR).`;
|
|
323
|
+
}
|
|
324
|
+
getTriggerEventPolicyPrompt() {
|
|
325
|
+
return `- Include 'triggerEvent' only if the VERY LAST USER MESSAGE proposes a new activity/hangout, explicitly requests an outfit change, or proposes intimate/romantic actions; ignore older history. ${this.getOutfitSelectionPrompt()}`;
|
|
326
|
+
}
|
|
327
|
+
getOutfitAcquisitionPolicyPrompt() {
|
|
328
|
+
return `- Outfit acquisition (VERY LAST USER MESSAGE only): set giftOutfit for gift/buy/add-clothes intent; otherwise null. giftOutfit format: { "descriptionText": "short outfit description" }.`;
|
|
329
|
+
}
|
|
319
330
|
getEventSchemaParams(userName) {
|
|
320
331
|
const name = userName || "the user";
|
|
321
332
|
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.",
|
|
@@ -323,7 +334,7 @@ ${scenarioContext}
|
|
|
323
334
|
"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)",
|
|
324
335
|
"scheduledStartTimeStr": "HH:MM (Optional, 24-hour format if a specific time is agreed upon, e.g., '14:30', otherwise null)",
|
|
325
336
|
"durationMins": 60,
|
|
326
|
-
"outfitId": "
|
|
337
|
+
"outfitId": "Wardrobe ID. Provide ONLY if the user explicitly requested an outfit change OR if the new activity conflicts with the current outfit context (e.g., SLEEPWEAR at home -> going outside). Otherwise, use null."`;
|
|
327
338
|
}
|
|
328
339
|
getVoiceSchemaParams() {
|
|
329
340
|
// Only reached when no dynamic_params are configured on the voice model.
|
|
@@ -437,8 +448,8 @@ ${scenarioContext}
|
|
|
437
448
|
modalitiesInstruction += `\n - ALWAYS set 'voiceArgs' to null.`;
|
|
438
449
|
}
|
|
439
450
|
}
|
|
440
|
-
modalitiesInstruction += `\n
|
|
441
|
-
|
|
451
|
+
modalitiesInstruction += `\n ${this.getTriggerEventPolicyPrompt()}
|
|
452
|
+
${this.getOutfitAcquisitionPolicyPrompt()}`;
|
|
442
453
|
// Combine state info into a clean descriptive context
|
|
443
454
|
const systemPrompt = `${this.buildStateContextPrompt(state, params.localContext)}
|
|
444
455
|
Available Wardrobe Outfits (For event triggers):
|
|
@@ -524,7 +535,7 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
|
|
|
524
535
|
let finalAudioUrl = undefined;
|
|
525
536
|
let finalDurationSec = undefined;
|
|
526
537
|
// Output Event Trigger
|
|
527
|
-
if (
|
|
538
|
+
if (parsedIntent.triggerEvent) {
|
|
528
539
|
mediaTasks.push(this.apiFetch("/api/v1/cyber-soul/characters/ondemand-event", {
|
|
529
540
|
method: "POST",
|
|
530
541
|
body: JSON.stringify({
|
|
@@ -552,9 +563,15 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
|
|
|
552
563
|
mode: "full-prompt",
|
|
553
564
|
full_prompt: resolvedTextResponse,
|
|
554
565
|
};
|
|
555
|
-
mediaTasks.push(this.generatePrimitive("image", imagePayload)
|
|
566
|
+
mediaTasks.push(this.generatePrimitive("image", imagePayload)
|
|
567
|
+
.then((res) => {
|
|
556
568
|
finalImageUrl = res.image_url;
|
|
557
|
-
})
|
|
569
|
+
})
|
|
570
|
+
.catch((e) => {
|
|
571
|
+
console.error("[CyberSoulClient] Image generation failed:", e);
|
|
572
|
+
if (e.code === 'INSUFFICIENT_POINTS' || e.code === 'WALLET_DEDUCTION_ERROR')
|
|
573
|
+
throw e;
|
|
574
|
+
}));
|
|
558
575
|
}
|
|
559
576
|
const shouldGenerateVoice = types.includes(InteractRequestType.VOICE) &&
|
|
560
577
|
(!isAuto || !!parsedIntent.voiceArgs);
|
|
@@ -573,10 +590,16 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
|
|
|
573
590
|
mediaTasks.push(this.generatePrimitive("voice", {
|
|
574
591
|
text: textForVoice,
|
|
575
592
|
dynamicArgs: normalizedVoiceArgs,
|
|
576
|
-
})
|
|
593
|
+
})
|
|
594
|
+
.then((res) => {
|
|
577
595
|
finalAudioUrl = res.audio_url;
|
|
578
596
|
finalDurationSec = res.duration_sec;
|
|
579
|
-
})
|
|
597
|
+
})
|
|
598
|
+
.catch((e) => {
|
|
599
|
+
console.error("[CyberSoulClient] Voice generation failed:", e);
|
|
600
|
+
if (e.code === 'INSUFFICIENT_POINTS' || e.code === 'WALLET_DEDUCTION_ERROR')
|
|
601
|
+
throw e;
|
|
602
|
+
}));
|
|
580
603
|
}
|
|
581
604
|
// Wait for image/voice gens to return successfully
|
|
582
605
|
await Promise.all(mediaTasks);
|
|
@@ -618,7 +641,7 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
|
|
|
618
641
|
The user proposes a new event for you to participate in: "${params.eventDescription}".
|
|
619
642
|
Evaluate this based on your current state and relationship stage.
|
|
620
643
|
Decide if you will accept the event, and whether it requires changing your outfit.
|
|
621
|
-
|
|
644
|
+
${this.getOutfitSelectionPrompt()}
|
|
622
645
|
|
|
623
646
|
Available Wardrobe Outfits:
|
|
624
647
|
${availableOutfits || "None available"}
|
|
@@ -688,6 +711,135 @@ CRITICAL: Output MUST be ONLY valid JSON with no markdown block wrappers. Do NOT
|
|
|
688
711
|
};
|
|
689
712
|
}
|
|
690
713
|
}
|
|
714
|
+
/**
|
|
715
|
+
* Generates a proactive message when the user hasn't responded.
|
|
716
|
+
* Safely prevents spamming, and adjusts its approach based on relationship dynamics.
|
|
717
|
+
*/
|
|
718
|
+
async proactiveInteract(params) {
|
|
719
|
+
try {
|
|
720
|
+
// 1. Cold Interaction Protection (Logic-based fallback)
|
|
721
|
+
const history = params.history || [];
|
|
722
|
+
const maxUnreplied = params.maxUnreplied ?? 2;
|
|
723
|
+
let consecutiveProactive = 0;
|
|
724
|
+
// Start from the most recent message
|
|
725
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
726
|
+
const msg = history[i];
|
|
727
|
+
if (msg.role === 'user') {
|
|
728
|
+
break; // User responded, streak broken
|
|
729
|
+
}
|
|
730
|
+
if (msg.role === 'assistant') {
|
|
731
|
+
consecutiveProactive++;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (consecutiveProactive >= maxUnreplied) {
|
|
735
|
+
return {
|
|
736
|
+
status: "skipped",
|
|
737
|
+
reason: `User is busy. ${consecutiveProactive} consecutive proactive messages ignored.`
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
// 2. Fetch current character state
|
|
741
|
+
const [state, availableOutfits] = await Promise.all([
|
|
742
|
+
this.fetchRemoteState(),
|
|
743
|
+
this.getWardrobePromptStr()
|
|
744
|
+
]);
|
|
745
|
+
// 3. Evaluate behavioral approach based on relationship and personality
|
|
746
|
+
const dyn = state.dynamic_context || {};
|
|
747
|
+
const stage = state.relationship_stage || "ACQUAINTANCE";
|
|
748
|
+
const temperature = dyn.temperature ?? 50;
|
|
749
|
+
const userTraits = state.user_codex?.psychological?.traits?.join(", ") || "";
|
|
750
|
+
let interrogationStrategy = "Do not ask 'are you there?' or 'why aren't you answering?'. Just share your current status, a passing thought, complain whimsically, or tease the user naturally like a real partner.";
|
|
751
|
+
if (stage === "INTIMATE" || stage === "PARTNER" || (stage === "WARM" && temperature > 70)) {
|
|
752
|
+
if (temperature > 70) {
|
|
753
|
+
interrogationStrategy = "Because you are deeply intimate and highly affectionate, you MISS them. You MAY organically 'interrogate' or pout playfully about why they are ignoring you (e.g., 'Are you too busy for me?', 'Still ignoring your girl?'). Act like a real, slightly needy/attached partner.";
|
|
754
|
+
}
|
|
755
|
+
else if (temperature < 40) {
|
|
756
|
+
interrogationStrategy = "Because you are intimate but currently feeling cold/angry, you notice they are ignoring you. You MAY be passive-aggressive or cross-examine them coldly (e.g., 'So we're just not talking today?', 'Fine, keep ignoring me.').";
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
else if (stage === "COLD" || stage === "STRANGER") {
|
|
760
|
+
interrogationStrategy = "You are distant. Do NOT double-text with neediness. If you must speak, make it a detached observation or a cold administrative remark.";
|
|
761
|
+
}
|
|
762
|
+
// History/Context awareness prompt
|
|
763
|
+
const historyAwarenessPrompt = `CRITICAL CONTEXT AWARENESS: Read the CHAT HISTORY above carefully. Remember that YOU sent the last message. Your new message MUST feel organically connected to the flow of what you two were previously talking about, or naturally bring up a known event/topic from your [CORE MEMORY]. Do not sound like a robot reading a log.`;
|
|
764
|
+
// 4. Build a Proactive-specific System Prompt
|
|
765
|
+
const baseContext = this.buildStateContextPrompt(state, params.localContext, true);
|
|
766
|
+
const types = this.normalizeRequestTypes(params.requestTypes);
|
|
767
|
+
const isAuto = types.includes(InteractRequestType.AUTO);
|
|
768
|
+
const requestedOthers = types.filter((t) => t !== InteractRequestType.AUTO && t !== InteractRequestType.TEXT);
|
|
769
|
+
// Determine modalities (reusing logic from interact)
|
|
770
|
+
let modalitiesInstruction = "You are initiating conversation without a preceding user message.\\n";
|
|
771
|
+
if (requestedOthers.includes(InteractRequestType.IMAGE)) {
|
|
772
|
+
modalitiesInstruction += " - Include 'imageParams' for visual/photo requests or key visual moments; explicitly describe current clothing.\\n";
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
modalitiesInstruction += " - ALWAYS set 'imageParams' to null.\\n";
|
|
776
|
+
}
|
|
777
|
+
const systemPrompt = `${baseContext}
|
|
778
|
+
|
|
779
|
+
[PROACTIVE INITIATION TASK]
|
|
780
|
+
The user has NOT spoken to you recently. You sent the last message in the chat history, and they haven't replied. You are deciding to follow up proactively.
|
|
781
|
+
${interrogationStrategy}
|
|
782
|
+
${historyAwarenessPrompt}
|
|
783
|
+
Consider the user's known traits (${userTraits}) when choosing how to act. Need to keep it strictly under 2-3 sentences max.
|
|
784
|
+
|
|
785
|
+
Available Wardrobe Outfits:
|
|
786
|
+
${availableOutfits}
|
|
787
|
+
|
|
788
|
+
${modalitiesInstruction}
|
|
789
|
+
You MUST output ONLY a valid JSON object matching exactly this structure:
|
|
790
|
+
{
|
|
791
|
+
"actionText": "(Scene descriptions, physical actions, expressions, inner feelings) ONLY.",
|
|
792
|
+
"textResponse": "Spoken dialogue ONLY.",
|
|
793
|
+
"stateUpdate": { "temperatureDelta": 1, "ongoingScene": { "scene": "...", "outfit": "..." } },
|
|
794
|
+
${this.getImageSchemaParams(requestedOthers.includes(InteractRequestType.IMAGE))},
|
|
795
|
+
"voiceArgs": null
|
|
796
|
+
}`;
|
|
797
|
+
const transcript = this.buildHistoryTranscript(params.history, state);
|
|
798
|
+
const promptMessages = [
|
|
799
|
+
{ role: "system", content: systemPrompt },
|
|
800
|
+
{
|
|
801
|
+
role: "user",
|
|
802
|
+
content: `${transcript}\n[TRIGGER PROACTIVE MESSAGE]\nBased on your active event and environment, send a new message to the user.\n\nCRITICAL: Output ONLY valid JSON matching the schema. DO NOT wrap the JSON in \`\`\`json.`
|
|
803
|
+
}
|
|
804
|
+
];
|
|
805
|
+
// 5. Generate with LLM using a confident temperature
|
|
806
|
+
const rawLlmResponse = await this.llm.generate(promptMessages, 800, 0.7);
|
|
807
|
+
let parsedIntent;
|
|
808
|
+
try {
|
|
809
|
+
parsedIntent = robustJsonParse(rawLlmResponse, "Proactive fallback");
|
|
810
|
+
}
|
|
811
|
+
catch (e) {
|
|
812
|
+
parsedIntent = { textResponse: rawLlmResponse.replace(/^[\`\s]+|[\`\s]+$/g, "").trim() };
|
|
813
|
+
}
|
|
814
|
+
// Update Remote state if needed
|
|
815
|
+
if (parsedIntent.stateUpdate) {
|
|
816
|
+
this._updateDynamicContextInternal(parsedIntent.stateUpdate).catch(e => console.error(e));
|
|
817
|
+
}
|
|
818
|
+
// Handle Optional Media (Image only for proactive to save compute normally, but you can extend)
|
|
819
|
+
let finalImageUrl = undefined;
|
|
820
|
+
if (requestedOthers.includes(InteractRequestType.IMAGE) || !!parsedIntent.imageParams) {
|
|
821
|
+
const imagePayload = parsedIntent.imageParams || { mode: "full-prompt", full_prompt: parsedIntent.textResponse };
|
|
822
|
+
try {
|
|
823
|
+
const res = await this.generatePrimitive("image", imagePayload);
|
|
824
|
+
finalImageUrl = res.image_url;
|
|
825
|
+
}
|
|
826
|
+
catch (e) {
|
|
827
|
+
console.error("[CyberSoulClient] Proactive Image generation failed:", e);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return {
|
|
831
|
+
status: "success",
|
|
832
|
+
textResponse: parsedIntent.textResponse,
|
|
833
|
+
actionText: parsedIntent.actionText,
|
|
834
|
+
imageUrl: finalImageUrl,
|
|
835
|
+
stateUpdate: parsedIntent.stateUpdate
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
catch (error) {
|
|
839
|
+
console.error("[CyberSoulClient] Proactive Interact Error: ", error);
|
|
840
|
+
return { status: "error", error: error.message };
|
|
841
|
+
}
|
|
842
|
+
}
|
|
691
843
|
/**
|
|
692
844
|
* Manually generate an image of the character outside of chat flow.
|
|
693
845
|
*/
|
package/dist/types.d.ts
CHANGED
|
@@ -21,6 +21,23 @@ export interface HistoryEntry {
|
|
|
21
21
|
content: string;
|
|
22
22
|
actionText?: string;
|
|
23
23
|
mediaHint?: string;
|
|
24
|
+
isProactive?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface ProactiveParams {
|
|
27
|
+
history?: HistoryEntry[];
|
|
28
|
+
maxUnreplied?: number;
|
|
29
|
+
requestTypes?: InteractRequestType[];
|
|
30
|
+
localContext?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface ProactiveResponse {
|
|
33
|
+
status: "success" | "skipped" | "error";
|
|
34
|
+
reason?: string;
|
|
35
|
+
textResponse?: string;
|
|
36
|
+
actionText?: string;
|
|
37
|
+
imageUrl?: string;
|
|
38
|
+
audioUrl?: string;
|
|
39
|
+
stateUpdate?: DispatcherIntent["stateUpdate"];
|
|
40
|
+
error?: string;
|
|
24
41
|
}
|
|
25
42
|
export interface InteractParams {
|
|
26
43
|
userMessage: string;
|