GraphAIの解説

GraphAIとは

GraphAIは、LLM/RAG/Tools/DBなどの複数のAgentを組み合わせて作る、Multi agentsシステムを効率よく構築するためにフレームワークです。

Multi agentsも数個の単純な組み合わせであればプログラムで記述することは容易です。しかし、複雑な組み合わせて、並列実行やデータの依存関係がある場合、並列で動くMulti agentsを作ることは容易ではありません。
最適化を考えないで作れば作ることはできますが、それぞれの処理に数秒かかるAgentを多く組み合わせるとユーザへのアウトプットが極端に遅くなります。また、スケーラビリティや、サーバ、クライアントでの役割、Stream処理など、現在のAIシステムは様々な要求があります。

GraphAIは

といった特徴があります。

GraphAIで使うAgentは

です。

現在は、receptronという組織で開発をしています。

Quick Start

まずは、簡単にGraphAIを使ってみましょう。npmの環境とOpenAIのapi keyを用意してください。

そして、GraphAI の cliをインストールします。

npm i -g  @receptron/graphai_cli

以下のyamlファイル(Graphデータ)をダウンロードして保存してください。

https://raw.githubusercontent.com/receptron/graphai/main/packages/cli/samples/business_idea_jp.yaml

そのファイルを保存したディレクトリーに.envというファイルをつくってOpenAIのAPI KEYを記述します。

OPENAI_API_KEY=sk-xxxx

用意は以上です。実行します。

graphai business_idea_jp.yaml

成功していれば、何も表示されないでしばらくまちます。

しばらくすると、「このビジネスアイデアコンテスト」のアイデア、評価、結果が表示されます。

yamlファイルをみると流れはわかると思いますがOpenAIのGPTに問い合わせるAgentが3回実行されています。
GPT(AI)への問い合わせと、その結果を使って更に問い合わせをするという流れです。

問い合わせの内容を変更すると、ビジネスアイデア以外の結果を得ることができるので、改良して使ってみてください。

このように、yamlファイルに定義することで、簡単にAIを使ったAgentを組み合わせて動かすことが可能となります。

Sample

yamlのサンプル

graphaiのcliコマンドで実行できるyamlサンプルです。@receptron/graphai_cliがインストール済みであれば

graphai {filename}

で実行できます。

tsのサンプル

typescriptで書かれたサンプルです。ソースをcloneして、それぞれのディレクトリーで以下のコマンドで実行できます。

yarn run samples {sampleFile}

簡単なGraphAIの使い方

こちらのチュートリアルを参考にしてください。英語ですが、ほとんどがソースなので翻訳を使いながらでも簡単に理解できます。

公式チュートリアル

このチュートリアルは、graphai_cliを使ってCliでyamlを読み込んでGraphAIを使います。
graphai_cliはGraphAI本体と、@graphai/agentsを含んでいて、全てのAgentを利用できます。

GraphAIの簡単な動作の流れ

GraphAIのNode.jsで使い方

GraphAIを動作させるのに必要な最小限のnpmはgraphaiといずれかのagentです。
最も簡単に使えるAgentのvanilla agents(@graphai/vanilla 他に依存がないagentをvanillaと呼んでいます)を使って簡単なGraphデータを作り、動かしてみます。

最初にnpmの初期化をして、必要なnpmを入れます。typescriptを実行するためにts-nodeも入れます。

npm init
yarn add graphai @graphai/vanilla ts-node

以下が最小限のスクリプトです。graphai.tsで保存します。
Static nodeのnode1でメッセージを指定して、そのデータを受け取ったComputed nodeのnode2でbypassAgentを実行します。
node2の結果を出力としてかえします。

import { GraphAI } from "graphai";
import * as agents from "@graphai/vanilla";

const graph_data = {
  version: 0.5,
  nodes: {
    node1: {
      value: "hello, GraphAI",
    },
    node2: {
      agent: "bypassAgent",
      inputs: [":node1"],
      isResult: true,
    },
  },
};

export const main = async () => {
  const graph = new GraphAI(graph_data, agents);
  const result = await graph.run();
  console.log(JSON.stringify(result));

};

if (process.argv[1] === __filename) {
  main();
}

実行します

$ npx ts-node graphai.ts
{"node2":["hello, GraphAI"]}

node1で指定して文字列がnode2に渡され、結果として表示されました。

Webのサンプル

Vue3で書かれたwebのサンプルです。

https://github.com/receptron/graphai-demo-web

サンプルサイト
https://graphai-demo.web.app/

WebのサンプルStream編

LLMをサーバで動かし、そのレスポンスをstreamで受け取るサンプルです。
サーバ側、クラアント側、両方でstreamをサポートしています。

https://github.com/isamu/graphai-stream-web

localで動作します。

root directoryとserver directoryでnpmのinstall.

yarn install

vueの起動

yarn run serve

serverの起動

yarn run server

で利用できます。

レポジトリとnpm構成

ソースコード

https://github.com/receptron/graphai

にあります。このレポジトリはモノレポとなっていて、GraphAI本体の他に、各種ツールやAgentが含まれます。

各packages/agents以下のディレクトリはそれぞれのnpmのパッケージとして提供されています。

GraphAI本体は https://github.com/receptron/graphai/blob/main/packages/graphai/src/ 以下の

の4つのファイルで、合わせても1000行程度とコンパクトに作られています。

本体以外にAgent/AgentFilter/Validator/GraphAIの機能を拡張するAgent(nestしたGraphやGraphを並列で動かす)などを組み合わせて使うため、シンプルなエンジンで複雑な処理を行うことが可能となります。

npm package

npmパッケージは以下で配布しています。

GraphAI本体

https://www.npmjs.com/package/graphai

GraphAI関連ツール

receptronのorganization配下で、ツールを提供しています。

yamlやjsonをcliで読み込んで使うツールやexpressのmiddlewareなどがあります。

receptron organization

Agents

graphai organization下で、Agentを配布しています。

用語説明

Graphデータの作り方

echoAgentを使った簡単なGraphの作成

echoAgent(指定されたデータを出力するAgent)を使って、簡単なGraphファイルを作ります。
GraphAIのGraphファイルの必須項目はversionとnodesです。

versionは、0.5を指定します。(2024/06現在)

nodesは、GraphAIで使う各ノードを書いていきます。
このサンプルではnodesにechoAgentを使った1つのnodeを追加します。
echoAgentはparamsで指定しているユーザからの入力値をそのまま返すAgentです。

追加するnodeは、node1というNode名をつけます。
このnodeはmessage: helloというデータを出力します。

このサンプルのYAMLは1つしかnodeがありませんが、結果を返すnodeはnode1なので、isResult: trueを追加し、このnodeの結果がこのGraphの結果と指定します。

version: 0.5
nodes:
  node1: 
    params:
      message: hello
    agent: echoAgent
    isResult: true
   

これをecho.yamlというファイル名で保存して、graphaiのcliで実行します

$ graphai echo.yaml 
{ node1: { message: 'hello' } }

2行目のmessage: helloが表示されていれば成功です。

inputsを追加する

次に複数のAgentを組み合わせ、inputsで入力を指定したYAMLを作ります。
入力を指定することで依存関係が定義でき、それによって実行順が制御されます。

bypassAgentは入力値をそのまま出力値で返すAgentです。
先程のechoAgentのyamlにnode2を追加します。node2のagentはbypassAgentを指定します。
入力のinputsとして、前のnode1を指定します。inputsは文字列の配列で、node名を指定します。

今回は出力はbypassAgentのnode2なので、node2にisResult: trueを指定します。node1のisResultは削除します。

version: 0.5
nodes:
  node1: 
    params:
      message: hello
    agent: echoAgent
  node2: 
    agent: bypassAgent
    inputs: [":node1"]
    isResult: true

実行される順に説明すると、

これを実行するとnode2の結果としてmessage: 'hello'が表示されます。
また結果はarrayになっています。

 $ graphai echo2.yaml
{ node2: [ { message: 'hello' } ] }

同じように、今度は入力を増やして試してみます。
node1と同じechoAgentをnode2とし、bypassAgentをnode3にします。

入力のinputsは今度は ["node1", "node2"]と2つ指定します。

node3がisResult: trueです。

version: 0.5
nodes:
  node1: 
    params:
      message: hello
    agent: echoAgent
  node2: 
    params:
      message: こんにちは
    agent: echoAgent
  node3: 
    agent: bypassAgent
    inputs: ["node1", "node2"]
    isResult: true
$ graphai echo3.yaml
{ node3: [ { message: 'hello' }, { message: 'こんにちは' } ] }

node3の結果として、2つの入力値がそのまま出力されます

このようにinputsを使って、複数のエージェントをつなげていくことができます。

inputsを持つagentは、入力となるnode1, node2のagentの実行結果を待ってから実行します。
入力のagentがデータベースに接続したり、APIを叩くような時間のかかる処理の場合でも、その前の処理が終わるのを待ってから実行されます。

Agentのdocument

各AgentのAgentFunctionInfoに書かれた情報を元に、機械的に生成したdocumentがAgentDocにあります。

AgentFunctionInfoについて

npmで配布されるagentは、実行されるagentの関数と、そのAgentの情報を含むAgentFunctionInfoの形式配布されます。

AgentFunctionInfoには、Agentの情報(nameやdescription)、サンプルの入出力、入力のスキーマなどの情報が含まれています。Sampleはドキュメントの自動生成やUnit testにも使われます。

Agentを作成してGraphAIで利用するときは、AgentFunctionInfoを作る必要があります。

(* 簡易的にagentを即時関数で使う方法や、AgentFunctionInfoのモックデータを使うなど、開発時には省略する方法もあります。別途説明予定)

Agentの開発方法

GraphAIで使うAgentの作り方の説明をします。

こちらのサンプルのagentのレポジトリがあります。
https://github.com/isamu/graphai_agent_template

Agentは、Agentの本体とそれをテストするテストコードで構成されます。
Agentの本体をsrc/以下(今回はsrc/sample_agent.ts), Agentのテストコードをtest/以下(今回はtests/test_agent.ts)に作ります。

今回作成するAgentはSampleAgentという名前です。
動作は、GraphAIからの入力値(設定ファイルのparams)のparamsと、前のagentからの入力値inputs、この2つの値をmergeしてobjectとして返す簡単なAgentです。

Agent

Agentの本体はこちらです。

import { AgentFunction } from "graphai";

export const sampleAgent: AgentFunction = async ({ params, inputs }) => {
  return { params, inputs };
};

Agentは必ずAgentFunctionの型で、非同期(async)な関数です。
最初の説明にあったように、paramsと入力をとって、それを返すだけのコードです。

どんなAgentも基本的にはこのように入力値を受け取って、何らかの結果を返す1つの関数です。

AgentFunctionInfo

Agentは、AgentFunctionInfoの型のデータとしてGraphAIにわたす必要があります。
AgentFunctionInfoはAgent本体とinputs/params/resultのサンプルの値、Agentのメタ情報を含みます。

src/sample_agent.tsに追加します。

// for test and document

以下にテストのサンプル値と、このAgentの情報(AgentFunctionInfo)を記載しています。
これらをsampleAgentInfoにまとめてexportします。

サンプル値はUnit Test(つまり、サンプルのドキュメントとUnit Testの両方を兼ねています)で使います。
後述する開発時にはこのサンプル値を使ってTestRunnerでテストをします。

Agentのファイルは、agentを含むパッケージ情報(AgentFunctionInfo)をdefaule exportします。

import { AgentFunctionInfo } from "graphai";

const sampleInput = [{message: "hello"}, {message: "test"}];
const sampleParams = { sample: "123" };
const sampleResult = { inputs: sampleInput, params: sampleParams };

const sampleAgentInfo: AgentFunctionInfo = {
  name: "sampleAgent",
  agent: sampleAgent,
  mock: sampleAgent,
  inputs: {
    type: "array",
  },
  samples: [
    {
      inputs: sampleInput,
      params: sampleParams,
      result: sampleResult,
    },
  ],
  description: "Sample agent",
  author: "isamu arimoto",
  repository: "https://github.com/isamu/graphai_doc",
  license: "MIT",
};

export default sampleAgentInfo;

複数のサンプル値を用意する場合は

  samples: [
    {
      inputs: sampleInput,
      params: sampleParams,
      result: sampleResult,
    },
    {
      inputs: sampleInput2,
      params: sampleParams2,
      result: sampleResult2,
    },
  ],

とします。
複数のサンプル値があるときはテストランナーは全てのケースをテストします。

Unit Test

Agentのパッケージの情報を使ってAgent単体のUnit Testをします。
GraphAIに含まれるagentTestRunnerにsampleAgentInfoを渡してUnit Testを実行します。
agentTestRunnerは、sampleAgentInfoに含まれるサンプル値を使ってAgentを実行します。
この関数は、内部でnode:testを使っています。
inputs, paramsのペアを使ってAgentを実行、結果とresultが一致すればテストは成功です。

import sampleAgentInfo from "@/sample_agent";
import { agentTestRunner } from "@receptron/test_utils";

agentTestRunner(sampleAgentInfo);

package.jsonにテスト実行のスクリプトがあるので

yarn run test

でテストを実行します。

開発

最初にAgentを開発するとき、このレポジトリをforkして使うと良いです。
Agent作成に必要な設定は package.json, eslintrc.js, .prettierrc, tsconfig.json に設定済みです

src/sample_agent.tsをベースに必要な実装を追加していき、期待すべきsamplesを更新、追加しながらUnit Testを動かします。
Unit TestをPassし、期待すべき動作がするようになればAgentは完成です。

Agentの受け取るデータ

sampleAgentでは、Agentの関数で{ params, inputs }を受け取りました。
実際はAgentFunctionContextの情報を受け取っています。

基本的にはこのうちの2つ(paramsと(inputs or namedInputs))を入力として受け取り、Agentの処理をします。結果はreturnで返します。
Agentの結果は、次に実行されるAgentのinputsなどで利用されます。(inputの記述方法は別途解説します)

inputsとinputsはGraphDataで

で、それ以外はNestedGraphで使う特別なデータなので、通常は利用しません。

Agentのdocument生成とテスト

AgentFunctionInfoに含まれる方法を使って、agentのunit testを実行する、documentを自動生成することが可能です。

Test runnerは、@receptron/test_utilsに含まれています。

import { agentTestRunner } from "@receptron/test_utils";

const main = async () => {
  await agentTestRunner(agentFunctionInfo);
};

main();

Documentの自動生成はこちらを参考に。今後、利用しやすい形式で提供予定です。

https://github.com/receptron/graphai/blob/main/packages/cli/src/docs.ts

AgentFilter

AgentFilterは、それぞれのComputed Nodeが実行される前に、なにかの処理を追加することができます。

@graphai/agent_filtersではhttpのstreamのためのfilterや、AgentFunctionInfoのinput schemaを使った入力値のvalidateを行うagent filterがあります。

namedInput Validator

namedInputの値をagentFunctionInfoのinput schemaの情報を元にvalidationします

Testコードでの利用例
https://github.com/isamu/graphai/blob/agentFilter/packages/agent_filters/tests/validation/test_agent_namedinput_validator.ts

import { GraphAI } from "graphai";
import * as agents from "@graphai/agents";
import { namedInputValidatorFilter } from "@graphai/agent_filters";

const agentFilters = [
  {
    name: "namedInputValidatorFilter",
    agent: namedInputValidatorFilter,
  },
];

const graph = new GraphAI(graph_data, agents, { agentFilters });
const results = await graph.run();

streamAgentFilterGenerator

filterParamsのstreamTokenCallback関数を通してstreamのデータを受信します。
サーバ、クライアントで利用可能です。

server 例

https://github.com/receptron/graphai_utils/blob/main/packages/express/src/express.ts

express server

    return async (req: express.Request, res: express.Response) => {
      res.setHeader("Content-Type", "text/event-stream;charset=utf-8");
      res.setHeader("Cache-Control", "no-cache, no-transform");
      res.setHeader("X-Accel-Buffering", "no");

      const callback = (context: AgentFunctionContext, token: string) => {
        if (token) {
          res.write(token);
        }
      };
      const streamAgentFilter = {
        name: "streamAgentFilter",
        agent: streamAgentFilterGenerator<string>(callback),
      };
      const agentFilters = [streamAgentFilter]

      const agentFilterRunner = agentFilterRunnerBuilder(agentFilters);
      const result = await agentFilterRunner(context, agent.agent);

      const json_data = JSON.stringify(result);
      res.write("___END___");
      res.write(json_data);
      return res.end();
   }

web client

https://github.com/isamu/graphai-stream-web/blob/main/src/views/Home.vue

const useAgentFilter = (callback: (context: AgentFunctionContext, data: T) => void) => {
  const streamAgentFilter = streamAgentFilterGenerator(callback);

  const agentFilters = [
    {
      name: "streamAgentFilter",
      agent: streamAgentFilter,
      agentIds: streamAgents,
    },
  ];
  return agentFilters;
};   

export default defineComponent({
  setup() {
    const streamingData = ref<Record<string, unknown>>({});

    const callback = (context: AgentFunctionContext, data: string) => {
      const { nodeId } = context.debugInfo;
      streamingData.value[nodeId] = (streamingData.value[nodeId] ?? "") + data;
    };
    const agentFilters = useAgentFilter(callback);
    
    const graphai = new GraphAI(graphData, agents, { agentFilters });
  }
})

httpAgentFilter

グラフのフローで、agentの実行をバイパスし、http経由でサーバのagentを実行します。
Webでのサンプルはこちらにあります。

https://github.com/isamu/graphai-stream-web/blob/main/src/views/Home.vue

agentFilterRunnerBuilder

GraphAIを使わないでagentFilterとagentを動かすRunnerです。
クライアントからサーバのagentを呼び出すときに、サーバ側で使います。
agentFilterやagentの単体テストでも利用可能です。

expressとtest時のサンプルはこちら。

express
https://github.com/receptron/graphai_utils/blob/main/packages/express/src/express.ts

test
https://github.com/isamu/graphai/blob/agentFilter/packages/agent_filters/tests/filters/test_filter_runner.ts