Prompt Flowでプロンプトを定量的に評価する

# やること

[前回](https://qiita.com/sakue_103/items/2c1db6c0c8b6a9145abe
)、「 **決算書PDFから必要なデータを抽出し、Excelに落とし込む** 」というタスクを対象として、プロンプトフローで処理実装を行いました。![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/f4682667-cc97-a1f4-72c8-90f1197086a4.png)

作成したフローの全体像は以下の通りです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/e3241ddb-c46d-6804-79cd-51d0b7ae5af0.png)

各コンポーネントの詳細は以下の通りです。

|ID|種別|処理概要|出力形式|
|--|--|--|--|
|find_keywords|Python|入力ファイル名に該当するファイルをBlobからダウンロードし、読み込み|string|
|extract_target_data|プロンプト|分析対象のデータに該当する文字列を抽出し、表形式として出力|string|
|extract_data_function_calling|Python|表形式から数値データなどを抽出|object|
|create_excel|Python|抽出したデータを用いてExcelに追記し、Blobにアップロード|string|


このフローに対して、何らかの評価指標を用いて良し悪しを数値で表すことがゴールです。
また次回の記事では、評価指標を基に```extract_target_data```のプロンプトエンジニアリングを実施していきたいと思います。



評価をするにあたりフローを少し変更します。具体的にはoutputを変更します。
今回評価したいのは「 **PDFファイルから期待通りのデータを抽出できているか？** 」といった内容のため、```extract_data_function_calling```の出力であるオブジェクトを評価に使用します。

フローを編集した結果は以下の通りです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/06d82ca3-3436-c001-1572-9e8c737752a7.png)



# 評価方法について

今回の評価方法では、「 **入力と期待する出力** 」の組み合わせを基に精度を算出します。
精度算出には評価用のフローを使用します。
そのため、**評価対象フローと評価用フローの2つのフローを作成**することになります。

## 評価対象フローの作成

評価対象フローはさきほど示したものとなります。「 **決算書PDFファイルからデータを抽出し、財務分析用Excelを作成** 」といった処理を行ってくれます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/06d82ca3-3436-c001-1572-9e8c737752a7.png)

このフローの入力と出力は以下の通りです。

|種別|概要|型|
|--|--|--|
|入力| 決算書PDFファイルの名前|string|
|出力| PDFから抽出できたデータ|object|

以下はデータの一例です。

```python:
# 入力例
file_name = "test.pdf"

# 出力例
prompt_output = {
  "予想修正の有無": "無",
  "会社名": "TEST株式会社",
  "営業活動キャッシュフロー": null,
  "固定負債合計": 134200,
  "固定資産合計": 152068,
  "売上原価": 78552,
  "売上高": 153506,
  "投資活動キャッシュフロー": null,
  "株式分割の有無": "無",
  "決算期": "2023年5月期 第3四半期",
  "流動負債合計": 111512,
  "流動資産合計": 84800,
  "純資産合計": 125355,
  "経常利益": 51649,
  "財務活動キャッシュフロー": null,
  "販管費": 23397
}
```

## 評価用データの作成

評価用のデータを作成しておきます。評価用データの形式は、**csv/tsv/json/jsonl**がサポートされています。
今回は以下のようなtsvファイルを用意しました。
```file_name=入力```  ,  ```ground_truth=期待する出力```となります。
(PDFファイルを私が確認し、必要な情報を抽出したものがground_truthです。)

```js:評価用のtsvファイル
file_name	ground_truth
kddi_2023_3qc_IRAfOL.pdf	{"予想修正の有無": "無","会社名": "ＫＤＤＩ株式会社", "営業活動キャッシュフロー": 842440, "固定負債合計": 1523555, "固定資産合計": 7975619, "売上原価": 2351364, "売上高": 4182893, "投資活動キャッシュフロー": -567964, "株式分割の有無": "無", "決算期": "2023年3月期", "流動負債合計": 4562389, "流動資産合計": 3637517, "純資産合計": 5527192, "経常利益": 843420, "財務活動キャッシュフロー": -524954, "販管費": 1037312}
fy23q3-tanshin-jp.pdf	{"予想修正の有無": "無", "会社名": "日本オラクル株式会社", "営業活動キャッシュフロー": null, "固定負債合計": 0, "固定資産合計": 151256, "売上原価": 85756, "売上高": 163226, "投資活動キャッシュフロー": null, "株式分割の有無": "無", "決算期": "2023年5月期 第3四半期", "流動負債合計": 92089, "流動資産合計": 81273, "純資産合計": 140439, "経常利益": 52815, "財務活動キャッシュフロー": null, "販管費": 24859}
```

## 評価指標の定義

今回のタスクでは「 **どのくらい正確に情報を抽出できているか？** 」を評価したいため、それに合わせた簡易的な評価指標を定義します。

>本番で使うとなると、指標の定義は重要なタスクになりそうです。
今回は簡易的な指標で進めていきます。

今回、評価対象フローは「 **PDFファイルから抽出したデータを辞書型で出力** 」するので、以下の2つの値を用いることにします。

|指標名|概要|
|--|--|
|correct_keys_percentage| 正しく抽出できたキーの割合|
|correct_values_percentage| 正しく抽出できた値の割合|

LLMの特性上、全てのキーを抽出してくれなかったり、正しい値を抽出してくれなかったりするので、上記の指標を用いてどのくらい正確に抽出してくれているかを数値化します。

具体例を以下に示します。

今回期待する辞書型のキーの数は16個です。以下のような場合、```correct_keys_percentage = 3/16*100 = 約19%```となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/7aff4499-5654-e8c7-7c90-5ca66a073057.png)

また、期待する値・抽出できた値が以下の場合、```correct_values_percentage = 1/3*100 = 約33%```となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/4c1b7b56-4dd5-480e-3ba0-7412965fd02f.png)


上記の例の場合、各指標は以下の表のようになります。数値化することで「 **そもそも抽出できていないキー(データ)が多くあるし、抽出できていても値が間違っているな** 」と気づけますね。
|指標名|値|
|--|--|
|correct_keys_percentage| 18%|
|correct_values_percentage| 33%|


プロンプトエンジニアリングを行った結果、これらの値が向上すればよい修正・そうでなければ悪い修正といった判断ができます。


# 評価ロジックの実装

評価用フローのテンプレートを少しだけカスタマイズしたもので評価ロジックを実装していきます。

## テンプレートの作成

Machine Learning Studioから、**プロンプトフロー**をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/9f09e2e9-9b9f-3bd9-9e9d-d649a8eb6966.png)

**作成**をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/b172e34a-f7df-93c6-50d1-478b3c1e64ea.png)

評価フローのテンプレートに該当するフローの**作成**ボタンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/25b063e4-d617-e54e-8c23-937fa654678f.png)


上記の手順で作成される評価用フローの全体像は以下の通りです。これらのコンポーネントを評価指標に合わせてカスタマイズしていきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/a2f58311-289f-761e-a1b8-f322ca12eed1.png)

右側のPythonコンポーネント(line_process)は、評価用データの各行を一つずつ処理します。
具体的には、コンポーネントへの入力を用いて、評価指標を算出するための値を作成します。

左側のPythonコンポーネント(aggregate)は、バリアントごとに評価指標を算出します。
今回のケースでは、各バリアントごとに **correct_keys_percentage** , **correct_values_percentage** を計算します。


以降では、各コンポーネントの詳細を見ていきます。


## line_process: 評価に用いる値の作成

評価用フローには以下の変数が入力として渡されます。

|入力変数名|概要|
|---|---|
|groundtruth|評価用データでground_truthとして指定したもの|
|prediction|評価対象フローの出力|
|variant_id|バリアントの識別子|
|line_number|指定したデータの行番号|

これらのうち、```groundtruth```と```prediction```を使用します。
バリアントごとの評価を一括で行えるため複数のプロンプトを同時に評価可能ですが、今回はバリアントが1つなので```variant_id```は使用しません。

評価指標である「**正しく抽出できたキーの割合**」と「**正しく抽出できた値の割合**」を計算するために必要な値を作成できるように処理内容を編集します。
具体的には、以下のような辞書型を作成する処理となります。

- resultオブジェクト
    - correct_keys： 正しく抽出できたキーの数
    - incorrect_keys： 正しく抽出できなかったキーの数
    - correct_values： 正しく抽出できた値の数
    - incorrect_values： 正しく抽出できなかった値の数

```py: line_processの処理内容
from promptflow import tool
import json

@tool
def line_process(groundtruth: str, prediction: dict):
    # 文字列を辞書型に変換
    ground_truth = json.loads(groundtruth)
    predict = prediction

    # 評価指標を初期化
    correct_keys = 0     # 正しいキーの数
    incorrect_keys = 0   # 誤ったキーの数
    correct_values = 0   # 正しい値の数
    incorrect_values = 0 # 誤った値の数

    # キーと値の検証
    for key, value in ground_truth.items():
        if key in predict.keys():
            correct_keys += 1
            if value == predict[key]:
                correct_values += 1
            else:
                incorrect_values += 1
        else:
            incorrect_keys += 1

    # 結果の辞書を生成
    result = {
        "correct_keys": correct_keys,
        "incorrect_keys": incorrect_keys,
        "correct_values": correct_values,
        "incorrect_values": incorrect_values,
    }

    return result
```


## aggregate: 評価指標の算出とログ出力

全てのデータを集計するためのPythonコンポーネントです。
今回のケースでは、```line_process```で作成した値を用いて評価指標を計算し、ログ出力する処理を担わせています。

入力は以下の通りです。同じ長さのリスト達が渡されます。

|入力変数名|概要|形式|
|---|---|---|
|processed_result|line_processで生成したresultオブジェクトのリスト|List|
|variant_ids|バリアントIDのリスト|List|
|line_numbers|評価用データの行番号リスト|List|

評価用データが2件 & バリアントが1つの場合、以下のような入力となります。

```sh:aggregateコンポーネントへの入力例
processed_results: [ { "correct_keys": 5, "incorrect_keys": 11, "correct_values": 2, "incorrect_values": 3 }, { "correct_keys": 16, "incorrect_keys": 0, "correct_values": 7, "incorrect_values": 9 } ]
variant_ids: [ "variant_0", "variant_0" ]
line_numbers: [ 0, 1 ]
```

これらのリストを用いてバリアントごとに評価結果を集約する処理を記述します。


```py:aggregateの処理内容
from typing import List
from promptflow import tool
from promptflow import log_metric

@tool
def aggregate(processed_results: List[dict], variant_ids: List[str], line_numbers: List[int]):

    print(variant_ids)
    print(processed_results)

    # 評価指標の定義
    metrics = ['correct_keys_percentage', 'correct_values_percentage']

    # 集計結果の初期化
    aggregated_results = {id: {metric: 0 for metric in metrics} for id in variant_ids}

    # Variantごとに評価指標を集計
    for variant_id, result in zip(variant_ids, processed_results):
        
        # 全てのキーと値の数を算出
        total_keys = result['correct_keys'] + result['incorrect_keys']
        total_values = result['correct_values'] + result['incorrect_values']

        # 正解率を計算 (ゼロ除算を避ける)
        correct_keys_percentage = (result['correct_keys'] / total_keys) * 100 if total_keys > 0 else 0
        correct_values_percentage = (result['correct_values'] / total_values) * 100 if total_values > 0 else 0

        # メトリクス名と値をマッピング
        percentages = {
            'correct_keys_percentage': correct_keys_percentage,
            'correct_values_percentage': correct_values_percentage
        }
        
        # 各評価指標の値を合算
        for metric in metrics:
            aggregated_results[variant_id][metric] += percentages[metric]

    for variant_id in set(variant_ids):
        for metric in metrics:
            # 平均を計算
            aggregated_results[variant_id][metric] /= ( len(line_numbers) / len(set(variant_ids)) )

            # 評価指標をログ出力
            log_metric(key=metric, value=aggregated_results[variant_id][metric], variant_id=variant_id)

    return aggregated_results
```




# 評価の実行

Machine Learning Studioから、**プロンプトフロー**をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/9f09e2e9-9b9f-3bd9-9e9d-d649a8eb6966.png)

**評価対象フロー**をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/2001d207-4bec-cb23-a77e-c7b93c4c99d8.png)

右上の**一括テスト**をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/ce40f09c-57f4-d75d-6959-f442d4cdfd48.png)

**ランタイム**を選択し、**Upload new data**を指定します。

>データにはさきほど作成した評価用のtsvファイルを指定しています。
ランタイムについては、[前回作成済みのもの]((https://qiita.com/sakue_103/items/2c1db6c0c8b6a9145abe#-%E7%8B%AC%E8%87%AA%E3%81%AEpython%E7%92%B0%E5%A2%83%E3%82%92%E7%94%A8%E3%81%84%E3%81%A6prompt-flow%E3%81%AE%E3%83%A9%E3%83%B3%E3%82%BF%E3%82%A4%E3%83%A0%E3%82%92%E4%BD%9C%E6%88%90))を使用しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/153e9842-3712-b228-363b-9576343366ba.png)

アップロードしたデータが正しく読み込まれているか確認した後、**次へ**をクリックします。（画面下部に自動的に表示されます）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/844eb2f2-1ac5-5010-7f69-a033ef6a060f.png)

**評価方法**を選択します。さきほど作成した評価用フロー(eval-flow)を指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/647b5077-9d19-db7e-eeb7-3b3910f0b18c.png)

**評価入力のマッピング**を設定します。「```ground_truth = 評価用データのground_truth```、 ```prediction = 評価対象フローの出力```」となるように指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/7ee95c64-f571-23ea-e917-ea33aaa3e818.png)

**次へ**をクリックすると、設定一覧が表示されます。**送信**をクリックすると評価が実行されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/d3889055-1add-ce15-018a-dd68744d65fd.png)



# 評価結果の確認

画面左側メニューの**Job**をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/978f764a-5341-fdc9-e897-04ce7dd07391.png)

評価対象フロー(extract_financial_data)の**最新のジョブ**をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/00c30015-e5c8-e2c0-34ea-6e39fa939023.png)


以下のような画面に遷移します。**出力**タブでは評価対象フローの出力を確認でき、**メトリック**タブでは評価指標の確認ができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/b60a697b-928d-28fb-7192-158a4a64e335.png)


**メトリック**タブをクリックし、評価指標を確認します。
正しく抽出できたキーの割合は```100%```、正しく抽出できた値の割合は```43.75%```でした。
つまり、「 **必要なキーは全て生成(抽出)できているが、それらのキーのうち半分程度は正しい値を抽出できていない** 」です。改善の余地が大いにありますね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/e9fc0060-3665-830a-065e-f0e83d0d22f8.png)

**出力**タブに移動し、評価対象フローが実際にどのような出力をしたのかを確認してみます。
一行目のデータの**output_prompt**を見ると、値が全て0になっています。これが原因でcorrect_values_percentageが下がっているようですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/9f83fc9a-4cf3-9faf-da54-e106917c64b5.png)

# まとめ
今回はPrompt Flowを用いてフローの評価を行いました。
数字でプロンプトの良し悪しを判断できるようになると意思決定がはやくなりそうだなあと思いながら色々試してました。

また、プロンプトエンジニアリングする際、「 **手探り感がすごいなあ** 」とか「 **局所最適になってる気がするなあ** 」と思っています。
これらの課題をPrompt Flowでは解決できそうです。次回はこの辺りに触れようかと思います。
具体的には、以下のような内容を深掘りたいですね。

- Variantsを使って、複数のプロンプトを並列に試せるようにする
- 全てのVariantsにテストデータを一括で流し込んで並列に評価を行う
- ベターなプロンプトを選定、改良する


# 付録. ソースコード・プロンプト

今回使用したソースコード・プロンプトを置いておきます。

<details><summary>評価対象フロー関連</summary>

```py:find_keywords
from promptflow import tool
from azure.storage.blob import BlobServiceClient
import re
import os
import pypdf

# Blob Storageの接続情報
connection_string = os.getenv("AZURE_STORAGE_CONNECTION_STRING")
account_name = re.search("AccountName=(.*?);", connection_string).group(1)
account_url = f"https://{account_name}.blob.core.windows.net"
container_name = os.getenv("STORAGE_CONTAINER_NAME")

# Blob Storageのコンテナに接続
blob_service_client = BlobServiceClient.from_connection_string(connection_string)
container_client = blob_service_client.get_container_client(container_name)

# 一時的にPDFファイルを保存
tmp_file_path = './temp.pdf'

@tool
def main(file_name: str) -> str:
  # Blobからファイルをダウンロードし、一時ファイルに保存
  download_file(file_name)

  # 特定のキーワードが含まれるページのみを抽出
  pdf_text = extract_text()

  return pdf_text


def download_file(file_name:str):
  # Blobからファイルをダウンロード
  blob_client = container_client.get_blob_client(file_name)
  download_stream = blob_client.download_blob()
  content_binary = download_stream.readall()

  # バイナリデータを一時的に保存する
  with open(tmp_file_path, 'wb') as f:
      f.write(content_binary)

def extract_text():
  reader1 = pypdf.PdfReader(tmp_file_path)

  # 抽出するキーワード
  keywords = [
      r"上場会社名", 
      r"営業外.益", 
      r"営業活動によるキャッシュ・フロー",
      # r"発行.*株数"
      r"(固定負債合計|固定負債計|非流動資産合計|非流動資産計)",
      r"(流動負債合計|流動負債計)",
      r"(流動資産合計|流動資産計)",
      r"(資本合計|純資産合計)",
  ]

  # マッチしたページとキーワードを格納するリスト
  match_pages = []
  match_keywords = []

  # テキストを抽出
  pdf_text = ""
  for i in range(len(reader1.pages)):
      page_text = reader1.pages[i].extract_text()
      
      for keyword in keywords:
          # 上場会社名が抽出できない場合は、スペースを削除
          if keyword == r"上場会社名" and re.search(keyword, page_text) == None:
              page_text = _remove_space(page_text)

          # テキスト整形処理
          page_text = _replace_number(page_text)
          page_text = _process_data(page_text)

          # キーワードが含まれ、かつ、既に抽出済みのページでなければ、抽出
          if re.search(keyword, page_text) and keyword not in match_keywords and i not in match_pages:
              match_pages.append(i)
              match_keywords.append(keyword)
              print(keyword)
              print("page:", i, "\n")
              # 1ページ目だけ文字列をカット
              if i == 0:
                  page_text = page_text[:200]
              pdf_text += page_text + "\n\n"
          else:
              pass

  return pdf_text

# 数字データを抽出するための整形処理
def _process_data(text):
  # 正規表現パターンを使って、小数点の後の数字の後ろにスペースを追加
  result = re.sub(r"\.(\d)", r".\1 ", text)

  # 正規表現パターンを使って、コンマの後の3つの数字にスペースを追加
  result = re.sub(r",(\d{3})", r",\1 ", result)

  # 正規表現パターンを使って、コンマの後に数字が4つ以上続く場合は、最後の3つの数字の後ろにスペースを追加
  result = re.sub(r",(\d{3})(\d)", r",\1 \2", result)

  # 売上が兆の場合は、無駄なスペースが入るので削除
  result = result.replace(" ,", ",")

  # "yyyy年m月期第q四半期" を空文字列に置換して切り落とす
  result = re.sub(r"\d{4}年\d{1,2}月期第\d{1}四半期", "", result)

  # "通期"を空文字列に置換して切り落とす
  result = result.replace("通期", "")

  return result


# 数字部分が全角になっている場合は半角数字に変換
def _replace_number(text:str) -> str:
  text = text.replace("０", "0")
  text = text.replace("１", "1")
  text = text.replace("２", "2")
  text = text.replace("３", "3")
  text = text.replace("４", "4")
  text = text.replace("５", "5")
  text = text.replace("６", "6")
  text = text.replace("７", "7")
  text = text.replace("８", "8")
  text = text.replace("９", "9")

  return text


# 無駄なスペースを削除
def _remove_space(text):
  result = text.replace(" ", "")

  return result
```



```sh:extract_target_data
system:
あなたはテキストからデータを抽出するAIです。
出力は表形式で、抽出したデータを含む表のみを出力します。

user:
# 変数の定義
${バランスシート}：流動資産合計・固定(非流動)資産合計・流動負債合計・固定(非流動)負債合計・純資産(資本)合計
${損益計算書}：売上高・売上原価・販管費・経常利益
${キャッシュフロー計算書}：営業活動・投資活動・財務活動

# 指示
「会社名と決算期」に該当しそうなデータを抽出する
今期の${バランスシート}に該当しそうなデータを抽出する
今期の${損益計算書}に該当しそうなデータを抽出する
今期の${キャッシュフロー計算書}に該当しそうなデータを抽出する
「直近に公表されている予想から修正があるか」抽出する
「株式分割があるか」抽出する

### 

{{user_input}}
```


```py:extract_data_function_calling
from promptflow import tool
import os
import openai
import json

# Azure OpenAIの設定
openai.api_type = "azure"
openai.api_key = os.getenv("AZURE_OPENAI_API_KEY")
openai.api_base = os.getenv("AZURE_OPENAI_ENDPOINT")
openai.api_version = os.getenv("AZURE_OPENAI_API_VERSION")


# システムのプロンプト
SYSTEM_PROMPT = """
あなたはユーザを助けるアシスタントです。
ユーザの入力に正しく回答を出力するために、ステップバイステップで慎重に考えることができます。
まずはゴール達成のためになにが必要かを考え、自分の思考と行動を説明します。
"""

messages = [
    {"role":"system", "content": SYSTEM_PROMPT},
]

@tool
def main(input1: str) -> dict:
  # Function Callingを使って、必要なデータを抽出
  result = exec_function_calling(input1)

  # データをチェック
  result = check_data(result)

  return result


def exec_function_calling(input1:str):
  # 使用したい関数を定義
  functions_metadata = [CreateFinancialDataDict.metadata]
  
  # ユーザの入力にサフィックスを追加しメッセージに追加
  SUFFIX = """
  ###

  上記のテキストから財務データの辞書を生成して。
  """
  messages.append({"role": "user", "content": input1 + SUFFIX})

  # 推論実行
  response = openai.ChatCompletion.create(
      engine = os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME"),
      messages = messages,
      functions=functions_metadata,
      function_call="auto",
      temperature=0,
  )

  # 関数呼び出し有無を判定
  if response["choices"][0]["message"].get("function_call"):
      msg = response["choices"][0]["message"]
      return json.loads(msg["function_call"]["arguments"])
  else:
      return None
  

def check_data(data:dict) -> dict:
  # キーを満たしているか確認
  keys = [
      "営業活動キャッシュフロー",
      "投資活動キャッシュフロー",
      "財務活動キャッシュフロー",
  ]

  # キーがない場合は値をNoneで追加
  for key in keys:
      if key not in data.keys():
          data[key] = None

  return data

# Function calling用関数のクラスを定義
class CreateFinancialDataDict:
  metadata = {
      "name": "create_financial_data_dict",
      "description": "会社の財務データを格納する辞書を作成する関数",
      "parameters": {
          "type": "object",
          "properties": {
              "会社名": {
                  "type": "string",
                  "description": "会社の名前"
              },
              "決算期": {
                  "type": "string",
                  "description": "会社の決算期"
              },
              "売上高": {
                  "type": "number",
                  "description": "会社の売上高"
              },
              "売上原価": {
                  "type": "number",
                  "description": "会社の売上原価"
              },
              "販管費": {
                  "type": "number",
                  "description": "会社の販管費"
              },
              "経常利益": {
                  "type": "number",
                  "description": "会社の経常利益"
              },
              "流動資産合計": {
                  "type": "number",
                  "description": "会社の流動資産合計"
              },
              "固定資産合計": {
                  "type": "number",
                  "description": "会社の固定資産合計"
              },
              "流動負債合計": {
                  "type": "number",
                  "description": "会社の流動負債合計"
              },
              "固定負債合計": {
                  "type": "number",
                  "description": "会社の固定負債合計"
              },
              "純資産合計": {
                  "type": "number",
                  "description": "会社の純資産合計"
              },
              "営業活動キャッシュフロー": {
                  "type": ["number", "null"],
                  "description": "会社の営業活動キャッシュフロー"
              },
              "投資活動キャッシュフロー": {
                  "type": ["number", "null"],
                  "description": "会社の投資活動キャッシュフロー"
              },
              "財務活動キャッシュフロー": {
                  "type": ["number", "null"],
                  "description": "会社の財務活動キャッシュフロー"
              },
              "株式分割の有無": {
                  "type": "string",
                  "enum": ["有", "無"],
                  "description": "株式分割の有無"
              },
              "予想修正の有無": {
                  "type": "string",
                  "enum": ["有", "無"],
                  "description": "予想修正の有無"
              }
          },
          "required": [
              "会社名", 
              "決算期", 
              "売上高", 
              "売上原価", 
              "販管費", 
              "経常利益", 
              "流動資産合計", 
              "固定資産合計", 
              "流動負債合計", 
              "固定負債合計", 
              "純資産合計", 
              "株式分割の有無",
              "予想修正の有無"
          ]
      }
  }
```



```py:create_excel
from promptflow import tool
import pandas as pd
from openpyxl import Workbook
from openpyxl.utils.dataframe import dataframe_to_rows
from openpyxl import load_workbook
from openpyxl.styles import Font
from azure.storage.blob import BlobServiceClient
import os
import re

# Blob Storageの接続情報
connection_string = os.getenv("AZURE_STORAGE_CONNECTION_STRING")
account_name = re.search("AccountName=(.*?);", connection_string).group(1)
account_url = f"https://{account_name}.blob.core.windows.net"
container_name = os.getenv("STORAGE_CONTAINER_NAME")

# Blob Storageのコンテナに接続
blob_service_client = BlobServiceClient.from_connection_string(connection_string)
container_client = blob_service_client.get_container_client(container_name)

# ローカルに保存する際のパス
excel_path = "financial_data2.xlsx"

@tool
def main(data: dict) -> str:
  # Excelをローカルファイルとして作成
  create_excel(data, excel_path)

  # Blobにアップロード
  blob_url = upload_to_blob(excel_path)

  return blob_url

def create_excel(data:dict, file_path:str):
  # データフレームを作成
  df_new = pd.DataFrame([data])
  
  # Excelが存在する場合は読み込み、存在しない場合は新規作成
  try:
      wb = load_workbook(file_path)
  except FileNotFoundError:
      wb = Workbook()

  # 会社名のシートが存在する場合は、そのシートに追記
  if data["会社名"] in wb.sheetnames:
      ws = wb[data["会社名"]]

      # データを追記
      for r in dataframe_to_rows(df_new, index=False, header=False):
          ws.append(r)
  else:
      # 会社名のシートが存在しない場合は、新規作成
      ws = wb.create_sheet(data["会社名"])
      
      # データを追記
      for r in dataframe_to_rows(df_new, index=False, header=True):
          ws.append(r)
      
      # ヘッダーのフォントを太字にする
      for cell in ws[1]:
          cell.font = Font(bold=True)

  wb.save(excel_path)


def upload_to_blob(file_path:str):
  with open(file_path, "rb") as data:
    blob_client = container_client.upload_blob(file_path, data=data, overwrite=True)
    blob_path = blob_client.get_blob_properties()["name"]
    blob_url = f"{account_url}/{container_name}/{blob_path}"
    
  return blob_url
```

</details>


<details><summary>評価用フロー関連</summary>

```py:line_process
from promptflow import tool
import json

@tool
def line_process(groundtruth: str, prediction: dict):
    # 文字列を辞書型に変換
    ground_truth = json.loads(groundtruth)
    predict = prediction

    # 評価指標を初期化
    correct_keys = 0     # 正しいキーの数
    incorrect_keys = 0   # 誤ったキーの数
    correct_values = 0   # 正しい値の数
    incorrect_values = 0 # 誤った値の数

    # キーと値の検証
    for key, value in ground_truth.items():
        if key in predict.keys():
            correct_keys += 1
            if value == predict[key]:
                correct_values += 1
            else:
                incorrect_values += 1
        else:
            incorrect_keys += 1

    # 結果の辞書を生成
    result = {
        "correct_keys": correct_keys,
        "incorrect_keys": incorrect_keys,
        "correct_values": correct_values,
        "incorrect_values": incorrect_values,
    }

    return result
```


```py:aggregete
from typing import List
from promptflow import tool
from promptflow import log_metric

@tool
def aggregate(processed_results: List[dict], variant_ids: List[str], line_numbers: List[int]):

    print(variant_ids)
    print(processed_results)

    # 評価指標の定義
    metrics = ['correct_keys_percentage', 'correct_values_percentage']

    # 集計結果の初期化
    aggregated_results = {id: {metric: 0 for metric in metrics} for id in variant_ids}

    # Variantごとに評価指標を集計
    for variant_id, result in zip(variant_ids, processed_results):
        
        # 全てのキーと値の数を算出
        total_keys = result['correct_keys'] + result['incorrect_keys']
        total_values = result['correct_values'] + result['incorrect_values']

        # 正解率を計算 (ゼロ除算を避ける)
        correct_keys_percentage = (result['correct_keys'] / total_keys) * 100 if total_keys > 0 else 0
        correct_values_percentage = (result['correct_values'] / total_values) * 100 if total_values > 0 else 0

        # メトリクス名と値をマッピング
        percentages = {
            'correct_keys_percentage': correct_keys_percentage,
            'correct_values_percentage': correct_values_percentage
        }
        
        # 各評価指標の値を合算
        for metric in metrics:
            aggregated_results[variant_id][metric] += percentages[metric]

    for variant_id in set(variant_ids):
        for metric in metrics:
            # 平均を計算
            aggregated_results[variant_id][metric] /= ( len(line_numbers) / len(set(variant_ids)) )

            # 評価指標をログ出力
            log_metric(key=metric, value=aggregated_results[variant_id][metric], variant_id=variant_id)

    return aggregated_results
```

</details>