@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 +14 -14
- package/dist/client.js +281 -274
- package/dist/types.d.ts +2 -1
- package/dist/utils/json.utils.d.ts +1 -1
- package/dist/utils/json.utils.js +45 -1
- 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";
|
|
@@ -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
|
|
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;
|
package/dist/utils/json.utils.js
CHANGED
|
@@ -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
|
}
|