@space3-npm/cybersoul-client 1.2.2 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +14 -14
- package/dist/client.js +275 -271
- package/package.json +1 -1
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";
|
|
@@ -295,6 +379,185 @@ ${scenarioContext}
|
|
|
295
379
|
});
|
|
296
380
|
return `[CHAT HISTORY]\n${mapped.join('\n')}\n\n`;
|
|
297
381
|
}
|
|
382
|
+
async interact(params) {
|
|
383
|
+
try {
|
|
384
|
+
// 1. Sync remote context and wardrobe (for event triggering)
|
|
385
|
+
// We cache the wardrobe payload for 5 minutes to avoid huge payloads on every chat turn
|
|
386
|
+
const [state, availableOutfits] = await Promise.all([
|
|
387
|
+
this.fetchRemoteState(),
|
|
388
|
+
this.getWardrobePromptStr()
|
|
389
|
+
]);
|
|
390
|
+
// 2. Build local Prompt
|
|
391
|
+
const types = this.normalizeRequestTypes(params.requestTypes);
|
|
392
|
+
const isAuto = types.includes(InteractRequestType.AUTO);
|
|
393
|
+
// Combine state info into a clean descriptive context
|
|
394
|
+
const systemPrompt = `${this.buildStateContextPrompt(state, params.localContext)}
|
|
395
|
+
Available Wardrobe Outfits (For event triggers):
|
|
396
|
+
${availableOutfits}
|
|
397
|
+
|
|
398
|
+
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.
|
|
399
|
+
|
|
400
|
+
${isAuto
|
|
401
|
+
? `Analyze the user's message and decide response modalities (text, image, voice).
|
|
402
|
+
- Always include 'textResponse'.
|
|
403
|
+
- Include 'imageParams' for visual/photo requests or key visual moments during active events; explicitly describe current clothing/exposure in image fields.
|
|
404
|
+
- 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.
|
|
405
|
+
- Include 'triggerEvent' only if the VERY LAST USER MESSAGE proposes a new activity/hangout; ignore older history.
|
|
406
|
+
- Outfit acquisition (VERY LAST USER MESSAGE only): set giftOutfit for gift/buy/add-clothes intent; otherwise null. giftOutfit format: { "descriptionText": "short outfit description" }.`
|
|
407
|
+
: `Requested types to fulfill: ${types.join(", ")}`}
|
|
408
|
+
Every turn adjusts trust: positive +1, negative -1, neutral 0. Always include 'stateUpdate' with integer 'temperatureDelta' (range guidance: 0 cold to 100 obsessive).
|
|
409
|
+
|
|
410
|
+
Always return 'stateUpdate.ongoingScene' as an object with both keys: { "scene": string, "outfit": string }.
|
|
411
|
+
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".
|
|
412
|
+
|
|
413
|
+
USER ANALYSIS WORKFLOW:
|
|
414
|
+
- Extract from VERY LAST USER MESSAGE only.
|
|
415
|
+
- Add only explicit new user facts from this turn (no inference).
|
|
416
|
+
- Categories: 'realName', 'occupation', 'age', 'gender', 'hobby', 'trait', 'communicationStyle', 'boundary'.
|
|
417
|
+
- Keep nicknames in stateUpdate; do not place them in newFactsLearned.
|
|
418
|
+
- If no new fact is explicit, set userAnalysis to null.
|
|
419
|
+
|
|
420
|
+
For 'isEndTurn', use true only when the interaction naturally concludes (confirmation/bye, event ending, or clear hard scene shift); otherwise false.
|
|
421
|
+
|
|
422
|
+
Voice direction for voiceArgs: ${this.getVoiceDirectorInstruction(state)}
|
|
423
|
+
|
|
424
|
+
Output JSON Schema:
|
|
425
|
+
{
|
|
426
|
+
"actionText": "(Scene descriptions, physical actions, expressions, inner feelings) ONLY. Never include spoken dialogue here.",
|
|
427
|
+
"textResponse": "Spoken dialogue ONLY. Never include actions or parentheses.",
|
|
428
|
+
"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" } },
|
|
429
|
+
"giftOutfit": { "descriptionText": "Concise description of the newly acquired outfit to add into wardrobe." },
|
|
430
|
+
"userAnalysis": { "newFactsLearned": [{ "category": "realName|occupation|age|gender|hobby|trait|communicationStyle|boundary", "value": "explicit new user fact from VERY LAST USER MESSAGE" }] },
|
|
431
|
+
"isEndTurn": false,
|
|
432
|
+
"triggerEvent": {
|
|
433
|
+
${this.getEventSchemaParams(state.dynamic_context?.userNickname)}
|
|
434
|
+
},
|
|
435
|
+
${this.getImageSchemaParams()},
|
|
436
|
+
${this.getVoiceSchemaFromState(state)}
|
|
437
|
+
}
|
|
438
|
+
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.`;
|
|
439
|
+
const transcript = this.buildHistoryTranscript(params.history, state);
|
|
440
|
+
const userName = state.dynamic_context?.userNickname || "User";
|
|
441
|
+
const promptMessages = [
|
|
442
|
+
{ role: "system", content: systemPrompt },
|
|
443
|
+
{
|
|
444
|
+
role: "user",
|
|
445
|
+
content: transcript +
|
|
446
|
+
`[VERY LAST USER MESSAGE]\n${userName}: ${params.userMessage}\n\n` +
|
|
447
|
+
"\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.",
|
|
448
|
+
},
|
|
449
|
+
];
|
|
450
|
+
// 3. Local Execute LLM
|
|
451
|
+
const rawLlmResponse = await this.llm.generate(promptMessages, 15000, 0.7);
|
|
452
|
+
// console.debug("[CyberSoulClient] Raw LLM Response:", rawLlmResponse);
|
|
453
|
+
let parsedIntent;
|
|
454
|
+
try {
|
|
455
|
+
parsedIntent = robustJsonParse(rawLlmResponse, "Dispatcher fallback");
|
|
456
|
+
}
|
|
457
|
+
catch (e) {
|
|
458
|
+
console.warn("[CyberSoulClient] JSON parse failed, falling back to raw text:", e);
|
|
459
|
+
// Fallback robust mode - just text if completely broken
|
|
460
|
+
parsedIntent = {
|
|
461
|
+
textResponse: rawLlmResponse.replace(/^[\`\s]+|[\`\s]+$/g, "").trim(),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
// console.debug("[CyberSoulClient] Parsed Intent:", parsedIntent);
|
|
465
|
+
// 4. Update Backend State async
|
|
466
|
+
if (parsedIntent && (parsedIntent.stateUpdate || parsedIntent.userAnalysis)) {
|
|
467
|
+
this._updateDynamicContextInternal(parsedIntent.stateUpdate, parsedIntent.userAnalysis);
|
|
468
|
+
}
|
|
469
|
+
const resolvedTextResponse = typeof parsedIntent.textResponse === "string" &&
|
|
470
|
+
parsedIntent.textResponse.trim().length > 0
|
|
471
|
+
? parsedIntent.textResponse
|
|
472
|
+
: params.userMessage;
|
|
473
|
+
// Fire text ready callback if provided
|
|
474
|
+
if (params.onTextReady && (resolvedTextResponse || parsedIntent.actionText)) {
|
|
475
|
+
params.onTextReady(resolvedTextResponse, parsedIntent.actionText);
|
|
476
|
+
}
|
|
477
|
+
// 5. Build Final Media Calls parallel
|
|
478
|
+
const mediaTasks = [];
|
|
479
|
+
let finalImageUrl = undefined;
|
|
480
|
+
let finalAudioUrl = undefined;
|
|
481
|
+
let finalDurationSec = undefined;
|
|
482
|
+
// Output Event Trigger
|
|
483
|
+
if (isAuto && parsedIntent.triggerEvent) {
|
|
484
|
+
mediaTasks.push(this.apiFetch("/api/v1/cyber-soul/characters/ondemand-event", {
|
|
485
|
+
method: "POST",
|
|
486
|
+
body: JSON.stringify({
|
|
487
|
+
eventTitle: parsedIntent.triggerEvent.eventTitle,
|
|
488
|
+
eventDescription: parsedIntent.triggerEvent.eventDescription,
|
|
489
|
+
durationMins: parsedIntent.triggerEvent.durationMins || 60,
|
|
490
|
+
outfitId: parsedIntent.triggerEvent.outfitId || undefined,
|
|
491
|
+
scheduledStartTimeStr: parsedIntent.triggerEvent.scheduledStartTimeStr || undefined,
|
|
492
|
+
scheduledDateStr: parsedIntent.triggerEvent.scheduledDateStr || undefined,
|
|
493
|
+
}),
|
|
494
|
+
}).catch(e => console.error("[CyberSoulClient] Auto-triggered ondemandEvent failed:", e)));
|
|
495
|
+
}
|
|
496
|
+
if (parsedIntent.giftOutfit &&
|
|
497
|
+
typeof parsedIntent.giftOutfit === "object" &&
|
|
498
|
+
typeof parsedIntent.giftOutfit.descriptionText === "string" &&
|
|
499
|
+
parsedIntent.giftOutfit.descriptionText.trim().length > 0) {
|
|
500
|
+
mediaTasks.push(this.giftOutfit(parsedIntent.giftOutfit.descriptionText.trim()).catch((e) => console.error("[CyberSoulClient] Auto giftOutfit failed:", e)));
|
|
501
|
+
}
|
|
502
|
+
const shouldGenerateImage = types.includes(InteractRequestType.IMAGE) ||
|
|
503
|
+
(isAuto && !!parsedIntent.imageParams);
|
|
504
|
+
if (shouldGenerateImage) {
|
|
505
|
+
const imagePayload = parsedIntent.imageParams && typeof parsedIntent.imageParams === "object"
|
|
506
|
+
? parsedIntent.imageParams
|
|
507
|
+
: {
|
|
508
|
+
mode: "full-prompt",
|
|
509
|
+
full_prompt: resolvedTextResponse,
|
|
510
|
+
};
|
|
511
|
+
mediaTasks.push(this.generatePrimitive("image", imagePayload).then((res) => {
|
|
512
|
+
finalImageUrl = res.image_url;
|
|
513
|
+
}).catch(e => console.error("[CyberSoulClient] Image generation failed:", e)));
|
|
514
|
+
}
|
|
515
|
+
const shouldGenerateVoice = types.includes(InteractRequestType.VOICE) ||
|
|
516
|
+
(isAuto && !!parsedIntent.voiceArgs);
|
|
517
|
+
if (shouldGenerateVoice) {
|
|
518
|
+
const normalizedVoiceArgs = parsedIntent.voiceArgs && typeof parsedIntent.voiceArgs === "object"
|
|
519
|
+
? parsedIntent.voiceArgs
|
|
520
|
+
: {};
|
|
521
|
+
let textForVoice = resolvedTextResponse;
|
|
522
|
+
// One final bulletproof regex wash to strip (smiles) and *laughs* just in case the LLM disobeys
|
|
523
|
+
if (typeof textForVoice === "string") {
|
|
524
|
+
textForVoice = textForVoice.replace(/[\((\[【\*].*?[\))\]】\*]/g, '').trim();
|
|
525
|
+
}
|
|
526
|
+
if (typeof textForVoice !== "string" || textForVoice.trim().length === 0) {
|
|
527
|
+
textForVoice = "...";
|
|
528
|
+
}
|
|
529
|
+
mediaTasks.push(this.generatePrimitive("voice", {
|
|
530
|
+
text: textForVoice,
|
|
531
|
+
dynamicArgs: normalizedVoiceArgs,
|
|
532
|
+
}).then((res) => {
|
|
533
|
+
finalAudioUrl = res.audio_url;
|
|
534
|
+
finalDurationSec = res.duration_sec;
|
|
535
|
+
}).catch(e => console.error("[CyberSoulClient] Voice generation failed:", e)));
|
|
536
|
+
}
|
|
537
|
+
// Wait for image/voice gens to return successfully
|
|
538
|
+
await Promise.all(mediaTasks);
|
|
539
|
+
return {
|
|
540
|
+
status: "success",
|
|
541
|
+
textResponse: resolvedTextResponse || "...",
|
|
542
|
+
actionText: parsedIntent.actionText || "",
|
|
543
|
+
imageUrl: finalImageUrl,
|
|
544
|
+
audioUrl: finalAudioUrl,
|
|
545
|
+
durationSec: finalDurationSec,
|
|
546
|
+
triggeredEvent: parsedIntent.triggerEvent || undefined,
|
|
547
|
+
stateUpdate: parsedIntent.stateUpdate,
|
|
548
|
+
userAnalysis: parsedIntent.userAnalysis,
|
|
549
|
+
isEndTurn: parsedIntent.isEndTurn,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
catch (error) {
|
|
553
|
+
console.error("[CyberSoulClient] Interface Error: ", error);
|
|
554
|
+
return {
|
|
555
|
+
status: "error",
|
|
556
|
+
textResponse: "System Error...",
|
|
557
|
+
error: error.message,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
}
|
|
298
561
|
/**
|
|
299
562
|
* Evaluates and triggers an on-demand event, intelligently deciding if an outfit change is needed.
|
|
300
563
|
*/
|
|
@@ -380,18 +643,6 @@ CRITICAL: Output MUST be ONLY valid JSON with no markdown block wrappers. Do NOT
|
|
|
380
643
|
};
|
|
381
644
|
}
|
|
382
645
|
}
|
|
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
646
|
/**
|
|
396
647
|
* Manually generate an image of the character outside of chat flow.
|
|
397
648
|
*/
|
|
@@ -466,6 +717,18 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
|
|
|
466
717
|
durationSec: res.duration_sec,
|
|
467
718
|
};
|
|
468
719
|
}
|
|
720
|
+
/**
|
|
721
|
+
* Fetches the current dynamic context and daily state.
|
|
722
|
+
*/
|
|
723
|
+
async getState() {
|
|
724
|
+
return this.fetchRemoteState();
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Updates the character's relationship temperature or mood.
|
|
728
|
+
*/
|
|
729
|
+
async updateDynamicContext(stateUpdate, userAnalysis) {
|
|
730
|
+
return this._updateDynamicContextInternal(stateUpdate, userAnalysis);
|
|
731
|
+
}
|
|
469
732
|
/**
|
|
470
733
|
* Gift a new outfit to the character's wardrobe inventory.
|
|
471
734
|
*/
|
|
@@ -499,265 +762,6 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
|
|
|
499
762
|
if (!res.ok)
|
|
500
763
|
throw new Error("Failed to generate daily script");
|
|
501
764
|
}
|
|
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
765
|
/**
|
|
762
766
|
* Automatically detect and summarize the story from the current chat history.
|
|
763
767
|
* It takes raw message history and returns a narrative paragraph representing the current story segment.
|