diff --git a/achievements/achievements.ts b/achievements/achievements.ts index 8cb34fcf..01ff351f 100644 --- a/achievements/achievements.ts +++ b/achievements/achievements.ts @@ -4302,6 +4302,15 @@ const achievements: Achievement[] = [ counter: 'oneiromancy-score-under-0', value: 1, }, + { + id: 'oneiromancy-newyear-analyzed-1', + difficulty: 'medium', + title: '一年の計', + condition: '夢占いBOTに初夢を占われる', + category: 'oneiromancy', + counter: 'oneiromancy-newyear-analyzed', + value: 1, + }, // city-symbol diff --git a/oneiromancy/index.ts b/oneiromancy/index.ts index 14881bc6..07f98ef1 100644 --- a/oneiromancy/index.ts +++ b/oneiromancy/index.ts @@ -1,9 +1,12 @@ import {readFile} from 'fs/promises'; import path from 'path'; +import {ReactionAddedEvent} from '@slack/web-api'; import {Mutex} from 'async-mutex'; import yaml from 'js-yaml'; +// eslint-disable-next-line import/no-named-as-default import OpenAI from 'openai'; import {increment} from '../achievements'; +import dayjs from '../lib/dayjs'; import logger from '../lib/logger'; import openai from '../lib/openai'; import {SlackInterface} from '../lib/slack'; @@ -13,10 +16,24 @@ import {Loader} from '../lib/utils'; const mutex = new Mutex(); const log = logger.child({bot: 'oneiromancy'}); -const promptLoader = new Loader(async () => { - const promptYaml = await readFile(path.join(__dirname, 'prompt.yml')); - const prompt = yaml.load(promptYaml.toString()) as OpenAI.Chat.ChatCompletionMessageParam[]; - return prompt; +const normalPromptIntro = 'ありがとうございます。以下の夢についても同じように、夢の内容を診断して、今日の運勢を100点満点で占ってください。また、今後の生活にどのように活かすべきかのアドバイスを含んだ夢占いをしてください。'; +const newyearPromptIntro = 'ありがとうございます。以下の夢についても同じように、私が1月1日の元日から1週間のうちに見た夢を書き表したものです。日本の「初夢」の習慣にならって、夢の内容をもとに縁起の良さを判定し、今年の運勢を「大吉」「中吉」「小吉」「吉」「半吉」「末吉」「凶」「小凶」「半凶」「末凶」「大凶」のいずれかで占ってください。また、今年1年の間にどのようなことが起きるかの予測を含んだ夢占いをしてください。'; + +interface OneiromancyPrompts { + normal: OpenAI.Chat.ChatCompletionMessageParam[], + newyear: OpenAI.Chat.ChatCompletionMessageParam[], +} + +const promptLoader = new Loader(async () => { + const prompts = await Promise.all(['prompt.yml', 'newyear-prompt.yml'].map(async (filename) => { + const promptYaml = await readFile(path.join(__dirname, filename)); + const prompt = yaml.load(promptYaml.toString()) as OpenAI.Chat.ChatCompletionMessageParam[]; + return prompt; + })); + return { + normal: prompts[0], + newyear: prompts[1], + }; }); interface StateObj { @@ -35,11 +52,13 @@ export default async (slackClients: SlackInterface) => { postedMessages: Object.create(null), }); - eventClient.on('reaction_added', (event) => { + eventClient.on('reaction_added', (event: ReactionAddedEvent) => { if (event.reaction !== 'crystal_ball') { return; } + const now = dayjs(parseFloat(event.item.ts) * 1000).tz('Asia/Tokyo'); + log.info(`reaction_added: ${event.item.channel} ${event.item.ts}`); mutex.runExclusive(async () => { @@ -84,7 +103,11 @@ export default async (slackClients: SlackInterface) => { messageUrl += `?thread_ts=${message.thread_ts}`; } const inputMessage = message.text.replaceAll(/[【】]/g, ''); - const prompt = await promptLoader.load(); + const prompts = await promptLoader.load(); + + const isNewYear = now.month() === 0 && now.date() <= 7; + const promptIntro = isNewYear ? newyearPromptIntro : normalPromptIntro; + const prompt = isNewYear ? prompts.newyear : prompts.normal; await slack.chat.postEphemeral({ channel: event.item.channel, @@ -96,12 +119,12 @@ export default async (slackClients: SlackInterface) => { log.info('Requesting to OpenAI API...'); const completion = await openai.chat.completions.create({ - model: 'gpt-3.5-turbo', + model: 'gpt-4o-mini', messages: [ ...prompt, { role: 'user', - content: `ありがとうございます。以下の夢についても同じように、夢の内容を診断して、今日の運勢を100点満点で占ってください。また、今後の生活にどのように活かすべきかのアドバイスを含んだ夢占いをしてください。\n【${inputMessage}】`, + content: `${promptIntro}\n【${inputMessage}】`, }, ], max_tokens: 1024, @@ -120,12 +143,14 @@ export default async (slackClients: SlackInterface) => { state.threadId = anchorMessage.ts; } + const resultIntro = isNewYear ? '🎌🎍初夢占い🎍🎌\n\n' : ''; + log.info(`threadId: ${threadId}`); const postedMessage = await slack.chat.postMessage({ channel: process.env.CHANNEL_SANDBOX, username: '夢占いBOT', icon_emoji: 'crystal_ball', - text: `${messageUrl}\n\n${result}`, + text: `${messageUrl}\n\n${resultIntro}${result}`, thread_ts: threadId, reply_broadcast: true, unfurl_links: true, @@ -137,6 +162,9 @@ export default async (slackClients: SlackInterface) => { if (event.item.channel === process.env.CHANNEL_SIG_DREAM) { await increment(event.item_user, 'oneiromancy-analyzed'); await increment(event.user, 'oneiromancy-analyze'); + if (isNewYear) { + await increment(event.item_user, 'oneiromancy-newyear-analyzed'); + } const scoreText = result.match(/今日の運勢は【\s*(?[-\d]+)\s*点\s*】/)?.groups?.score; const score = scoreText === undefined ? null : parseInt(scoreText); diff --git a/oneiromancy/newyear-prompt.yml b/oneiromancy/newyear-prompt.yml new file mode 100644 index 00000000..151c650b --- /dev/null +++ b/oneiromancy/newyear-prompt.yml @@ -0,0 +1,37 @@ +- role: user + content: | + 以下の文章は、私が1月1日の元日から1週間のうちに見た夢を書き表したものです。日本の「初夢」の習慣にならって、夢の内容をもとに縁起の良さを判定し、今年の運勢を「大吉」「中吉」「小吉」「吉」「半吉」「末吉」「凶」「小凶」「半凶」「末凶」「大凶」のいずれかで占ってください。また、今年1年の間にどのようなことが起きるかの予測を含んだ夢占いをしてください。 + 【富士山が世界有数の湿地帯になったので、環境を壊さないよう投稿を控える。自転車に踏み荒らされるなどのハプニングもありながらも、湿地はぬかるんだ状態を維持し続けた。朝食用に湿地の泥を炒めていると、フライパンの熱さに耐えられず富士山を湿地にした犯人が尻尾を出す。】 +- role: assistant + content: | + 【今年の運勢】 + 「富士山」は初夢で見ると特に縁起の良いものとして、日本で伝統的に伝えられているものです。また、「フライパン」は「揚げる」が「(運気を)上げる」につながることから縁起の良いものとして見られることが多いです。一方で、「湿地」という要素は、土地を失うという意味の「失地」に通じることから縁起が悪いとされています。 + これらを総合して、今年の運勢は【中吉】と判定します。 + + 【今年の出来事】 + ● 3月下旬: 良縁に恵まれる + この夢には、自然環境や人間の行動に関連した象徴が多く含まれています。このことから、今年の春の訪れを感じられる時期に、今後の人生を共にするような重要な人物との出会いがある可能性が高いです。 + + ● 5月10日前後: 大金を失う + 夢の中の自然環境は、現実世界での金銭的な利益や損得に関連しています。夢の中で湿地が踏み荒らされることによって環境が破壊されたことは、現実世界でもあなたがなにか金銭的な被害を被ることを示唆しているでしょう。 + + ● 11月末日ごろ: 部屋の中が泥まみれになる + 夢の中で富士山が泥まみれになっていることから、現実でもあなたの周辺の環境が泥まみれになることが予測されます。釧路湿原と自宅がワームホールで繋がらないように注意しましょう。 +- role: user + content: | + ありがとうございます。以下の夢についても同じように、私が1月1日の元日から1週間のうちに見た夢を書き表したものです。日本の「初夢」の習慣にならって、夢の内容をもとに縁起の良さを判定し、今年の運勢を「大吉」「中吉」「小吉」「吉」「半吉」「末吉」「凶」「小凶」「半凶」「末凶」「大凶」のいずれかで占ってください。また、今年1年の間にどのようなことが起きるかの予測を含んだ夢占いをしてください。 + 【高校の頃の英語の先生がいて、小テストをするらしい。内容は文章読解か翻訳かそんな感じ。どうやら予習内容があったっぽいが、微塵も勉強していないのでわかるわけがない。悩んでると、いつの間にか手元に別の回の小テストっぽいものがあり、よく見るとちょうど今回の問題文に書いてない方の言語での訳が書いてあった。ちらちら見ながら埋めようとするが、どうにも設問が合わなくて、そこで夢だと気づき、目が覚めた。】 +- role: assistant + content: | + 【今年の運勢】 + 夢の中に「高校の英語の先生」が登場するのは、学びや成長に関するメッセージを象徴しています。ただし、「英語」という教科は日本ではない国を連想させることから一般に縁起が悪いとされています。また、「予習をしていない」「設問が合わない」といった要素は、現実での計画性の欠如や、状況に適応するのが難しい時期があることを示唆しています。これらを総合して、今年の運勢は【大凶】と判定します。 + + 【今年の出来事】 + ● 2月上旬: 知識やスキルを試される場面が訪れる + 夢に登場した「小テスト」は現実での試練や評価を意味します。この時期に仕事や学業で成果を問われる状況がありそうです。準備を怠らず、自己成長のチャンスと捉えることで乗り越えられるでしょう。 + + ● 7月20日ごろ: 思いがけないサポートを得る + 夢の中で「別の回の小テスト」が手元に現れたように、今年の夏頃には他者からの助けや偶然の発見があなたを助ける出来事が起きそうです。ただし、それを最大限活かすには冷静な判断が必要です。 + + ● 12月: おしりにできたでっかいニキビがつぶれる + 夢から目覚めるカタルシスは、ニキビが潰れるときの気持ちに似ています。この出来事は、長い間溜め込んでいたストレスや問題が解消されることを象徴しているでしょう。年末には、心身のデトックスが進み、清々しい気分で新年を迎えることができます。