ShallowResearchを作ってGraphAIを学ぶ

## GraphAIとは
流行りのAIエージェントを、データフロープログラミングに基づいて、簡単に構築できるフレームワーク
https://zenn.dev/singularity/articles/graphai-about
https://github.com/receptron/graphai

## GraphAIの基本的な使い方
DeepResearchならぬShallowResearchを作って学んでいきましょう。GraphAIを使えばすぐできます。

### パッケージのインストール
[色々綺麗に分かれているんですが](https://www.npmjs.com/org/graphai)、とりあえず以下をいれればいい気がします。
```sh
// npm init
// Core部分
npm i graphai

// エージェントの実装
npm i @graphai/agents

// streaming処理とかできる
npm i @graphai/agent_filters

// CLIもあるので入れとく
npm i -g @receptron/graphai_cli
```

### AIエージェントにやらせたい処理をyamlかjsonで書く
AIエージェントを活用したプロダクト開発では、複数の処理を並列実行したり、データの準備完了を契機に次のアクションを実行するといった複雑なワークフローが必要になります。GraphAIがこれらの複雑さを隠蔽してくれるため、開発者は宣言的なアプローチでタスクを記述するだけで済むのが嬉しい点の１つです。

ShallowResearchのラフスケッチを、
1. ユーザーが調べたいことを教えてもらう
1. ユーザーが調べたいことに関してGoogle検索をする
1. 検索結果上位3件のサイトへ並列にアクセスしコンテンツを取ってくる
1. 取得した各コンテンツをLLMに要約してもらう
1. 各コンテンツの要約が揃ったらLLMにリサーチ結果をまとめてもらう

として、yamlを書いていきます。各エージェントの入力と出力は、[ドキュメント](https://github.com/receptron/graphai/blob/f45bb0e6f7f68b868fad4b7555176c0febc39fc3/docs/agentDocs/README.md)を参考に実装していきます。（3と4は統合できますが、今回は分けて書いていきます）


ちょこちょこコメント書いてるので、使い方のご参考に。
また、Google検索を行うために、以下を参考にAPIキーと検索エンジンIDを取得しておいてください。
[ref1](https://developers.google.com/custom-search/v1/overview?hl=ja)
[ref2](https://qiita.com/zak_y/items/42ca0f1ea14f7046108c)
```yaml:shallow_research.yaml
version: 0.5
nodes:
  # 1. ユーザーの検索クエリを取得
  # - agentキーに使いたいAgentの名前を書く
  # - textInputAgentで入力を受け取れる
  userQuery:
    agent: textInputAgent
    params:
      message: 調査したい事を教えてください：
      required: true

  # 2. Google検索を実行
  # - fetchAgentでGoogle検索APIを利用してJSON取ってくる
  searchResults:
    console:
      before: Google検索を実行します
    agent: fetchAgent
    inputs:
      url: https://www.googleapis.com/customsearch/v1
      queryParams:
        key: hoge # Google検索APIで取得したキーを書いてください
        cx: foo # Google検索APIで設定した検索エンジンIDを書いてください
        q: :userQuery.text # コロンを前につけて前のnodeの値を渡せます
        num: 3

  # 3. 各URLを並列に取得
  # mapAgentでMap-Reduce的なことができる
  contentFetcher:
    console:
      before: 3件のウェブページを並列取得中
    agent: mapAgent
    inputs:
      rows: :searchResults.items #ここで渡す配列の各要素に対して以下の処理が並列で走る
    graph:
      nodes:
        fetchPage:
          console:
            before: ページを取得中
          agent: vanillaFetchAgent # vanillaFetchAgentもある、違いは後で多分かく
          inputs:
            url: :row.link
          params:
            type: text
          isResult: true

  # 4. 取得したコンテンツをLLMによって要約
  contentSummarizer:
    console:
      before: 取得した各コンテンツを要約中
    agent: mapAgent
    inputs:
      rows: :contentFetcher
    params:
      compositeResult: true
    graph:
      nodes:
        summarize:
          console:
            before: コンテンツを要約中
          agent: openAIAgent
          inputs:
            prompt: :row.fetchPage
          params:
            model: gpt-4o-mini
            system: あなたはウェブページの内容を要約するエキスパートです。与えられたHTMLコンテンツから重要な情報を抽出し、300字以内でまとめてください。HTMLタグは無視して内容だけに集中してください。
        retrieveText:
          agent: copyAgent # textプロパティだけ取って文字列の配列を作る
          params:
            namedKey: text
          inputs:  
            text: :summarize.text
          isResult: true

  # 5. 各コンテンツの要約をもつ文字列配列をjoinする
  # - arrayJoinAgentを用いる、separaterでjoinの仕方を指定可能
  joinedWebContentString:
    agent: arrayJoinAgent
    params:
      separator: \n
    inputs:
      array: :contentSummarizer.retrieveText

  # 6. 最終レポート作成のプロンプトを準備
  reportPrompt:
    agent: stringTemplateAgent
    inputs:
      query: :userQuery.text
      summaries: :joinedWebContentString.text
    params:
      template: |-
        # リサーチリクエスト
        「${query}」というトピックについて調査レポートを作成してください。
        
        # 参考サイト
        以下は調査に使用した3件のウェブページの要約です：
        ${summaries}

  # 7. レポート生成
  finalReport:
    agent: openAIAgent
    inputs:
      prompt: :reportPrompt
    params:
      model: gpt-4o-mini
      stream: true
    isResult: true
```

## 実行する
LLMはOpenAIのAPIを利用しているので、実行する前に、以下などを参考に環境変数(OPENAI_API_KEY)を設定しておいてください。[ref](https://qiita.com/kurata04/items/a10bdc44cc0d1e62dad3)

(コンテキスト長の考慮してないので、クエリによってはエラーで落ちます)

### CLIによる実行 
```sh
graphai shallow_research.yaml
```

### TypeScriptでの実行
まず、書いたyamlをjsonへCLIで変換します(逆ももちろんできます)。ここら辺の内部実装を見て、自分で書いてもよいです。[ref1](https://github.com/receptron/graphai/blob/main/packages/cli/src/graphai_cli.ts#L51), [ref2](https://github.com/receptron/graphai/blob/main/packages/test_utils/src/file_utils.ts#L11)

```sh
graphai --json shallow_research.yaml
```

その後、以下のようなコードを書きます。
```typescript:index.ts
import { AgentFilterInfo, AgentFunctionContext, GraphAI } from "graphai";
import * as agents from "@graphai/agents";
import { streamAgentFilterGenerator } from "@graphai/agent_filters";

const graph_data = // ここに先ほどのyamlをjsonに変換したのをコピペ


const main = async () => {
    const streamingCallback = (context: AgentFunctionContext, data: string) => {
        process.stdout.write(data);
    };
    const streamAgentFilter = streamAgentFilterGenerator(streamingCallback);
    const agentFilters: AgentFilterInfo[] = [
        {
            name: "streamAgentFilter",
            agent: streamAgentFilter,
            agentIds: ["openAIAgent"]
        },
    ]
    const graph = new GraphAI(graph_data, agents, { agentFilters });
    const result = await graph.run();  
};
  
main();
```

ts-nodeを入れて実行します。
```sh
npm i ts-node
npx ts-node index.ts
```

こんな感じになりました。画質悪いので再度行ったもののログも貼っておきます。
![タイトルなし.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223338/ebd35fe9-6ec0-405b-9b43-46210599da05.gif)
<details><summary>クエリ「2025/03/22に開催されるハッカソン」の実行結果</summary>

 調査したい事を教えてください： 2025/03/22に開催されるハッカソン
Google検索を実行します
3件のウェブページを並列取得中
ページを取得中
ページを取得中
ページを取得中
取得した各コンテンツを要約中
コンテンツを要約中
コンテンツを要約中
コンテンツを要約中
# リサーチレポート: 2025年3月22日に開催されるハッカソン

## 概要
2025年3月22日、東京・渋谷のAbema Towersにて、二つの異なるハッカソンイベントが開催される予定です。これらのイベントは、参加者に技術的なスキルを活かし、創造的な解決策を提供する機会を提供します。各イベントの目的、参加要件、及び開催形式について詳しく調査しました。

### 1. GraphAI Contribution Fes 2025
- **日程**: 2025年3月22日～23日
- **場所**: 東京・渋谷、Abema Towers
- **主催者**: Singularity Society
- **テーマ**: AI-nativeな製品やサービスの企画・開発
- **参加資格**: エンジニア・非エンジニア問わず参加可能
- **賞品**: 最大4名の入賞者にはApple Mac mini（2024年モデル）が贈られる
- **参加方法**: 
  - 事前に主催者に必要情報を提出
  - Slackに参加
  - オンラインでの開発活動も可能で、成果物はGitHubを通じて提出
- **特徴**: アイデアソンとハッカソンの形式を併せ持ち、様々なバックグラウンドを持つ人々が協力して新しいアイデアを模索します。

### 2. Web Speed Hackathon 2025
- **日程**: 2025年3月22日～23日
- **場所**: 東京・渋谷、Abema Towers
- **テーマ**: 事前に準備されたWebアプリケーションのパフォーマンス改善
- **対象**: 一般参加者と学生
- **参加形式**: オンラインとオフラインの両方で参加可能
- **参加費**: 無料（オフライン参加者は事前登録が必要）
- **登録期間**: 2024年12月10日から2025年3月20日
- **イベント内容**:
  - フロントエンドやNode.jsに関する技術的なチューニングに挑戦
  - 「架空の動画配信サービス」を題材とした競技
  - ZoomやDiscordを利用した進行

## ハッカソンの重要性
ハッカソンは、異なるスキルを持つ人々が集まり、特定のテーマや課題に取り組むことで、革新的なアイデアや技術的なソリューションを生み出す場です。参加者はチームを組み、プロトタイプやビジネスプランを開発することが求められます。このようなイベントは、企業、大学、非営利団体などによって開催され、技術革新を促進する重要な役割を果たしています。

## 参加者へのアドバイス
- **事前準備**: 参加希望者は、各イベントのテーマに関連する知識や技術を事前に学習し、アイデアを考えておくことが推奨されます。
- **チーム編成**: 異なるスキルを持つメンバーとチームを組むことで、より多角的なアプローチが可能になります。
- **ネットワーキング**: 他の参加者やメンターとの交流を通じて、新たな知見を得る機会を最大限に活かしましょう。

## 結論
2025年3月22日に開催されるハッカソンは、技術者やアイデアを持つ人々が集まり、協力して新しいソリューションを生み出す貴重な機会です。参加者は、個々のスキルを生かしつつ、チームでの協力を通じて、より良い成果を目指すことが期待されています。%
　
</details>

プロンプトのせいかLLMがハッカソンの意義に関しても語ってたり、情報の信憑性(再実行したログの結果は嘘が混じっています)など改善は必要ですが、GraphAIの使い方を学ぶためのサンプルとしてはすぐ作れて楽しめました。

agent_filterはstreaming用に使っているだけなので、以下のシンプルなコードでもOKです。
streamingやagent_filtersでできることは以下などを参考にしてください。
[ref1](https://zenn.dev/singularity/articles/graphai-streaming)
[ref2](https://zenn.dev/singularity/articles/graphai-agent-filter)
```typescript
    const graph = new GraphAI(graph_data, agents);
    const result = await graph.run();
    console.log(result)
```

streamingに関してですが、コードは[ここ](https://github.com/receptron/graphai/blob/10e812a0292e740d33727902f460b1f0ab900460/packages/agent_filters/src/filters/stream.ts#L9)らへんが参考になります。


## Q&A
Q. Yaml書く時、どうやって次の処理にデータを渡すの？
A. コロン+キーで渡せます。GOD Formatというそうです([ref](https://zenn.dev/singularity/articles/graphai-god-format))

Q. 独自のエージェントは作れるの？
A. 作れるようです([ref](https://zenn.dev/singularity/articles/graphai-about#agent%E3%81%AE%E9%96%8B%E7%99%BA%E6%96%B9%E6%B3%95))

## トラブル解決
1. Streamingがうまくいかない
=> isResult: trueにする,　paramsのstream: trueにする
2. なんかうまくいかない
=> mapAgent使う時のgraph配下でisResult: trueを書いているか？
=> その他随時更新