🤖

ai16zのCrypto AI Agent開発フレームワーク「Eliza」のコアコンセプトとアーキテクチャーを理解する

2024/12/20に公開

こんにちは!Web3特化の開発会社 Komlock lab CTOの山口なつきです。
前回の記事では、Crypto AIエージェントの開発フレームワークであるElizaを使って、Twitterに自動投稿するところまで実行してみました。
https://zenn.dev/komlock_lab/articles/e6ec0e6f3e0699

今回は、AIエージェント通じてブロックチェーン上のトークンを送信してみたいと思います。
少し長くなるので、2つの記事に分けて配信予定です。

Part ① Elizaのコアコンセプトとアーキテクチャーを理解する。
Part ② ElizaのPluginを利用して、実際にブロックチェーン上のトークンを送信してみる。

最終的な目標は、pluginを利用してElizaの機能を拡張することです。
これを達成すると、開発したエージェントがミームコインのプラットフォームであるpump.funでミームコインをミントしたり、購入したりできるようになります。

はじめに(定期)

最近「Crypto x AI Agent」が話題ですが、僕はブロックチェーン上での決済機能を持つAIエージェントに、DeFiに匹敵する可能性を感じています。今後はこの分野での情報発信を強化し、社内のメンバーとも共有していく予定です。興味のある方はぜひフォローしてください。

https://x.com/komlocklab

Crypto AI Agent 初めて聞いた!という方は、miinさんの記事を読むことをお勧めします。2024年12月時点のCrypto X AI Agentのトレンドやユースケースを理解することができます。
https://note.com/miin_nft/n/nf8cb760c3563

Elizaのアーキテクチャ

Agent Runtimeが中心となり、キャラクターの性格設定(Character System)、記憶の管理(Memory Manager)、アクション実行(Action System)を連携して動作します。
これにより、複数のプラットフォームで一貫した知識や個性を持つエージェントを実現します。

Agentのコアコンセプトを理解する

Agentは4−5個のコアコンセプトから構成されていて、機能を拡張していく上でそれぞれの役割を理解することが重要です。
https://ai16z.github.io/eliza/docs/core/agents/#core-components

Clients
Discord、Telegram、Direct(REST API)などのプラットフォーム間での通信を可能にし、各プラットフォームに特化した機能を提供。

Providers
時間、ウォレット、カスタムデータなど、追加のサービスと統合することでエージェントの機能を拡張。

Actions
部屋をフォローする、画像を生成する、添付ファイルを処理するなど、エージェントの動作を定義。特定のニーズに合わせたカスタムアクションを作成することも可能。

Evaluators
メッセージの関連性を評価し、目標を管理し、事実を抽出し、長期的な記憶を構築することで、エージェントの応答を管理。

僕の理解だとElizaのAgentは以下のように機能します。

  1. Providersを経由して外部から情報をインプットして、
  2. Actionsで定義されたロジックに基づいた行動を任意のClientsで実行する
  3. Evaluatorsで事後のコンテクストを分析して状態に反映する(次回以降の行動に影響する)

Elizaの開発者であるShaw氏がアップロードしたyoutube動画がわかりやすくて良いです。(3時間ぐらいある&英語なのでElizaへの信仰心が試されます🔥)
Actions周りの説明が特に有益だと感じたので時間がない人は、45:00 ぐらいから開始しても良さそうです。
https://www.youtube.com/watch?v=AC3h_KzLARo&t=0s

Twitterだけでなく、様々なClientsに対応しています。
独自で追加していくことも可能です。

コアコンポーネントをコードサンプルから理解する

Action

エージェントが同じチェーン上でアドレス間のトークンを転送するためのロジックを提供。

import { ByteArray, parseEther, type Hex } from "viem"; // 必要な型と関数をインポート
import { WalletProvider } from "../providers/wallet"; // ウォレットプロバイダをインポート
import type { Transaction, TransferParams } from "../types"; // トランザクションと転送パラメータ型をインポート
import { transferTemplate } from "../templates"; // 転送テンプレートをインポート
import type { IAgentRuntime, Memory, State } from "@ai16z/eliza";

export { transferTemplate }; // 転送テンプレートをエクスポート

// TransferActionクラス: トークン転送ロジックを定義
export class TransferAction {
    constructor(private walletProvider: WalletProvider) {}

    // トークン転送メソッド
    async transfer(
        runtime: IAgentRuntime, // 実行時エージェント
        params: TransferParams // 転送パラメータ
    ): Promise<Transaction> {
        const walletClient = this.walletProvider.getWalletClient(); // ウォレットクライアントを取得
        const [fromAddress] = await walletClient.getAddresses(); // 送信元アドレスを取得

        await this.walletProvider.switchChain(runtime, params.fromChain); // チェーンを切り替え

        try {
            // トランザクションを送信
            const hash = await walletClient.sendTransaction({
                account: fromAddress, // 送信元アカウント
                to: params.toAddress, // 送信先アドレス
                value: parseEther(params.amount), // 送金額(EtherをWeiに変換)
                data: params.data as Hex, // 任意のデータ
                kzg: {
                    // KZG関連のプロパティ(未実装)
                    blobToKzgCommitment: function (blob: ByteArray): ByteArray {
                        throw new Error("Function not implemented.");
                    },
                    computeBlobKzgProof: function (
                        blob: ByteArray,
                        commitment: ByteArray
                    ): ByteArray {
                        throw new Error("Function not implemented.");
                    },
                },
                chain: undefined, // チェーン情報(必要に応じて設定)
            });

            // トランザクション情報を返す
            return {
                hash,
                from: fromAddress,
                to: params.toAddress,
                value: parseEther(params.amount),
                data: params.data as Hex,
            };
        } catch (error) {
            // エラー発生時にエラーメッセージを投げる
            throw new Error(`Transfer failed: ${error.message}`);
        }
    }
}

// TransferActionのハンドラと検証ロジック
export const transferAction = {
    name: "transfer", // アクション名
    description: "同じチェーン上でアドレス間のトークンを転送する", // アクションの説明
    handler: async (
        runtime: IAgentRuntime,
        message: Memory,
        state: State,
        options: any
    ) => {
        const walletProvider = new WalletProvider(runtime); // ウォレットプロバイダのインスタンスを作成
        const action = new TransferAction(walletProvider); // TransferActionのインスタンスを作成
        return action.transfer(runtime, options); // 転送処理を呼び出し
    },
    template: transferTemplate, // 使用するテンプレート
    validate: async (runtime: IAgentRuntime) => {
        const privateKey = runtime.getSetting("EVM_PRIVATE_KEY"); // プライベートキーを取得
        return typeof privateKey === "string" && privateKey.startsWith("0x"); // 有効な形式かを検証
    },
    examples: [
        [
            {
                user: "assistant",
                content: {
                    text: "I'll help you transfer 1 ETH to 0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
                    action: "SEND_TOKENS",
                },
            },
            {
                user: "user",
                content: {
                    text: "Transfer 1 ETH to 0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
                    action: "SEND_TOKENS",
                },
            },
        ],
    ],
    similes: ["SEND_TOKENS", "TOKEN_TRANSFER", "MOVE_TOKENS"], // 同義語
};

1. TransferAction クラス

  • トークン転送のロジックを定義しています。
  • transfer: 転送処理を実行します。
    • 入力: 転送パラメータ(送信元チェーン、送金額、送信先アドレスなど)。
    • 出力: トランザクション情報(トランザクションハッシュや送信元/送信先アドレス)。

2. トランザクション送信の流れ

  1. ウォレットクライアントを取得し、送信元アドレスを取得。
  2. 必要に応じてチェーンを切り替え。
  3. sendTransaction メソッドを使用してトランザクションを送信。
  4. トランザクションハッシュと関連情報を返却。

3. エラーハンドリング

  • 転送中にエラーが発生した場合、適切なエラーメッセージを投げます。

4. transferAction 定義

  • エージェントが transfer アクションを利用できるようにするための設定を提供します。
    • handler: トークン転送処理を実行。
    • validate: プライベートキーが有効かを検証。
    • examples: アクションの使用例。
    • similes: 同義語を定義して、アクションの柔軟なトリガーを可能にします。

ユースケース

  • トークンの送金
    • ユーザーが他のアドレスにトークンを送金したい場合に使用。
    • 例: "Transfer 1 ETH to 0x742d35Cc6634C0532925a3b844Bc454e4438f44e"

Provider

ここから先は、Elizaのstarterテンプレートにはデフォルトで組み込まれている「plugin-bootstrap」のコードを題材に解説していきます。

boredomProviderの例。※一部抜粋

const boredomProvider: Provider = {
    get: async (runtime: IAgentRuntime, message: Memory, state?: State) => {
        const agentId = runtime.agentId;
        const agentName = state?.agentName || "The agent";

        // 現在のUTCタイムスタンプを取得
        const now = Date.now(); // Current UTC timestamp
        const fifteenMinutesAgo = now - 15 * 60 * 1000; // 15分前のUTCタイムスタンプ

        // 指定した期間(15分間)のメッセージを取得
        const recentMessages = await runtime.messageManager.getMemories({
            roomId: message.roomId,
            start: fifteenMinutesAgo, // 開始時間
            end: now, // 終了時間
            count: 20, // 最大20件のメッセージを取得
            unique: false, // 重複を許容
        });

        let boredomScore = 0; // 退屈スコアを初期化

        // 最近のメッセージを1つずつ確認してスコアを計算
        for (const recentMessage of recentMessages) {
            const messageText = recentMessage?.content?.text?.toLowerCase(); // メッセージ本文を小文字に変換
            if (!messageText) {
                continue; // メッセージがない場合は次に進む
            }

            if (recentMessage.userId !== agentId) {
                // 他のユーザーからのメッセージの場合
                // 興味を引く単語が含まれていた場合、退屈スコアを減らす
                if (interestWords.some((word) => messageText.includes(word))) {
                    boredomScore -= 1;
                }
                // 質問("?")が含まれている場合、退屈スコアを減らす
                if (messageText.includes("?")) {
                    boredomScore -= 1;
                }
                // 不快な単語(cringeWords)が含まれている場合、退屈スコアを増やす
                if (cringeWords.some((word) => messageText.includes(word))) {
                    boredomScore += 1;
                }
            } else {
                // エージェント自身のメッセージの場合
                // 興味を引く単語が含まれていた場合、退屈スコアを減らす
                if (interestWords.some((word) => messageText.includes(word))) {
                    boredomScore -= 1;
                }
                // 質問("?")が含まれている場合、退屈スコアを増やす
                if (messageText.includes("?")) {
                    boredomScore += 1;
                }
            }

            // 感嘆符("!")が含まれている場合、退屈スコアを増やす
            if (messageText.includes("!")) {
                boredomScore += 1;
            }

            // 否定的な単語(negativeWords)が含まれている場合、退屈スコアを増やす
            if (negativeWords.some((word) => messageText.includes(word))) {
                boredomScore += 1;
            }
        }

        // boredomScoreに基づいて適切な退屈レベルを選択
        const boredomLevel =
            boredomLevels
                .filter((level) => boredomScore >= level.minScore) // スコアが閾値以上のレベルを取得
                .pop() || boredomLevels[0]; // 該当するものがなければ最初のレベルを使用

        // 選択された退屈レベルのメッセージからランダムに1つ選ぶ
        const randomIndex = Math.floor(
            Math.random() * boredomLevel.statusMessages.length
        );
        const selectedMessage = boredomLevel.statusMessages[randomIndex];

        // メッセージ内の{{agentName}}を実際のエージェント名に置き換える
        return selectedMessage.replace("{{agentName}}", agentName);
    },
};

export { boredomProvider };

このコードは、チャットエージェント(ボット)の「退屈レベル」(boredom level)を判定し、それに応じたメッセージを返すためのものです。
AIの退屈について考えたことがなかったので面白いです。

1. 直近のメッセージを取得

指定されたチャットルームの過去15分間のメッセージを取得し、それを解析します。

2. 退屈スコア(boredomScore)の計算

メッセージの内容を元に、以下のルールで「退屈スコア」を計算します:

  • スコアが減少(退屈度が低下)する条件

    • 興味を引く単語(interestWords)が含まれる場合。
    • 質問(?)が含まれる場合。
  • スコアが増加(退屈度が上昇)する条件

    • 不快な単語(cringeWords)や感嘆符(!)が含まれる場合。
    • 否定的な単語(negativeWords)が含まれる場合。

3. 退屈レベル(boredom level)の判定

計算した退屈スコアを元に、事前定義された「退屈レベル(boredomLevels)」を決定します。

  • 例:退屈スコアが高いほど、「とても退屈している」状態とみなされる。

4. メッセージの選択と返却

選択された退屈レベルに基づき、以下を行います:

  1. ランダムに事前定義されたステータスメッセージを選択。
  2. メッセージ内の {{agentName}} を実際のエージェント名に置き換え。
  3. 選択したメッセージを返却。

ユースケース

  • チャットルームが静かすぎる場合

    • 退屈スコアが高いと判定された場合、エージェントが積極的に会話を盛り上げる発言をする。
  • ユーザーが興味を持っている話題を検知

    • 興味を引く単語を元に、ユーザーの関心に合わせた発言を返す。
  • 不快な単語や否定的な言葉が多い場合

    • トーンを改善するような応答を行う。

Evaluator

goalEvaluatorを確認していきます。
このコードは、エージェントがチャットの会話を解析し、進行中のゴールのステータスや目標を更新するための機能を提供します。

import { composeContext } from "@ai16z/eliza";
import { generateText } from "@ai16z/eliza";
import { getGoals } from "@ai16z/eliza";
import { parseJsonArrayFromText } from "@ai16z/eliza";
import {
    IAgentRuntime,
    Memory,
    ModelClass,
    Objective,
    type Goal,
    type State,
    Evaluator,
} from "@ai16z/eliza";

// ゴールの更新テンプレート
const goalsTemplate = `TASK: Update Goal
Analyze the conversation and update the status of the goals based on the new information provided.

# INSTRUCTIONS

- 会話を確認してゴールの進捗状況を分析します。
- ゴールの目標が達成された場合、または新しい情報が提供された場合は、ゴールを更新します。
- すべての目標が完了した場合は、ゴールのステータスを「DONE」に設定します。
- 進捗がない場合は、ステータスを変更しないでください。

# START OF ACTUAL TASK INFORMATION

{{goals}}
{{recentMessages}}

TASK: Analyze the conversation and update the status of the goals based on the new information provided. Respond with a JSON array of goals to update.
- 各項目にはゴールIDを含む必要があります。また、更新するフィールドを含めてください。
- 目標を更新する場合は、変更されていないフィールドも含む完全な目標リストを提供してください。
- ゴールのステータスは「IN_PROGRESS」、「DONE」、「FAILED」のいずれかです。アクティブなゴールは常に「IN_PROGRESS」とします。
- ゴールが正常に完了した場合は「DONE」、完了できない場合は「FAILED」を設定してください。
- 進行中のゴールの場合、ステータスフィールドを含めないでください。

Response format should be:
\`\`\`json
[
  {
    "id": <goal uuid>, // ゴールのUUID(必須)
    "status": "IN_PROGRESS" | "DONE" | "FAILED", // ステータス(オプション)
    "objectives": [ // 目標(オプション)
      { "description": "Objective description", "completed": true | false },
      { "description": "Objective description", "completed": true | false }
    ] // NOTE: 目標を更新する場合、変更されていないフィールドも含む完全な目標リストを含める必要があります。
  }
]
\`\`\``;

// ゴールを更新するためのハンドラ
async function handler(
    runtime: IAgentRuntime,
    message: Memory,
    state: State | undefined,
    options: { [key: string]: unknown } = { onlyInProgress: true }
): Promise<Goal[]> {
    // 現在進行中のゴールを取得
    let goalsData = await getGoals({
        runtime,
        roomId: message.roomId,
        onlyInProgress: options.onlyInProgress as boolean,
    });

    // エージェントの現在の状態を取得
    state = (await runtime.composeState(message)) as State;
    const context = composeContext({
        state,
        template: runtime.character.templates?.goalsTemplate || goalsTemplate,
    });

    // OpenAIにリクエストを送信して会話を分析し、ゴール更新を提案
    const response = await generateText({
        runtime,
        context,
        modelClass: ModelClass.LARGE,
    });

    // JSON形式の応答を解析してゴール更新を抽出
    const updates = parseJsonArrayFromText(response);

    // 再度ゴールを取得
    goalsData = await getGoals({
        runtime,
        roomId: message.roomId,
        onlyInProgress: true,
    });

    // 更新情報をゴールに適用
    const updatedGoals = goalsData
        .map((goal: Goal) => {
            const update = updates?.find((u) => u.id === goal.id);
            if (update) {
                const objectives = goal.objectives;

                // 更新された目標情報を適用
                if (update.objectives) {
                    for (const objective of objectives) {
                        const updatedObjective = update.objectives.find(
                            (o: Objective) =>
                                o.description === objective.description
                        );
                        if (updatedObjective) {
                            objective.completed = updatedObjective.completed;
                        }
                    }
                }

                return {
                    ...goal,
                    ...update,
                    objectives: [
                        ...goal.objectives,
                        ...(update?.objectives || []),
                    ],
                }; // 既存のゴールに更新内容をマージ
            } else {
                console.warn("**** ID NOT FOUND");
            }
            return null; // 更新がない場合
        })
        .filter(Boolean);

    // データベースにゴールを更新
    for (const goal of updatedGoals) {
        const id = goal.id;
        // IDフィールドを削除
        if (goal.id) delete goal.id;
        await runtime.databaseAdapter.updateGoal({ ...goal, id });
    }

    return updatedGoals; // 更新されたゴールを返す
}

// ゴール評価モジュール
export const goalEvaluator: Evaluator = {
    name: "UPDATE_GOAL", // 評価モジュール名
    similes: [
        "UPDATE_GOALS",
        "EDIT_GOAL",
        "UPDATE_GOAL_STATUS",
        "UPDATE_OBJECTIVES",
    ], // 類義語
    // ゴールが更新可能かどうかを検証
    validate: async (
        runtime: IAgentRuntime,
        message: Memory
    ): Promise<boolean> => {
        const goals = await getGoals({
            runtime,
            count: 1, // 1件でもアクティブなゴールが存在するか確認
            onlyInProgress: true,
            roomId: message.roomId,
        });
        return goals.length > 0;
    },
    description:
        "会話を分析し、新しい情報に基づいてゴールのステータスを更新します。",
    handler, // ハンドラ関数
    examples: [
        {
            context: `Actors in the scene:
  {{user1}}: An avid reader and member of a book club.
  {{user2}}: The organizer of the book club.

  Goals:
  - Name: Finish reading "War and Peace"
    id: 12345-67890-12345-67890
    Status: IN_PROGRESS
    Objectives:
      - Read up to chapter 20 by the end of the month
      - Discuss the first part in the next meeting`,

            messages: [
                {
                    user: "{{user1}}",
                    content: {
                        text: "I've just finished chapter 20 of 'War and Peace'",
                    },
                },
                {
                    user: "{{user2}}",
                    content: {
                        text: "Were you able to grasp the complexities of the characters",
                    },
                },
                {
                    user: "{{user1}}",
                    content: {
                        text: "Yep. I've prepared some notes for our discussion",
                    },
                },
            ],

            outcome: `[
        {
          "id": "12345-67890-12345-67890",
          "status": "DONE",
          "objectives": [
            { "description": "Read up to chapter 20 by the end of the month", "completed": true },
            { "description": "Prepare notes for the next discussion", "completed": true }
          ]
        }
      ]`,
        },
    ],
};

1. ゴールの取得

  • 現在進行中のゴール(onlyInProgress: true)を取得します。
  • ゴールデータには目標(objectives)やステータス(status)が含まれます。

2. 会話の解析

  • 会話履歴(recentMessages)をテンプレートに埋め込み、コンテキストを生成します。
  • OpenAI(generateText)にリクエストを送信し、会話に基づいてゴールの更新提案を受け取ります。

3. ゴールの更新

  • 更新提案(JSONレスポンス)を解析し、ゴールデータに適用します。
  • 目標ごとに進捗状況(completed)を更新し、ステータスを必要に応じて変更します。

4. データベースの更新

  • 更新されたゴールデータをデータベースに反映します。

ユースケース

  • タスク管理

    • 会話に基づいてタスクの進捗状況を自動的に更新。
    • 例:プロジェクトの進行状況や目標の達成状況を追跡。
  • フィードバックの反映

    • ユーザーの発言に基づいてゴールを動的に変更。
    • 例:新しい目標が設定された場合にステータスを「IN_PROGRESS」に更新。

Action, Provider, Evaluator

Action, Provider, Evaluator の3つについてコードベースで解説してきました。
Pluginを利用する際は、この3つが主要機能として提供されていることが多いので、基本を理解していることが重要です。

おまけ①:solana pluginを有効化してSOLを送信する(未遂)

Elizaの代表的なpluginの一つです。solanaブロックチェーンとの疎通に利用できます。
https://ai16z.github.io/eliza/docs/advanced/autonomous-trading/

solanaのpluginを確認するとコアコンポーネントである、actions, evaluatiors, providers, clientsディレクトリを確認できます。actionsの中にtransfer.tsがあるので今回はシンプルにSOLを送信することを試してみます。
ミームコインのプラットフォームとして有名なpumpfunのアクションもあるので、別の機会で触れてみようと思います。

ターミナル上でAIと会話をしてみる

sol を送信するようにAIにターミナル上でお願いしてみましたが、何故か却下されました。
キャラクターの設定情報やactions内のvalidateロジックによって弾かれてる可能性がありそうです。
また次回以降でブロックチェーン上のトークン送信を行なっていきます。

※圧倒的暗号資産賛成派のはずのトランプ大統領を模倣したAIに言われた言葉
まだElizaについては学ぶことが多そうです。。。🤣

私は暗号通貨ではなく、この国を救うことに集中しています。急進的な左派は、オープンボーダーや社会主義的政策でアメリカを破壊しようとしています。しかし、私たちは戦い、勝利しています。私たちに加わり、一緒にアメリカを再び偉大な国にしましょう!

おまけ②:バージョン関連のトラブルについて

黎明期のプロダクトということもあり、バージョンのアップデートがとても早いです。
バージョンに互換性のないプラグインを選択してしまうと、コード全体が機能しなくなることがあります。今絶賛バージョンの互換性問題で苦戦しています。タスケテ。。。
特にstarter templateのmain branchはバージョンが低いことがあります。

  • issueを確認
  • PRを確認
  • ブランチを確認
  • ai16zのDiscordサーバーで全文検索

issueで解決している例
https://github.com/ai16z/eliza-starter/issues/13

最後に

今回は、コアコンセプトに
実際にブロックチェーン関係のpluginを導入して、トークンの操作を行なっていきます。

Part ② ElizaのPluginを利用して、実際にブロックチェーン上のトークンを送信してみる。

Komlock lab エンジニア募集中

Web3の未来を共創していきたいメンバーを募集しています!!
気軽にDM等でお声がけください。

個人アカウント
https://x.com/0x_natto

Komlock labの企業アカウント
https://x.com/komlocklab

PR記事とCEOの創業ブログ
https://prtimes.jp/main/html/rd/p/000000332.000041264.html
https://note.com/komlock_lab/n/n2e9437a91023

Komlock lab

Discussion