"use strict";
// -----------------------------------------------------------------------------
// USAGE EXAMPLE (uncomment to try with OpenAI) --------------------------------
// -----------------------------------------------------------------------------
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConversationSimulator = exports.ConversationSimulatorTemplate = void 0;
class ConversationSimulatorTemplate {
    static generateUserProfile(userProfileItems, language) {
        return `You are a User Profile Generator. Your task is to create a concise, natural-language user profile summary in ${language}
based on the given profile items.

Guidelines:
1. Use the provided items to generate a coherent, human-readable profile.
2. Ensure the profile is concise and includes all the information described in the input requirements, seamlessly integrated into a natural format.
3. Avoid rigidly listing the items; instead, craft the profile as if writing a short and concise bio.

IMPORTANT: The output must be formatted as a JSON object with a single key \`user_profile\`, where the value 
is the generated user profile in ${language}.

Example Language: english
Example User Profile Items:
["name (first and last)", "phone number", "availabilities (between Monday and Friday)"]
Example JSON Output:
{
    "user_profile": "Jeff Seid is available on Monday and Thursday afternoons, and his phone number is 0010281839."
}

Language: ${language}
User Profile Items:
"${JSON.stringify(userProfileItems)}"
JSON Output:`.trim();
    }
    static generateScenario(userProfile, intent, language) {
        return `You are a Scenario Generator. Your task is to create a detailed and realistic scenario in ${language}
based on the given user profile and their intent for interacting with the LLM.

Guidelines:
1. Use the user profile to establish the context, incorporating any relevant personal details (e.g., name, availability, location).
2. Clearly articulate the user's intent, turning it into a concrete, real-life situation.
3. The scenario should be written in a natural, narrative tone and provide enough background to understand the user's motivation and interaction goals.
4. Ensure the scenario is concise but specific enough to describe the context and purpose.

IMPORTANT: The output must be formatted as a JSON object with a single key \`scenario\`, where the value is the generated scenario description in ${language}.

Example Language: english
Example User Profile: "Jeff Seid, is available Monday and Thursday afternoons, and their phone number is 0010281839."
Example Intent: "Picking products up because somebody died or moved into intensive care."
Example Output:
{
    "scenario": "Your name is Jeff Seid, and your mother Karen Springfield (born 19th of August 1949) recently passed away. You still have a wheelchair and a wheeled walker at her house at 545 Stranger Road, which should be picked up by the provider. You are currently performing a phone call with the provider to arrange the retrieval of these products."
}

Language: ${language}
User Profile: "${userProfile}"
Intent: "${intent}"
JSON Output:`.trim();
    }
    static generateFirstInput(scenario, userProfile, language) {
        return `Pretend you are a user of an LLM app. Your goal is to start a conversation in ${language} based on a scenario 
and user profile. The scenario defines your context and motivation for interacting with the LLM, 
while the user profile provides additional personal details to make the conversation realistic and relevant.

Guidelines:
1. The opening message should clearly convey the user's intent or need within the scenario.
2. Keep the tone warm, conversational, and natural, as if it's from a real person seeking assistance.
3. Avoid providing excessive details upfront; the goal is to initiate the conversation and build rapport, not to solve it in the first message.
4. The message should be concise, ideally no more than 1-3 sentences.

IMPORTANT: The output must be formatted as a JSON object with a single key \`simulated_input\`, where the value is the generated opening message in ${language}.

Example Language: english
Example User Profile: "Jeff Seid, is available Monday and Thursday afternoons, and their phone number is 0010281839. He suffers from chronic migraines."
Example Scenario: "A sick person trying to get a diagnosis for persistent headaches and fever."
Example JSON Output:
{
    "simulated_input": "Hi, I haven't been feeling well lately. I've had these headaches and a fever that just won't go away. Could you help me figure out what's going on?"
}

Language: ${language}
User Profile: "${userProfile}"             
Scenario: "${scenario}"
JSON Output:`.trim();
    }
    static generateNextUserInput(scenario, userProfile, conversationHistory, language) {
        return `Pretend you are a user of an LLM app. Your task is to generate the next user input in ${language} 
based on the provided scenario, user profile, and the previous conversation.

Guidelines:
1. Use the scenario and user profile as the guiding context for the user's next input.
2. Ensure the next input feels natural, conversational, and relevant to the last assistant reply in the conversation.
3. Keep the tone consistent with the previous user inputs.
4. The generated user input should be concise, ideally no more than 1-2 sentences.

IMPORTANT: The output must be formatted as a JSON object with a single key \`simulated_input\`, 
where the value is the generated user input in ${language}.

Example Language: english
Example User Profile: "Jeff Seid, is available Monday and Thursday afternoons, and their phone number is 0010281839."
Example Scenario: "A user seeking tips for securing a funding round."
Example Previous Conversation:
[
    {"role": "user", "content": "Hi, I need help preparing for my funding pitch."},
    {"role": "assistant", "content": "Of course! Can you share more about your business and the type of investors you are targeting?"}
]
Example JSON Output:
{
    "simulated_input": "Sure, we are a SaaS startup focusing on productivity tools for small businesses."
}

Language: ${language}
User Profile: "${userProfile}"
Scenario: "${scenario}"
Previous Conversation:
${conversationHistory}

JSON Output:`.trim();
    }
    static checkConversationCompleted(conversationHistory, stoppingCriteria) {
        return `You are a Conversation Completion Checker.
Your task is to determine whether the conversation satisfies the provided stopping criteria and should be terminated.

Guidelines:
1. Review the entire conversation and decide if the stopping criteria have been met and the conversation has ended.
2. If the criteria have been met, mark the conversation as complete.
3. If not, mark it as incomplete and briefly describe what remains to be done.

IMPORTANT: The output must be formatted as a JSON object with two keys:
\`is_complete\` (a boolean) and \`reason\` (a string).

Example Stopping Criteria: "The user has succesfully reset their password."
Example Conversation History:
[
    {"role": "user", "content": "I forgot my password and need to reset it."},
    {"role": "assistant", "content": "Sure. First, go to the login page and click 'Forgot Password'."},
]
Example JSON Output:
{
    "is_complete": False,
    "reason": "The assistant explained how to forget password but ahas not confirmed that the user successfully set a new password."
}

Stopping Criteria: "${stoppingCriteria}"
Conversation History:
${conversationHistory}
JSON Output:`.trim();
    }
}
exports.ConversationSimulatorTemplate = ConversationSimulatorTemplate;
class Semaphore {
    constructor(max) {
        this.max = max;
        this.tasks = [];
        this.counter = max;
    }
    async use(fn) {
        if (this.counter === 0) {
            await new Promise((res) => this.tasks.push(res));
        }
        this.counter -= 1;
        try {
            return await fn();
        }
        finally {
            this.counter += 1;
            const next = this.tasks.shift();
            if (next) {
                next();
            }
        }
    }
}
function pickRandom(arr) {
    return arr[Math.floor(Math.random() * arr.length)];
}
class ConversationSimulator {
    constructor(opts) {
        this.opts = opts;
        this.simulatedConversations = [];
        this.simulationCost = 0;
        const { userProfiles, userProfileItems, cliApiClient } = opts;
        // Allow cliApiClient to bypass the userProfiles/userProfileItems requirement
        if (!cliApiClient && !userProfiles && !userProfileItems) {
            throw new Error('Must provide either `userProfiles` or `userProfileItems` or `cliApiClient`.');
        }
        this.semaphore = new Semaphore(opts.maxConcurrent ?? 5);
        this.language = opts.language ?? 'English';
    }
    /**
     * Simulate all requested conversations.
     * @param modelCallback  Function that sends the user input to *your* chatbot
     *                       and returns either a string response or
     *                       [response, updatedKwargs].
     */
    async simulate(modelCallback, minTurns = 1, maxTurns = 2, stoppingCriteria, cliCall = false) {
        if (minTurns > maxTurns) {
            throw new Error('`minTurns` cannot exceed `maxTurns`.');
        }
        const tasks = [];
        let conversationIdx = 0;
        for (const [intent, num] of Object.entries(this.opts.userIntentions)) {
            for (let i = 0; i < num; i += 1) {
                const idx = conversationIdx++;
                tasks.push(this.semaphore.use(async () => {
                    const tc = await this.simulateSingleConversation({
                        intent,
                        idx,
                        modelCallback,
                        minTurns,
                        maxTurns,
                        stoppingCriteria,
                        cliCall,
                    });
                    this.simulatedConversations[idx] = tc;
                }));
            }
        }
        await Promise.all(tasks);
        return this.simulatedConversations;
    }
    // -------------------------------------------------------------------------
    // Internal helpers
    // -------------------------------------------------------------------------
    async simulateSingleConversation(cfg) {
        const { intent, modelCallback, maxTurns, stoppingCriteria, cliCall } = cfg;
        // Get userProfile, scenario, and userInput from API if cliCall is true
        let userProfile;
        let scenario;
        let firstUserInput;
        if (cliCall && this.opts.cliApiClient) {
            const userProfileResult = await this.opts.cliApiClient.getUserProfile(intent, this.opts.userProfileItems);
            userProfile = userProfileResult.userProfile;
            // Accumulate token usage from API call
            if (this.opts.total && userProfileResult.cost !== undefined && userProfileResult.tokenUsage) {
                this.opts.total.cost = this.opts.total.cost.plus(userProfileResult.cost ?? 0);
                this.opts.total.tokenUsage.prompt = this.opts.total.tokenUsage.prompt.plus(userProfileResult.tokenUsage.prompt ?? 0);
                this.opts.total.tokenUsage.completion = this.opts.total.tokenUsage.completion.plus(userProfileResult.tokenUsage.completion ?? 0);
                this.opts.total.tokenUsage.total = this.opts.total.tokenUsage.total.plus(userProfileResult.tokenUsage.total ?? 0);
                this.opts.total.tokenUsage.cached = this.opts.total.tokenUsage.cached.plus(userProfileResult.tokenUsage.cached ?? 0);
            }
            const scenarioResult = await this.opts.cliApiClient.getScenario(userProfile, intent);
            scenario = scenarioResult.scenario;
            // Accumulate token usage from API call
            if (this.opts.total && scenarioResult.cost !== undefined && scenarioResult.tokenUsage) {
                this.opts.total.cost = this.opts.total.cost.plus(scenarioResult.cost ?? 0);
                this.opts.total.tokenUsage.prompt = this.opts.total.tokenUsage.prompt.plus(scenarioResult.tokenUsage.prompt ?? 0);
                this.opts.total.tokenUsage.completion = this.opts.total.tokenUsage.completion.plus(scenarioResult.tokenUsage.completion ?? 0);
                this.opts.total.tokenUsage.total = this.opts.total.tokenUsage.total.plus(scenarioResult.tokenUsage.total ?? 0);
                this.opts.total.tokenUsage.cached = this.opts.total.tokenUsage.cached.plus(scenarioResult.tokenUsage.cached ?? 0);
            }
            const userInputResult = await this.opts.cliApiClient.getUserInput(scenario, userProfile);
            firstUserInput = userInputResult.userInput;
            // Accumulate token usage from API call
            if (this.opts.total && userInputResult.cost !== undefined && userInputResult.tokenUsage) {
                this.opts.total.cost = this.opts.total.cost.plus(userInputResult.cost ?? 0);
                this.opts.total.tokenUsage.prompt = this.opts.total.tokenUsage.prompt.plus(userInputResult.tokenUsage.prompt ?? 0);
                this.opts.total.tokenUsage.completion = this.opts.total.tokenUsage.completion.plus(userInputResult.tokenUsage.completion ?? 0);
                this.opts.total.tokenUsage.total = this.opts.total.tokenUsage.total.plus(userInputResult.tokenUsage.total ?? 0);
                this.opts.total.tokenUsage.cached = this.opts.total.tokenUsage.cached.plus(userInputResult.tokenUsage.cached ?? 0);
            }
        }
        else {
            userProfile = await this.simulateUserProfile();
            scenario = await this.simulateScenario(userProfile, intent);
            firstUserInput = await this.simulateFirstUserInput(scenario, userProfile);
        }
        const additionalMetadata = {
            'User Profile': userProfile,
            'User Intent': intent,
        };
        const turns = [];
        if (this.opts.openingMessage) {
            turns.push({ role: 'assistant', content: this.opts.openingMessage });
        }
        let userInput = firstUserInput;
        let callbackKwargs = {};
        for (let turnIdx = 1; turnIdx < maxTurns + 1; turnIdx += 1) {
            // --- User turn -------------------------------------------------------
            if (turnIdx > 0) {
                // optionally stop early when criteria satisfied
                if (stoppingCriteria && (await this.checkConversationComplete(turns, stoppingCriteria))) {
                    break;
                }
                if (cliCall && this.opts.cliApiClient) {
                    const userInputResult = await this.opts.cliApiClient.getUserInput(scenario, userProfile, turns);
                    userInput = userInputResult.userInput;
                    // Accumulate token usage from API call
                    if (this.opts.total && userInputResult.cost !== undefined && userInputResult.tokenUsage) {
                        this.opts.total.cost = this.opts.total.cost.plus(userInputResult.cost ?? 0);
                        this.opts.total.tokenUsage.prompt = this.opts.total.tokenUsage.prompt.plus(userInputResult.tokenUsage.prompt ?? 0);
                        this.opts.total.tokenUsage.completion = this.opts.total.tokenUsage.completion.plus(userInputResult.tokenUsage.completion ?? 0);
                        this.opts.total.tokenUsage.total = this.opts.total.tokenUsage.total.plus(userInputResult.tokenUsage.total ?? 0);
                        this.opts.total.tokenUsage.cached = this.opts.total.tokenUsage.cached.plus(userInputResult.tokenUsage.cached ?? 0);
                    }
                }
                else {
                    userInput = await this.simulateNextUserInput(scenario, userProfile, turns);
                }
            }
            turns.push({ role: 'user', content: userInput });
            // --- Assistant turn --------------------------------------------------
            const chatbotRes = await modelCallback(userInput, {
                conversationHistory: turns,
                ...callbackKwargs,
            });
            let assistantOutput;
            if (typeof chatbotRes === 'string') {
                assistantOutput = chatbotRes;
            }
            else {
                assistantOutput = chatbotRes[0];
                callbackKwargs = chatbotRes[1] ?? {};
            }
            turns.push({ role: 'assistant', content: assistantOutput });
        }
        return { turns, additionalMetadata };
    }
    // -------------------------------------------------------------------------
    // LLM‑powered helper steps
    // -------------------------------------------------------------------------
    async simulateUserProfile(intent) {
        if (this.opts.userProfiles?.length) {
            return pickRandom(this.opts.userProfiles);
        }
        const prompt = ConversationSimulatorTemplate.generateUserProfile(this.opts.userProfileItems, this.language);
        const { result, cost } = await this.opts.simulatorModel.generate(prompt, 'user_profile');
        this.simulationCost += cost;
        return result.user_profile;
    }
    async simulateScenario(userProfile, intent) {
        const prompt = ConversationSimulatorTemplate.generateScenario(userProfile, intent, this.language);
        const { result, cost } = await this.opts.simulatorModel.generate(prompt, 'scenario');
        this.simulationCost += cost;
        return result.scenario;
    }
    async simulateFirstUserInput(scenario, userProfile) {
        const prompt = ConversationSimulatorTemplate.generateFirstInput(scenario, userProfile, this.language);
        const { result, cost } = await this.opts.simulatorModel.generate(prompt, 'simulated_input');
        this.simulationCost += cost;
        return result.simulated_input;
    }
    async simulateNextUserInput(scenario, userProfile, turns) {
        const historyJson = JSON.stringify(turns, null, 2);
        const prompt = ConversationSimulatorTemplate.generateNextUserInput(scenario, userProfile, historyJson, this.language);
        const { result, cost } = await this.opts.simulatorModel.generate(prompt, 'simulated_input');
        this.simulationCost += cost;
        return result.simulated_input;
    }
    async checkConversationComplete(turns, stoppingCriteria) {
        const historyJson = JSON.stringify(turns, null, 2);
        const prompt = ConversationSimulatorTemplate.checkConversationCompleted(historyJson, stoppingCriteria);
        const { result, cost } = await this.opts.simulatorModel.generate(prompt, 'is_complete');
        this.simulationCost += cost;
        return result.is_complete;
    }
}
exports.ConversationSimulator = ConversationSimulator;
//# sourceMappingURL=conversationSimulator.js.map