1
This commit is contained in:
@@ -35,45 +35,34 @@ import {
|
||||
import {
|
||||
AIResponse,
|
||||
Character,
|
||||
CharacterChatTurn,
|
||||
CustomWorldCreatorIntent,
|
||||
CustomWorldGenerationMode,
|
||||
CustomWorldProfile,
|
||||
Encounter,
|
||||
SceneEncounterResult,
|
||||
SceneHostileNpc,
|
||||
SceneNpc,
|
||||
StoryMoment,
|
||||
StoryOption,
|
||||
ThemePack,
|
||||
WorldStoryGraph,
|
||||
WorldType,
|
||||
} from '../types';
|
||||
import {
|
||||
buildOfflineCharacterPanelChatReply as buildOfflineCharacterPanelChatReplyFromFallback,
|
||||
buildOfflineCharacterPanelChatSuggestions as buildOfflineCharacterPanelChatSuggestionsFromFallback,
|
||||
buildOfflineCharacterPanelChatSummary as buildOfflineCharacterPanelChatSummaryFromFallback,
|
||||
buildOfflineNpcChatDialogue as buildOfflineNpcChatDialogueFromFallback,
|
||||
buildOfflineNpcRecruitDialogue as buildOfflineNpcRecruitDialogueFromFallback,
|
||||
} from './aiFallbacks';
|
||||
import type {
|
||||
CustomWorldSceneImageRequest,
|
||||
CustomWorldSceneImageResult,
|
||||
StoryGenerationContext,
|
||||
StoryRequestOptions,
|
||||
TextStreamOptions,
|
||||
} from './aiTypes';
|
||||
import { fetchWithApiAuth } from './apiClient';
|
||||
import {
|
||||
buildCharacterPanelChatPrompt,
|
||||
buildCharacterPanelChatSuggestionPrompt,
|
||||
buildCharacterPanelChatSummaryPrompt,
|
||||
CHARACTER_PANEL_CHAT_SUGGESTION_SYSTEM_PROMPT,
|
||||
CHARACTER_PANEL_CHAT_SUMMARY_SYSTEM_PROMPT,
|
||||
CHARACTER_PANEL_CHAT_SYSTEM_PROMPT,
|
||||
CharacterChatPromptContext,
|
||||
CharacterChatTargetStatus,
|
||||
} from './characterChatPrompt';
|
||||
generateCharacterPanelChatSuggestions as generateCharacterPanelChatSuggestionsFromServer,
|
||||
generateCharacterPanelChatSummary as generateCharacterPanelChatSummaryFromServer,
|
||||
generateInitialStory as generateInitialStoryFromServer,
|
||||
generateNextStep as generateNextStepFromServer,
|
||||
streamCharacterPanelChatReply as streamCharacterPanelChatReplyFromServer,
|
||||
streamNpcChatDialogue as streamNpcChatDialogueFromServer,
|
||||
streamNpcRecruitDialogue as streamNpcRecruitDialogueFromServer,
|
||||
} from './aiService';
|
||||
import { fetchWithApiAuth } from './apiClient';
|
||||
import {
|
||||
buildCustomWorldRawProfileFromFramework,
|
||||
type CustomWorldGenerationFramework,
|
||||
@@ -105,20 +94,8 @@ import {
|
||||
requestPlainTextCompletion as requestPlainTextCompletionFromClient,
|
||||
streamPlainTextCompletion as streamPlainTextCompletionFromClient,
|
||||
} from './llmClient';
|
||||
import {
|
||||
parseJsonResponseText as parseJsonResponseTextFromParser,
|
||||
parseLineListContent as parseLineListContentFromParser,
|
||||
} from './llmParsers';
|
||||
import { hasMixedNarrativeLanguage } from './narrativeLanguage';
|
||||
import {
|
||||
buildNpcRecruitDialoguePrompt,
|
||||
buildStrictNpcChatDialoguePrompt,
|
||||
buildUserPrompt,
|
||||
describeWorld,
|
||||
NPC_CHAT_DIALOGUE_STRICT_SYSTEM_PROMPT,
|
||||
NPC_RECRUIT_DIALOGUE_SYSTEM_PROMPT,
|
||||
SYSTEM_PROMPT,
|
||||
} from './prompt';
|
||||
import { parseJsonResponseText as parseJsonResponseTextFromParser } from './llmParsers';
|
||||
import type { CharacterChatTargetStatus } from './rpgRuntimeChatTypes';
|
||||
import {
|
||||
buildFallbackActorNarrativeProfile,
|
||||
normalizeActorNarrativeProfile,
|
||||
@@ -1388,23 +1365,6 @@ function cloneStoryOption(option: StoryOption): StoryOption {
|
||||
};
|
||||
}
|
||||
|
||||
function buildCharacterChatPromptContext(
|
||||
context: StoryGenerationContext,
|
||||
): CharacterChatPromptContext {
|
||||
return {
|
||||
playerHp: context.playerHp,
|
||||
playerMaxHp: context.playerMaxHp,
|
||||
playerMana: context.playerMana,
|
||||
playerMaxMana: context.playerMaxMana,
|
||||
inBattle: context.inBattle,
|
||||
playerFacing: context.playerFacing,
|
||||
playerAnimation: context.playerAnimation,
|
||||
sceneName: context.sceneName ?? null,
|
||||
sceneDescription: context.sceneDescription ?? null,
|
||||
customWorldProfile: context.customWorldProfile ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveOptionsFromProvidedOptions(
|
||||
items: RawOptionItem[],
|
||||
availableOptions: StoryOption[],
|
||||
@@ -1505,357 +1465,9 @@ function getFallbackOptions(
|
||||
);
|
||||
}
|
||||
|
||||
function buildOfflineResponse(
|
||||
world: WorldType,
|
||||
character: Character,
|
||||
monsters: SceneHostileNpc[],
|
||||
context: StoryGenerationContext,
|
||||
choice?: string,
|
||||
requestOptions: StoryRequestOptions = {},
|
||||
): AIResponse {
|
||||
const scene = getScenePresetById(world, context.sceneId);
|
||||
const fallbackEncounter = context.pendingSceneEncounter
|
||||
? normalizeEncounterResult(
|
||||
scene?.npcs[0]
|
||||
? { kind: 'npc', npcId: scene.npcs[0].id }
|
||||
: { kind: 'none' },
|
||||
world,
|
||||
context,
|
||||
)
|
||||
: undefined;
|
||||
const resolution = buildEncounterDrivenResolution(
|
||||
world,
|
||||
monsters,
|
||||
context,
|
||||
fallbackEncounter,
|
||||
);
|
||||
const constrainedOptions =
|
||||
requestOptions.availableOptions?.map(cloneStoryOption) ??
|
||||
requestOptions.optionCatalog?.map(cloneStoryOption);
|
||||
const options =
|
||||
constrainedOptions ??
|
||||
getFallbackOptions(world, character, resolution.monsters, {
|
||||
...context,
|
||||
inBattle: resolution.inBattle,
|
||||
});
|
||||
const primaryMonster =
|
||||
resolution.monsters.find((monster) => monster.hp > 0) ??
|
||||
resolution.monsters[0];
|
||||
const encounterName = context.encounterName || '前方的人影';
|
||||
export const generateInitialStoryStrict = generateInitialStoryFromServer;
|
||||
|
||||
if (!resolution.inBattle || !primaryMonster) {
|
||||
return {
|
||||
storyText: constrainedOptions
|
||||
? choice
|
||||
? `${encounterName}的态度与周围气氛都出现了新的变化,你需要立刻判断接下来如何应对。`
|
||||
: `${context.sceneName || describeWorld(world)}的气氛仍在缓慢推进,眼前的${encounterName}正等待你的下一步反应。`
|
||||
: choice
|
||||
? `主角暂时脱离了正面厮杀,四周重新安静下来,${context.sceneName || describeWorld(world)}的前路正等着继续探索。`
|
||||
: `主角踏入${describeWorld(world)}世界的${context.sceneName || '前方区域'},眼前暂时没有新的敌对角色逼近。`,
|
||||
options,
|
||||
encounter: resolution.encounter,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
storyText: choice
|
||||
? `主角刚做出新的动作,前方的${primaryMonster.name}${primaryMonster.action},局势仍在持续绷紧。`
|
||||
: `主角刚踏入战场,前方的${primaryMonster.name}${primaryMonster.action},战斗压力已经逼到眼前。`,
|
||||
options,
|
||||
encounter: resolution.encounter,
|
||||
};
|
||||
}
|
||||
|
||||
function buildStoryLanguageRepairPrompt(response: AIResponse) {
|
||||
return [
|
||||
'请把下面 JSON 中的 storyText 与 options[].actionText 修复为自然中文。',
|
||||
'只改写叙事和选项文案,不要改变 encounter、options 数量、顺序或 functionId。',
|
||||
'如果英文只是角色名或地点名,可以保留名字本体;其余英文句子、英文解释和中英混杂表达都必须改成中文。',
|
||||
JSON.stringify(
|
||||
{
|
||||
storyText: response.storyText,
|
||||
encounter: response.encounter ?? null,
|
||||
options: response.options.map((option) => ({
|
||||
functionId: option.functionId,
|
||||
actionText: option.actionText,
|
||||
})),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
].join('\n\n');
|
||||
}
|
||||
|
||||
function needsStoryLanguageRepair(response: AIResponse) {
|
||||
return hasMixedNarrativeLanguage(response.storyText);
|
||||
}
|
||||
|
||||
function buildStoryLanguageFallbackText(
|
||||
context: StoryGenerationContext,
|
||||
inBattle: boolean,
|
||||
) {
|
||||
if (inBattle) {
|
||||
return '敌意仍压在眼前,战斗局势还没有真正松开。';
|
||||
}
|
||||
|
||||
if (context.encounterName) {
|
||||
return `${context.encounterName}的态度与周围气氛都出现了新的变化,你需要立刻判断接下来如何应对。`;
|
||||
}
|
||||
|
||||
return `${context.sceneName || '眼前区域'}里的气氛又有了新的变化,你需要继续判断下一步。`;
|
||||
}
|
||||
|
||||
function finalizeStoryNarrativeLanguage(
|
||||
response: AIResponse,
|
||||
context: StoryGenerationContext,
|
||||
inBattle: boolean,
|
||||
): AIResponse {
|
||||
if (!needsStoryLanguageRepair(response)) {
|
||||
return response;
|
||||
}
|
||||
|
||||
return {
|
||||
...response,
|
||||
storyText: buildStoryLanguageFallbackText(context, inBattle),
|
||||
};
|
||||
}
|
||||
|
||||
async function repairStoryNarrativeLanguage(
|
||||
response: AIResponse,
|
||||
worldType: WorldType,
|
||||
character: Character,
|
||||
monsters: SceneHostileNpc[],
|
||||
context: StoryGenerationContext,
|
||||
requestOptions: StoryRequestOptions,
|
||||
) {
|
||||
const responseBattleState = buildEncounterDrivenResolution(
|
||||
worldType,
|
||||
monsters,
|
||||
context,
|
||||
response.encounter,
|
||||
).inBattle;
|
||||
|
||||
if (!needsStoryLanguageRepair(response)) {
|
||||
return finalizeStoryNarrativeLanguage(
|
||||
response,
|
||||
context,
|
||||
responseBattleState,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const repairedContent = await requestChatMessageContent(
|
||||
STORY_LANGUAGE_REPAIR_SYSTEM_PROMPT,
|
||||
buildStoryLanguageRepairPrompt(response),
|
||||
{
|
||||
debugLabel: 'story-language-repair',
|
||||
},
|
||||
);
|
||||
const repairedResponse = normalizeResponse(
|
||||
parseJsonResponseTextFromParser(repairedContent),
|
||||
worldType,
|
||||
character,
|
||||
monsters,
|
||||
context,
|
||||
requestOptions,
|
||||
);
|
||||
const repairedBattleState = buildEncounterDrivenResolution(
|
||||
worldType,
|
||||
monsters,
|
||||
context,
|
||||
repairedResponse.encounter,
|
||||
).inBattle;
|
||||
return finalizeStoryNarrativeLanguage(
|
||||
repairedResponse,
|
||||
context,
|
||||
repairedBattleState,
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn('Failed to repair mixed-language story response:', error);
|
||||
return finalizeStoryNarrativeLanguage(
|
||||
response,
|
||||
context,
|
||||
responseBattleState,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeResponse(
|
||||
raw: unknown,
|
||||
worldType: WorldType,
|
||||
character: Character,
|
||||
monsters: SceneHostileNpc[],
|
||||
context: StoryGenerationContext,
|
||||
requestOptions: StoryRequestOptions = {},
|
||||
): AIResponse {
|
||||
const parsedEncounter = normalizeEncounterResult(
|
||||
(raw as Record<string, unknown> | null)?.encounter,
|
||||
worldType,
|
||||
context,
|
||||
);
|
||||
const resolution = buildEncounterDrivenResolution(
|
||||
worldType,
|
||||
monsters,
|
||||
context,
|
||||
parsedEncounter,
|
||||
);
|
||||
const responseContext = {
|
||||
...context,
|
||||
inBattle: resolution.inBattle,
|
||||
};
|
||||
const fallbackOptions =
|
||||
requestOptions.availableOptions?.map(cloneStoryOption) ??
|
||||
requestOptions.optionCatalog?.map(cloneStoryOption) ??
|
||||
getFallbackOptions(
|
||||
worldType,
|
||||
character,
|
||||
resolution.monsters,
|
||||
responseContext,
|
||||
);
|
||||
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
return {
|
||||
storyText: responseContext.inBattle
|
||||
? '前方敌意仍在持续逼近,局势只允许继续交锋或抽身脱离。'
|
||||
: '周围暂时平静下来,你可以继续探索或前往别处。',
|
||||
options: fallbackOptions,
|
||||
encounter: resolution.encounter,
|
||||
};
|
||||
}
|
||||
|
||||
const data = raw as Record<string, unknown>;
|
||||
const rawOptions = Array.isArray(data.options) ? data.options : [];
|
||||
const optionItems = rawOptions
|
||||
.map((option) => {
|
||||
if (!option || typeof option !== 'object') return null;
|
||||
const item = option as Record<string, unknown>;
|
||||
const functionId =
|
||||
typeof item.functionId === 'string' ? item.functionId.trim() : '';
|
||||
if (!functionId) return null;
|
||||
return {
|
||||
functionId,
|
||||
actionText:
|
||||
typeof item.actionText === 'string'
|
||||
? item.actionText.trim()
|
||||
: undefined,
|
||||
} satisfies RawOptionItem;
|
||||
})
|
||||
.filter(Boolean) as RawOptionItem[];
|
||||
|
||||
const options = requestOptions.availableOptions
|
||||
? resolveOptionsFromProvidedOptions(
|
||||
optionItems,
|
||||
requestOptions.availableOptions,
|
||||
)
|
||||
: requestOptions.optionCatalog
|
||||
? resolveOptionsFromOptionCatalog(
|
||||
optionItems,
|
||||
requestOptions.optionCatalog,
|
||||
)
|
||||
: resolveOptionsFromFunctionIds(
|
||||
optionItems,
|
||||
worldType,
|
||||
character,
|
||||
resolution.monsters,
|
||||
responseContext,
|
||||
);
|
||||
|
||||
return {
|
||||
storyText:
|
||||
typeof data.storyText === 'string' && data.storyText.trim()
|
||||
? data.storyText.trim()
|
||||
: responseContext.inBattle
|
||||
? '敌人仍在前方压迫而来,战斗还没有结束。'
|
||||
: '前路重新安静下来,可以继续决定接下来的探索方向。',
|
||||
options: options.length > 0 ? options : fallbackOptions,
|
||||
encounter: resolution.encounter,
|
||||
};
|
||||
}
|
||||
|
||||
async function requestCompletion(
|
||||
userPrompt: string,
|
||||
worldType: WorldType,
|
||||
character: Character,
|
||||
monsters: SceneHostileNpc[],
|
||||
context: StoryGenerationContext,
|
||||
requestOptions: StoryRequestOptions = {},
|
||||
): Promise<AIResponse> {
|
||||
const content = await requestChatMessageContent(SYSTEM_PROMPT, userPrompt, {
|
||||
debugLabel: 'story-completion',
|
||||
});
|
||||
|
||||
const response = normalizeResponse(
|
||||
parseJsonResponseTextFromParser(content),
|
||||
worldType,
|
||||
character,
|
||||
monsters,
|
||||
context,
|
||||
requestOptions,
|
||||
);
|
||||
|
||||
return repairStoryNarrativeLanguage(
|
||||
response,
|
||||
worldType,
|
||||
character,
|
||||
monsters,
|
||||
context,
|
||||
requestOptions,
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateInitialStoryStrict(
|
||||
world: WorldType,
|
||||
character: Character,
|
||||
monsters: SceneHostileNpc[],
|
||||
context: StoryGenerationContext,
|
||||
requestOptions: StoryRequestOptions = {},
|
||||
): Promise<AIResponse> {
|
||||
return requestCompletion(
|
||||
buildUserPrompt(
|
||||
world,
|
||||
character,
|
||||
monsters,
|
||||
[],
|
||||
context,
|
||||
undefined,
|
||||
requestOptions.availableOptions,
|
||||
requestOptions.optionCatalog,
|
||||
),
|
||||
world,
|
||||
character,
|
||||
monsters,
|
||||
context,
|
||||
requestOptions,
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateNextStepStrict(
|
||||
world: WorldType,
|
||||
character: Character,
|
||||
monsters: SceneHostileNpc[],
|
||||
history: StoryMoment[],
|
||||
choice: string,
|
||||
context: StoryGenerationContext,
|
||||
requestOptions: StoryRequestOptions = {},
|
||||
): Promise<AIResponse> {
|
||||
return requestCompletion(
|
||||
buildUserPrompt(
|
||||
world,
|
||||
character,
|
||||
monsters,
|
||||
history,
|
||||
context,
|
||||
choice,
|
||||
requestOptions.availableOptions,
|
||||
requestOptions.optionCatalog,
|
||||
),
|
||||
world,
|
||||
character,
|
||||
monsters,
|
||||
context,
|
||||
requestOptions,
|
||||
);
|
||||
}
|
||||
export const generateNextStepStrict = generateNextStepFromServer;
|
||||
|
||||
export async function generateCustomWorldSceneImage({
|
||||
profile,
|
||||
@@ -2218,297 +1830,19 @@ export async function generateCustomWorldProfile(
|
||||
}
|
||||
}
|
||||
|
||||
export async function streamCharacterPanelChatReply(
|
||||
world: WorldType,
|
||||
playerCharacter: Character,
|
||||
targetCharacter: Character,
|
||||
storyHistory: StoryMoment[],
|
||||
context: StoryGenerationContext,
|
||||
conversationHistory: CharacterChatTurn[],
|
||||
conversationSummary: string,
|
||||
playerMessage: string,
|
||||
targetStatus: CharacterChatTargetStatus,
|
||||
options: TextStreamOptions = {},
|
||||
) {
|
||||
const userPrompt = buildCharacterPanelChatPrompt({
|
||||
world,
|
||||
playerCharacter,
|
||||
targetCharacter,
|
||||
storyHistory,
|
||||
context: buildCharacterChatPromptContext(context),
|
||||
conversationHistory,
|
||||
conversationSummary,
|
||||
playerMessage,
|
||||
targetStatus,
|
||||
});
|
||||
export const streamCharacterPanelChatReply =
|
||||
streamCharacterPanelChatReplyFromServer;
|
||||
|
||||
try {
|
||||
const reply = await streamPlainTextCompletionFromClient(
|
||||
CHARACTER_PANEL_CHAT_SYSTEM_PROMPT,
|
||||
userPrompt,
|
||||
options,
|
||||
);
|
||||
return (
|
||||
reply.trim() ||
|
||||
buildOfflineCharacterPanelChatReplyFromFallback(
|
||||
targetCharacter,
|
||||
playerMessage,
|
||||
conversationSummary,
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
if (isLlmConnectivityErrorFromClient(error)) {
|
||||
const fallbackText = buildOfflineCharacterPanelChatReplyFromFallback(
|
||||
targetCharacter,
|
||||
playerMessage,
|
||||
conversationSummary,
|
||||
);
|
||||
options.onUpdate?.(fallbackText);
|
||||
return fallbackText;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
export const generateCharacterPanelChatSuggestions =
|
||||
generateCharacterPanelChatSuggestionsFromServer;
|
||||
|
||||
export async function generateCharacterPanelChatSuggestions(
|
||||
world: WorldType,
|
||||
playerCharacter: Character,
|
||||
targetCharacter: Character,
|
||||
storyHistory: StoryMoment[],
|
||||
context: StoryGenerationContext,
|
||||
conversationHistory: CharacterChatTurn[],
|
||||
conversationSummary: string,
|
||||
targetStatus: CharacterChatTargetStatus,
|
||||
) {
|
||||
const fallbackSuggestions =
|
||||
buildOfflineCharacterPanelChatSuggestionsFromFallback(targetCharacter);
|
||||
const userPrompt = buildCharacterPanelChatSuggestionPrompt({
|
||||
world,
|
||||
playerCharacter,
|
||||
targetCharacter,
|
||||
storyHistory,
|
||||
context: buildCharacterChatPromptContext(context),
|
||||
conversationHistory,
|
||||
conversationSummary,
|
||||
targetStatus,
|
||||
});
|
||||
export const generateCharacterPanelChatSummary =
|
||||
generateCharacterPanelChatSummaryFromServer;
|
||||
|
||||
try {
|
||||
const text = await requestPlainTextCompletionFromClient(
|
||||
CHARACTER_PANEL_CHAT_SUGGESTION_SYSTEM_PROMPT,
|
||||
userPrompt,
|
||||
);
|
||||
const parsedSuggestions = parseLineListContentFromParser(text, 3);
|
||||
if (parsedSuggestions.length === 0) {
|
||||
return fallbackSuggestions;
|
||||
}
|
||||
return [...parsedSuggestions, ...fallbackSuggestions].slice(0, 3);
|
||||
} catch (error) {
|
||||
if (isLlmConnectivityErrorFromClient(error)) {
|
||||
return fallbackSuggestions;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
export const generateInitialStory = generateInitialStoryFromServer;
|
||||
|
||||
export async function generateCharacterPanelChatSummary(
|
||||
world: WorldType,
|
||||
playerCharacter: Character,
|
||||
targetCharacter: Character,
|
||||
storyHistory: StoryMoment[],
|
||||
context: StoryGenerationContext,
|
||||
conversationHistory: CharacterChatTurn[],
|
||||
previousSummary: string,
|
||||
targetStatus: CharacterChatTargetStatus,
|
||||
) {
|
||||
const fallbackSummary = buildOfflineCharacterPanelChatSummaryFromFallback(
|
||||
targetCharacter,
|
||||
conversationHistory,
|
||||
previousSummary,
|
||||
);
|
||||
const userPrompt = buildCharacterPanelChatSummaryPrompt({
|
||||
world,
|
||||
playerCharacter,
|
||||
targetCharacter,
|
||||
storyHistory,
|
||||
context: buildCharacterChatPromptContext(context),
|
||||
conversationHistory,
|
||||
previousSummary,
|
||||
targetStatus,
|
||||
});
|
||||
export const generateNextStep = generateNextStepFromServer;
|
||||
|
||||
try {
|
||||
const text = await requestPlainTextCompletionFromClient(
|
||||
CHARACTER_PANEL_CHAT_SUMMARY_SYSTEM_PROMPT,
|
||||
userPrompt,
|
||||
);
|
||||
return text.trim() || fallbackSummary;
|
||||
} catch (error) {
|
||||
if (isLlmConnectivityErrorFromClient(error)) {
|
||||
return fallbackSummary;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
export const streamNpcChatDialogue = streamNpcChatDialogueFromServer;
|
||||
|
||||
export async function generateInitialStory(
|
||||
world: WorldType,
|
||||
character: Character,
|
||||
monsters: SceneHostileNpc[],
|
||||
context: StoryGenerationContext,
|
||||
requestOptions: StoryRequestOptions = {},
|
||||
): Promise<AIResponse> {
|
||||
try {
|
||||
return await requestCompletion(
|
||||
buildUserPrompt(
|
||||
world,
|
||||
character,
|
||||
monsters,
|
||||
[],
|
||||
context,
|
||||
undefined,
|
||||
requestOptions.availableOptions,
|
||||
requestOptions.optionCatalog,
|
||||
),
|
||||
world,
|
||||
character,
|
||||
monsters,
|
||||
context,
|
||||
requestOptions,
|
||||
);
|
||||
} catch (error) {
|
||||
if (isLlmConnectivityErrorFromClient(error)) {
|
||||
return buildOfflineResponse(
|
||||
world,
|
||||
character,
|
||||
monsters,
|
||||
context,
|
||||
undefined,
|
||||
requestOptions,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateNextStep(
|
||||
world: WorldType,
|
||||
character: Character,
|
||||
monsters: SceneHostileNpc[],
|
||||
history: StoryMoment[],
|
||||
choice: string,
|
||||
context: StoryGenerationContext,
|
||||
requestOptions: StoryRequestOptions = {},
|
||||
): Promise<AIResponse> {
|
||||
try {
|
||||
return await requestCompletion(
|
||||
buildUserPrompt(
|
||||
world,
|
||||
character,
|
||||
monsters,
|
||||
history,
|
||||
context,
|
||||
choice,
|
||||
requestOptions.availableOptions,
|
||||
requestOptions.optionCatalog,
|
||||
),
|
||||
world,
|
||||
character,
|
||||
monsters,
|
||||
context,
|
||||
requestOptions,
|
||||
);
|
||||
} catch (error) {
|
||||
if (isLlmConnectivityErrorFromClient(error)) {
|
||||
return buildOfflineResponse(
|
||||
world,
|
||||
character,
|
||||
monsters,
|
||||
context,
|
||||
choice,
|
||||
requestOptions,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function streamNpcChatDialogue(
|
||||
world: WorldType,
|
||||
character: Character,
|
||||
encounter: Encounter,
|
||||
monsters: SceneHostileNpc[],
|
||||
history: StoryMoment[],
|
||||
context: StoryGenerationContext,
|
||||
topic: string,
|
||||
resultSummary: string,
|
||||
options: TextStreamOptions = {},
|
||||
) {
|
||||
const userPrompt = buildStrictNpcChatDialoguePrompt(
|
||||
world,
|
||||
character,
|
||||
encounter,
|
||||
monsters,
|
||||
history,
|
||||
context,
|
||||
topic,
|
||||
resultSummary,
|
||||
);
|
||||
|
||||
try {
|
||||
return await streamPlainTextCompletionFromClient(
|
||||
NPC_CHAT_DIALOGUE_STRICT_SYSTEM_PROMPT,
|
||||
userPrompt,
|
||||
options,
|
||||
);
|
||||
} catch (error) {
|
||||
if (isLlmConnectivityErrorFromClient(error)) {
|
||||
const fallbackText = buildOfflineNpcChatDialogueFromFallback(
|
||||
encounter,
|
||||
topic,
|
||||
);
|
||||
options.onUpdate?.(fallbackText);
|
||||
return fallbackText;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function streamNpcRecruitDialogue(
|
||||
world: WorldType,
|
||||
character: Character,
|
||||
encounter: Encounter,
|
||||
monsters: SceneHostileNpc[],
|
||||
history: StoryMoment[],
|
||||
context: StoryGenerationContext,
|
||||
invitationText: string,
|
||||
recruitSummary: string,
|
||||
options: TextStreamOptions = {},
|
||||
) {
|
||||
const userPrompt = buildNpcRecruitDialoguePrompt(
|
||||
world,
|
||||
character,
|
||||
encounter,
|
||||
monsters,
|
||||
history,
|
||||
context,
|
||||
invitationText,
|
||||
recruitSummary,
|
||||
);
|
||||
|
||||
try {
|
||||
return await streamPlainTextCompletionFromClient(
|
||||
NPC_RECRUIT_DIALOGUE_SYSTEM_PROMPT,
|
||||
userPrompt,
|
||||
options,
|
||||
);
|
||||
} catch (error) {
|
||||
if (isLlmConnectivityErrorFromClient(error)) {
|
||||
const fallbackText =
|
||||
buildOfflineNpcRecruitDialogueFromFallback(encounter);
|
||||
options.onUpdate?.(fallbackText);
|
||||
return fallbackText;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
export const streamNpcRecruitDialogue = streamNpcRecruitDialogueFromServer;
|
||||
|
||||
Reference in New Issue
Block a user