Amazon BedrockとAmazon SageMakerを利用して自分占いを作ってみた

はじめに

個人的に、私はMTGが長かったり天気が悪いと、当日・翌日元気がなくなることが経験的にわかっています。

ただ、完全にルール化できるものではなく、なんとなくMTGの長さをチェックしたり、天気予報を見たりすることで随時元気度を自己管理していました。

この管理を安定させるため、AIを使ってシステム化したいと思い、いわゆる「AI」と言われるシステムを組み合わせて、元気度の予測機を作ってみることにしました

前提

  • 元気度の算出はMLで行う
    • あとから「元気度」推定ロジックの精度を追及したいため、LLMではなくMLを選択
  • 最終回答はLLMで行う
    • 「元気度」だけが出てもアクションが取りづらいので、自分特有のルールと、最近起きたトピックを考慮した回答が返せるように、LLMを利用した回答とします
    • モデルは中庸なTitanを利用します
    • 最終回答の調整ができるように、自分特有のルール=自分マニュアルや最新のトピックを頻繁に外部から更新で可能とします。そのためにRAG構成とします

やってみたこと

全体構成の検討

事前準備(S3セットアップ等)

CLI用IAMユーザの作成

KeyValueNote
ユーザ名fortune-ai-admin任意名で構いません。
ポリシーAdministratorAccess

データ格納用S3バケット作成

KeyValueNote
S3バケット名katoaki-fortune-ai-us-20251103任意名で構いません。
リージョンus-east-1今回はus-east-1を利用します。

データ作成

diary.csv

date	weather	weather_score	mtg_hours	E
2025-10-20	sunny	1	1.5	4
2025-10-21	sunny	1	2.0	4
2025-10-22	cloudy	0	3.5	3
2025-10-23	rainy	-1	5.0	2
2025-10-24	sunny	1	1.0	5
2025-10-25	cloudy	0	2.5	3
2025-10-26	rainy	-1	4.0	2
2025-10-27	sunny	1	0.5	5
2025-10-28	cloudy	0	3.0	3
2025-10-29	sunny	1	1.5	4
2025-10-30	rainy	-1	6.0	1
2025-10-31	cloudy	0	2.0	3
2025-11-01	sunny	1	2.0	4
2025-11-02	rainy	-1	5.0	2

manual.txt

個人マニュアル - 自分の取扱説明書

# 朝のルーティン
- 朝は散歩を優先すること。15分でも良いので外を歩くと頭がすっきりする。
- 朝食は必ず取る。バナナとコーヒーだけでも良い。
- 重要な意思決定は午前中に行う。午後は実行タスクに充てる。

# 会議への対応
- 長い会議(2時間以上)の直前は10分のリカバリ時間を確保する。
- 会議が連続する日は、間に5分の休憩を必ず入れる。
- 会議後はすぐに議事録を書く。後回しにするとストレスになる。

# エネルギー管理
- 元気度が3以下のときは、無理に難しいタスクをしない。
- ルーティンワークや整理整頓など、頭を使わない作業に切り替える。
- 夕方以降に重い意思決定をしない。翌朝に持ち越す。

# 睡眠とリカバリ
- 睡眠を最優先する。7時間は確保したい。
- 就寝1時間前はスマホを見ない。
- 疲れたら20分の仮眠を取る。30分以上は逆効果。

# 天気との付き合い方
- 雨の日は無理に外出しない。在宅ワークの日にする。
- 曇りの日はカフェで仕事するのも良い。気分転換になる。
- 晴れの日は積極的に外で活動する。散歩や買い物など。

# ストレス対策
- ストレスを感じたら、5分間目を閉じて深呼吸する。
- 運動は最高のストレス解消法。軽いジョギングでも効果的。
- 友人や家族と話す。一人で抱え込まない。

# 食事
- 昼食は軽めにする。重いと午後眠くなる。
- 夕食は19時までに済ませる。
- お菓子は適度に。完全に我慢するとストレスになる。

# 仕事の進め方
- タスクは午前中に集中させる。
- 午後は会議やコミュニケーション中心。
- 17時以降は新しいタスクを始めない。

# 週末の過ごし方
- 完全にオフにする日を作る。
- 趣味の時間を確保する。
- 月曜の準備は日曜の夜に軽く行う。

# 自分への約束
- 無理をしない。
- 完璧を求めない。
- 自分を責めない。
- 小さな成功を喜ぶ。

recent.md

# 最近の出来事

## 2025年10月最終週〜11月初旬

### 睡眠状況

- ここ3日間、睡眠時間は6時間前後。理想の7時間には届いていない。
- 寝つきは悪くないが、夜中に1回目が覚めることが多い。
- 朝はアラームで起きているが、スッキリ感はやや低め。

### 仕事の状況

- プロジェクトのスプリント終盤で、タスクが詰まっている。
- 今週は会議が多い週だった:
  - 月曜: 午前に2時間の定例会議
  - 火曜: 午後に1.5時間のレビュー会議
  - 水曜: 午前に1時間の1on1、午後に2時間の計画会議
- 集中作業の時間が取りにくく、少しストレスを感じている。

### 体調・気分

- 全体的には悪くないが、疲れが溜まってきている感じ。
- 先週末は久しぶりにゆっくり休めたので、週初めは調子が良かった。
- 水曜日あたりから疲労感が増してきた。

### 運動・活動

- 先週は2回ジョギングができた(各30分)。
- 今週はまだ運動できていない。時間が取れていない。
- 通勤の往復で1万歩程度は歩いている。

### 天気と気分

- 先週末は晴れていて気分が良かった。外で過ごす時間が多かった。
- 今週は曇りや雨の日が多く、少し気分が沈みがち。
- 雨の日は在宅ワークにしたが、それが幸いして移動ストレスは減った。

### 食事

- 朝食はほぼ毎日取れている。バナナ+ヨーグルト+コーヒーのパターン。
- 昼食はサラダとサンドイッチが多い。
- 夕食は19時を過ぎることもあるが、なるべく早めに済ませている。
- 間食でチョコレートを食べることが増えた(疲れのサイン?)。

### 今週の予定(11月第1週)

- 木曜: 午前に1時間のミーティング、午後は集中作業時間を確保したい。
- 金曜: 午前中に2時間のレビュー会議、午後は週次レポート作成。
- 土日: しっかり休む予定。日曜は趣味のDIYをする予定。

### 気づき・メモ

- 会議が多い日は元気度が下がる傾向にある。
- 睡眠時間を確保することが最優先課題。
- 運動不足を感じているので、短時間でも良いので体を動かしたい。
- 雨の日は無理に外出せず、在宅で効率よく仕事をするのが良さそう。

### やりたいこと・目標

- 毎日15分でも良いので散歩する。
- 睡眠時間を7時間確保する(就寝時刻を30分早める)。
- 週に2回は運動する。
- 会議の合間に5分の休憩を意識的に取る。

S3へデータをアップロード

以下コマンドを利用して、各データをS3へアップします

# データファイルをアップロード
aws s3 cp data/diary.csv s3://${BUCKET_NAME}/data/diary.csv

# Knowledge Base用ドキュメントをアップロード
aws s3 cp kb/manual.txt s3://${BUCKET_NAME}/kb/manual.txt
aws s3 cp kb/recent.md s3://${BUCKET_NAME}/kb/recent.md

# 確認
aws s3 ls s3://${BUCKET_NAME}/ --recursive
データファイル名Note
天気リストdiary.csv
自分マニュアルmanual.txt
最近のトピックrecent.md

SageMaker AIのセットアップ

ドメインの作成

  • シングルユーザ向けを選択

ユーザープロファイルの作成

  • デフォルトプロファイルがない場合には手動で作成

Studioの起動

Studio→モデル学習

Jupyter Notebookを利用して、次を行います

  • 学習データをS3にアップロード
  • XGBoostによる学習
    • csvのweather_scoreとmtg_hoursを説明変数(X)に定義
    • E(元気度)を目的変数(y)に定義
    • X → yの導出パターンを学習
  • 学習済みモデルを利用し、新しいXからyを予測する予測機をReadyに

これらを、次の.ipynbとしてまとめています。これを利用して進めていきます。(解説はまた別な記事で)

01_train_and_predict.ipynb

JupyterLabを開く

JupyterLab Spaceを作成

JupyterLab Spaceを開く

Upload Files から「」を開く

カーネル選択を求められたら Python 3を選択

環境設定のバケット設定部分をデータ格納用S3バケット名に修正

bucket = "your-bucket-mlops-demo"  # 👈 変更必須

Bedrockのセットアップ

SageMakerにより算出された元気度を利用し、「自分マニュアル」に記載のルールを踏まえながら、「最近のトピック」も考慮しつつ、ユーザに適切な情報を返す部分を作成します。

適切な情報を返せるようにするため、単純なLLMの回答ではなく、BedrockのKnowledge Baseを利用し、「自分マニュアル」と「最近のトピック」を参照した結果を返すようにします。

Knowledge Baseの作成

Amazon Bedrockのページに移動し、ナレッジベースを作成します。

Data Sourceのセットアップ

作成したナレッジベースに対し、先ほど作成したデータ格納用S3バケットのURIをセットします

KeyValueNote
S3のURIs3://katoaki-fortune-ai-us-20251103/kb/連携データの格納先パスを指定する必要があるため、”/kb”まで必要な点に注意

結合

Lambda用IAMロールの作成

KeyValueNote
ロール名fortune-lambda-role任意名で構いません。
ポリシー名AWSLambdaBasicExecutionRole
AmazonSageMakerFullAccess
AmazonBedrockFullAccess


実処理(orchestration.py)の作成

"""
自分占いAI - オーケストレーションLambda関数

このLambda関数は以下の処理を行います:
1. SageMakerエンドポイントで元気度を予測
2. Bedrock Knowledge Baseで関連ドキュメントを検索
3. Claude 3.5 Sonnetで具体的なアクション提案を生成
"""

import os
import json
import boto3
from typing import Dict, Any, List

# 環境変数
SM_ENDPOINT = os.environ.get("SM_ENDPOINT", "fortune-xgb-endpoint")
KB_ID = os.environ.get("KB_ID", "")  # Bedrock Knowledge Base ID
MODEL_ID = os.environ.get("MODEL_ID", "anthropic.claude-3-5-sonnet-20240620-v1:0")

# リージョンは Lambda の実行リージョン(AWS_REGION)を使用
# AWS_REGION は Lambda が自動設定するため、環境変数として設定不要
REGION = os.environ.get("AWS_REGION", "us-east-1")

# AWSクライアント
sagemaker_runtime = boto3.client("sagemaker-runtime", region_name=REGION)
bedrock_agent = boto3.client("bedrock-agent-runtime", region_name=REGION)
bedrock_runtime = boto3.client("bedrock-runtime", region_name=REGION)


def predict_energy(weather_score: float, mtg_hours: float) -> float:
    """
    SageMakerエンドポイントで元気度を予測
    
    Args:
        weather_score: 天気スコア(-1〜1)
        mtg_hours: 会議時間(時間)
    
    Returns:
        予測された元気度(1〜5)
    """
    payload = f"{weather_score},{mtg_hours}"
    
    response = sagemaker_runtime.invoke_endpoint(
        EndpointName=SM_ENDPOINT,
        ContentType="text/csv",
        Body=payload.encode("utf-8")
    )
    
    result = response["Body"].read().decode("utf-8").strip()
    return float(result)


def retrieve_knowledge(query: str, kb_id: str, max_results: int = 5) -> List[Dict[str, Any]]:
    """
    Bedrock Knowledge Baseから関連ドキュメントを検索
    
    Args:
        query: 検索クエリ
        kb_id: Knowledge Base ID
        max_results: 取得する最大結果数
    
    Returns:
        検索結果のリスト
    """
    if not kb_id:
        # KB IDが設定されていない場合は空を返す
        return []
    
    try:
        response = bedrock_agent.retrieve(
            knowledgeBaseId=kb_id,
            retrievalQuery={
                "text": query
            },
            retrievalConfiguration={
                "vectorSearchConfiguration": {
                    "numberOfResults": max_results
                }
            }
        )
        
        return response.get("retrievalResults", [])
    except Exception as e:
        print(f"Knowledge Base検索エラー: {e}")
        return []


def generate_advice(e_hat: float, context: str, weather_desc: str, mtg_hours: float) -> str:
    """
    Claudeで具体的なアクション提案を生成
    
    Args:
        e_hat: 予測された元気度
        context: RAGで取得したコンテキスト
        weather_desc: 天気の説明
        mtg_hours: 会議時間
    
    Returns:
        生成されたアドバイス
    """
    # プロンプト作成
    user_prompt = f"""あなたの今日の元気度は {e_hat:.2f} と予測されます。

【今日の状況】
- 天気: {weather_desc}
- 会議時間: {mtg_hours}時間

【あなたのマニュアルと最近の出来事】
{context}

上記を踏まえ、今日を良い日にするための具体的なアクション提案を3つ以内で箇条書きしてください。
各提案は1行で、実行可能で具体的なものにしてください。

出力形式:
- [アクション1]
- [アクション2]
- [アクション3]
"""

    # Bedrock呼び出し
    request_body = {
        "anthropic_version": "bedrock-2023-05-31",
        "max_tokens": 500,
        "temperature": 0.7,
        "messages": [
            {
                "role": "user",
                "content": [
                    {
                        "type": "text",
                        "text": user_prompt
                    }
                ]
            }
        ]
    }
    
    try:
        response = bedrock_runtime.invoke_model(
            modelId=MODEL_ID,
            body=json.dumps(request_body)
        )
        
        response_body = json.loads(response["body"].read().decode("utf-8"))
        advice = response_body["content"][0]["text"]
        return advice.strip()
    
    except Exception as e:
        print(f"Bedrock呼び出しエラー: {e}")
        return "アドバイスの生成に失敗しました。後でもう一度お試しください。"


def handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
    """
    Lambda ハンドラー
    
    Args:
        event: API Gatewayからのイベント
        context: Lambdaコンテキスト
    
    Returns:
        API Gatewayレスポンス
    """
    print(f"Event: {json.dumps(event)}")
    
    try:
        # リクエストボディの解析
        if "body" in event:
            body = json.loads(event["body"]) if isinstance(event["body"], str) else event["body"]
        else:
            body = event
        
        # パラメータ取得
        weather_score = float(body.get("weather_score", 0))
        mtg_hours = float(body.get("mtg_hours", 2.0))
        weather_desc = body.get("weather_desc", "不明")
        
        print(f"入力: weather_score={weather_score}, mtg_hours={mtg_hours}")
        
        # 1. 元気度予測
        e_hat = predict_energy(weather_score, mtg_hours)
        print(f"予測元気度: {e_hat}")
        
        # 2. Knowledge Base検索(KB IDが設定されている場合のみ)
        context = ""
        if KB_ID:
            query = f"元気度が{e_hat:.1f}で、会議が{mtg_hours}時間ある日の過ごし方"
            kb_results = retrieve_knowledge(query, KB_ID)
            
            if kb_results:
                context_parts = []
                for result in kb_results[:3]:  # 上位3件
                    content = result.get("content", {}).get("text", "")
                    if content:
                        context_parts.append(content)
                
                context = "\n\n".join(context_parts)
                print(f"KB検索結果: {len(kb_results)}件")
            else:
                print("KB検索結果なし、デフォルトコンテキストを使用")
                context = """
【マニュアル抜粋】
- 朝は散歩を優先。長い会議の直前は10分のリカバリ時間を確保。
- 夕方以降に重い意思決定をしない。睡眠を優先。
- 元気度が3以下のときは、無理に難しいタスクをしない。

【最近の状況】
- ここ数日、睡眠は6時間前後。やや不足気味。
- 会議が多い週で、少し疲れが溜まっている。
"""
        else:
            # KB IDが設定されていない場合のデフォルトコンテキスト
            print("KB未設定、デフォルトコンテキストを使用")
            context = """
【マニュアル抜粋】
- 朝は散歩を優先。長い会議の直前は10分のリカバリ時間を確保。
- 夕方以降に重い意思決定をしない。睡眠を優先。
- 元気度が3以下のときは、無理に難しいタスクをしない。

【最近の状況】
- ここ数日、睡眠は6時間前後。やや不足気味。
- 会議が多い週で、少し疲れが溜まっている。
"""
        
        # 3. アドバイス生成
        advice = generate_advice(e_hat, context, weather_desc, mtg_hours)
        print(f"アドバイス生成完了")
        
        # レスポンス作成
        response_body = {
            "E_hat": round(e_hat, 2),
            "weather_score": weather_score,
            "weather_desc": weather_desc,
            "mtg_hours": mtg_hours,
            "advice": advice,
            "status": "success"
        }
        
        return {
            "statusCode": 200,
            "headers": {
                "Content-Type": "application/json",
                "Access-Control-Allow-Origin": "*"  # CORS対応
            },
            "body": json.dumps(response_body, ensure_ascii=False)
        }
    
    except Exception as e:
        print(f"エラー: {str(e)}")
        import traceback
        traceback.print_exc()
        
        return {
            "statusCode": 500,
            "headers": {
                "Content-Type": "application/json",
                "Access-Control-Allow-Origin": "*"
            },
            "body": json.dumps({
                "error": str(e),
                "status": "error"
            }, ensure_ascii=False)
        }


# ローカルテスト用
if __name__ == "__main__":
    # テストイベント
    test_event = {
        "body": json.dumps({
            "weather_score": 1,
            "weather_desc": "晴れ",
            "mtg_hours": 2.0
        })
    }
    
    result = handler(test_event, None)
    print("\n" + "="*60)
    print("テスト結果:")
    print("="*60)
    print(json.dumps(json.loads(result["body"]), indent=2, ensure_ascii=False))

デプロイパッケージの作成

orchestration.pyをLambda Function用にzip化

cd /Users/kaguser/dev/sandbox/20251103_try-ml/lambda

# zipファイルを作成
zip function.zip orchestration.py

# 確認
ls -lh function.zip

Lambda Functionの作成

先ほど作成したロールのARNを利用します

LAMBDA_ROLE_ARNにはfortune-lambda-role用のARNを。REGIONにはus-east-1をセット

# 環境変数を設定(さっきコピーしたARN)
export LAMBDA_ROLE_ARN="arn:aws:iam::123456789012:role/fortune-lambda-role"

# Lambda関数を作成
aws lambda create-function \
  --function-name fortune-orchestration \
  --runtime python3.11 \
  --role ${LAMBDA_ROLE_ARN} \
  --handler orchestration.handler \
  --zip-file fileb://function.zip \
  --timeout 60 \
  --memory-size 512 \
  --region ${REGION}

環境変数をセット

# 環境変数を設定
export KB_ID="NKxxxxxxxx"  # BedrockのKnowledge Base ID

aws lambda update-function-configuration \
  --function-name fortune-orchestration \
  --environment Variables="{SM_ENDPOINT=fortune-xgb-endpoint,KB_ID=${KB_ID},MODEL_ID=anthropic.claude-3-5-sonnet-20240620-v1:0}" \
  --region ${REGION}

API Gatewayの設定

LambdaのHTTPトリガを作成

KeyValueNote
API名fortune-api
リソース名fortune
リソースパス/fortune
/fortuneのメソッドPOST, OPTIONSCORS有効化
/fortuneのLambdaプロキシ統合fortune-orchestration

フロントエンドの作成

cli.ts

#!/usr/bin/env ts-node
/**
 * 自分占いAI - CLI フロントエンド
 * 
 * 使用方法:
 *   npm run predict -- <weather_score> <mtg_hours> [weather_desc]
 * 
 * 例:
 *   npm run predict -- 1 2.0 "晴れ"
 *   npm run predict -- -1 5.0 "雨"
 *   npm run predict -- 0 3.5 "曇り"
 */

import fetch from 'node-fetch';

interface FortuneRequest {
  weather_score: number;
  weather_desc: string;
  mtg_hours: number;
}

interface FortuneResponse {
  E_hat: number;
  weather_score: number;
  weather_desc: string;
  mtg_hours: number;
  advice: string;
  status: string;
}

interface ErrorResponse {
  error: string;
  status: string;
}

// 天気スコアから説明を生成
function getWeatherDesc(score: number): string {
  if (score > 0.5) return "晴れ";
  if (score < -0.5) return "雨";
  return "曇り";
}

// 元気度に応じた絵文字を返す
function getEnergyEmoji(e: number): string {
  if (e >= 4.5) return "😄";
  if (e >= 3.5) return "🙂";
  if (e >= 2.5) return "😐";
  if (e >= 1.5) return "😔";
  return "😞";
}

// 天気に応じた絵文字を返す
function getWeatherEmoji(weatherScore: number): string {
  if (weatherScore > 0.5) return "☀️";
  if (weatherScore < -0.5) return "🌧️";
  return "☁️";
}

async function callFortuneAPI(
  apiUrl: string,
  weatherScore: number,
  mtgHours: number,
  weatherDesc?: string
): Promise<void> {
  const requestBody: FortuneRequest = {
    weather_score: weatherScore,
    weather_desc: weatherDesc || getWeatherDesc(weatherScore),
    mtg_hours: mtgHours
  };

  console.log("\n" + "=".repeat(60));
  console.log("🔮 自分占いAI - 今日の運勢");
  console.log("=".repeat(60));
  console.log(`\n📊 入力情報:`);
  console.log(`  ${getWeatherEmoji(weatherScore)} 天気: ${requestBody.weather_desc} (スコア: ${weatherScore})`);
  console.log(`  📅 会議時間: ${mtgHours}時間`);
  console.log(`\n⏳ 占い中...`);

  try {
    const response = await fetch(apiUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(requestBody)
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    // API Gatewayのレスポンスを取得
    const apiResponse = await response.json() as any;
    
    // bodyをパース(API Gatewayのプロキシ統合により、bodyは文字列)
    const data = JSON.parse(apiResponse.body || apiResponse) as FortuneResponse | ErrorResponse;

    if ('error' in data) {
      console.error(`\n❌ エラー: ${data.error}`);
      process.exit(1);
    }

    const result = data as FortuneResponse;

    console.log("\n" + "=".repeat(60));
    console.log("✨ 占い結果");
    console.log("=".repeat(60));
    console.log(`\n${getEnergyEmoji(result.E_hat)} 予測元気度: ${result.E_hat.toFixed(2)} / 5.0`);
    
    // 元気度ゲージ
    const gaugeLength = 20;
    const filledLength = Math.round((result.E_hat / 5.0) * gaugeLength);
    const gauge = "█".repeat(filledLength) + "░".repeat(gaugeLength - filledLength);
    console.log(`   [${gauge}]`);

    console.log(`\n💡 今日のアクション提案:`);
    console.log("-".repeat(60));
    
    // アドバイスを整形して表示
    const adviceLines = result.advice.split('\n').filter(line => line.trim());
    adviceLines.forEach(line => {
      console.log(`  ${line}`);
    });

    console.log("\n" + "=".repeat(60));
    console.log("Have a great day! 🌟");
    console.log("=".repeat(60) + "\n");

  } catch (error) {
    if (error instanceof Error) {
      console.error(`\n❌ APIエラー: ${error.message}`);
    } else {
      console.error(`\n❌ 不明なエラーが発生しました`);
    }
    process.exit(1);
  }
}

// メイン処理
async function main() {
  const apiUrl = process.env.API_URL;

  if (!apiUrl) {
    console.error("❌ エラー: 環境変数 API_URL が設定されていません");
    console.error("\n使用方法:");
    console.error("  export API_URL=\"https://your-api-gateway-url/prod/fortune\"");
    console.error("  npm run predict -- <weather_score> <mtg_hours> [weather_desc]");
    console.error("\n例:");
    console.error("  export API_URL=\"https://abc123.execute-api.ap-northeast-1.amazonaws.com/prod/fortune\"");
    console.error("  npm run predict -- 1 2.0 \"晴れ\"");
    process.exit(1);
  }

  const args = process.argv.slice(2);

  if (args.length < 2) {
    console.error("❌ エラー: 引数が不足しています");
    console.error("\n使用方法:");
    console.error("  npm run predict -- <weather_score> <mtg_hours> [weather_desc]");
    console.error("\n引数:");
    console.error("  weather_score: 天気スコア(-1=雨, 0=曇り, 1=晴れ)");
    console.error("  mtg_hours: 会議時間(時間単位)");
    console.error("  weather_desc: 天気の説明(オプション)");
    console.error("\n例:");
    console.error("  npm run predict -- 1 2.0 \"晴れ\"");
    console.error("  npm run predict -- -1 5.0 \"雨\"");
    console.error("  npm run predict -- 0 3.5");
    process.exit(1);
  }

  const weatherScore = parseFloat(args[0]);
  const mtgHours = parseFloat(args[1]);
  const weatherDesc = args[2];

  if (isNaN(weatherScore) || isNaN(mtgHours)) {
    console.error("❌ エラー: weather_score と mtg_hours は数値である必要があります");
    process.exit(1);
  }

  if (weatherScore < -1 || weatherScore > 1) {
    console.warn("⚠️  警告: weather_score は通常 -1 〜 1 の範囲です");
  }

  if (mtgHours < 0 || mtgHours > 24) {
    console.warn("⚠️  警告: mtg_hours は通常 0 〜 24 の範囲です");
  }

  await callFortuneAPI(apiUrl, weatherScore, mtgHours, weatherDesc);
}

// 実行
main().catch(error => {
  console.error("予期しないエラー:", error);
  process.exit(1);
});

package.json

{
  "name": "fortune-ai-frontend",
  "version": "1.0.0",
  "description": "自分占いAI フロントエンドCLI",
  "main": "dist/cli.js",
  "scripts": {
    "build": "tsc",
    "predict": "ts-node cli.ts",
    "test": "ts-node cli.ts 1 2.0"
  },
  "keywords": [
    "sagemaker",
    "bedrock",
    "fortune",
    "ai"
  ],
  "author": "",
  "license": "MIT",
  "devDependencies": {
    "@types/node": "^20.10.0",
    "ts-node": "^10.9.2",
    "typescript": "^5.3.3"
  },
  "dependencies": {
    "node-fetch": "^2.7.0",
    "@types/node-fetch": "^2.6.9"
  }
}

セットアップ

$ npm install

# 環境変数に設定(ステップ7でメモしたURL)
$ export API_URL="https://abcd1234.execute-api.us-east-1.amazonaws.com/prod/fortune"

動作確認

実行

$ npm run predict -- 1 2.0 "晴れ"

結果

============================================================
🔮 自分占いAI - 今日の運勢
============================================================

📊 入力情報:
  ☀️ 天気: 晴れ (スコア: 1)
  📅 会議時間: 2時間

⏳ 占い中...

============================================================
✨ 占い結果
============================================================

😄 予測元気度: 4.23 / 5.0
   [████████████████░░░░]

💡 今日のアクション提案:
------------------------------------------------------------
  - 朝の15分散歩で頭をすっきりさせてから、午前中に重要な意思決定を行う
  - 2時間の会議の前に10分のリカバリ時間を確保し、深呼吸や軽いストレッチをする
  - 晴れの良い天気を活かして、昼休みは外でリフレッシュし、午後は実行タスクに集中する

============================================================
Have a great day! 🌟
============================================================

さいごに

BedrockとSageMakerを組み合わせて、計算と出力を行う占いアプリを作ってみました。

それぞれ役割を分けたことで、「元気度」の計算精度を追求したり、計算結果を元にした「伝え方」の改善を行ったりしやすい構成とできました。

投稿者: hirobel

JavaScriptが好きです

コメントをどうぞ

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

PAGE TOP
Close Bitnami banner
Bitnami