Skip to content

Instantly share code, notes, and snippets.

@ryoppippi
Last active May 24, 2024 09:28
Show Gist options
  • Save ryoppippi/bf3c6ad2fce408fdcb0c65b706632c80 to your computer and use it in GitHub Desktop.
Save ryoppippi/bf3c6ad2fce408fdcb0c65b706632c80 to your computer and use it in GitHub Desktop.
OpenAI JSON mode with zod schema (Based on this article https://tech.algomatic.jp/entry/2024/05/23/140219)
{
"imports": {
"openai": "npm:openai@^4.46.0",
"zod": "npm:zod@^3.23.8"
}
}
{
"version": "3",
"packages": {
"specifiers": {
"npm:openai@^4.46.0": "npm:openai@4.46.0",
"npm:zod@^3.23.8": "npm:zod@3.23.8"
},
"npm": {
"@types/node-fetch@2.6.11": {
"integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==",
"dependencies": {
"@types/node": "@types/node@18.16.19",
"form-data": "form-data@4.0.0"
}
},
"@types/node@18.16.19": {
"integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==",
"dependencies": {}
},
"@types/node@18.19.33": {
"integrity": "sha512-NR9+KrpSajr2qBVp/Yt5TU/rp+b5Mayi3+OlMlcg2cVCfRmcG5PWZ7S4+MG9PZ5gWBoc9Pd0BKSRViuBCRPu0A==",
"dependencies": {
"undici-types": "undici-types@5.26.5"
}
},
"abort-controller@3.0.0": {
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"dependencies": {
"event-target-shim": "event-target-shim@5.0.1"
}
},
"agentkeepalive@4.5.0": {
"integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==",
"dependencies": {
"humanize-ms": "humanize-ms@1.2.1"
}
},
"asynckit@0.4.0": {
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dependencies": {}
},
"combined-stream@1.0.8": {
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "delayed-stream@1.0.0"
}
},
"delayed-stream@1.0.0": {
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dependencies": {}
},
"event-target-shim@5.0.1": {
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"dependencies": {}
},
"form-data-encoder@1.7.2": {
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
"dependencies": {}
},
"form-data@4.0.0": {
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "asynckit@0.4.0",
"combined-stream": "combined-stream@1.0.8",
"mime-types": "mime-types@2.1.35"
}
},
"formdata-node@4.4.1": {
"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
"dependencies": {
"node-domexception": "node-domexception@1.0.0",
"web-streams-polyfill": "web-streams-polyfill@4.0.0-beta.3"
}
},
"humanize-ms@1.2.1": {
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
"dependencies": {
"ms": "ms@2.1.3"
}
},
"mime-db@1.52.0": {
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dependencies": {}
},
"mime-types@2.1.35": {
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "mime-db@1.52.0"
}
},
"ms@2.1.3": {
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dependencies": {}
},
"node-domexception@1.0.0": {
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"dependencies": {}
},
"node-fetch@2.7.0": {
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dependencies": {
"whatwg-url": "whatwg-url@5.0.0"
}
},
"openai@4.46.0": {
"integrity": "sha512-l0Betzsx3WIjdagqQiH14hWmwYouUzUCcB1ENvKyfG5XOqh6YC2XT7OukzEBTnweutMC91pW2ToddWn8uyD4SA==",
"dependencies": {
"@types/node": "@types/node@18.19.33",
"@types/node-fetch": "@types/node-fetch@2.6.11",
"abort-controller": "abort-controller@3.0.0",
"agentkeepalive": "agentkeepalive@4.5.0",
"form-data-encoder": "form-data-encoder@1.7.2",
"formdata-node": "formdata-node@4.4.1",
"node-fetch": "node-fetch@2.7.0",
"web-streams-polyfill": "web-streams-polyfill@3.3.3"
}
},
"tr46@0.0.3": {
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"dependencies": {}
},
"undici-types@5.26.5": {
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dependencies": {}
},
"web-streams-polyfill@3.3.3": {
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"dependencies": {}
},
"web-streams-polyfill@4.0.0-beta.3": {
"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
"dependencies": {}
},
"webidl-conversions@3.0.1": {
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"dependencies": {}
},
"whatwg-url@5.0.0": {
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "tr46@0.0.3",
"webidl-conversions": "webidl-conversions@3.0.1"
}
},
"zod@3.23.8": {
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"dependencies": {}
}
}
},
"remote": {},
"workspace": {
"dependencies": [
"npm:openai@^4.46.0",
"npm:zod@^3.23.8"
]
}
}
フリーレン
本作の主人公[9]。魔王を討伐した勇者パーティーの魔法使い。長命なエルフ族の出身で、少女のような外見に反して1000年以上の歳月を生き続けている。人間とは時間の感覚が大きく異なるため、数か月から数年単位の作業をまったく苦にせず、ヒンメルらかつての仲間たちとの再会も50年の月日が経ってからのことだった。ヒンメルが天寿を全うして他界したのを機に、自身にとってはわずか10年足らずの旅の中でヒンメルの人となりを詳しく知ろうともしなかったことを深く後悔し、趣味の魔法収集を兼ねて人間を知るための旅を始める。生前時のヒンメルに対する意識は希薄であったが、幻影鬼(アインザーム)との遭遇時や、奇跡のグラオザームに「楽園へと導く魔法(アンシレーシエラ)」を使われた際などは幻想の中でヒンメルを思い描くなど、無自覚に意識しているような描写が散見されている。
1000年以上前、故郷の集落を魔族に襲われ死にかけた際に、自身を救ってくれた大魔法使いフランメの弟子となる。生来の天才的資質に加えて、フランメから教わった戦闘や魔力制御の技術を1000年以上も研鑽し続けた結果、きわめて強大な魔力を得ている。さらに、その魔力をほぼ完全に隠匿する技術[注 1]も習得しており、敵の魔族に自身の実力を過小評価させた隙を突く戦法を得意とする。その実力は魔王亡き後の現在の魔族を弱いと感じ、七崩賢の一角である断頭台のアウラにさえ完勝するほど。魔族側からは、歴史上もっとも多くの同胞を葬り去った存在として「葬送のフリーレン」と呼び恐れられている[注 2]。ただし、自身の魔法を発動する一瞬だけ魔力探知が途切れるという弱点があり[注 3]、自身よりも魔力の低い魔法使いに計11回敗北した経験があるとも語っている[注 4]。
「服が透けて見える魔法」や「かき氷を作る魔法」など、およそ戦闘に役に立たない魔法を収集するのが趣味で、そうした魔導書を対価に仕事を引き受けたりもする。再会したハイターの差し金で人間のフェルンを弟子に取って以降は、自身の旅に同行させている。
性格はドライで厳しい一面もあるが、普段はやさしく面倒見も悪くない。普段は表情に乏しく淡々としており、一般的な富や地位、名声には興味を示さないが、大好きな魔導書を手に入れるために無茶をしたり、食い意地が張っていたり、朝が弱く寝坊がちだったり、自身の貧相な体型を気にしていたり、実年齢で年寄り扱いされるのを嫌うなど、これらの際の感情表現は豊かである。長命なエルフゆえに、人間など短命な他種族の思考・思想には鈍感で、それらの人々とのコミュニケーションはやや不器用。自身の故郷と仲間を奪った魔族に対する憎悪は深く、感情を表に出すことこそないながらも、敵対する魔族に対しては周囲の状況を顧みず問答無用で葬ろうとする。これには、「人間の言葉で人間を欺き人間の言葉が通じない猛獣」という魔族の本質を理解している理由もある。
「歴史上で最もダンジョンを攻略したパーティーの魔法使い」と自称するだけあり、ダンジョンには詳しい。道中で宝箱を発見するとその中身に異常なまでの興味を示し、判別魔法で99パーセントミミック(宝箱に化けた魔物)とみやぶってなお、残り1パーセントの可能性[注 5]に賭けて宝箱を開け、上半身をミミックに噛まれてもがくという場面が何度も描かれている。
----
シュタルク
勇者パーティーの戦士アイゼンの弟子で、師匠と同じく斧使い。17歳→19歳。極端に憶病かつ自己評価が低い性格であるが、実際は巨大な断崖に斧で亀裂を入れるほどの実力者。師匠とけんか別れをしたあと、紅鏡竜の脅威にさらされた村に3年ほど滞在していた。アイゼンの推薦でフリーレンの仲間に指名され、無自覚ながらも紅鏡竜を一撃で倒す能力を発揮し、彼女たちの旅に同行することとなる。中央諸国クレ地方にあった戦士の村出身で、幼少時は魔物とまともに戦えない失敗作だと父親から見下されていたが、兄のシュトルツからは認められ可愛がられていた。
アイゼンから「とんでもない戦士になる」と言わしめるほどの素質の持ち主で、フェルンからは化け物かと疑われるほどの膂力と頑強さをもつ。男性に免疫がないフェルンからは無意識な恐れを抱かれ、自身も女性の扱いが苦手な一方で、互いに憎からぬ感情を抱いており、不機嫌になったフェルンに謝罪したり、デートのように連れ歩いたりするさまから、ザインからは「もう付き合っちゃえよ」などと漏らされている。男性の象徴に対する評価は芳しくなく、「服が透けて見える魔法」で自身の下半身を見たフェルンからは「ちっさ」と漏らされて傷つく場面がある。好物は自身の誕生日にアイゼンがふるまってくれるハンバーグ。
import OpenAI from "openai";
import { outputSchema } from "./schema.ts";
const client = new OpenAI({
apiKey: Deno.env.get("OPENAI_API_KEY"),
});
const text = await Deno.readTextFile("./input.txt");
const rawZodSchema = await Deno.readTextFile("./schema.ts");
const schemaText = rawZodSchema.split("\n")
.filter((line) => line.trim() !== "")
.filter((line) => line.startsWith("import"))
.join("\n");
const chat = await client.chat.completions.create({
stream: false,
response_format: { type: "json_object" },
messages: [
{
role: "system",
content: `
これから与えられるテキストはファンタジー作品のものです
このテキストを読んで、以下のzod schemaに従ってjson形式で返してください
これはzod schemaで書かれています
arrayは配列を表します.
optionalは省略可能なので、もし存在しない場合は省略してください
${schemaText}
`,
},
{
role: "user",
content: text,
},
],
model: "gpt-4o",
});
const res = chat.choices.at(0)?.message.content;
const json = JSON.parse(res ?? "{}");
const parsed = outputSchema.parse(json);
console.log(parsed);
await Deno.writeTextFile(
"./output.json",
JSON.stringify(parsed, null, 2),
);
{
"characters": [
{
"name": "フリーレン",
"age": 1000,
"attributes": [
"長命なエルフ族",
"魔法使い",
"勇者パーティーの一員"
],
"personality": "ドライで厳しい一面もあるが、普段はやさしく面倒見も悪くない",
"stats": {
"strength": 15,
"intelligence": 18,
"dexterity": 16,
"agility": 17,
"luck": 14
},
"background": "魔王を討伐した勇者パーティーの魔法使い。長命なエルフ族の出身で、1000年以上生き続けている。大魔法使いフランメの弟子として育ち、強大な魔力を得ている。人間を知るための旅をしている。",
"magic": "楽園へと導く魔法(アンシレーシエラ)"
},
{
"name": "シュタルク",
"age": 19,
"attributes": [
"勇者パーティーの戦士アイゼンの弟子",
"斧使い",
"膂力と頑強さ"
],
"personality": "極端に憶病かつ自己評価が低いが、実力は高い",
"stats": {
"strength": 18,
"intelligence": 10,
"dexterity": 12,
"agility": 14,
"luck": 13
},
"background": "中央諸国クレ地方の戦士の村出身。青年期にアイゼンの弟子となり、村の脅威を守るために3年間滞在していた。その後フリーレンの仲間として旅を始める。"
}
]
}
import { z } from "zod";
export const outputSchema = z.object({
characters: 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("使える魔法を一つ"),
}),
),
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment