Skip to content

Instantly share code, notes, and snippets.

@erukiti
Created May 23, 2024 02:55
Show Gist options
  • Save erukiti/8a9ebab033f8e6a5bd83dc8f95ceddb8 to your computer and use it in GitHub Desktop.
Save erukiti/8a9ebab033f8e6a5bd83dc8f95ceddb8 to your computer and use it in GitHub Desktop.

Revisions

  1. erukiti created this gist May 23, 2024.
    250 changes: 250 additions & 0 deletions index.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,250 @@
    import { z } from "zod";
    import { format } from "prettier";
    import { js2xml } from "xml-js";

    /**
    * 文字列に含まれたJSONをすべて抽出する
    */
    export const extractJSON = (text: string): string[] => {
    // アルゴリズム:
    // 1. まず最初の `{` を探す
    // 2. `}` を探して JSON.parse が通るか試す。通るならresultに追加
    // 3. JSON.parseが通らない場合、次の `}` を探す。通るならresultに追加
    // 4. 繰り返してだめだったら、次の `{` を探す
    // 5. `{` がなくなれば終了

    const result: string[] = [];
    let start = text.indexOf("{");
    while (start !== -1) {
    let end = text.indexOf("}", start);
    if (end === -1) {
    break;
    }

    while (end !== -1) {
    try {
    const json = text.slice(start, end + 1);
    JSON.parse(json);
    result.push(json);
    break;
    } catch (e) {
    end = text.indexOf("}", end + 1);
    }
    }
    start = text.indexOf("{", start + 1);
    }

    return result;
    };

    const outputSchema = z.array(
    z.object({
    name: z.string().describe("キャラクターの名前"),
    age: z.number().describe("キャラクターの年齢"),
    attributes: z.array(z.string()).describe("キャラクターの属性"),
    personality: z.string().describe("キャラクターの性格"),
    stats: z
    .object({
    strength: z.number().describe("筋力"),
    intelligence: z.number().describe("知力"),
    dexterity: z.number().describe("器用さ"),
    agility: z.number().describe("素早さ"),
    luck: z.number().describe("運"),
    })
    .describe("キャラクターの能力値を3-18で"),
    background: z.string().describe("生い立ち"),
    magic: z.string().optional().describe("使える魔法を一つ"),
    })
    );

    type SchemaPrimitive = {
    type: "string" | "number" | "boolean";
    optional?: boolean;
    array?: boolean;
    description: string;
    children?: never;
    };

    type SchemaArray = {
    type: "array";
    optional?: boolean;
    description: string;
    children: SchemaDef;
    };

    type SchemaObject = {
    type: "object";
    optional?: boolean;
    description: string;
    children: Record<string, SchemaDef>;
    };

    type SchemaUnion = {
    type: "union";
    optional?: boolean;
    description: string;
    children: SchemaDef[];
    };

    type SchemaLiteral = {
    type: "literal";
    value: string | number | boolean;
    description: string;
    optional?: boolean;
    };

    export type SchemaDef =
    | SchemaPrimitive
    | SchemaArray
    | SchemaObject
    | SchemaUnion
    | SchemaLiteral;

    export const getTypeFromSchema = (schema: z.ZodSchema): SchemaDef => {
    if (schema instanceof z.ZodString) {
    return {
    type: "string",
    description: schema._def.description || "",
    };
    }
    if (schema instanceof z.ZodNumber) {
    return {
    type: "number",
    description: schema._def.description || "",
    };
    }

    if (schema instanceof z.ZodOptional) {
    const innerType = getTypeFromSchema(schema._def.innerType);
    return {
    ...innerType,
    optional: true,
    description: schema._def.description || innerType.description,
    };
    }

    if (schema instanceof z.ZodArray) {
    const children = getTypeFromSchema(schema._def.type);
    return {
    type: "array",
    description: schema._def.description || "",
    children,
    };
    }

    if (schema instanceof z.ZodObject) {
    const children: Record<string, SchemaDef> = {};
    for (const key in schema.shape) {
    children[key] = getTypeFromSchema(schema.shape[key]);
    }
    return {
    type: "object",
    description: schema._def.description || "",
    children,
    };
    }

    if (schema instanceof z.ZodUnion) {
    // biome-ignore lint/suspicious/noExplicitAny: <explanation>
    const children = schema._def.options.map((option: any) =>
    getTypeFromSchema(option)
    );
    return {
    type: "union",
    description: schema._def.description || "",
    children,
    };
    }

    if (schema instanceof z.ZodLiteral) {
    return {
    type: "literal",
    value: schema._def.value,
    description: schema._def.description || "",
    };
    }

    throw new Error(`Unsupported schema type ${schema.constructor.name}`);
    };

    export const getTypeScriptDefintion = (schema: SchemaDef): string => {
    if (schema.type === "string") {
    return "string";
    }
    if (schema.type === "number") {
    return "number";
    }
    if (schema.type === "boolean") {
    return "boolean";
    }
    if (schema.type === "array") {
    return `Array<${getTypeScriptDefintion(schema.children)}>`;
    }
    if (schema.type === "object") {
    const children = Object.entries(schema.children)
    .map(([key, child]) => {
    const description = child.description
    ? `/** ${child.description} */\n`
    : "";
    const optional = child.optional ? "?" : "";
    return `${description}${key}${optional}: ${getTypeScriptDefintion(
    child
    )};`;
    })
    .join("\n");
    return `{\n${children}\n}`;
    }

    throw new Error("Unsupported schema type");
    };

    export const createPrompt = async (
    rule: string,
    outputSchema: z.ZodSchema,
    data: unknown
    ): Promise<string> => {
    const xml = js2xml(
    {
    prompt: {
    rule,
    format: `type Output = ${await format(
    getTypeScriptDefintion(getTypeFromSchema(outputSchema)),
    { parser: "babel-ts" }
    )}`,
    data,
    },
    },
    { compact: true, spaces: 2 }
    );

    const prompt = `必ずpromptに書かれたruleにそってdataを処理してください。
    結果はformatに従ってJSONを出力してください。
    formatはTypeScriptの型定義ですが、必ずJSONを出力してください。
    ${xml}
    `;
    return prompt;
    };

    const data = `フリーレン
    本作の主人公[9]。魔王を討伐した勇者パーティーの魔法使い。長命なエルフ族の出身で、少女のような外見に反して1000年以上の歳月を生き続けている。人間とは時間の感覚が大きく異なるため、数か月から数年単位の作業をまったく苦にせず、ヒンメルらかつての仲間たちとの再会も50年の月日が経ってからのことだった。ヒンメルが天寿を全うして他界したのを機に、自身にとってはわずか10年足らずの旅の中でヒンメルの人となりを詳しく知ろうともしなかったことを深く後悔し、趣味の魔法収集を兼ねて人間を知るための旅を始める。生前時のヒンメルに対する意識は希薄であったが、幻影鬼(アインザーム)との遭遇時や、奇跡のグラオザームに「楽園へと導く魔法(アンシレーシエラ)」を使われた際などは幻想の中でヒンメルを思い描くなど、無自覚に意識しているような描写が散見されている。
    1000年以上前、故郷の集落を魔族に襲われ死にかけた際に、自身を救ってくれた大魔法使いフランメの弟子となる。生来の天才的資質に加えて、フランメから教わった戦闘や魔力制御の技術を1000年以上も研鑽し続けた結果、きわめて強大な魔力を得ている。さらに、その魔力をほぼ完全に隠匿する技術[注 1]も習得しており、敵の魔族に自身の実力を過小評価させた隙を突く戦法を得意とする。その実力は魔王亡き後の現在の魔族を弱いと感じ、七崩賢の一角である断頭台のアウラにさえ完勝するほど。魔族側からは、歴史上もっとも多くの同胞を葬り去った存在として「葬送のフリーレン」と呼び恐れられている[注 2]。ただし、自身の魔法を発動する一瞬だけ魔力探知が途切れるという弱点があり[注 3]、自身よりも魔力の低い魔法使いに計11回敗北した経験があるとも語っている[注 4]。
    「服が透けて見える魔法」や「かき氷を作る魔法」など、およそ戦闘に役に立たない魔法を収集するのが趣味で、そうした魔導書を対価に仕事を引き受けたりもする。再会したハイターの差し金で人間のフェルンを弟子に取って以降は、自身の旅に同行させている。
    性格はドライで厳しい一面もあるが、普段はやさしく面倒見も悪くない。普段は表情に乏しく淡々としており、一般的な富や地位、名声には興味を示さないが、大好きな魔導書を手に入れるために無茶をしたり、食い意地が張っていたり、朝が弱く寝坊がちだったり、自身の貧相な体型を気にしていたり、実年齢で年寄り扱いされるのを嫌うなど、これらの際の感情表現は豊かである。長命なエルフゆえに、人間など短命な他種族の思考・思想には鈍感で、それらの人々とのコミュニケーションはやや不器用。自身の故郷と仲間を奪った魔族に対する憎悪は深く、感情を表に出すことこそないながらも、敵対する魔族に対しては周囲の状況を顧みず問答無用で葬ろうとする。これには、「人間の言葉で人間を欺き人間の言葉が通じない猛獣」という魔族の本質を理解している理由もある。
    「歴史上で最もダンジョンを攻略したパーティーの魔法使い」と自称するだけあり、ダンジョンには詳しい。道中で宝箱を発見するとその中身に異常なまでの興味を示し、判別魔法で99パーセントミミック(宝箱に化けた魔物)とみやぶってなお、残り1パーセントの可能性[注 5]に賭けて宝箱を開け、上半身をミミックに噛まれてもがくという場面が何度も描かれている。
    ----
    シュタルク
    勇者パーティーの戦士アイゼンの弟子で、師匠と同じく斧使い。17歳→19歳。極端に憶病かつ自己評価が低い性格であるが、実際は巨大な断崖に斧で亀裂を入れるほどの実力者。師匠とけんか別れをしたあと、紅鏡竜の脅威にさらされた村に3年ほど滞在していた。アイゼンの推薦でフリーレンの仲間に指名され、無自覚ながらも紅鏡竜を一撃で倒す能力を発揮し、彼女たちの旅に同行することとなる。中央諸国クレ地方にあった戦士の村出身で、幼少時は魔物とまともに戦えない失敗作だと父親から見下されていたが、兄のシュトルツからは認められ可愛がられていた。
    アイゼンから「とんでもない戦士になる」と言わしめるほどの素質の持ち主で、フェルンからは化け物かと疑われるほどの膂力と頑強さをもつ。男性に免疫がないフェルンからは無意識な恐れを抱かれ、自身も女性の扱いが苦手な一方で、互いに憎からぬ感情を抱いており、不機嫌になったフェルンに謝罪したり、デートのように連れ歩いたりするさまから、ザインからは「もう付き合っちゃえよ」などと漏らされている。男性の象徴に対する評価は芳しくなく、「服が透けて見える魔法」で自身の下半身を見たフェルンからは「ちっさ」と漏らされて傷つく場面がある。好物は自身の誕生日にアイゼンがふるまってくれるハンバーグ。`;

    const prompt = await createPrompt(
    "dataのテキストをformatに沿ってJSONで出力せよ",
    outputSchema,
    data
    );

    console.log(prompt);