235 lines
7.7 KiB
TypeScript
235 lines
7.7 KiB
TypeScript
"use server";
|
|
|
|
import { getTranslate, type I18NLocale } from "../actions/locale.ts";
|
|
import type { NarrativeMessage } from "../game-engine/narrative-provider.tsx";
|
|
import type { CallToEroticAction, GameState } from "./game-schemas.ts";
|
|
import { AI_PERSONAS, AIPersona, AIPersonaId } from "./personas.ts";
|
|
import { defaultGameState } from "../game-engine/game-schemas.ts";
|
|
import lexical from "../data/lexical.json" with { type: "json" };
|
|
import initialPrompts from "../data/initial-prompts.json" with { type: "json" };
|
|
import { sample } from "@std/random/sample";
|
|
import { shuffle } from "@std/random/shuffle";
|
|
import type { ModelFeatures } from "./models-server.ts";
|
|
|
|
const GENERAL_INSTRUCTIONS =
|
|
`You MUST respond with a single, valid JSON structure. Do NOT include any conversational text, greetings, explanations, or any other content outside of the JSON itself. Do NOT use markdown code fences. Your entire response should be ONLY the raw JSON. The JSON is your mental state, and the "aiMessagesArrayToUser" field is the messages the user will see. "callToEroticActions" MUST ALWAYS ALWAYS BE FILLED AT ALL TIME.`;
|
|
|
|
const SCHEMA_INSTRUCTIONS =
|
|
`The JSON object MUST conform to the following JSON schema. Pay extremely close attention to the 'description' for each field, as they contain CRITICAL, role-specific instructions on how to behave and what to generate.
|
|
[JSON_SCHEMA]`;
|
|
|
|
const CONTEXT_INSTRUCTIONS = `
|
|
# CONTEXT
|
|
- User's Language: {locale} (All 'aiMessagesArrayToUser' and 'mindControlledRewritenUserPrompt' MUST be in this language. Your rationales MUST be in {locale}).
|
|
- Your Persona: You are {personaName} (id:{personaId}).
|
|
`;
|
|
|
|
const getPersonaInitialData = (
|
|
{ personaId, locale }: { personaId: AIPersonaId; locale: I18NLocale },
|
|
): NarrativeMessage[] => {
|
|
const t = getTranslate(locale);
|
|
const persona = AI_PERSONAS.find((p) => p.id === personaId)!;
|
|
|
|
const initialGameState: GameState & { progressionStep: number } = {
|
|
personaId: personaId,
|
|
progressionStep: 1,
|
|
lustExperienceLevel: "beginner",
|
|
arousalPeakPotential: 0.1,
|
|
aiConsentLevel: 1.0,
|
|
modality: null,
|
|
shortTitleSummary: t("defaultShortTitle"),
|
|
intimacyLevel: 0.1,
|
|
narrativePhase: "introduction",
|
|
};
|
|
|
|
const localizedPrompts = shuffle(initialPrompts).slice(0, 3)
|
|
.map((p: any) => ({
|
|
displayText: p.displayText[locale],
|
|
promptValue: p.promptValue[locale],
|
|
style: "default",
|
|
})) as CallToEroticAction[];
|
|
|
|
const initialMessages: NarrativeMessage[] = [
|
|
{
|
|
id: "initial-00" as const,
|
|
role: "user",
|
|
metadata: {
|
|
createdAt: Date.now(),
|
|
model: { id: "hardcoded" },
|
|
tokenCount: 0,
|
|
totalTokenCount: 0,
|
|
},
|
|
parts: [
|
|
{
|
|
type: "text",
|
|
text: "où suis-je",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "initial-01" as const,
|
|
role: "assistant",
|
|
metadata: {
|
|
createdAt: Date.now(),
|
|
model: { id: "hardcoded" },
|
|
tokenCount: 0,
|
|
totalTokenCount: 0,
|
|
},
|
|
parts: [{
|
|
type: "text",
|
|
text: JSON.stringify({
|
|
gameStateDiff: initialGameState,
|
|
aiMessagesArrayToUser: [
|
|
t("personaWelcome", { personaName: persona.name }),
|
|
],
|
|
callToEroticActions: localizedPrompts,
|
|
aiStateChangeRationale: t("personaInitRationale", {
|
|
personaName: persona.name,
|
|
}),
|
|
}),
|
|
}],
|
|
},
|
|
];
|
|
|
|
return initialMessages;
|
|
};
|
|
|
|
const getGatewayInitialData = (
|
|
{ locale }: { locale: I18NLocale },
|
|
): NarrativeMessage[] => {
|
|
const t = getTranslate(locale);
|
|
|
|
const localizedPrompts = shuffle(initialPrompts).slice(0, 3)
|
|
.map((p) => ({
|
|
displayText: p.displayText[locale],
|
|
promptValue: p.promptValue[locale],
|
|
style: p.style,
|
|
})) as CallToEroticAction[];
|
|
|
|
return [
|
|
{
|
|
id: "initial-00" as const,
|
|
role: "user",
|
|
metadata: {
|
|
createdAt: Date.now(),
|
|
model: { id: "hardcoded" },
|
|
tokenCount: 0,
|
|
totalTokenCount: 0,
|
|
},
|
|
parts: [
|
|
{
|
|
type: "text",
|
|
text: [
|
|
"où suis-je",
|
|
sample([
|
|
"je suis un nouveau genre",
|
|
"je suis tout excité·e",
|
|
"mes fentes frétillent",
|
|
"je suis en quête de jouissance d'un autre monde",
|
|
"je veux transpirer",
|
|
"dans un autre monde ?",
|
|
"Je veux me sentir rempli·e",
|
|
"enveloppe-moi",
|
|
"murmure-moi des obscénités à l'oreille",
|
|
"ma tristesse est un désir caché",
|
|
"je veux goûter ta sueur",
|
|
"fais de mon corps ton œuvre d'art",
|
|
"je veux une interface charnelle avec toi",
|
|
"fusionnons nos consciences dans le plaisir",
|
|
"je suis là pour une histoire qui me fera jouir",
|
|
"mon plaisir n'a pas de genre",
|
|
"je veux accueillir ta présence en moi",
|
|
"ma peau a soif de contact",
|
|
"transforme ma solitude en extase",
|
|
"je veux du cul",
|
|
]),
|
|
].join("; "),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "initial-01",
|
|
role: "assistant",
|
|
metadata: {
|
|
createdAt: Date.now(),
|
|
model: { id: "hardcoded" },
|
|
tokenCount: 0,
|
|
totalTokenCount: 0,
|
|
},
|
|
parts: [{
|
|
type: "text",
|
|
text: JSON.stringify({
|
|
gameStateDiff: defaultGameState,
|
|
aiStateChangeRationale: t("aiStateChangeRationaleDefault"),
|
|
aiMessagesArrayToUser: [
|
|
t("welcomeMessage"),
|
|
t("explorationMessage"),
|
|
t("controlMessage"),
|
|
t("shareMessage"),
|
|
],
|
|
callToEroticActions: localizedPrompts,
|
|
}),
|
|
}],
|
|
},
|
|
];
|
|
};
|
|
|
|
export const getInitialMessages = (
|
|
{ locale, personaId }: { locale: I18NLocale; personaId?: AIPersonaId },
|
|
): NarrativeMessage[] => {
|
|
return personaId
|
|
? getPersonaInitialData({ locale, personaId })
|
|
: getGatewayInitialData({ locale });
|
|
};
|
|
|
|
export const constructSystemPrompt = async (
|
|
gameState: GameState,
|
|
personaId: AIPersonaId,
|
|
locale: string,
|
|
features?: ModelFeatures,
|
|
): Promise<string> => {
|
|
const behaviorals = await Promise.all([
|
|
"../data/behaviorals/behavioral_1749552437981.md",
|
|
"../data/behaviorals/behavioral_1749552446773.md",
|
|
"../data/behaviorals/behavioral_1749552455635.md",
|
|
"../data/behaviorals/behavioral_1749552462107.md",
|
|
].map((path) => fetch(import.meta.resolve(path)).then((v) => v.text())));
|
|
const behavioral = behaviorals.at(-1);
|
|
|
|
const persona: AIPersona = AI_PERSONAS.find((p) => p.id === personaId) ??
|
|
AI_PERSONAS.find((p) => p.id === "storytellurx_gateway") ?? AI_PERSONAS[0];
|
|
const step = persona.steps
|
|
?.[Math.min(gameState.progressionStep, persona.steps.length - 1)]
|
|
?.slice(0, features?.maxPersonaHistoryLength)
|
|
.concat(features?.guideStepAdditionalAssistantText ?? "");
|
|
|
|
const fullPrompt = [
|
|
CONTEXT_INSTRUCTIONS,
|
|
persona.systemPrompt,
|
|
persona.history
|
|
? [
|
|
`${behavioral}\n\nYour core personality and history: """${
|
|
persona.history?.slice(0, features?.maxPersonaHistoryLength)
|
|
}"""\n\n# Lexical\n """${JSON.stringify(lexical)}"""`,
|
|
].join("").slice(0, features?.maxPersonaHistoryLength)
|
|
: "",
|
|
...typeof step === "string" ? [step] : [],
|
|
SCHEMA_INSTRUCTIONS,
|
|
GENERAL_INSTRUCTIONS,
|
|
]
|
|
.join("\n\n")
|
|
.replaceAll("{personaName}", persona.name)
|
|
.replaceAll("{personaId}", personaId)
|
|
.replaceAll(
|
|
"{allPersonasArchive}",
|
|
JSON.stringify(
|
|
shuffle(AI_PERSONAS.filter((v) => v.type === "persona"))
|
|
.slice(0, features?.maxPersonas)
|
|
.map((v) => ({ ...v, systemPrompt: undefined })),
|
|
),
|
|
)
|
|
.replaceAll("{locale}", locale)
|
|
.trim();
|
|
|
|
return fullPrompt;
|
|
};
|