"use strict";
// -*- mode: typescript; indent-tabs-mode: nil; js-basic-offset: 4 -*-
//
// This file is part of Genie
//
// Copyright 2021 The Board of Trustees of the Leland Stanford Junior University
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: Giovanni Campagna <gcampagn@cs.stanford.edu>
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.execute = exports.initArgparse = void 0;
const fs = __importStar(require("fs"));
const readline = __importStar(require("readline"));
const events = __importStar(require("events"));
const seedrandom_1 = __importDefault(require("seedrandom"));
const ThingTalk = __importStar(require("thingtalk"));
const string_interp_1 = __importDefault(require("string-interp"));
const ThingTalkUtils = __importStar(require("../lib/utils/thingtalk"));
const StreamUtils = __importStar(require("../lib/utils/stream-utils"));
const random_1 = require("../lib/utils/random");
const ParserClient = __importStar(require("../lib/prediction/parserclient"));
const parsers_1 = require("../lib/dataset-tools/parsers");
const execution_dialogue_agent_1 = __importDefault(require("../lib/dialogue-agent/execution_dialogue_agent"));
const dialogue_policy_1 = __importDefault(require("../lib/dialogue-agent/dialogue_policy"));
const value_category_1 = __importDefault(require("../lib/dialogue-agent/value-category"));
const engine_1 = __importDefault(require("../lib/engine"));
const multi_json_database_1 = __importDefault(require("./lib/multi_json_database"));
const cmdline_platform_1 = __importDefault(require("./lib/cmdline-platform"));
class Annotator extends events.EventEmitter {
    constructor(rl, options) {
        super();
        this._rl = rl;
        this._locale = options.locale;
        this._rng = seedrandom_1.default.alea(options.random_seed);
        this._parser = ParserClient.get(options.nlu_server, options.locale);
        this._platform = new cmdline_platform_1.default(undefined, options.locale, options.thingpedia_url);
        this._tpClient = this._platform.getCapability('thingpedia-client');
        if (options.execution_mode === 'simulation') {
            this._schemas = new ThingTalk.SchemaRetriever(this._tpClient, null, true);
            const simulatorOptions = {
                rng: seedrandom_1.default.alea('almond is awesome'),
                locale: options.locale,
                timezone: options.timezone,
                thingpediaClient: this._tpClient,
                schemaRetriever: this._schemas,
                interactive: true
            };
            if (options.database_file) {
                this._simulatorDatabase = new multi_json_database_1.default(options.database_file);
                simulatorOptions.database = this._simulatorDatabase;
            }
            this._executor = ThingTalkUtils.createSimulator(simulatorOptions);
        }
        else {
            this._engine = new engine_1.default(this._platform);
            this._executor = new execution_dialogue_agent_1.default(this._engine, this, false);
            this._schemas = this._engine.schemas;
        }
        this._dialoguePolicy = new dialogue_policy_1.default({
            thingpedia: this._tpClient,
            schemas: this._schemas,
            locale: this._locale,
            timezone: options.timezone,
            rng: this._rng,
            debug: false
        });
        this._state = 'loading';
        this._serial = 0;
        this._outputDialogue = [];
        this._currentTurnIdx = 0;
        this._outputTurn = {
            context: 'null',
            agent: '',
            agent_target: '',
            intermediate_context: '',
            user: '',
            user_target: '',
        };
        this._context = null;
        this._executorState = undefined;
        this._preprocessed = undefined;
        this._entities = {};
        this._candidates = undefined;
        rl.on('line', async (line) => {
            if (this._state === 'done')
                return;
            line = line.trim();
            if (line.length === 0 || this._state === 'loading') {
                rl.prompt();
                return;
            }
            if (line === 'h' || line === '?') {
                this._help();
                return;
            }
            if (line === 'q') {
                this._quit();
                return;
            }
            if (line === 'd') {
                this.next();
                return;
            }
            if (this._state === 'input') {
                this._handleUtterance(line).catch((e) => this.emit('error', e));
                return;
            }
            if (this._state === 'code') {
                this._learnThingTalk(line).catch((e) => this.emit('error', e));
                return;
            }
            if (Number.isFinite(parseInt(line))) {
                this._learnNumber(parseInt(line));
            }
            else if (line === 'n') {
                this._more();
            }
            else if (line === 'e') {
                this._edit(undefined);
            }
            else if (line.startsWith('e ')) {
                this._edit(parseInt(line.substring(2).trim()));
            }
            else if (line === 't') {
                this._state = 'code';
                rl.setPrompt('TT: ');
                rl.prompt();
            }
            else {
                //console.log('Invalid command');
                //rl.prompt();
                this._learnThingTalk(line).catch((e) => this.emit('error', e));
            }
        });
    }
    // implementation of the abstract dialogue loop interface, which the
    // execution dialogue agent calls sometimes
    get _() {
        return (x) => x;
    }
    get icon() {
        return null;
    }
    set icon(v) {
        // do nothing
    }
    get isAnonymous() {
        return false;
    }
    get platformData() {
        return {};
    }
    interpolate(msg, args) {
        return string_interp_1.default(msg, args, {
            locale: this._locale,
            timezone: this._platform.timezone
        }) || '';
    }
    async reply(msg) {
        console.log('A: ' + msg);
    }
    async replyLink(title, link) {
        console.log('A: ' + title + ' ' + link);
    }
    async replyInterp(msg, args) {
        if (args === undefined)
            return this.reply(msg);
        else
            return this.reply(this.interpolate(msg, args));
    }
    async ask(cat, question) {
        if (cat === value_category_1.default.Location)
            return new ThingTalk.Ast.LocationValue(new ThingTalk.Ast.AbsoluteLocation(37.4299908, -122.175519, "Gates Computer Science"));
        if (cat === value_category_1.default.Time)
            return new ThingTalk.Ast.TimeValue(new ThingTalk.Ast.AbsoluteTime(random_1.randint(9, 12, this._rng), 0, 0));
        throw new TypeError(`Unexpected question of type ${cat}`);
    }
    async askChoices(question, choices) {
        return random_1.randint(0, choices.length - 1, this._rng);
    }
    _quit() {
        if (this._outputTurn.user_target)
            this._outputDialogue.push(this._outputTurn);
        if (this._outputDialogue.length > 0) {
            this.emit('learned', {
                id: String(this._serial),
                turns: this._outputDialogue,
            });
        }
        this.emit('quit');
    }
    _help() {
        console.log('Available commands:');
        console.log('q: quit');
        console.log('d: (done) complete the current dialog and start the next one');
        console.log('<0-9>: make a choice');
        console.log('n: (next) show more choices');
        console.log('e <0-9>: edit a choice');
        console.log('t: (thingtalk) write code directly');
        console.log('? or h: this help');
    }
    async start() {
        if (this._simulatorDatabase)
            await this._simulatorDatabase.load();
        if (this._engine)
            await this._engine.open();
        await this._parser.start();
    }
    async stop() {
        await this._parser.stop();
        if (this._engine)
            await this._engine.stop();
    }
    async _learnThingTalk(code) {
        let newState;
        try {
            const program = await ThingTalkUtils.parse(code, this._schemas);
            newState = await this._inputToDialogueState(program);
            if (!newState) {
                console.log(`Not a valid dialogue state`);
                this._rl.setPrompt('TT: ');
                this._rl.prompt();
                return;
            }
            // check that the entities are correct by serializing the program once
            ThingTalkUtils.serializePrediction(newState, this._preprocessed, this._entities, {
                locale: this._locale
            }).join(' ');
        }
        catch (e) {
            console.log(`${e.name}: ${e.message}`);
            this._rl.setPrompt('TT: ');
            this._rl.prompt();
            return;
        }
        const oldContext = this._context;
        this._context = ThingTalkUtils.computeNewState(this._context, newState, 'user');
        const prediction = ThingTalkUtils.computePrediction(oldContext, this._context, 'user');
        this._outputTurn.user_target = prediction.prettyprint();
        this._nextTurn();
    }
    _edit(i) {
        let program;
        if (i === undefined) {
            program = this._context;
        }
        else {
            if (Number.isNaN(i) || i < 1 || i > this._candidates.length) {
                console.log('Invalid number');
                this._rl.setPrompt('$ ');
                this._rl.prompt();
                return;
            }
            i -= 1;
            program = this._candidates[i];
        }
        this._state = 'code';
        this._rl.setPrompt('TT: ');
        this._rl.write(program.prettyprint().replace(/\n/g, ' '));
        this._rl.prompt();
    }
    _learnNumber(i) {
        if (i < 1 || i > this._candidates.length) {
            console.log('Invalid number');
            this._rl.setPrompt('$ ');
            this._rl.prompt();
            return;
        }
        i -= 1;
        const program = this._candidates[i];
        const oldContext = this._context;
        this._context = ThingTalkUtils.computeNewState(this._context, program, 'user');
        const prediction = ThingTalkUtils.computePrediction(oldContext, this._context, 'user');
        this._outputTurn.user_target = prediction.prettyprint();
        this._nextTurn();
    }
    _more() {
        if (this._state === 'top3') {
            this._state = 'full';
            const candidates = this._candidates;
            for (let i = 0; i < candidates.length; i++)
                console.log(`${i + 1}) ${candidates[i].prettyprint()}`);
            this._rl.setPrompt('$ ');
            this._rl.prompt();
        }
        else {
            this._state = 'code';
            this._rl.setPrompt('TT: ');
            this._rl.prompt();
        }
    }
    next() {
        if (this._outputTurn.user_target)
            this._outputDialogue.push(this._outputTurn);
        if (this._outputDialogue.length > 0) {
            this.emit('learned', {
                id: String(this._serial),
                turns: this._outputDialogue,
            });
        }
        this._serial++;
        console.log(`Dialog #${this._serial}`);
        this._outputDialogue = [];
        this._context = null;
        this._executorState = undefined;
        this._currentTurnIdx = -1;
        this._nextTurn();
    }
    async _nextTurn() {
        if (this._outputTurn.user_target)
            this._outputDialogue.push(this._outputTurn);
        this._currentTurnIdx++;
        this._outputTurn = {
            context: 'null',
            agent: '',
            agent_target: '',
            intermediate_context: '',
            user: '',
            user_target: '',
        };
        if (this._currentTurnIdx > 0) {
            try {
                const { newDialogueState, newExecutorState } = await this._executor.execute(this._context, this._executorState);
                this._context = newDialogueState;
                this._executorState = newExecutorState;
            }
            catch (e) {
                if (e.code === 'ECANCELLED') {
                    this.next();
                    return;
                }
                else {
                    throw e;
                }
            }
            console.log();
            const contextCode = this._context.prettyprint();
            for (const line of contextCode.trim().split('\n'))
                console.log('C: ' + line);
            this._outputTurn.context = contextCode;
            // run the agent
            const policyResult = await this._dialoguePolicy.chooseAction(this._context);
            if (!policyResult) {
                console.log('Dialogue policy error: no reply at this state');
                this.next();
                return;
            }
            const [dialogueStateAfterAgent, , utterance] = policyResult;
            console.log('A: ' + utterance);
            const prediction = ThingTalkUtils.computePrediction(this._context, dialogueStateAfterAgent, 'agent');
            this._outputTurn.agent = utterance;
            this._outputTurn.agent_target = prediction.prettyprint();
            this._context = dialogueStateAfterAgent;
        }
        this._state = 'input';
        this._rl.setPrompt('U: ');
        this._rl.prompt();
    }
    async _inputToDialogueState(input) {
        return ThingTalkUtils.inputToDialogueState(this._dialoguePolicy, this._context, input);
    }
    async _handleUtterance(utterance) {
        this._outputTurn.user = utterance;
        this._state = 'loading';
        let contextCode, contextEntities;
        if (this._context !== null) {
            const context = ThingTalkUtils.prepareContextForPrediction(this._context, 'user');
            [contextCode, contextEntities] = ThingTalkUtils.serializeNormalized(context);
        }
        else {
            contextCode = ['null'];
            contextEntities = {};
        }
        const parsed = await this._parser.sendUtterance(utterance, contextCode, contextEntities, {
            tokenized: false,
            skip_typechecking: true
        });
        this._state = 'top3';
        this._preprocessed = parsed.tokens.join(' ');
        this._entities = parsed.entities;
        const candidates = await ThingTalkUtils.parseAllPredictions(parsed.candidates, parsed.entities, {
            thingpediaClient: this._tpClient,
            schemaRetriever: this._schemas,
            loadMetadata: true
        });
        this._candidates = (await Promise.all(candidates.map((c) => this._inputToDialogueState(c)))).filter((c) => c !== null);
        if (this._candidates.length > 0) {
            for (let i = 0; i < 3 && i < this._candidates.length; i++)
                console.log(`${i + 1}) ${this._candidates[i].prettyprint()}`);
        }
        else {
            console.log(`No candidates for this command`);
        }
        this._rl.setPrompt('$ ');
        this._rl.prompt();
    }
}
const THINGPEDIA_URL = 'https://dev.almond.stanford.edu/thingpedia';
const NL_SERVER_URL = 'https://nlp-staging.almond.stanford.edu';
function initArgparse(subparsers) {
    const parser = subparsers.add_parser('interactive-annotate', {
        add_help: true,
        description: `Interactively annotate a dialog dataset, by annotating each user sentence and running the real agent.`
    });
    parser.add_argument('-o', '--output', {
        required: true,
    });
    parser.add_argument('-l', '--locale', {
        required: false,
        default: 'en-US',
        help: `BGP 47 locale tag of the natural language being processed (defaults to en-US).`
    });
    parser.add_argument('--timezone', {
        required: false,
        default: undefined,
        help: `Timezone to use to print dates and times (defaults to the current timezone).`
    });
    parser.add_argument('--thingpedia-url', {
        required: false,
        default: THINGPEDIA_URL,
        help: 'URL of Thingpedia to use, or local path pointing to a thingpedia.tt file.'
    });
    parser.add_argument('--thingpedia-dir', {
        required: false,
        nargs: '+',
        help: 'Path to a directory containing Thingpedia device definitions (overrides --thingpedia-url).'
    });
    parser.add_argument('--database-file', {
        required: false,
        help: `Path to a file pointing to JSON databases used to simulate queries.`,
    });
    parser.add_argument('--nlu-server', {
        required: false,
        default: NL_SERVER_URL,
        help: `The URL of the natural language server to parse user utterances. Use a file:// URL pointing to a model directory to use a local instance of genienlp.`
    });
    parser.add_argument('--execution-mode', {
        required: false,
        default: 'real',
        choices: ['simulation', 'real'],
        help: `Whether to simulate API calls or execute them for real.`
    });
    parser.add_argument('--append', {
        action: 'store_true',
        help: 'Append to the output file instead of overwriting',
        default: false,
    });
    parser.add_argument('--no-append', {
        action: 'store_false',
        dest: 'append',
        help: 'Overwrite the output file instead of appending',
    });
}
exports.initArgparse = initArgparse;
async function execute(args) {
    const learned = new parsers_1.DialogueSerializer({ annotations: true });
    const output = fs.createWriteStream(args.output, { flags: (args.append ? 'a' : 'w') });
    learned.pipe(output);
    const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
    rl.setPrompt('$ ');
    function quit() {
        learned.end();
        rl.close();
        //process.exit();
    }
    const annotator = new Annotator(rl, args);
    await annotator.start();
    annotator.on('end', quit);
    annotator.on('learned', (dlg) => {
        learned.write(dlg);
    });
    annotator.on('quit', quit);
    rl.on('SIGINT', quit);
    annotator.next();
    //process.stdin.on('end', quit);
    await StreamUtils.waitFinish(output);
    await annotator.stop();
}
exports.execute = execute;
//# sourceMappingURL=interactive-annotate.js.map