Python版 Semantic Kernelのプランナーを理解しよう。~実装を通して学んだことの備忘録~

# 0. まとめ

※ソースコードと睨めっこし、色々試しながら実行して得た見解ですので、正確ではないかもしれません。
（ドキュメント読めば書いてあるんですかね、、）

用語については下記記事にとてもわかりやすくまとめられています。

https://qiita.com/nohanaga/items/430b59209b02c298ef2a


## スキル間の結果の受け渡しは勝手に行われる
- 受け渡す必要がない場合でも、受け取りは発生する
- 受け取った側で使わなければOK
- 内部の処理では、コンテキストオブジェクトで行われる(```context['input']```に前の結果が入っている)


## ネイティブ関数で前のスキルの実行結果を受け取る際はcontextを使う
- 上述の通り、スキル間の値の受け渡しはコンテキストを用いるため
- （ドキュメントをちゃんと読んでおらず、ほかにも方法があるかもなので要調査です）


## セマンティック関数のconfig.jsonを書く際は**input**を含めた方がいい
- 無くてもエラーにならないが、```input: <空白>```という文字列がプロンプトに含まれてしまう
- スキル間で値を受け渡すのことを前提としてSemantic kernel側でプロンプト作成されているため
- 使わなくてもとりあえずinputは指定しておく（```input: Not use``` みたいな感じなのかな、、要調査です）


## プランナーで作成されたプランをチェックした方がいい
- temperatureが高めに設定されているので、同じ入力に対して毎回同じプランが作成されるわけではない
- 「**JSON形式でプランを出力してね**」というプロンプトが裏で動いているが、たまに変な形式で出力される
- プラン作成後に、作成されたプランが期待通りかどうかのエラーハンドリングを入れた方がいい


## Langchainとは違い、Observationフェーズがない
- Semantic Kernelは、ゴールから必要なプランを考えて作成し、一方通行で順番に実行する
- Langchainは実行結果がサブタスクを満たしているかどうか判断し、再試行するフェーズがある


## デフォルトのプロンプトを編集した方がいいケースがある
- 社内ナレッジ検索のような用途の場合、「**ユーザの入力をそのまま**」検索用のスキルに渡したい
- デフォルトのプロンプトのFew-Shotでは、最初のスキルに渡す内容が「**ユーザの入力の要約**」となっている
- 各タスクに合わせてプロンプトを書き換えた方がよさそう(本環境では書き換えています。)


------------------------------------------

# 1. 全体構成と動作イメージ

## ざっくり構成図

今回用意したスキルは2種類です。
正確には4種類ですが、残り2つは本質ではないのでここでは割愛します。
また、本質ではないスキルを含めてしまうとSemantic Kernelが迷ってしまうという点も考慮し、
本タスクを達成するためのスキルとは分けて実行しています。

- ①ベクトル類似検索スキル
    - ユーザのクエリをベクトルに変換後、Cognitive Searchに対してベクトル類似検索を行う
- ②回答生成スキル
    - ①の検索結果と元々のユーザのクエリに基づいて最終的な回答を生成する 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/fc224dcb-91c7-f06c-e0a2-9b377c4a44ad.png)


## 動作イメージ①

今回はテスト用に以下のようなファイルに見立てた情報をCognitive Searchにベクトル化して格納しているので、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/9c15e375-0841-680e-d368-3c38601b249c.png)


ユーザが以下のような入力をすると、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/90f2093f-3e23-d8d6-06c8-65e28faaebc5.png)


以下のようなJSON形式で回答+参照ファイル名を教えてくれます。
（**ファイル名を含めてJSONフォーマットで返してね**というプロンプトを通しています。）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/59e4f303-cdf9-d15d-5748-cae37cb10f36.png)




## 動作イメージ②

入力を少し変えて試してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/98801eab-720d-cbcf-6a79-48363c044836.png)

以下のような回答となりました。（日本語怪しいですが、なんとか許容できる範囲、、？）
3ファイルの情報を基に回答していますね。

```json:回答
{
    "answer": "ポリシー.pdfによると、ChatGPTへのメールアドレスのアップロードは許可されていないし、会社外で機密情報を扱うことも許可されていません。利用許可.pdfによると、AIを使ってライフアドバイスを提供することは許可されています。利用申請.pdfによると、SaaSへの申請が必要であり、情報システム部門と相談する必要があります。",
    "sources": ["ポリシー.pdf", "利用許可.pdf", "利用申請.pdf"]
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/8e1622c0-a55e-191a-a438-84299b5385f6.png)




# 2. 使用したスキル・プロンプト群

## ベクトル類似検索スキル（ネイティブ関数）

こちらはネイティブ関数(Pythonの自作関数)を使用しました。外部のソースと連携する主体です。
（本来はコネクターでやるんですかね、、？要調査です。）

定義部分を載せておきます。
スキル用のクラスを作成し、Semantic Kernelから呼び出したい関数はデコレーター@sk_functionで修飾します。
また、引数に```context```を指定することで、他スキルの実行結果などを受け取ることができます。

```py:
# ユーザ持ち込みのスキルを作成
class SearchFileSkill:
    # ユーザの入力をベクトルに変換する
    def _embed_text(self, query:str):
        response = openai.Embedding.create(
            engine=os.getenv("EMBEDDING_MODEL_NAME"),
            input=query, 
        )
        embeddings = response['data'][0]['embedding']

        return embeddings
    
    # Cognitive Searchのベクトル類似検索をする関数
    @sk_function(
        description="Search file",
        name="VectorSearchFile",
        input_description = "the user input"  #インプットはセマンティックカーネル側で勝手に受け取ってくれる
    )
    def search_vector(self, context: SKContext) -> str:
        try:
            # Cognitive Searchクライアントを初期化
            service_endpoint = os.getenv("SEARCH_ENDPOINT")  
            index_name = os.getenv("SEARCH_INDEX_NAME")  
            key = os.getenv("SEARCH_API_KEY")  
            credential = AzureKeyCredential(key)

            search_client = SearchClient(endpoint=service_endpoint, index_name=index_name, credential=credential)
        
            # ベクトル類似検索を実行
            results = search_client.search(  
                search_text="",  
                vector=Vector(value=self._embed_text(context['input']), k=3, fields="contentVector"),  
                select=["title", "content"] 
            )  

            # 検索結果を文字列に変換
            results_str = ""
            for result in results:
                print(f"Score: {result['@search.score']},  Title: {result['title']}")
                results_str += f"Title:{result['title']}\nContent:{result['content']}\n\n"
            
            # デバッグ用にログ吐き出し
            with open("./search_vector_log.txt", "w") as f:
                f.write(context['input'] + "\n")
                f.write(results_str)
            
            return results_str
        
        except Exception as e:
            print(e)
            raise e
```

## 回答生成スキル（セマンティック関数）

Few-Shot(One-Shot)で例を与えて、続きを生成させるようにしています。
使用するモデルが```text-davinci-003```なので、続きを生成させるイメージで書いています。


>**【プロンプト内容のイメージ】
ユーザの入力とTitleとContentが与えられたら、それらを基に回答を生成してね。
参照したファイル名も忘れずに出力してね。**

- ```$input```には前のスキルの結果が入ります。今回はベクトル類似検索スキルの結果が入ります。
- ```$query```にはユーザの入力が入ります。

※実際にはtxtファイルなのでコメントは含めない方がいいです。

```sh:skprompt.txt
# 役割を定義
You are an assistant who helps users based on both the Title and Content. 

#### <Few-Shot> ####
<User> 
What is AWS? Answer based on the following Title and Content. 
Please tell me "sources".

Title: Overview-of-AWS.pdf
Content: AWS is an abbreviation for Amazon Web Services and is a cloud computing platform provided by Amazon. AWS offers various cloud-based services including computing resources, storage, databases, and networking. This enables businesses and individuals to utilize a flexible and scalable infrastructure for application development, deployment, and scaling.

Title: Application-Form.xlsx
Content: Permission from the Information Systems Department is required. Please be sure to apply before using.


<Assistant>
According to Overview-of-AWS.pdf,
it states that AWS is a cloud platform provided by Amazon, offering various services such as computing and storage to facilitate application development and deployment.
sources: Overview-of-AWS.pdf

#### </Few-Shot> ####

<User>
{{$query}} Answer based on the following Title and Content.
Please tell me "sources".

{{$input}}


<Assistant>
```


## 日本語に変換 & JSONにするスキル

```sh:
以下の文章をJSONフォーマットに変換して。answerは日本語に変換して。
JSONだけを出力すること。

文章：
{{$input}}

'''
JSON = {
    "answer": 文章を日本語に変換したもの,
    "sources": list[ファイル名]
}
'''

JSON = 
```

## プランナーが使用するプロンプト

今回デフォルトのプロンプトを少し編集しました。

<details>
<summary>変更後のプロンプト</summary>
<div>

```py:
PROMPT = """
You are a planner for the Semantic Kernel.
Your job is to create a properly formatted JSON plan step by step, to satisfy the goal given.
Create a list of subtasks based off the [GOAL] provided.
Each subtask must be from within the [AVAILABLE FUNCTIONS] list. Do not use any functions that are not in the list.
Base your decisions on which functions to use from the description and the name of the function.
Sometimes, a function may take arguments. Provide them if necessary.
The plan should be as short as possible.
If the description contains unknown, be sure to ignore the function.

For example:

[AVAILABLE FUNCTIONS]
WriterSkill.Brainstorm
description: Brainstorm ideas
args:
- input: the input to brainstorm about

EdgarAllenPoeSkill.Poe
description: Write in the style of author Edgar Allen Poe
args:
- input: the input to write about

WriterSkill.EmailTo
description: Write an email to a recipient
args:
- input: the input to write about
- recipient: the recipient's email address.

WriterSkill.Translate
description: translate the input to another language
args:
- input: the text to translate
- language: the language to translate to

_GLOBAL_FUNCTIONS_.f_33c6f6b6_b519_4fd9_9b4b_4a76b4741d7f
description: Generic function, unknown purpose
args:
- available_functions: 
- goal: 

_GLOBAL_FUNCTIONS_.f_33c6f6b6_b519_4fd9_9b4b_4a76b4741d7f
description: Generic function, unknown purpose
args:
- available_functions: 
- goal: 

[GOAL]
"Tomorrow is Valentine's day. I need to come up with a few date ideas.
She likes Edgar Allen Poe so write using his style.
E-mail these ideas to my significant other. Translate it to French."

[OUTPUT]
    {
        "input": "Tomorrow is Valentine's day. I need to come up with a few date ideas.She likes Edgar Allen Poe so write using his style.E-mail these ideas to my significant other. Translate it to French.",
        "subtasks": [
            {"function": "WriterSkill.Brainstorm"},
            {"function": "EdgarAllenPoeSkill.Poe"},
            {"function": "WriterSkill.EmailTo", "args": {"recipient": "significant_other"}},
            {"function": "WriterSkill.Translate", "args": {"language": "French"}}
        ]
    }

[AVAILABLE FUNCTIONS]
{{$available_functions}}

[GOAL]
{{$goal}}

[OUTPUT]
"""
```
</div>
</details>




# 3. Semantic Kernelの環境構築
詳細はドキュメントをご参照ください。


https://learn.microsoft.com/en-us/semantic-kernel/get-started/?tabs=python

必要なパッケージをインストールします。

```sh:
python -m pip install semantic-kernel==0.2.7.dev0
```

**.envファイル**を作成し、モデルの情報を記載します。
本環境ではAzure OpenAIを使用しています。

```:.env
AZURE_OPENAI_ENDPOINT=""
AZURE_OPENAI_API_KEY=""
AZURE_OPENAI_DEPLOYMENT_NAME=""
```


# 4. セマンティック関数を作成

セマンティック関数は、プロンプトとモデルのパラメータなどをフォルダ/ファイルとして管理するやつです。
切り出しできるのは結構嬉しいですよね。再利用できたり、可読性あがったり。

githubのチュートリアルでは以下のような構成となっています。
（以下はイメージなのでgithubを見に行くの推奨です）

https://github.com/microsoft/semantic-kernel/tree/main/samples/skills

- ```skills```: skill全体を格納するフォルダ
    - ```Translate```: 翻訳用のスキルフォルダ
        - ```Japanese```:日本語に変換するスキルフォルダ
            - ```skprompt.txt```:  プロンプトを記述するファイル
            - ```config.json```: パラメータや引数を記述するファイル
        - ```English```: 英語に変換するスキルフォルダ
            - ...
            - ...


## セマンティック関数を定義する

今回は、回答生成スキルをセマンティック関数として作成します。

**skills/SummarizeSkill/Summarize**を作成し、
その下に、**skprompt.txt**と**config.json**を作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/0d92f4a2-b97f-6eca-5dc0-b19b7852e3b1.png)


```sh:skprompt.txt
You are an assistant who helps users based on both the Title and Content. 


<User> 
What is AWS? Answer based on the following Title and Content. 
Please tell me "sources".

Title: Overview-of-AWS.pdf
Content: AWS is an abbreviation for Amazon Web Services and is a cloud computing platform provided by Amazon. AWS offers various cloud-based services including computing resources, storage, databases, and networking. This enables businesses and individuals to utilize a flexible and scalable infrastructure for application development, deployment, and scaling.

Title: Application-Form.xlsx
Content: Permission from the Information Systems Department is required. Please be sure to apply before using.


<Assistant>
According to Overview-of-AWS.pdf,
it states that AWS is a cloud platform provided by Amazon, offering various services such as computing and storage to facilitate application development and deployment.
sources: Overview-of-AWS.pdf

<User>
{{$query}} Answer based on the following Title and Content.
Please tell me "sources".

{{$input}}


<Assistant>
```

```json:config.json
{
    "schema": 1,
    "description": "Generate answer based on result of search function.",
    "type": "completion",
    "completion": {
      "max_tokens": 128,
      "temperature": 0,
      "top_p": 0.95,
      "presence_penalty": 0.0,
      "frequency_penalty": 0.0
    },
    "input": {
      "parameters": [
        {
          "name": "input",
          "description": "the context to answer",
          "defaultValue": ""
        },
        {
          "name": "query",
          "description": "user's input (=GOAL)",
          "defaultValue": ""
        }
      ]
    }
  }
```


```config.json```ではパラメータ・引数の設定を行います。

ポイントは以下の通りです。

- 引数の```name:input```はスキル間の値の受け渡しに使用される
- 引数の```name:query```は最終的に答えさせたい問いを指定しています
    - 今回は社内ナレッジ検索という想定なので、descriptionで「**ユーザの入力=GOAL**」だよと教えています


これらの情報に基づいてプランが生成されます。
以下画像のサブタスクの二行目が、プランナーによって実行される回答生成スキルに該当します。
繰り返しになりますが、```input```という引数は自動で受け渡しされますので、```args```に含まれていないのが正しい挙動です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/a08aacd0-87df-2796-2f35-8625f5177f16.png)


## セマンティック関数を使用する

**実行例**

スキルフォルダの場所を指定すると、自動で```skprompt.txt```と```config.json```を読み込みんでくれます。

```py:
import semantic_kernel as sk
from semantic_kernel.connectors.ai.open_ai import AzureTextCompletion

# カーネル作成
kernel = sk.Kernel()
deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env()
kernel.add_text_completion_service("dv", AzureTextCompletion(deployment, endpoint, api_key))

# スキルを読み込み
skills_dir = "./skills"
summarize_skills = kernel.import_semantic_skill_from_directory(skills_dir, "SummarizeSkill")
summarize = summarize_skills["Summarize"]

# コンテキストを作成
context = kernel.create_new_context()
context["input"] = 'Title:ポリシー.pdf\nContent:メールアドレスをChatGPTにアップロードしてはいけません。また、社外秘情報を取り扱うことは許されていません。\n\nTitle:利用許可.pdf\nContent:生成AIを使って人生相談することは許されています。\n\nTitle:利用申請.pdf\nContent:SaaSの利用には申請が必要です。情報システム部にご相談ください。\n\nTitle:資格申請.pdf\nContent:資格支援の上限金額は3万円です。\n\n'
context["query"] = "ChatGPTって社内で使っていいの？"

# 回答生成処理を実行
response = await summarize.invoke_async(context=context)

# 結果取り出しと表示
result = response.result
print(result)
```


**実行結果**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/d5f72f1d-84b5-962c-7367-b3279fa1e816.png)




# 5. ネイティブ関数を作成

外部API連携する主体です。（本来はコネクターでやるのかも、、？要調査です）
本環境では、自作の関数を組み込めるので、自作関数からAPIを叩きにいくという構成にしています。


## 作成例

```py:Cognitive Searchに対してベクトル類似検索を行うネイティブ関数
# ユーザ持ち込みのスキルを作成
class SearchFileSkill:
    # ユーザの入力をベクトルに変換する
    def _embed_text(self, query:str):
        response = openai.Embedding.create(
            engine=os.getenv("EMBEDDING_MODEL_NAME"),
            input=query, 
        )
        embeddings = response['data'][0]['embedding']

        return embeddings
    
    # Cognitive Searchのベクトル類似検索をする関数
    @sk_function(
        description="Search file",
        name="VectorSearchFile",
        input_description = "the user input"  #インプットはセマンティックカーネル側で勝手に受け取ってくれる
    )
    def search_vector(self, context: SKContext) -> str:
        try:
            # Cognitive Searchクライアントを初期化
            service_endpoint = os.getenv("SEARCH_ENDPOINT")  
            index_name = os.getenv("SEARCH_INDEX_NAME")  
            key = os.getenv("SEARCH_API_KEY")  
            credential = AzureKeyCredential(key)

            search_client = SearchClient(endpoint=service_endpoint, index_name=index_name, credential=credential)
        
            # ベクトル類似検索を実行
            results = search_client.search(  
                search_text="",  
                vector=Vector(value=self._embed_text(context['input']), k=3, fields="contentVector"),  
                select=["title", "content"] 
            )  

            # 検索結果を文字列に変換
            results_str = ""
            for result in results:
                print(f"Score: {result['@search.score']},  Title: {result['title']}")
                results_str += f"Title:{result['title']}\nContent:{result['content']}\n\n"
                
            with open("./search_vector_log.txt", "w") as f:
                f.write(context['input'] + "\n")
                f.write(results_str)
            
            return results_str
        
        except Exception as e:
            print(e)
            raise e
```


## 注意点
自作の関数を組み込む際はデコレーターを使用します。（@sk_function）

https://github.com/microsoft/semantic-kernel/blob/main/python/semantic_kernel/skill_definition/sk_function_decorator.py


ここで、まとめに書いた注意ポイント再掲です。**引数はContextで渡すこと** に注意します。
（githubのサンプルを見ていると他にも渡し方がありそうですが、とりあえず動いたのでよしとしています。）

Semantic Kernelでは以下のような処理を行っています。

- ①：プラン(スキルの実行順序)をプランナーが生成
- ②：①で定められた順序で一つずつスキルが実行される
- ③：前のスキルの結果を次のスキルに受け渡す

③については、Semantic Kernel側で```context['input']```で値の受け渡しが行われています。
ユーザ側で、**prev_results**とか**output**とか勝手に名前を付けられません。
（cloneしてきて、ソースコードを編集すれば可能そうですが、、）


実態は、①で洗い出されたスキルをループで実行しているだけで、ループ処理内で```context```を使い回しているイメージです。
該当ソースコードとポイントは以下の通りです。


>**スキルnを実行**
output = await sk_function.invoke_async(variables=context)
　　　　　　　↓
>**スキルnの実行結果でコンテキスト更新**
context["input"] = output.result
　　　　　　　↓
>**スキルn+1の実行開始時に、スキルnの実行結果がコンテキストに含まれている**


```py:
async def execute_plan_async(self, plan: Plan, kernel: Kernel) -> str:
        """
        Given a plan, execute each of the functions within the plan
        from start to finish and output the result.
        """
        generated_plan = json.loads(plan.generated_plan.result)

        context = ContextVariables()
        context["input"] = generated_plan["input"]
        subtasks = generated_plan["subtasks"]
           
        ##### <スキル処理ループ> ######
        for subtask in subtasks:
            skill_name, function_name = subtask["function"].split(".")
            sk_function = kernel.skills.get_function(skill_name, function_name)

            # Get the arguments dictionary for the function
            args = subtask.get("args", None)
            if args:
                for key, value in args.items():
                    context[key] = value
                ### !!! スキルn実行 !!! ###
                output = await sk_function.invoke_async(variables=context)

            else:
                output = await sk_function.invoke_async(variables=context)

            ### !!! スキルnの実行結果でコンテキスト更新 !!! ###
            context["input"] = output.result

            ### !!! スキルn+1にcontext引継ぎ !!! ###
        
        ##### </スキル処理ループ> ######

        # At the very end, return the output of the last function
        return output.result
```

https://github.com/microsoft/semantic-kernel/blob/main/python/semantic_kernel/planning/basic_planner.py


## ネイティブ関数の使い方

```py:
from semantic_kernel import SKContext
import semantic_kernel as sk
from semantic_kernel.connectors.ai.open_ai import AzureTextCompletion


# カーネル作成
kernel = sk.Kernel()
deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env()
kernel.add_text_completion_service("dv", AzureTextCompletion(deployment, endpoint, api_key))


# ユーザ持ち込みのスキルを作成
class SearchFileSkill:
    # Cognitive Searchのベクトル類似検索をする関数(テスト)
    def _test_results(self, query:str):  
        # テスト用：ベクトル類似検索結果
        results_str = """
Title:ポリシー.pdf
Content:メールアドレスをChatGPTにアップロードしてはいけません。また、社外秘情報を取り扱うことは許されていません。

Title:利用許可.pdf
Content:生成AIを使って人生相談することは許されています。

Title:利用申請.pdf
Content:SaaSの利用には申請が必要です。情報システム部にご相談ください。
"""
        
        return results_str
        
        
    # Semantic Kernekから呼び出される関数
    @sk_function(
        description="Search file",
        name="VectorSearchFile",
        input_description = "the user input"
    )
    def search_vector(self, context: SKContext) -> str:   
        try:
            # 検索処理 (テスト)
            results_str = self._test_results(context['input'])
                
            with open("./search_vector_log.txt", "w") as f:
                f.write(context['input'] + "\n")
                f.write(results_str)
            
            return results_str
        
        except Exception as e:
            print(e)
            raise e
            

# スキルを読み込み
skills_dir = "./skills"
searchfile_skill = kernel.import_skill(SearchFileSkill(), skill_name="SearchFilesSkill")
            
# コンテキストを作成
context = kernel.create_new_context()
context["input"] = "ChatGPTって社内で使っていいの？"

# 回答生成処理を実行
response = await searchfile_skill["VectorSearchFile"].invoke_async(context=context)
print(response.result)
```


# 6. 入出力で英語⇔日本語の変換が必要かも？

プランを作成する際、英語で入力した方が精度が良さそうです。
英語で入力したときと、日本語で入力したときを見比べていると、
**日本語の方がブレ幅が大きい & 無駄にスキルを繰り返す**ということが発生していました。



そのため、本環境では変換処理を挟んでいます。

- ⓪：ユーザの入力
- ①：**ユーザの入力を英語に変換**
- ②：プランに沿って色々処理
- ③：**出力を日本語 & JSONに変換**


## ユーザの入力を英語に変換

「**文章を英語に変換してね**」というプロンプトを実行しているだけです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/27bee940-304f-e80d-c9ab-6ff3b31f1476.png)


## 出力を日本語 & JSONに変換

Semantic Kernelへの入力を英語にし、上述したセマンティック関数ではプロンプトが英語のため、当然ですが英語で出力がきます。

プロンプト内で「**in Japanese**」などを追記しても中国語に変換されてしまったため、
力業ですが日本語に変換するプロンプトを最後に実行してあげています。
ついでに、後の処理で使いやすいようにJSONに加工するようにプロンプトで指示しています。

プロンプト自体は単純ですが、ポイントとしては使用しているモデルが**text-davinci-003**のため、続きを生成させるイメージで書くといったところでしょうか

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/e3d53d6b-3205-73a9-9f00-2c63d975210f.png)



# 7. プランナーで使用されるデフォルトプロンプトを編集する

プランナーで使用されるデフォルトのプロンプトはざっくり以下のような内容です。

>**関数の実行計画を作成してね。使用できる関数は限られているよ。
最終的な出力はinput, subtasksというキーを持つJSON形式だよ。
input:最初の関数に渡す引数, subtasks:それ以降に実行する必要のある関数のリストだよ。
実行時の引数も考慮してね。
いくつかの例を示すよ(few-shot提示)**


<details>
<summary>デフォルトのプロンプト</summary>
<div>

```py:
PROMPT = """
You are a planner for the Semantic Kernel.
Your job is to create a properly formatted JSON plan step by step, to satisfy the goal given.
Create a list of subtasks based off the [GOAL] provided.
Each subtask must be from within the [AVAILABLE FUNCTIONS] list. Do not use any functions that are not in the list.
Base your decisions on which functions to use from the description and the name of the function.
Sometimes, a function may take arguments. Provide them if necessary.
The plan should be as short as possible.
For example:

[AVAILABLE FUNCTIONS]
EmailConnector.LookupContactEmail
description: looks up the a contact and retrieves their email address
args:
- name: the name to look up

WriterSkill.EmailTo
description: email the input text to a recipient
args:
- input: the text to email
- recipient: the recipient's email address. Multiple addresses may be included if separated by ';'.

WriterSkill.Translate
description: translate the input to another language
args:
- input: the text to translate
- language: the language to translate to

WriterSkill.Summarize
description: summarize input text
args:
- input: the text to summarize

FunSkill.Joke
description: Generate a funny joke
args:
- input: the input to generate a joke about

[GOAL]
"Tell a joke about cars. Translate it to Spanish"

[OUTPUT]
    {
        "input": "cars",
        "subtasks": [
            {"function": "FunSkill.Joke"},
            {"function": "WriterSkill.Translate", "args": {"language": "Spanish"}}
        ]
    }

[AVAILABLE FUNCTIONS]
WriterSkill.Brainstorm
description: Brainstorm ideas
args:
- input: the input to brainstorm about

EdgarAllenPoeSkill.Poe
description: Write in the style of author Edgar Allen Poe
args:
- input: the input to write about

WriterSkill.EmailTo
description: Write an email to a recipient
args:
- input: the input to write about
- recipient: the recipient's email address.

WriterSkill.Translate
description: translate the input to another language
args:
- input: the text to translate
- language: the language to translate to

[GOAL]
"Tomorrow is Valentine's day. I need to come up with a few date ideas.
She likes Edgar Allen Poe so write using his style.
E-mail these ideas to my significant other. Translate it to French."

[OUTPUT]
    {
        "input": "Valentine's Day Date Ideas",
        "subtasks": [
            {"function": "WriterSkill.Brainstorm"},
            {"function": "EdgarAllenPoeSkill.Poe"},
            {"function": "WriterSkill.EmailTo", "args": {"recipient": "significant_other"}},
            {"function": "WriterSkill.Translate", "args": {"language": "French"}}
        ]
    }

[AVAILABLE FUNCTIONS]
{{$available_functions}}

[GOAL]
{{$goal}}

[OUTPUT]
"""
```

</div>
</details>


<details>
<summary>変更後のプロンプト</summary>
<div>

```py:
PROMPT = """
You are a planner for the Semantic Kernel.
Your job is to create a properly formatted JSON plan step by step, to satisfy the goal given.
Create a list of subtasks based off the [GOAL] provided.
Each subtask must be from within the [AVAILABLE FUNCTIONS] list. Do not use any functions that are not in the list.
Base your decisions on which functions to use from the description and the name of the function.
Sometimes, a function may take arguments. Provide them if necessary.
The plan should be as short as possible.
If the description contains unknown, be sure to ignore the function.

For example:

[AVAILABLE FUNCTIONS]
WriterSkill.Brainstorm
description: Brainstorm ideas
args:
- input: the input to brainstorm about

EdgarAllenPoeSkill.Poe
description: Write in the style of author Edgar Allen Poe
args:
- input: the input to write about

WriterSkill.EmailTo
description: Write an email to a recipient
args:
- input: the input to write about
- recipient: the recipient's email address.

WriterSkill.Translate
description: translate the input to another language
args:
- input: the text to translate
- language: the language to translate to

_GLOBAL_FUNCTIONS_.f_33c6f6b6_b519_4fd9_9b4b_4a76b4741d7f
description: Generic function, unknown purpose
args:
- available_functions: 
- goal: 

_GLOBAL_FUNCTIONS_.f_33c6f6b6_b519_4fd9_9b4b_4a76b4741d7f
description: Generic function, unknown purpose
args:
- available_functions: 
- goal: 

[GOAL]
"Tomorrow is Valentine's day. I need to come up with a few date ideas.
She likes Edgar Allen Poe so write using his style.
E-mail these ideas to my significant other. Translate it to French."

[OUTPUT]
    {
        "input": "Tomorrow is Valentine's day. I need to come up with a few date ideas.She likes Edgar Allen Poe so write using his style.E-mail these ideas to my significant other. Translate it to French.",
        "subtasks": [
            {"function": "WriterSkill.Brainstorm"},
            {"function": "EdgarAllenPoeSkill.Poe"},
            {"function": "WriterSkill.EmailTo", "args": {"recipient": "significant_other"}},
            {"function": "WriterSkill.Translate", "args": {"language": "French"}}
        ]
    }

[AVAILABLE FUNCTIONS]
{{$available_functions}}

[GOAL]
{{$goal}}

[OUTPUT]
"""
```
</div>
</details>


今回、社内ナレッジ検索という想定ですが、デフォルトのプロンプトだと問題があります。
```input:最初の関数に渡す引数```が、**ユーザの入力の要約**になっています。
今回のような社内ナレッジ検索では、**ユーザの入力をそのまま**渡したかったので、一部変更しました。
```[OUTPUT]に含まれるinputの値を[GOAL]と同一の内容にする```ことで、ユーザの入力をそのまま出力させることができます。

```sh:デフォルトのプロンプトの一部（変更前）
[GOAL]
"Tomorrow is Valentine's day. I need to come up with a few date ideas.
She likes Edgar Allen Poe so write using his style.
E-mail these ideas to my significant other. Translate it to French."

[OUTPUT]
    {
        "input": "Valentine's Day Date Ideas",
        "subtasks": [
            {"function": "WriterSkill.Brainstorm"},
            {"function": "EdgarAllenPoeSkill.Poe"},
            {"function": "WriterSkill.EmailTo", "args": {"recipient": "significant_other"}},
            {"function": "WriterSkill.Translate", "args": {"language": "French"}}
        ]
    }
```

```sh:デフォルトのプロンプトの一部（変更後）
[GOAL]
"Tomorrow is Valentine's day. I need to come up with a few date ideas.
She likes Edgar Allen Poe so write using his style.
E-mail these ideas to my significant other. Translate it to French."

[OUTPUT]
    {
        "input": "Tomorrow is Valentine's day. I need to come up with a few date ideas.She likes Edgar Allen Poe so write using his style.E-mail these ideas to my significant other. Translate it to French.",
        "subtasks": [
            {"function": "WriterSkill.Brainstorm"},
            {"function": "EdgarAllenPoeSkill.Poe"},
            {"function": "WriterSkill.EmailTo", "args": {"recipient": "significant_other"}},
            {"function": "WriterSkill.Translate", "args": {"language": "French"}}
        ]
    }
```

その他の変更点を記載しておきます。

- 例を2つから1つに変更
    - 1つでも十分そうだったため、Few-Shotの例を減らしました。
- スキル実行後に「**GLOBAL_FUNCTION: Generic function**」が追加されるので、それを無視するような指示を追加
    - GLOBAL_FUNCTIONはキャッシュのような役割なのでしょうか、、？
    - Generic functionはC#で使用されている用語、、？（C#全く分からないので要調査です）


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/caa8d23e-2dd3-18f0-aed0-3d45901ae1ff.png)


プランナーでプラン作成をする関数を呼び出す時に、自作のプロンプトを使うことを明示します。


```py:プランナーで使用するプロンプトを指定する
import semantic_kernel as sk
from semantic_kernel.connectors.ai.open_ai import AzureTextCompletion
from semantic_kernel.planning.basic_planner import BasicPlanner

# カーネル作成
kernel = sk.Kernel()
deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env()
kernel.add_text_completion_service("dv", AzureTextCompletion(deployment, endpoint, api_key))

# プランナーで使用するプロンプトを定義
MYPROMPT = """
自作のプロンプト
"""

# プラン作成時に自作のプロンプトを指定
planner = BasicPlanner()
plan = await planner.create_plan_async(user_input, kernel, MYPROMPT)
```



# 8. プランナーで作成されたプランのエラーハンドリング

プランナーでは、関数の実行計画をJSON形式で出力します。
temperatureが高めに設定されているため、たまに変な出力をすることがあります。(特に日本語のまま入力した場合)
そのため、プラン作成後にチェックするのはありかなと思っています。


```py:プランのエラーハンドリング
# ユーザの入力を基にプランを作成
planner = BasicPlanner()
plan = await planner.create_plan_async(user_input, kernel, PROMPT)

num_retry = 3  #プラン再作成の上限回数

for i in range(num_retry):
    # プラン作成
    plan = await planner.create_plan_async(user_input, kernel, PROMPT)

    # スキルとプランの確認
    try:
        # 使用可能なスキルを表示
        print(f"スキル:\n{planner._create_available_functions_string(kernel)}\n")
        
        # 作成されたプランはJSON形式 → 辞書型に変換可能
        plan_dict = json.loads(plan.generated_plan["input"])
        
        # inputを確認
        print(f"最初のタスクに渡されるinput: {plan_dict['input']}\n")

        # 関数の実行順序を確認
        print("サブタスク:")
        for task in plan_dict['subtasks']:
            print(f" - {task}")
        break
    except Exception as e:
        print(plan.generated_plan.result)
        if i==(num_retry-1):
            raise e
        else:
            continue
```



![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/b6a48737-60c1-6b2e-f693-f01354a281bf.png)


今回は```プランを再作成```しているだけですが、関数の引数に```input```が含まれていると期待しない挙動となってしまうので、```inputの存在チェック```なども考えられますね。



# まとめ・所感
Semantic Kernelは便利そうですが、Pythonでの実装例がないのとドキュメントが少ないのが辛いですね、、。

あと、実行順序を正確に制御するにはデフォルトのプロンプトでは物足りない気がしています。
自分でプロンプトを書いた方が色々制御できますし精度は良さそうです。
（Few-Shot書くのめんどくさいですが、、）

実行順序が明確に決まっているのであれば、プランナーを使用せずスキルを順に定義して実行すればよさそうですね。

ソースコードはとりあえず動けばいいやといった感じで殴り書きなのでリファクタリングしてから公開しようかと思います。。
また、Cognitive Searchのベクトル類似検索は申請制で、申請が通るとプライベートリポジトリに招待される形です。
公開していいかどうか分からないのでその辺りも調査しつつ、色々整えてから公開します、、