Gemini APIを使ってPower Appsのドキュメント作成をしてみる

# [Gemini 1.5 Pro](https://developers.googleblog.com/en/new-features-for-the-gemini-api-and-google-ai-studio/)

2Mコンテキストに対応しているという[Gemini 1.5 Pro](https://developers.googleblog.com/en/new-features-for-the-gemini-api-and-google-ai-studio/)。

https://developers.googleblog.com/en/new-features-for-the-gemini-api-and-google-ai-studio/

どれだけの容量なのか・・・と調査してみると[OpenAIのHP](https://openai.com/api/pricing/)が参考になりました。

> You can think of tokens as pieces of words used for natural language processing. For English text, 1 token is approximately 4 characters or 0.75 words. As a point of reference, the collected works of Shakespeare are about 900,000 words or 1.2M tokens
> To learn more about how tokens work and estimate your usage…
> - Experiment with our interactive Tokenizer tool(opens in a new window).
>  - Log in to your account and enter text into the Playground. The counter in the footer will display how many tokens are in your text.
>> [What’s a token? - OpenAI](https://openai.com/api/pricing/)

シェイクスピアの作品集で1.2M。それをさばけるのですから恐ろしい量です。

大量の文字の処理となると何が挙げられるか。
自分が思いつくものは、[先日からプレビューで実行できているPower AppsのYAMLコード](https://www.microsoft.com/en-us/power-platform/blog/power-apps/introducing-code-view-in-power-apps-studio-public-preview/)です。

https://www.microsoft.com/en-us/power-platform/blog/power-apps/introducing-code-view-in-power-apps-studio-public-preview/

コードで可視化できることは便利ですが、全スクリーンのコードを抜き出すと容量が多くなります。

今回は*Power Apps*のテンプレートである*Service Desk*をもとにドキュメント作成ができないか、*Python*と*Gemini API*で試してみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/d851103d-c4da-7428-7f61-daab526fcda8.png)

# *Service Desk*のトークン数
[OpenAIのTokenizer Tool](https://platform.openai.com/tokenizer)で*Power Apps*のテンプレートの*Service Desk*の*YAML*コードを入れてみると
- Tokens 28,325
- Characters 114,929

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/c79f0d75-7c84-d769-ff04-8f9388190917.png)


と出てきます。うーむ、案外ほかのモデルでも行けそうですね🧐
とりあえず今回はコチラの情報をGemini APIでさばいてみてみましょう。

# Vertex AIでGemini APIを実行する
今回は*Google Cloud Platform*で*Vertex AI*を使用します。
*Vertex AI*で`Vertex AI Studio`をクリックすると、`Gemini 1.5 Pro`はスグに使うことができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/6dc90a3f-4845-4cb9-f37a-dd0b3a5a7f22.png)

# 事前に検証した結果
ほかのアプリで依存関係を表にしてほしい！というプロンプトを送った場合でも、`Gemini`は上手い具合に処理してくれていました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Gemini 1.5 Proやるなー<br>Power Appsのテンプレの相関関係も表にできる <a href="https://t.co/sxexXNHYSB">pic.twitter.com/sxexXNHYSB</a></p>&mdash; 出戻りガツオ🐟 Microsoft MVP (@DemodoriGatsuo) <a href="https://twitter.com/DemodoriGatsuo/status/1805988338526199970?ref_src=twsrc%5Etfw">June 26, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

ホントに恐るべしです。
これを更にAPIで実行出来たら夢が広がります。

## Pythonで使用するために
`Vertex AI Studio`の右上に`Python`のサンプルコードもあるため、入口は入りやすいです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/e1185a38-c9a0-49aa-66ae-1bf22152673b.png)

しかし、コードを試すために、まずは`Google Cloud CLI `が必要です。
[cloud CLI をインストールする](https://cloud.google.com/sdk/docs/install?hl=ja)を参考に`gcloud CLI`を準備しましょう。

https://cloud.google.com/sdk/docs/install?hl=ja

 - 環境変数に`gcloud CLI`が指定されない場合、コマンドから`gcloud CLI`を実行できません

完了後にVisual Studio Codeでフォルダを指定し、環境を設定します。

```powershell:仮想環境の準備
python -m venv venv
```

環境を有効化して必要なライブラリのインストールと認証を実行します。

```powershell:ライブラリのインストールと認証
pip install --upgrade google-cloud-aiplatform
gcloud auth application-default login
```

こちらをVisual Studio Codeのターミナルで実行し、認証を完了させます。
完了させると画面のように認証が完了したことを示す画面に遷移します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/0b83693d-760f-4119-04e4-462e3716de18.png)

# Power Appsのドキュメントを作成する

今回は`YAML`を使うため、`pyyaml`をインストールします。

```
pip install pyyaml
```

- 実行ファイルと同一階層に*YAML*ファイルを格納するフォルダを設ける
    - 対象のフォルダのファイルをまとめて読み取る
- *Gemini API*を実行
- 結果を*Markdown*で返す

上記のシナリオで進めてみます。

## コード
`GPT-4o`の力を借りまくって作成しています。
事前に環境変数の設定をしておいてください。

```python:main.py
import os
import yaml
import vertexai
from vertexai.generative_models import GenerativeModel
import vertexai.preview.generative_models as generative_models
from pathlib import Path

# 環境変数の設定
project = os.getenv('PROJECT')
location = os.getenv('LOCATION')

# YAMLファイルの読み込み
def load_yaml(file_path):
    """
    指定されたファイルパスからYAMLファイルを読み込み、辞書として返す。
    もしYAMLエラーが発生した場合、もしくは辞書形式でない場合は
    ファイルの内容を単純なテキストとして辞書に格納して返す。

    :param file_path: 読み込むYAMLファイルのパス
    :return: YAML内容の辞書、もしくはテキスト内容を含む辞書
    """
    try:
        with open(file_path, 'r') as file:
            content = yaml.safe_load(file)
            if isinstance(content, dict):
                return content
            else:
                print(f"Warning: {file_path} did not contain a valid YAML dictionary. Treating as plain text.")
                return {'content': content}
    except yaml.YAMLError as e:
        print(f"YAML error in {file_path}: {e}")
        with open(file_path, 'r') as file:
            return {'content': file.read()}

# 複数のYAMLコンテンツを統合する関数
def merge_yaml_contents(yaml_contents):
    """
    複数のYAML内容を統合し、1つの辞書として返す。
    辞書でない内容は 'raw_content' キーに統合する。

    :param yaml_contents: YAML内容のリスト
    :return: 統合されたYAML内容の辞書
    """
    merged_content = {}
    for content in yaml_contents:
        if isinstance(content, dict):
            merged_content.update(content)
        else:
            merged_content['raw_content'] = merged_content.get('raw_content', '') + "\n" + content.get('content', '')
    return merged_content

# 依存関係を可視化する関数
def visualize_dependencies(yaml_content):
    """
    統合されたYAML内容をもとに依存関係を解析し、Markdown形式で返す。

    :param yaml_content: 統合されたYAML内容の辞書
    :return: 依存関係のMarkdown形式のテキスト
    """
    prompt = f"""
    Analyze the following Power Apps YAML configuration and visualize the dependencies between controls and functions. Highlight the dependencies clearly in Markdown format:
    ```yaml
    {yaml.dump(yaml_content)}
    ```
    Provide the dependencies in Markdown format.
    """

    vertexai.init(project=project, location=location)
    model = GenerativeModel("gemini-1.5-pro-001")
    response = model.generate_content([prompt], generation_config=generation_config, safety_settings=safety_settings)

    # `GenerationResponse`オブジェクトからテキストを取得
    markdown_output = response.candidates[0].content.parts[0].text

    return markdown_output

# Markdownをファイルに保存する関数
def save_markdown(file_path, content):
    """
    指定されたファイルパスにコンテンツをMarkdown形式で保存する。

    :param file_path: 保存するファイルのパス
    :param content: 保存するMarkdown形式のテキスト
    """
    with open(file_path, 'w') as file:
        file.write(content)

# "YAML"フォルダ内のすべてのYAMLファイルを処理し統合する関数
def process_yaml_files():
    """
    "YAML"フォルダ内のすべてのYAMLファイルを読み込み、統合し、依存関係を解析してMarkdownファイルに保存する。
    """
    current_dir = Path(__file__).parent
    yaml_folder = current_dir / 'YAML'
    yaml_files = yaml_folder.glob('*.yaml')
    
    # 各YAMLファイルを読み込み
    yaml_contents = [load_yaml(yaml_file) for yaml_file in yaml_files]
    # 読み込んだ内容を統合
    merged_yaml_content = merge_yaml_contents(yaml_contents)
    # 統合された内容から依存関係を可視化
    markdown_output = visualize_dependencies(merged_yaml_content)
    
    # Markdownファイルとして保存
    markdown_file_path = yaml_folder / 'merged_dependencies.md'
    save_markdown(markdown_file_path, markdown_output)
    print(f"Dependencies for merged YAML files saved to {markdown_file_path}")

# 依存関係の生成設定
generation_config = {
    "max_output_tokens": 8192,
    "temperature": 1,
    "top_p": 0.95,
}

# セーフティ設定
safety_settings = {
    generative_models.HarmCategory.HARM_CATEGORY_HATE_SPEECH: generative_models.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
    generative_models.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: generative_models.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
    generative_models.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: generative_models.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
    generative_models.HarmCategory.HARM_CATEGORY_HARASSMENT: generative_models.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
}

# メイン処理
if __name__ == "__main__":
    process_yaml_files()
```

実行前に`project-id`や`location`は一時的に環境変数として設定しています。

```powershell:環境変数の設定
$env:PROJECT = "PROJECT_ID"
$env:LOCATION = "REGION"
```

単純に*YAML*を読み込むとエラーが出るため、読み取れないファイルは、テキストとして読み取り、マージしています。


# 結果
非常に優秀な結果が出ました・・・（最後に記載しています）。
これもうAI使いまくって、本当にドキュメント作成を効率化したいですね。

依存関係の図式化の例も[PPログ](https://x.com/PowerPlatform4/status/1806486277283557402)さんが書かれています。

https://powerplatformnikki.com/claudemermaiddependencies/

プロンプトを工夫すればテストケースもイケそう。

やってみるしかないですね。

```markdown:Geminiの出力
## Power Apps Control & Function Dependencies Visualization

This Markdown document visualizes the dependencies between controls and functions within the provided Power Apps YAML configuration.

**Page:** TicketdetailsPage

**Controls and their Dependencies:**

- **TextBox5_57 (Label)**
    - **Text:** `EditRecord.DateCreated` (Data Source: EditRecord)
- **TextBox5_51 (Label)**
- **TextBox3 (Label)**
    - **Text:** `Today()` (Function)
    - **Visible:** `false`
- **Group21 (Group)**
    - **Children:**
        - **icon2_7 (Icon)**
            - **OnSelect:** 
                - `Navigate(PriorityPage, ScreenTransition.Fade, { priority: priority })` (Navigation, Data: priority)
        - **TextBox5_60 (Label)**
            - **Text:** `priority` (Data Source)
            - **OnSelect:** 
                - `Navigate(PriorityPage, ScreenTransition.Fade, { priority: priority })` (Navigation, Data: priority)
- **Group20 (Group)**
    - **Children:**
        - **icon2_6 (Icon)**
            - **OnSelect:** 
                - `Navigate(AreaPage, ScreenTransition.Fade, { Area: Area })` (Navigation, Data: Area)
        - **TextBox5_59 (Label)**
            - **Text:** `Area` (Data Source)
            - **OnSelect:** 
                - `Navigate(AreaPage, ScreenTransition.Fade, { Area: Area })` (Navigation, Data: Area) 
- **Group19 (Group)**
    - **Children:**
        - **icon2_5 (Icon)**
            - **OnSelect:** 
                - `Navigate(AssigntoPage, ScreenTransition.Fade, { assign: assign })` (Navigation, Data: assign)
        - **TextBox5_26 (Label)**
            - **Text:** `If(IsBlank(assign), "None", assign)` (Function, Data: assign)
            - **OnSelect:** 
                - `Navigate(AssigntoPage, ScreenTransition.Fade, { assign: assign })` (Navigation, Data: assign)
- **Group18 (Group)**
    - **Children:**
        - **icon2_4 (Icon)**
            - **OnSelect:** 
                - `Navigate(CasestatusPage, ScreenTransition.Fade, { type: type })` (Navigation, Data: type)
        - **TextBox5_25 (Label)**
            - **Text:** `type` (Data Source)
            - **OnSelect:** 
                - `Navigate(CasestatusPage, ScreenTransition.Fade, { type: type })` (Navigation, Data: type)
- **Rectangle7_14 (Rectangle)**
- **TextInput4 (TextInput)**
    - **Default:** `EditRecord.Description` (Data Source: EditRecord)
    - **DisplayMode:** `If(description_disabled, DisplayMode.Disabled, false, DisplayMode.View, DisplayMode.Edit)` (Function, Data: description_disabled)
    - **Fill:** `description_fill` (Data Source)
    - **Reset:** `textreset` (Data Source)
- **TextInput3 (TextInput)**
    - **Default:** `EditRecord.Comment` (Data Source: EditRecord)
    - **DisplayMode:** `If(commentdisabled, DisplayMode.Disabled, false, DisplayMode.View, DisplayMode.Edit)` (Function, Data: commentdisabled)
    - **Fill:** `commentfill` (Data Source)
    - **Reset:** `textreset` (Data Source)
- **TextInput2 (TextInput)**
    - **Default:** `EditRecord.Subject` (Data Source: EditRecord)
    - **DisplayMode:** `If(subjectdisabled, DisplayMode.Disabled, false, DisplayMode.View, DisplayMode.Edit)` (Function, Data: subjectdisabled)
    - **Fill:** `subjectfill` (Data Source)
    - **Reset:** `textreset` (Data Source)
- **Rectangle7_8 (Rectangle)**
- **TextBox5_56 (Label)**
    - **Text:** `EditRecord.ID` (Data Source: EditRecord)
- **TextBox5_50 (Label)**
- **TextBox5_58 (Label)**
    - **Text:** `EditRecord.Owner` (Data Source: EditRecord)
- **TextBox5_52 (Label)**
- **TextBox5_53 (Label)**
- **TextBox5_54 (Label)**
- **TextBox5_61 (Label)**
    - **Text:** `If(IsBlank(EditRecord.DateClosed), "--", EditRecord.DateClosed)` (Function, Data: EditRecord.DateClosed)
- **TextBox5_55 (Label)**
- **Button1 (Button)**
    - **DisplayMode:** `If(false, DisplayMode.View, DisplayMode.Edit)`
    - **OnSelect:**  (Complex function with dependencies on multiple controls & data)
        - `UpdateIf(TicketsCollect1, ID = EditRecord.ID, { Status: TextBox5_25.Text, AssignedTo: assign, DateClosed: "", AccountName: TextBox5_59.Text, Priority: TextBox5_60.Text, Subject: TextInput2.Text, Description: TextInput4.Text, Comment: TextInput3.Text })`
        - `UpdateIf(TicketsCollect1, ID = EditRecord.ID, { Status: TextBox5_25.Text, AssignedTo: assign, DateClosed: TextBox3.Text, AccountName: TextBox5_59.Text, Priority: TextBox5_60.Text, Subject: TextInput2.Text, Description: TextInput4.Text, Comment: TextInput3.Text })`
        - `Navigate(DashboardPage, ScreenTransition.Fade)` 
    - **Visible:** `If(type = "Completed", false, true)` (Data: type)
- **TextBox5_24 (Label)**
- **TextBox5_23 (Label)**
- **Rectangle7_7 (Rectangle)**
- **Rectangle7_6 (Rectangle)**
- **Rectangle7_5 (Rectangle)**
- **TextBox5_21 (Label)**
- **Rectangle7_4 (Rectangle)**
- **TextBox5_20 (Label)**
- **Rectangle7_3 (Rectangle)**
- **TextBox5_17 (Label)**
- **TextBox4_1 (Label)**
- **Rectangle1_1 (Rectangle)**
- **Rectangle5_7 (Rectangle)**
- **Rectangle1_14 (Rectangle)**
    - **OnSelect:** `If(type = "Completed", "", Navigate(AssigntoPage, ScreenTransition.Fade, { assign: assign }))` (Data: type, Navigation, Data: assign)
- **Group3 (Group)**
    - **Children:**
        - **icon4 (Icon)**
            - **OnSelect:** 
                - `UpdateContext({ textreset: false })` 
                - `UpdateContext({ textreset: true })`
                - `Navigate(DashboardPage, ScreenTransition.Fade)` 
        - **Rectangle1_3 (Rectangle)**
            - **OnSelect:** 
                - `UpdateContext({ textreset: false })`
                - `UpdateContext({ textreset: true })`
                - `Navigate(DashboardPage, ScreenTransition.Fade)` 

**Data Sources:**

- EditRecord
- priority
- Area
- assign
- type
- description_disabled
- description_fill
- commentdisabled
- commentfill
- subjectdisabled
- subjectfill
- textreset

**Functions:**

- Today()
- IsBlank()
- If()
- UpdateIf()
- Navigate()

This visualization shows the interconnectedness of controls and functions within the TicketdetailsPage. You can analyze how data is used, how user interaction (OnSelect events) triggers navigation or updates, and the overall flow of information within this screen. 

```