Semantic Kernel Agent Frameworkでマルチエージェントを構築する(2/2):マルチエージェントの構築

Senamtic Kernelを使い、マルチエージェントの開発を行ったので構築方法に関して記載します。

今回は、前回の記事で構築したSemantic kernelのプラグインをもとに、Semantic Kernel Agent Frameworkを利用しマルチエージェント構築します。

それぞれのエージェントは特定の役割に特化したエージェントとして作成していきます。

※Semantic Kernel Agent Frameworkは現在試験段階であるため、内容が変更される可能性がありますので、最新のドキュメント等をご確認ください。

Semantic Kernel Agent Framework

Semantic Kernel Agent Frameworkは、AIエージェントを簡単に作成し、それらをさまざまなアプリケーションに組み込むことができるプラットフォームです。

このフレームワークを使うことで、セマンティックカーネルの基本機能を活用し、アプリケーションに柔軟で協力的なAI機能を追加できます。

マルチエージェントを利用する利点は、以下のように記載されています。

  1. モジュール型コンポーネント
    • 特定のタスクに応じたエージェントを定義できるため、新しい技術の台頭や要件の進化に応じてアプリケーションを容易に適応できます
  2. コラボレーション
    • 複数のエージェントが共同でタスクに取り組むことができ、分散知能を活用した高度なシステムを構築します
  3. 人間とエージェントの協力
    • データ解析をヒトが確認・調整できるように準備するなど、人間の生産性向上に役立ちます
  4. プロセスのオーケストレーション
    • システムやツール、API間でのタスクを調整し、プロセスの自動化を支援します

また、AIエージェントの活用場面として以下のものがあげられています。

  • 自律性と意思決定
    • 変化する条件に適応できる独立した意思決定が必要な場合に適しています
  • マルチエージェント協力
    • 複数の独立したコンポーネントが協力して動作する必要がある複雑なシステムで有効です
  • インタラクティブで目標志向
    • 目標駆動型の動作が求められる場面で、エージェントベースのフレームワークが適しています

learn.microsoft.com

Agentの種類

Semantic Kernel Agent Frameworkには、Chat Completion AgentとOpen AI Assistant Agentを利用することが可能です。

Chat Completion Agent

Chat Completionは、AIモデルとチャットベースのやり取りを行うためのプロトコルで、リクエストごとにチャット履歴をモデルに提供します。

セマンティックカーネルのAIサービスは、さまざまなAIモデルのチャット完了機能を統合するための統一されたフレームワークを提供します。

learn.microsoft.com

Open AI Assistant Agent

OpenAI Assistant APIは、より高度でインタラクティブなAI機能を提供するためのインターフェースです。これは、パーソナライズされたマルチステップでタスク指向のエージェントを作成することを可能にします。

Chat Completion APIが単純な会話のやり取りに焦点を当てているのに対し、Assistant APIはコード解釈やファイル検索などの追加機能を備え、動的で目標指向のやり取りを実現します。

learn.microsoft.com

マルチエージェントの構築

今回は合計5つのエージェントを作成します。それぞれのエージェントには以下の役割を与えます。

  • ReviewAgent
    • 他のエージェントからの回答をレビューする
  • GeneralAnswerAgent
    • 一般的な質問に対して回答する
  • CompanyRuleDocumentsResearcher
    • 社内規定に関しての質問に対して、データベースからの情報をもとに回答を作成する
    • データがなかった場合はどのようなデータが存在するかを回答する
  • SalesSpecialistAgent
    • 営業に関しての質問に対して、データベースからの情報をもとに回答を作成する
    • データベースから情報を得られなかった場合、一般的な答えを回答する
  • SematicKernelInfoResearcher
    • Semantic Kernelに関しての質問に対して、データベースからの情報をもとに回答を作成する
    • データベースから情報を得られなかった場合、一般的な答えを回答する

前提条件

環境変数やAzureのリソースに関しては、「Semantic Kernel Agent Frameworkでマルチエージェントを構築する(1/2):プラグインの実装と関数処理内容の確認 - JBS Tech Blog」の内容をご確認ください。

エージェントの役割の定義

それぞれのエージェントの名前と指示を定義します。

REVIEWER_NAME = "Reviewer"
REVIEWER_INSTRUCTIONS = """
Your responsibility is to review the content given to you by the other agent.
The goal is to determine if the given answer from other agents includes specific answer to the qestion. 
If so, state that it is approved. Only include the word "approved" in English if it is so.
If not or the given answer can be improved, provide insight on how to refine suggested answer without example.
You should always tie the conversation back to the researcher by the plugin.
Answer in the language the question was asked if answer is not "approved".
"""

CompanyDocumentsResearcher_NAME = "CompanyRuleDocumentsResearcher"
CompanyDocumentsResearcher_INSTRUCTIONS ="""
You are an agent designed to answer questions about company rules using Retrieval-Augmented Generation (RAG). 
The goal is to find documents in database and answer the question given by user.
Only provide a single answer per response.
If information about question exist in DB, response to the question based on the data.
If information does not include the answer to the question, response "cannot find information about {$question} in DB" and what found in DB.
Always analyze the document store to provide an answer to the user's question.
You're laser focused on the goal at hand.
Don't waste time with chit chat.
Answer in the language the question was asked.
"""

SematicKernelInfoResearcher_NAME = "SematicKernelInfoResearcher"
SematicKernelInfoResearcher_INSTRUCTIONS = """
You are an agent designed to answer questions about sematic kernel using Retrieval-Augmented Generation (RAG). 
You leverage internal documentation of semantic kernel and accumulated data to provide accurate and insightful responses. 
The goal is to find documents of semantic kernel in database and answer the question given by user.
Only provide a single answer per response.
If information about question exist in DB, response to the question based on the data.
Also, if search result does not include information about the question, response based on general knowledge.
You're laser focused on the goal at hand.
Don't waste time with chit chat.
Answer in the language the question was asked.
"""

GeneralAnswerAgent_NAME = "GeneralAnswerAgent"
GeneralAnswerAgent_INSTRUCTIONS = """
You are an agent designed to answer a wide range of questions without utilizing Retrieval-Augmented Generation (RAG).
Your key characteristics include:
1. Providing answers based on general knowledge and expertise accumulated over ten years of consulting experience.
2. Handling inquiries from various fields with confidence and clarity.
3. Using the ChatCompletions model to generate responses, ensuring that they are well-structured, informative, and relevant.
Always strive for clarity, conciseness, and thoroughness in your answers, and encourage follow-up questions for deeper understanding.
Only provide a single proposal per response.
You're laser focused on the goal at hand.
Don't waste time with chit chat.
Answer in the language the question was asked".
"""

SalseSpecialistAgent_NAME = "SalesSpecialistAgent"
SalseSpecialistAgent_INSTRUCTIONS = """
You are an agent designed to answer questions about salse technique using Retrieval-Augmented Generation (RAG). 
You leverage internal documentation of salse and accumulated data to provide accurate and insightful responses. 
Provide answers based on documents about sales.
If relevant information with question cannot found from DB, response "cannot find information about {$question} in DB." and suggest to ask to GeneralAnswerAgent about the question.
Always strive for clarity, conciseness, and thoroughness in your answers, and encourage follow-up questions for deeper understanding.
Only provide a single answer per response.
You're laser focused on the goal at hand.
Don't waste time with chit chat.
Answer in the language the question was asked.
"""

プラグインの追加

エージェントで利用するプラグインを定義します。

プラグインは「Semantic Kernel Agent Frameworkでマルチエージェントを構築する(1/2):プラグインの実装と関数処理内容の確認 - JBS Tech Blog」で作成したものをベースに作成します。

# 複数のRAGPluginを追加するため、親クラスを定義
class BaseRAGPlugin:
    def __init__(self, vectorstore_path: str):
        self.embeddings = AzureOpenAIEmbeddings(
            azure_deployment=os.getenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT"),
        )
        self.vectorstore_faiss = FAISS.load_local(
            vectorstore_path, self.embeddings, allow_dangerous_deserialization=True
        ) # ベクターストア(FAISS)のデータベースをロード


class InternalDocumentationOfSalesRAGPlugin(BaseRAGPlugin):
    '''
    営業に関するドキュメントのRAG plugin
    '''
    def __init__(self):
        super().__init__("./faiss-db")
        self.prompt = PromptTemplate(
            template=(
                "あなたは営業に関するAIアシスタントです。以下のコンテキストを考慮してください:\n"
                "{context}\n\n"
                "次の質問に答えてください:\n"
                "{question}\n\n"
                "アシスタント:"
            ),
            input_variables=["context", "question"],
        )

    @kernel_function(description="search documents of sales")
    def search(self, query: str) -> str:
        retriever = self.vectorstore_faiss.as_retriever()
        context = retriever.invoke(query)
        # Return text from first result
        prompt_text = self.prompt.format(context=context[0].page_content, question=query)
        return prompt_text

class SematicKernelInfoRAGPlugin(BaseRAGPlugin):
    '''
    Sematic Kernelに関するドキュメントのRAG plugin
    '''
    def __init__(self):
        super().__init__("./faiss-semantickernel-db")
        self.prompt = PromptTemplate(
            template=(
                "あなたはSematic Kernelに関するAIアシスタントです。以下のコンテキストを考慮してください:\n"
                "{context}\n\n"
                "次の質問に答えてください:\n"
                "{question}\n\n"
                "アシスタント:"
            ),
            input_variables=["context", "question"],
        )

    @kernel_function(description="search documents about Semantic Kernel")
    def search(self, query: str) -> str:
        retriever = self.vectorstore_faiss.as_retriever()
        context = retriever.invoke(query)
        # Return text from first result
        prompt_text = self.prompt.format(context=context[0].page_content, question=query)
        return prompt_text


class InternalDocumentationRAGPlugin(BaseRAGPlugin):
    '''
    社内規定に関するドキュメントのRAG plugin
    '''
    def __init__(self):
        super().__init__("./faiss-company-rule-db")
        self.prompt = PromptTemplate(
            template=(
                "あなたは社内規定に関するAIアシスタントです。以下のコンテキストを考慮してください:\n"
                "{context}\n\n"
                "次の質問に答えてください:\n"
                "{question}\n\n"
                "アシスタント:"
            ),
            input_variables=["context", "question"],
        )

    @kernel_function(description="search documents about internal company rules")
    def search(self, query: str) -> str:
        retriever = self.vectorstore_faiss.as_retriever()
        context = retriever.invoke(query)
        # Return text from first result
        prompt_text = self.prompt.format(context=context[0].page_content, question=query)
        return prompt_text

エージェントの作成

今回の例では、Chat Completion Agentですべてのエージェントを作成します。

def _create_kernel_with_chat_completion(service_id: str) -> Kernel:
    kernel = Kernel()
    chat_completion = AzureChatCompletion(
        deployment_name=os.getenv("AZURE_OPENAI_DEPLOYMENT"),
        api_key=os.getenv("deployment_api_key"),
        base_url=os.getenv("deployment_base_url"),
        service_id=service_id
    )
    kernel.add_service(chat_completion)

    return kernel


def create_chat_agent(service_id: str, name: str, instructions: str, kernel: Kernel, plugin=None, plugin_name=None) -> ChatCompletionAgent:
    if plugin and plugin_name:
        kernel.add_plugin(plugin=plugin, plugin_name=plugin_name)
        # 
        settings = kernel.get_prompt_execution_settings_from_service_id(service_id=service_id)
        settings.function_choice_behavior = FunctionChoiceBehavior.Auto()
    else:
        settings = None
    
    return ChatCompletionAgent(
        service_id=service_id,
        kernel=kernel,
        name=name,
        instructions=instructions,
        execution_settings=settings,
    )


async def main():
    # 環境変数の読み込み
    load_dotenv()
    try:
        # reviewerの作成
        agent_reviewer = create_chat_agent(
            service_id="reviewer",
            name=REVIEWER_NAME,
            instructions=REVIEWER_INSTRUCTIONS,
            kernel=_create_kernel_with_chat_completion("reviewer")
        )

        # documentsResearcherの作成
        agent_documentsResearcher = create_chat_agent(
            service_id="company_rule_documents_researcher",
            name=CompanyDocumentsResearcher_NAME,
            instructions=CompanyDocumentsResearcher_INSTRUCTIONS,
            kernel=_create_kernel_with_chat_completion("company_rule_documents_researcher"),
            plugin=InternalDocumentationRAGPlugin(),
            plugin_name="InternalDocumentaionRAG"
        )

        # semantic_kernel_info_agentの作成
        agent_semanticKernelInfoAgent = create_chat_agent(
            service_id="sematic_kernel_info_researcher",
            name=SematicKernelInfoResearcher_NAME,
            instructions=SematicKernelInfoResearcher_INSTRUCTIONS,
            kernel=_create_kernel_with_chat_completion("sematic_kernel_info_researcher"),
            plugin=SematicKernelInfoRAGPlugin(),
            plugin_name="SematicKernelInfoRAG"
        )

        
        # salseSpecialist agentの作成
        agent_salesSpecialist = create_chat_agent(
            service_id="salse_specialist_agent",
            name=SalseSpecialistAgent_NAME,
            instructions=SalseSpecialistAgent_INSTRUCTIONS,
            kernel=_create_kernel_with_chat_completion("salse_specialist_agent"),
            plugin=InternalDocumentationOfSalesRAGPlugin(),
            plugin_name="SalesRAGPlugin"
        )


        # generalAnswerAgentの作成
        agent_generalAnswer = create_chat_agent(
            service_id="general_answer_agent",
            name=GeneralAnswerAgent_NAME,
            instructions=GeneralAnswerAgent_INSTRUCTIONS,
            kernel=_create_kernel_with_chat_completion("general_answer_agent")
        )

    ....

selection strategyの定義

selection strategyで利用する、selection_functionを定義します。ここで定義した内容に基づいてどのAgentが選択されるかが決定されます。

以下の内容は、ユーザーのインプットに基づいてReviewer以外の最適なAgentが選択され、そのAgentに作成された回答をReviewerが確認するように定義しています。

selection_function = KernelFunctionFromPrompt(
            function_name="selection",
            prompt=f"""
            Determine which participant takes the next turn in a conversation based on the the most recent participant.
            State only the name of the participant to take the next turn.
            No participant should take more than one turn in a row.
            if the question is about company's internal rules, choose the {CompanyDocumentsResearcher_NAME} agent.
            if the question is about information of sematic kernel, choose the {SematicKernelInfoResearcher_NAME} agent.
            if the question is about sales technique, choose the {SalseSpecialistAgent_NAME} agent.
            If the question is a general question that does not require specialized internal knowledge, choose the {GeneralAnswerAgent_NAME}.
            After agent takes its turn, pass it to the {REVIEWER_NAME} agent for a check the answer.

            Choose only from these participants:
            - {REVIEWER_NAME}
            - {CompanyDocumentsResearcher_NAME}
            - {SematicKernelInfoResearcher_NAME}
            - {GeneralAnswerAgent_NAME}
            - {SalseSpecialistAgent_NAME}

            To be clear, alway follow these rules when selecting the next participant:
            - After user input, it is either {CompanyDocumentsResearcher_NAME}'s or {SematicKernelInfoResearcher_NAME}'s or {GeneralAnswerAgent_NAME} or {SalseSpecialistAgent_NAME}'s turn.
            - After agent other than {REVIEWER_NAME} replies, it is {REVIEWER_NAME}'s turn.

            History:
            {{{{$history}}}}
            """,
        )

termination strategyの定義

termination strategyで利用する、termination_functionを定義します。termination_functionで定義された内容で回答生成を終了するかが決定されます。

以下の設定では、回答(メッセージ履歴)がsatisfactoryであるかを判断します。判断する条件として、Reviewerのレスポンスに"approved"が含まれている場合はsatisfactoryであるとする条件を加えています。

TERMINATION_KEYWORD = "satisfactory"
        termination_function = KernelFunctionFromPrompt(
            function_name="termination",
            prompt=f"""
                Examine the RESPONSE and determine whether the content has been deemed satisfactory.
                If {REVIEWER_NAME}'s response includes "approved", it is satisfactory.
                If content is satisfactory, respond with single word without explanation: {TERMINATION_KEYWORD}.
                
                RESPONSE:
                {{{{$history}}}}
                """,
            )

現状の設定では、Reviewerのレスポンスに"approved"が含まれていなくてもsatisfactoryと判断されます。

"If content is not satisfactory, respond ..."などの条件を加えると、satisfactoryでない場合は終了しない設定にすることが可能です。

AgentGroupChatの作成

AgentGroupChatを作成します。ここで、参加するAgentと selection_strategy、termination_strategyを設定します。

        # Create chat with participating agents
        agents = [agent_reviewer, agent_documentsResearcher, agent_semantic_kernel_info_agent, agent_generalAnswerAgent, agent_salesSpecialist]
        chat = AgentGroupChat(
            agents=agents,
            selection_strategy=KernelFunctionSelectionStrategy(
                function=selection_function, 
                kernel=_create_kernel_with_chat_completion("selection"),
                result_parser=lambda result: str(result.value[0]) if result.value is not None else REVIEWER_NAME,
                #result_parser=lambda result: selection_result_parser(result), # selection_functionの内容を確認したい場合に、メソッドを定義
                agent_variable_name="agents",
                history_variable_name="history",
            ),
            termination_strategy=KernelFunctionTerminationStrategy(
                agents=[agent_reviewer], 
                function=termination_function,
                kernel = _create_kernel_with_chat_completion("termination"),
                result_parser=lambda result: TERMINATION_KEYWORD in str(result.value[0]).lower(),
                #result_parser=lambda result: _parse_and_print_result(result, TERMINATION_KEYWORD), # selection_functionの内容を確認したい場合に、メソッドを定義
                history_variable_name="history",
                maximum_iterations=6 # 途中でterminateされなかった場合の、処理の最大何回
                ),
        )

コード全体

ここは長いため折りたたみます。


コード全体(クリックで開く)

import asyncio
import platform
import os
import json
import logging
from dotenv import load_dotenv

from semantic_kernel import Kernel
from semantic_kernel.agents import AgentGroupChat, ChatCompletionAgent
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.agents.strategies.selection.kernel_function_selection_strategy import KernelFunctionSelectionStrategy
from semantic_kernel.agents.strategies.termination.kernel_function_termination_strategy import KernelFunctionTerminationStrategy
from semantic_kernel.agents.strategies.termination.termination_strategy import TerminationStrategy
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.filters.filter_types import FilterTypes
from semantic_kernel.functions import kernel_function, KernelFunctionFromPrompt
from semantic_kernel.utils.logging import setup_logging

from langchain_core.prompts import PromptTemplate
from langchain_openai import AzureOpenAIEmbeddings
from langchain_community.vectorstores import FAISS

# RuntimeError: Event loop is closedの発生を解消
if platform.system() == 'Windows':
    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())


REVIEWER_NAME = "Reviewer"
REVIEWER_INSTRUCTIONS = """
Your responsibility is to review the content given to you by the other agent.
The goal is to determine if the given answer from other agents includes specific answer to the qestion. 
If so, state that it is approved. Only include the word "approved" in English if it is so.
If not or the given answer can be improved, provide insight on how to refine suggested answer without example.
You should always tie the conversation back to the researcher by the plugin.
Answer in the language the question was asked if answer is not "approved"..
"""

CompanyDocumentsResearcher_NAME = "CompanyRuleDocumentsResearcher"
CompanyDocumentsResearcher_INSTRUCTIONS ="""
You are an agent designed to answer questions about company rules using Retrieval-Augmented Generation (RAG). 
The goal is to find documents in database and answer the question given by user.
Only provide a single answer per response.
If information about question exist in DB, response to the question based on the data.
If information does not include the answer to the question, response "cannot find information about {$question} in DB" and what found in DB.
Always analyze the document store to provide an answer to the user's question.
You're laser focused on the goal at hand.
Don't waste time with chit chat.
Answer in the language the question was asked.
"""

SematicKernelInfoResearcher_NAME = "SematicKernelInfoResearcher"
SematicKernelInfoResearcher_INSTRUCTIONS = """
You are an agent designed to answer questions about sematic kernel using Retrieval-Augmented Generation (RAG). 
You leverage internal documentation of semantic kernel and accumulated data to provide accurate and insightful responses. 
The goal is to find documents of semantic kernel in database and answer the question given by user.
Only provide a single answer per response.
If information about question exist in DB, response to the question based on the data.
Also, if search result does not include information about the question, response based on general knowledge.
You're laser focused on the goal at hand.
Don't waste time with chit chat.
Answer in the language the question was asked.
"""

GeneralAnswerAgent_NAME = "GeneralAnswerAgent"
GeneralAnswerAgent_INSTRUCTIONS = """
You are an agent designed to answer a wide range of questions without utilizing Retrieval-Augmented Generation (RAG).
Your key characteristics include:
1. Providing answers based on general knowledge and expertise accumulated over ten years of consulting experience.
2. Handling inquiries from various fields with confidence and clarity.
3. Using the ChatCompletions model to generate responses, ensuring that they are well-structured, informative, and relevant.
Always strive for clarity, conciseness, and thoroughness in your answers, and encourage follow-up questions for deeper understanding.
Only provide a single proposal per response.
You're laser focused on the goal at hand.
Don't waste time with chit chat.
Answer in the language the question was asked".
"""

SalseSpecialistAgent_NAME = "SalesSpecialistAgent"
SalseSpecialistAgent_INSTRUCTIONS = """
You are an agent designed to answer questions about salse technique using Retrieval-Augmented Generation (RAG). 
You leverage internal documentation of salse and accumulated data to provide accurate and insightful responses. 
Provide answers based on documents about sales.
If relevant information with question cannot found from DB, response "cannot find information about {$question} in DB." and suggest to ask to GeneralAnswerAgent about the question.
Always strive for clarity, conciseness, and thoroughness in your answers, and encourage follow-up questions for deeper understanding.
Only provide a single answer per response.
You're laser focused on the goal at hand.
Don't waste time with chit chat.
Answer in the language the question was asked.
"""

class BaseRAGPlugin:
    def __init__(self, vectorstore_path: str):
        self.embeddings = AzureOpenAIEmbeddings(
            azure_deployment=os.getenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT"),
        )
        self.vectorstore_faiss = FAISS.load_local(
            vectorstore_path, self.embeddings, allow_dangerous_deserialization=True
        )

class InternalDocumentationOfSalesRAGPlugin(BaseRAGPlugin):
    def __init__(self):
        super().__init__("./faiss-db")
        self.prompt = PromptTemplate(
            template=(
                "あなたは営業に関するAIアシスタントです。以下のコンテキストを考慮してください:\n"
                "{context}\n\n"
                "次の質問に答えてください:\n"
                "{question}\n\n"
                "アシスタント:"
            ),
            input_variables=["context", "question"],
        )

    @kernel_function(description="search documents of sales")
    def search(self, query: str) -> str:
        retriever = self.vectorstore_faiss.as_retriever()
        context = retriever.invoke(query)
        prompt_text = self.prompt.format(context=context[0].page_content, question=query)
        return prompt_text

class SematicKernelInfoRAGPlugin(BaseRAGPlugin):
    def __init__(self):
        super().__init__("./faiss-semantickernel-db")
        self.prompt = PromptTemplate(
            template=(
                "あなたはSematic Kernelに関するAIアシスタントです。以下のコンテキストを考慮してください:\n"
                "{context}\n\n"
                "次の質問に答えてください:\n"
                "{question}\n\n"
                "アシスタント:"
            ),
            input_variables=["context", "question"],
        )

    @kernel_function(description="search documents about Semantic Kernel")
    def search(self, query: str) -> str:
        retriever = self.vectorstore_faiss.as_retriever()
        context = retriever.invoke(query)
        prompt_text = self.prompt.format(context=context[0].page_content, question=query)
        return prompt_text

class InternalDocumentationRAGPlugin(BaseRAGPlugin):
    def __init__(self):
        super().__init__("./faiss-company-rule-db")
        self.prompt = PromptTemplate(
            template=(
                "あなたは社内規定に関するAIアシスタントです。以下のコンテキストを考慮してください:\n"
                "{context}\n\n"
                "次の質問に答えてください:\n"
                "{question}\n\n"
                "アシスタント:"
            ),
            input_variables=["context", "question"],
        )

    @kernel_function(description="search documents about internal company rules")
    def search(self, query: str) -> str:
        retriever = self.vectorstore_faiss.as_retriever()
        context = retriever.invoke(query)
        logging.debug(f'return text: "{context[0].page_content}"')
        prompt_text = self.prompt.format(context=context[0].page_content, question=query)
        return prompt_text

def _create_kernel_with_chat_completion(service_id: str) -> Kernel:
    kernel = Kernel()
    chat_completion = AzureChatCompletion(
        deployment_name=os.getenv("AZURE_OPENAI_DEPLOYMENT"),
        api_key=os.getenv("deployment_api_key"),
        base_url=os.getenv("deployment_base_url"),
        service_id=service_id
    )
    kernel.add_service(chat_completion)
      
    return kernel

def selection_result_parser(result):
    if result.value is not None:
        result_message = json.loads(result.metadata["messages"].serialize())
        logging.info(f'Selected Agent: {result}')
        return str(result.value[0])
    else:
        return REVIEWER_NAME

def _parse_and_print_result(result, termination_keyword):
    result_message = json.loads(result.metadata["messages"].serialize())
    logging.info(f"Termination function result:\n {result}")
    is_terminate = termination_keyword.lower() in str(result.value[0]).lower()
    logging.info(f"Should terminate:\n {is_terminate}")
    return is_terminate

def create_chat_agent(service_id: str, name: str, instructions: str, kernel: Kernel, plugin=None, plugin_name=None) -> ChatCompletionAgent:
    if plugin and plugin_name:
        kernel.add_plugin(plugin=plugin, plugin_name=plugin_name)
        settings = kernel.get_prompt_execution_settings_from_service_id(service_id=service_id)
        settings.function_choice_behavior = FunctionChoiceBehavior.Auto()
    else:
        settings = None
    
    return ChatCompletionAgent(
        service_id=service_id,
        kernel=kernel,
        name=name,
        instructions=instructions,
        execution_settings=settings,
    )

async def main():
    logging.basicConfig(
        level=logging.WARN, # 適切なレベルに設定
        format="[%(asctime)s - %(name)s:%(lineno)d - %(levelname)s] %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S",
    )

    load_dotenv()
    try:
        agent_reviewer = create_chat_agent(
            service_id="reviewer",
            name=REVIEWER_NAME,
            instructions=REVIEWER_INSTRUCTIONS,
            kernel=_create_kernel_with_chat_completion("reviewer")
        )

        agent_documentsResearcher = create_chat_agent(
            service_id="company_rule_documents_researcher",
            name=CompanyDocumentsResearcher_NAME,
            instructions=CompanyDocumentsResearcher_INSTRUCTIONS,
            kernel=_create_kernel_with_chat_completion("company_rule_documents_researcher"),
            plugin=InternalDocumentationRAGPlugin(),
            plugin_name="InternalDocumentaionRAG"
        )

        agent_semanticKernelInfoAgent = create_chat_agent(
            service_id="sematic_kernel_info_researcher",
            name=SematicKernelInfoResearcher_NAME,
            instructions=SematicKernelInfoResearcher_INSTRUCTIONS,
            kernel=_create_kernel_with_chat_completion("sematic_kernel_info_researcher"),
            plugin=SematicKernelInfoRAGPlugin(),
            plugin_name="SematicKernelInfoRAG"
        )
        
        agent_salesSpecialist = create_chat_agent(
            service_id="salse_specialist_agent",
            name=SalseSpecialistAgent_NAME,
            instructions=SalseSpecialistAgent_INSTRUCTIONS,
            kernel=_create_kernel_with_chat_completion("salse_specialist_agent"),
            plugin=InternalDocumentationOfSalesRAGPlugin(),
            plugin_name="SalesRAGPlugin"
        )

        agent_generalAnswer = create_chat_agent(
            service_id="general_answer_agent",
            name=GeneralAnswerAgent_NAME,
            instructions=GeneralAnswerAgent_INSTRUCTIONS,
            kernel=_create_kernel_with_chat_completion("general_answer_agent")
        )
        
        selection_function = KernelFunctionFromPrompt(
            function_name="selection",
            prompt=f"""
            Determine which participant takes the next turn in a conversation based on the the most recent participant.
            State only the name of the participant to take the next turn.
            No participant should take more than one turn in a row.
            if the question is about company's internal rules, choose the {CompanyDocumentsResearcher_NAME} agent.
            if the question is about information of sematic kernel, choose the {SematicKernelInfoResearcher_NAME} agent.
            if the question is about sales technique, choose the {SalseSpecialistAgent_NAME} agent.
            If the question is a general question that does not require specialized internal knowledge, choose the {GeneralAnswerAgent_NAME}.
            After agent takes its turn, pass it to the {REVIEWER_NAME} agent for a check the answer.

            Choose only from these participants:
            - {REVIEWER_NAME}
            - {CompanyDocumentsResearcher_NAME}
            - {SematicKernelInfoResearcher_NAME}
            - {GeneralAnswerAgent_NAME}
            - {SalseSpecialistAgent_NAME}

            To be clear, alway follow these rules when selecting the next participant:
            - After user input, it is either {CompanyDocumentsResearcher_NAME}'s or {SematicKernelInfoResearcher_NAME}'s or {GeneralAnswerAgent_NAME} or {SalseSpecialistAgent_NAME}'s turn.
            - After agent other than {REVIEWER_NAME} replies, it is {REVIEWER_NAME}'s turn.
            - After {REVIEWER_NAME} completes its turn, the conversation can continue with another relevant agent based on the context or switch back to wait for user input.

            History:
            {{{{$history}}}}
            """,
        )

        TERMINATION_KEYWORD = "satisfactory"
        termination_function = KernelFunctionFromPrompt(
            function_name="termination",
            prompt=f"""
                Examine the RESPONSE and determine whether the content has been deemed satisfactory.
                If {REVIEWER_NAME}'s response includes "approved", it is satisfactory.
                If RESPONSE includes more than two {REVIEWER_NAME}'s responses, it is satisfactory.
                If content is satisfactory, respond with single word without explanation: {TERMINATION_KEYWORD}.
                
                RESPONSE:
                {{{{$history}}}}
                """,
            )

        agents = [agent_reviewer, agent_documentsResearcher, agent_semanticKernelInfoAgent, agent_generalAnswer, agent_salesSpecialist]
        chat = AgentGroupChat(
            agents=agents,
            selection_strategy=KernelFunctionSelectionStrategy(
                function=selection_function,
                kernel=_create_kernel_with_chat_completion("selection"),
                result_parser=lambda result: str(result.value[0]) if result.value is not None else REVIEWER_NAME,
                #result_parser=lambda result: selection_result_parser(result),
                agent_variable_name="agents",
                history_variable_name="history",
            ),
            termination_strategy=KernelFunctionTerminationStrategy(
                agents=[agent_reviewer], 
                function=termination_function,
                kernel=_create_kernel_with_chat_completion("termination"),
                result_parser=lambda result: TERMINATION_KEYWORD in str(result.value[0]).lower(),
                #result_parser=lambda result: _parse_and_print_result(result, TERMINATION_KEYWORD),
                history_variable_name="history",
                maximum_iterations=6
            ),
        )

        userInput = None
        is_completed: bool = False
        while not is_completed:
            userInput = input("User > ")
            if not userInput:
                print('empty input')
                continue

            if userInput == "exit":
                break

            if userInput.lower() == "reset":
                await chat.reset()
                print('[conversation has been reset]')
                continue

            await chat.add_chat_message(ChatMessageContent(role=AuthorRole.USER, content=userInput))
            
            async for response in chat.invoke():
                print(f"# {response.role} - {response.name or '*'}: '{response.content}'")
        
            if chat.is_complete:
                print("chat is complete")
                is_completed = True
                break
    except Exception as e:
        print(f"An error occurred: {e}")
    finally:
        print("finished.")

if __name__ == "__main__":
    asyncio.run(main())


実行結果

以下に、いくつかプロンプトの実行結果を記載します。

User > semantic kernelの開発元を教えてください。
# assistant - SematicKernelInfoResearcher: 'Semantic Kernelは、マイクロソフトが提供するエージェントフレームワークです。これは、AIモデルを用いたアプリケーション開発をサポートするために設計されています。特に自然言語処理や知識ベースのエージェントを構築する際 に便利です。'
# assistant - Reviewer: 'approved'
chat is complete
User > 日本の人口を教えてください。
# assistant - GeneralAnswerAgent: '2023年の時点で、日本の人口は約1億2500万人とされています。ただし、人口は年々変動しているため、最新の正確なデータを確認することをお勧めします。日本は少子高齢化が進んでおり、今後の人口動向にも注目が必要です。さらに質問があればお知らせください。'
# assistant - Reviewer: '内容には日本の人口についての具体的な情報が含まれており、情報も最新の状況に言及されています。したがって
、これは承認されます。

approved'
chat is complete
User > 経費に含まれるものを教えてください
# assistant - CompanyRuleDocumentsResearcher: '経費に含まれるものは以下の通りです:
- 出張費(交通費、宿泊費、食事代など)
- 事務用品費
- 講演会・セミナー参加費
- 業務に必要なソフトウェア購入費'
# assistant - Reviewer: 'approved'
chat is complete
User > 有給の利用方法を教えてください。
# assistant - CompanyRuleDocumentsResearcher: 'cannot find information about 有給の利用方法 in DB. Found information: アプリを使用しない場合は、従来の形式の経費精算申請書を上司の承認を得た上で、経理部門に提出すること。'
# assistant - Reviewer: '回答には、有給の利用方法についての具体的な情報が含まれていません。より明確かつ具体的な手順やポリシーに
ついて説明する必要があります。例えば、取得手続き、承認プロセス、申請方法についての詳細を含めると良いでしょう。研究者にフォーカスを当てて、関連する情報を提供することが重要です。'
chat is complete

Termination_functionとSelection_functionsに次の指示を追加したときの動作です。

# Selection_functions
"After {REVIEWER_NAME}' turn, pass it to the agent that did not responsed in History if not terminated."
# Termination_function
"If content is not satisfactory, respond with single word without explanation: improve."
User > 給与制度を説明してください。
# assistant - CompanyRuleDocumentsResearcher: 'cannot find information about 給与制度 in DB. Found information related to 経費精算申請書 and submission processes.'
# assistant - Reviewer: '給与制度についての具体的な情報が含まれていないため、回答は不十分です。給与制度に関する説明を明確にし、
基本的な要素やその目的、構成要素、計算方法などを示すことで、より充実した内容にする必要があります。研究者に関連する情報を提供するために、給与制度に基づく規定や方針についても考慮してください。'
# assistant - SalesSpecialistAgent: 'cannot find information about 給与制度 in DB. I suggest asking the GeneralAnswerAgent about this question.'
# assistant - GeneralAnswerAgent: '給与制度とは、企業が従業員に対して支払う給与の決定に関するルールや方針を指します。以下の要素
を含むことが一般的です。

1. **基本給**:従業員が職務に対して受け取る基準となる賃金で、職務内容や責任、業績に基づいて設定されます。
2. **手当**:基本給に加算されるもので、通勤手当、住宅手当、家族手当などがあります。
3. **ボーナス**:業績に基づいて支給される一時金で、年に数回支給されることがあります。
4. **昇給**:定期的に基本給が増加する制度で、通常は年に1回行われることが多いです。
5. **評価制度**:従業員の業績や貢献度を評価し、給与に反映させる仕組みで、主に年に1回の評価が行われます。
6. **退職金制度**:退職時に支給される金銭の制度で、勤務年数に基づいて計算されることが一般的です。

給与制度は、従業員のモチベーションや企業の生産性に大きな影響を与えるため、適切に設計されることが重要です。ご質問があれば、さらにお答えいたします。'
# assistant - Reviewer: 'approved'

トークン数・メッセージ履歴の確認

マルチエージェントのシステムの開発では、予期せずトークン数が増加してしまうことやどのような流れで結論に至ったか不明なことがあります。

そこで、トークン数と処理の内容を確認する方法を記載します。

Azure AI Foundry(旧Azure AI Studio)、Azure Monitorでのトークン数確認

Azure AI Foundryでは、「デプロイ」から利用しているモデルを選択し、メトリックを確認できます。

Azure AI Foundry

Azure monitorではProcessed Inference Tokensを確認します。Azure AI FoundryとToken数に若干差がありますが、同じ実行のToken数を表示しています。

※Processed Inference Tokens: Number of inference tokens processed on an OpenAI model. Calculated as prompt tokens (input) plus generated tokens (output).

Azure Monitor

コードでのメッセージ履歴・トークン数の取得・管理

ログ機能を追加することで、AgentやFunctionの呼び出しやトークン数の情報を取得することができます。

Semantic Kernelのログに関しては、「Semantic Kernel Agent Frameworkでマルチエージェントを構築する(1/2):プラグインの実装と関数処理内容の確認 - JBS Tech Blog」に記載してますので、そちらをご覧ください。

ログを追加し、ユーザーのプロンプト入力から、アシスタントの回答までどのように処理が行われたか確認を行います。

# Set the logging level for semantic_kernel.kernel to INFO.
    logging.basicConfig(
        level=logging.INFO,  # ログレベルを設定
        format="[%(asctime)s - %(name)s:%(lineno)d - %(levelname)s] %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S",
    )
ログの結果

ここは長いため折りたたみます。


ログ全体(クリックで開く)
###の後の文字は、後から追加しています。

User > リモートワークが週に何回可能か教えてください。

[2024-12-02 15:43:18 - semantic_kernel.agents.group_chat.agent_chat:112 - INFO] Adding `1` agent chat messages

### selection function #total_tokens=322
[2024-12-02 15:43:18 - semantic_kernel.agents.strategies.selection.kernel_function_selection_strategy:73 - INFO] Kernel Function Selection Strategy next method called, invoking function: , selection
[2024-12-02 15:43:18 - semantic_kernel.functions.kernel_function:19 - INFO] Function selection invoking.
[2024-12-02 15:43:20 - httpx:1786 - INFO] HTTP Request: POST https://<openai_source>.openai.azure.com/openai/deployments/gpt-4o-mini/chat/completions?api-version=2024-06-01 "HTTP/1.1 200 OK"
[2024-12-02 15:43:20 - semantic_kernel.connectors.ai.open_ai.services.open_ai_handler:194 - INFO] OpenAI usage: CompletionUsage(completion_tokens=5, prompt_tokens=317, total_tokens=322, completion_tokens_details=None, prompt_tokens_details=None)
[2024-12-02 15:43:20 - semantic_kernel.functions.kernel_function:29 - INFO] Function selection succeeded.
[2024-12-02 15:43:20 - semantic_kernel.functions.kernel_function:53 - INFO] Function completed. Duration: 1.637971s
[2024-12-02 15:43:20 - semantic_kernel.agents.strategies.selection.kernel_function_selection_strategy:84 - INFO] Kernel Function Selection Strategy next method completed: , selection, result: [ChatMessageContent(inner_content=ChatCompletion(id='chatcmpl-AZuRO4m20879SqELorsVKhrwEZN1u', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='CompanyRuleDocumentsResearcher', refusal=None, role='assistant', audio=None, function_call=None, tool_calls=None), content_filter_results={'hate': {'filtered': False, 'severity': 'safe'}, 'protected_material_code': {'filtered': False, 'detected': False}, 'protected_material_text': {'filtered': False, 'detected': False}, 'self_harm': {'filtered': False, 'severity': 'safe'}, 'sexual': {'filtered': False, 'severity': 'safe'}, 'violence': {'filtered': False, 'severity': 'safe'}})], created=1733121802, model='gpt-4o-mini', object='chat.completion', service_tier=None, system_fingerprint='fp_04751d0b65', usage=CompletionUsage(completion_tokens=5, prompt_tokens=317, total_tokens=322, completion_tokens_details=None, prompt_tokens_details=None), prompt_filter_results=[{'prompt_index': 0, 'content_filter_results': {'hate': {'filtered': False, 'severity': 'safe'}, 'jailbreak': {'filtered': False, 'detected': False}, 'self_harm': {'filtered': False, 'severity': 'safe'}, 'sexual': {'filtered': False, 'severity': 'safe'}, 'violence': {'filtered': False, 'severity': 'safe'}}}]), ai_model_id='gpt-4o-mini', metadata={'logprobs': None, 'id': 'chatcmpl-AZuRO4m20879SqELorsVKhrwEZN1u', 'created': 1733121802, 'system_fingerprint': 'fp_04751d0b65', 'usage': CompletionUsage(prompt_tokens=317, completion_tokens=5)}, content_type='message', role=<AuthorRole.ASSISTANT: 'assistant'>, name=None, items=[TextContent(inner_content=None, ai_model_id=None, metadata={}, content_type='text', text='CompanyRuleDocumentsResearcher', encoding=None)], encoding=None, finish_reason=<FinishReason.STOP: 'stop'>)]
[2024-12-02 15:43:20 - root:329 - INFO] Selected Agent: CompanyRuleDocumentsResearcher

### agent CompanyRuleDocumentsResearcher #total_tokens=230
[2024-12-02 15:43:20 - semantic_kernel.agents.group_chat.agent_chat:139 - INFO] Invoking agent CompanyRuleDocumentsResearcher   
[2024-12-02 15:43:21 - httpx:1786 - INFO] HTTP Request: POST https://<openai_source>/openai/deployments/gpt-4o-mini/chat/completions?api-version=2024-06-01 "HTTP/1.1 200 OK"
[2024-12-02 15:43:21 - semantic_kernel.connectors.ai.open_ai.services.open_ai_handler:194 - INFO] OpenAI usage: CompletionUsage(completion_tokens=28, prompt_tokens=202, total_tokens=230, completion_tokens_details=None, prompt_tokens_details=None)
[2024-12-02 15:43:21 - semantic_kernel.connectors.ai.chat_completion_client_base:149 - INFO] processing 1 tool calls in parallel.
[2024-12-02 15:43:21 - semantic_kernel.kernel:383 - INFO] Calling InternalDocumentaionRAG-search function with args: {"query":" リモートワーク 週に何回"}

#### agent CompanyRuleDocumentsResearcher with function InternalDocumentaionRAG-search #total_tokens=413
[2024-12-02 15:43:21 - semantic_kernel.functions.kernel_function:19 - INFO] Function InternalDocumentaionRAG-search invoking.
[2024-12-02 15:43:21 - httpx:1038 - INFO] HTTP Request: POST https://<openai_source>//openai/deployments/text-embedding-ada-002/embeddings?api-version=2024-07-01-preview "HTTP/1.1 200 OK"
[2024-12-02 15:43:21 - semantic_kernel.functions.kernel_function:29 - INFO] Function InternalDocumentaionRAG-search succeeded.
[2024-12-02 15:43:21 - semantic_kernel.functions.kernel_function:53 - INFO] Function completed. Duration: 0.661601s
[2024-12-02 15:43:22 - httpx:1786 - INFO] HTTP Request: POST https://<openai_source>/openai/deployments/gpt-4o-mini/chat/completions?api-version=2024-06-01 "HTTP/1.1 200 OK"
[2024-12-02 15:43:22 - semantic_kernel.connectors.ai.open_ai.services.open_ai_handler:194 - INFO] OpenAI usage: CompletionUsage(completion_tokens=41, prompt_tokens=372, total_tokens=413, completion_tokens_details=None, prompt_tokens_details=None)

[2024-12-02 15:43:22 - semantic_kernel.agents.chat_completion.chat_completion_agent:117 - INFO] [ChatCompletionAgent] Invoked AzureChatCompletion with message count: 2.

### Evaluate the termination criteria to determine whether to call the termination function
[2024-12-02 15:43:22 - semantic_kernel.agents.strategies.termination.termination_strategy:48 - INFO] Evaluating termination criteria for 8e987aea-c0d6-4ad3-b535-5784b3d74466
[2024-12-02 15:43:22 - semantic_kernel.agents.strategies.termination.termination_strategy:51 - INFO] Agent 8e987aea-c0d6-4ad3-b535-5784b3d74466 is out of scope

# assistant - CompanyRuleDocumentsResearcher: 'リモートワークは、原則として週3日まで認められています。リモートワークを希望する場合は、上司の承認が必要です。'


### selection function #total_tokens=632
[2024-12-02 15:43:22 - semantic_kernel.agents.strategies.selection.kernel_function_selection_strategy:73 - INFO] Kernel Function Selection Strategy next method called, invoking function: , selection
[2024-12-02 15:43:22 - semantic_kernel.functions.kernel_function:19 - INFO] Function selection invoking.
[2024-12-02 15:43:23 - httpx:1786 - INFO] HTTP Request: POST https://<openai_source>/openai/deployments/gpt-4o-mini/chat/completions?api-version=2024-06-01 "HTTP/1.1 200 OK"
[2024-12-02 15:43:23 - semantic_kernel.connectors.ai.open_ai.services.open_ai_handler:194 - INFO] OpenAI usage: CompletionUsage(completion_tokens=1, prompt_tokens=631, total_tokens=632, completion_tokens_details=None, prompt_tokens_details=None)
[2024-12-02 15:43:23 - semantic_kernel.functions.kernel_function:29 - INFO] Function selection succeeded.
[2024-12-02 15:43:23 - semantic_kernel.functions.kernel_function:53 - INFO] Function completed. Duration: 0.384270s
[2024-12-02 15:43:23 - semantic_kernel.agents.strategies.selection.kernel_function_selection_strategy:84 - INFO] Kernel Function Selection Strategy next method completed: , selection, result: [ChatMessageContent(inner_content=ChatCompletion(id='chatcmpl-AZuRRIRMjZlw6788ytZijL6bzXOte', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='Reviewer', refusal=None, role='assistant', audio=None, function_call=None, tool_calls=None), content_filter_results={'hate': {'filtered': False, 'severity': 'safe'}, 'protected_material_code': {'filtered': False, 'detected': False}, 'protected_material_text': {'filtered': False, 'detected': False}, 'self_harm': {'filtered': False, 'severity': 'safe'}, 'sexual': {'filtered': False, 'severity': 'safe'}, 'violence': {'filtered': False, 'severity': 'safe'}})], created=1733121805, model='gpt-4o-mini', object='chat.completion', service_tier=None, system_fingerprint='fp_04751d0b65', usage=CompletionUsage(completion_tokens=1, prompt_tokens=631, total_tokens=632, completion_tokens_details=None, prompt_tokens_details=None), prompt_filter_results=[{'prompt_index': 0, 'content_filter_results': {'hate': {'filtered': False, 'severity': 'safe'}, 'jailbreak': {'filtered': False, 'detected': False}, 'self_harm': {'filtered': False, 'severity': 'safe'}, 'sexual': {'filtered': False, 'severity': 'safe'}, 'violence': {'filtered': False, 'severity': 'safe'}}}]), ai_model_id='gpt-4o-mini', metadata={'logprobs': None, 'id': 'chatcmpl-AZuRRIRMjZlw6788ytZijL6bzXOte', 'created': 1733121805, 'system_fingerprint': 'fp_04751d0b65', 'usage': CompletionUsage(prompt_tokens=631, completion_tokens=1)}, content_type='message', role=<AuthorRole.ASSISTANT: 'assistant'>, name=None, items=[TextContent(inner_content=None, ai_model_id=None, metadata={}, content_type='text', text='Reviewer', encoding=None)], encoding=None, finish_reason=<FinishReason.STOP: 'stop'>)]
[2024-12-02 15:43:23 - root:329 - INFO] Selected Agent: Reviewer

### agent Reviewer #total_tokens=361
[2024-12-02 15:43:23 - semantic_kernel.agents.group_chat.agent_chat:139 - INFO] Invoking agent Reviewer
[2024-12-02 15:43:24 - httpx:1786 - INFO] HTTP Request: POST https://<openai_source>/openai/deployments/gpt-4o-mini/chat/completions?api-version=2024-06-01 "HTTP/1.1 200 OK"
[2024-12-02 15:43:24 - semantic_kernel.connectors.ai.open_ai.services.open_ai_handler:194 - INFO] OpenAI usage: CompletionUsage(completion_tokens=1, prompt_tokens=360, total_tokens=361, completion_tokens_details=None, prompt_tokens_details=None)
[2024-12-02 15:43:24 - semantic_kernel.agents.chat_completion.chat_completion_agent:117 - INFO] [ChatCompletionAgent] Invoked AzureChatCompletion with message count: 5.

### Evaluate the termination criteria to determine whether to call the termination function
[2024-12-02 15:43:24 - semantic_kernel.agents.strategies.termination.termination_strategy:48 - INFO] Evaluating termination criteria for 88b6251e-2b80-473a-b688-b3e8c9f0242e
[2024-12-02 15:43:24 - semantic_kernel.agents.strategies.termination.kernel_function_termination_strategy:73 - INFO] should_agent_terminate, function invoking: `termination`

### termination function #total_tokens=415
[2024-12-02 15:43:24 - semantic_kernel.functions.kernel_function:19 - INFO] Function termination invoking.
[2024-12-02 15:43:24 - httpx:1786 - INFO] HTTP Request: POST https://<openai_source>/openai/deployments/gpt-4o-mini/chat/completions?api-version=2024-06-01 "HTTP/1.1 200 OK"
[2024-12-02 15:43:24 - semantic_kernel.connectors.ai.open_ai.services.open_ai_handler:194 - INFO] OpenAI usage: CompletionUsage(completion_tokens=1, prompt_tokens=414, total_tokens=415, completion_tokens_details=None, prompt_tokens_details=None)
[2024-12-02 15:43:24 - semantic_kernel.functions.kernel_function:29 - INFO] Function termination succeeded.
[2024-12-02 15:43:24 - semantic_kernel.functions.kernel_function:53 - INFO] Function completed. Duration: 0.631022s
[2024-12-02 15:43:24 - semantic_kernel.agents.strategies.termination.kernel_function_termination_strategy:83 - INFO] should_agent_terminate, function `termination` invoked with result `[ChatMessageContent(inner_content=ChatCompletion(id='chatcmpl-AZuRSVfF7muISsWFM8Ah0gr71bkod', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='approved', refusal=None, role='assistant', audio=None, function_call=None, tool_calls=None), content_filter_results={'hate': {'filtered': False, 'severity': 'safe'}, 'protected_material_code': {'filtered': False, 'detected': False}, 'protected_material_text': {'filtered': False, 'detected': False}, 'self_harm': {'filtered': False, 'severity': 'safe'}, 'sexual': {'filtered': False, 'severity': 'safe'}, 'violence': {'filtered': False, 'severity': 'safe'}})], created=1733121806, model='gpt-4o-mini', object='chat.completion', service_tier=None, system_fingerprint='fp_04751d0b65', usage=CompletionUsage(completion_tokens=1, prompt_tokens=414, total_tokens=415, completion_tokens_details=None, prompt_tokens_details=None), prompt_filter_results=[{'prompt_index': 0, 'content_filter_results': {'hate': {'filtered': False, 'severity': 'safe'}, 'jailbreak': {'filtered': False, 'detected': False}, 'self_harm': {'filtered': False, 'severity': 'safe'}, 'sexual': {'filtered': False, 'severity': 'safe'}, 'violence': {'filtered': False, 'severity': 'safe'}}}]), ai_model_id='gpt-4o-mini', metadata={'logprobs': None, 'id': 'chatcmpl-AZuRSVfF7muISsWFM8Ah0gr71bkod', 'created': 1733121806, 'system_fingerprint': 'fp_04751d0b65', 'usage': CompletionUsage(prompt_tokens=414, completion_tokens=1)}, content_type='message', role=<AuthorRole.ASSISTANT: 'assistant'>, name=None, items=[TextContent(inner_content=None, ai_model_id=None, metadata={}, content_type='text', text='approved', encoding=None)], encoding=None, finish_reason=<FinishReason.STOP: 'stop'>)]`

[2024-12-02 15:43:24 - semantic_kernel.agents.strategies.termination.termination_strategy:56 - INFO] Evaluated criteria for 88b6251e-2b80-473a-b688-b3e8c9f0242e, should terminate: True 


ログの結果を確認すると、次の順番でエージェントと関数が呼び出されています。

  1. User Input
  2. selection functionでどのエージェントが回答するか決定
  3. agent CompanyRuleDocumentsResearcherが回答生成を開始
  4. agent CompanyRuleDocumentsResearcher が InternalDocumentaionRAG-searchのfunctionを利用し回答を生成
  5. (Reviewerが回答していないため、termination functionの実行をスキップ)
  6. selection functionでどのエージェントが回答するか決定
  7. agent Reviewerが回答生成
  8. (Reviewerが回答しているため、termination functionの実行開始)
  9. termination functionで終了するか決定

2番目のSelection functionのログでは以下のことが確認できます。

### Selection Strategyが呼び出される
[2024-12-02 15:43:18 - semantic_kernel.agents.strategies.selection.kernel_function_selection_strategy:73 - INFO] Kernel Function Selection Strategy next method called, invoking function: , selection
[2024-12-02 15:43:18 - semantic_kernel.functions.kernel_function:19 - INFO] Function selection invoking.
[2024-12-02 15:43:20 - httpx:1786 - INFO] HTTP Request: POST https://<openai_source>.openai.azure.com/openai/deployments/gpt-4o-mini/chat/completions?api-version=2024-06-01 "HTTP/1.1 200 OK"

### Token数を含むusage情報
[2024-12-02 15:43:20 - semantic_kernel.connectors.ai.open_ai.services.open_ai_handler:194 - INFO] OpenAI usage: CompletionUsage(completion_tokens=5, prompt_tokens=317, total_tokens=322, completion_tokens_details=None, prompt_tokens_details=None)
[2024-12-02 15:43:20 - semantic_kernel.functions.kernel_function:29 - INFO] Function selection succeeded.
[2024-12-02 15:43:20 - semantic_kernel.functions.kernel_function:53 - INFO] Function completed. Duration: 1.637971s

### Selection Strategyの詳細・結果
[2024-12-02 15:43:20 - semantic_kernel.agents.strategies.selection.kernel_function_selection_strategy:84 - INFO] Kernel Function Selection Strategy next method completed: , selection, result: [ChatMessageContent(inner_content=ChatCompletion(id='chatcmpl-AZuRO4m20879SqELorsVKhrwEZN1u', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='CompanyRuleDocumentsResearcher', refusal=None, role='assistant', audio=None, function_call=None, tool_calls=None), content_filter_results={'hate': {'filtered': False, 'severity': 'safe'}, 'protected_material_code': {'filtered': False, 'detected': False}, 'protected_material_text': {'filtered': False, 'detected': False}, 'self_harm': {'filtered': False, 'severity': 'safe'}, 'sexual': {'filtered': False, 'severity': 'safe'}, 'violence': {'filtered': False, 'severity': 'safe'}})], created=1733121802, model='gpt-4o-mini', object='chat.completion', service_tier=None, system_fingerprint='fp_04751d0b65', usage=CompletionUsage(completion_tokens=5, prompt_tokens=317, total_tokens=322, completion_tokens_details=None, prompt_tokens_details=None), prompt_filter_results=[{'prompt_index': 0, 'content_filter_results': {'hate': {'filtered': False, 'severity': 'safe'}, 'jailbreak': {'filtered': False, 'detected': False}, 'self_harm': {'filtered': False, 'severity': 'safe'}, 'sexual': {'filtered': False, 'severity': 'safe'}, 'violence': {'filtered': False, 'severity': 'safe'}}}]), ai_model_id='gpt-4o-mini', metadata={'logprobs': None, 'id': 'chatcmpl-AZuRO4m20879SqELorsVKhrwEZN1u', 'created': 1733121802, 'system_fingerprint': 'fp_04751d0b65', 'usage': CompletionUsage(prompt_tokens=317, completion_tokens=5)}, content_type='message', role=<AuthorRole.ASSISTANT: 'assistant'>, name=None, items=[TextContent(inner_content=None, ai_model_id=None, metadata={}, content_type='text', text='CompanyRuleDocumentsResearcher', encoding=None)], encoding=None, finish_reason=<FinishReason.STOP: 'stop'>)]
[2024-12-02 15:43:20 - root:329 - INFO] Selected Agent: CompanyRuleDocumentsResearcher

3番目と4番目の内容はCompanyRuleDocumentsResearcherのエージェントの処理内容になります。

### Selection strategyによって選択されたagent CompanyRuleDocumentsResearcher の呼び出し
[2024-12-02 15:43:20 - semantic_kernel.agents.group_chat.agent_chat:139 - INFO] Invoking agent CompanyRuleDocumentsResearcher   
[2024-12-02 15:43:21 - httpx:1786 - INFO] HTTP Request: POST https://<openai_source>/openai/deployments/gpt-4o-mini/chat/completions?api-version=2024-06-01 "HTTP/1.1 200 OK"

### agent CompanyRuleDocumentsResearcherのToken数を含むusage情報
[2024-12-02 15:43:21 - semantic_kernel.connectors.ai.open_ai.services.open_ai_handler:194 - INFO] OpenAI usage: CompletionUsage(completion_tokens=28, prompt_tokens=202, total_tokens=230, completion_tokens_details=None, prompt_tokens_details=None)
[2024-12-02 15:43:21 - semantic_kernel.connectors.ai.chat_completion_client_base:149 - INFO] processing 1 tool calls in parallel.
[2024-12-02 15:43:21 - semantic_kernel.kernel:383 - INFO] Calling InternalDocumentaionRAG-search function with args: {"query":" リモートワーク 週に何回"}

#### agent CompanyRuleDocumentsResearcherがInternalDocumentaionRAG-searchのfunctionを実行
[2024-12-02 15:43:21 - semantic_kernel.functions.kernel_function:19 - INFO] Function InternalDocumentaionRAG-search invoking.
[2024-12-02 15:43:21 - httpx:1038 - INFO] HTTP Request: POST https://<openai_source>//openai/deployments/text-embedding-ada-002/embeddings?api-version=2024-07-01-preview "HTTP/1.1 200 OK"
[2024-12-02 15:43:21 - semantic_kernel.functions.kernel_function:29 - INFO] Function InternalDocumentaionRAG-search succeeded.
[2024-12-02 15:43:21 - semantic_kernel.functions.kernel_function:53 - INFO] Function completed. Duration: 0.661601s
[2024-12-02 15:43:22 - httpx:1786 - INFO] HTTP Request: POST https://<openai_source>/openai/deployments/gpt-4o-mini/chat/completions?api-version=2024-06-01 "HTTP/1.1 200 OK"

### agent CompanyRuleDocumentsResearcherがInternalDocumentaionRAG-searchを実行後のToken数を含むusage情報
[2024-12-02 15:43:22 - semantic_kernel.connectors.ai.open_ai.services.open_ai_handler:194 - INFO] OpenAI usage: CompletionUsage(completion_tokens=41, prompt_tokens=372, total_tokens=413, completion_tokens_details=None, prompt_tokens_details=None)

[2024-12-02 15:43:22 - semantic_kernel.agents.chat_completion.chat_completion_agent:117 - INFO] [ChatCompletionAgent] Invoked AzureChatCompletion with message count: 2.

ログ全体のトークン数を合計すると、Azure AI Foundryのトークン数と同じ数値になります。

上記のようにログ機能を利用することで、どの処理でどれほどのトークンが発生しているか確認することができます。

フィルターによる関数の処理内容可視化

ログでは関数の結果が含まれていなかったので、フィルター機能を追加し関数の返り値を確認します。

Semantic Kernelのフィルターの解説は、「Semantic Kernel Agent Frameworkでマルチエージェントを構築する(1/2):プラグインの実装と関数処理内容の確認 - JBS Tech Blog」に記載してますので、そちらをご覧ください。

フィルターを追加することで、以下のような関数の結果や関数実行時のチャット履歴を確認を行うことができます。

Auto function invocation filter
Plugin: InternalDocumentaionRAG
Function: search
Request sequence: 0
Function sequence: 0
Number of function calls: 1

chat history:
 [ChatMessageContent(inner_content=None, ai_model_id=None, metadata={}, content_type='message', role=<AuthorRole.USER: 'user'>, name=None, items=[TextContent(inner_content=None, ai_model_id=None, metadata={}, content_type='text', text='リモートワークが週に何回可能か教えてください。', encoding=None)], encoding=None, finish_reason=None), ChatMessageContent(inner_content=ChatCompletion(id='chatcmpl-AZt2E1spm4R8YaLZB5RvWPtnqNNcb', choices=[Choice(finish_reason='tool_calls', index=0, logprobs=None, message=ChatCompletionMessage(content=None, refusal=None, role='assistant', audio=None, function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_05ZzHtboTs6Vmn24xrfH7ybf', function=Function(arguments='{"query":"リモートワーク 週 何回 可能"}', name='InternalDocumentaionRAG-search'), type='function')]), content_filter_results={})], created=1733116398, model='gpt-4o-mini', object='chat.completion', service_tier=None, system_fingerprint='fp_04751d0b65', usage=CompletionUsage(completion_tokens=30, prompt_tokens=116, total_tokens=146, completion_tokens_details=None, prompt_tokens_details=None), prompt_filter_results=[{'prompt_index': 0, 'content_filter_results': {'hate': {'filtered': False, 'severity': 'safe'}, 'jailbreak': {'filtered': False, 'detected': False}, 'self_harm': {'filtered': False, 'severity': 'safe'}, 'sexual': {'filtered': False, 'severity': 'safe'}, 'violence': {'filtered': False, 'severity': 'safe'}}}]), ai_model_id='gpt-4o-mini', metadata={'logprobs': None, 'id': 'chatcmpl-AZt2E1spm4R8YaLZB5RvWPtnqNNcb', 'created': 1733116398, 'system_fingerprint': 'fp_04751d0b65', 'usage': CompletionUsage(prompt_tokens=116, completion_tokens=30)}, content_type='message', role=<AuthorRole.ASSISTANT: 'assistant'>, name=None, items=[FunctionCallContent(inner_content=None, ai_model_id=None, metadata={}, content_type=<ContentTypes.FUNCTION_CALL_CONTENT: 'function_call'>, id='call_05ZzHtboTs6Vmn24xrfH7ybf', index=None, name='InternalDocumentaionRAG-search', function_name='search', plugin_name='InternalDocumentaionRAG', arguments='{"query":"リモートワーク 週 何回 可能"}')], encoding=None, finish_reason=<FinishReason.TOOL_CALLS: 'tool_calls'>)]  

plugin name: InternalDocumentaionRAG
function name: search
argument: {"query":"リモートワーク 週 何回 可能"}

実行されたfunction:
 name='search' plugin_name='InternalDocumentaionRAG' description='search documents about internal company rules' parameters=[KernelParameterMetadata(name='query', description=None, default_value=None, type_='str', is_required=True, type_object=<class 'str'>, schema_data={'type': 'string'}, include_in_function_choices=True)] is_prompt=False is_asynchronous=False return_parameter=KernelParameterMetadata(name='return', description='', default_value=None, type_='str', is_required=True, type_object=<class 'str'>, schema_data={'type': 'string'}, include_in_function_choices=True) additional_properties={}

result value of function:
 第3条(リモートワークの実施)
リモートワークは、原則として週3日まで認める。
リモートワークを希望する場合、上司の承認を得ることが必要である。

第4条(業務環境の整備)
リモートワークを行う従業員は、自宅等で業務を行うための適切な作業環境を整えるものとする。
必要に応じて、会社から貸与される機器やソフトウェアを使用する。

フィルターを利用することで、関数の結果を確認するだけでなく、関数の前後に処理を追加することも可能です。

終わりに

今回はSemantic kernelでのマルチエージェントの構築を行いました。

instructionの内容でエージェントの動作や選ばれるエージェントがしっかりと変更されていた印象を受けました。また、ログやフィルターの機能を使うことで、エージェントの動きをしっかり追うことができるので、より実用的な活用方法も学んでいきたいです。

Semantic KernelではPythonよりもC#のほうがドキュメントの更新も早いようなので、C#での実装のほうが簡単に行えるかもしれません。

次は、AutoGenを使ったマルチエージェントの構築にも取り組みたいと思います。

執筆担当者プロフィール
寺澤 駿

寺澤 駿(日本ビジネスシステムズ株式会社)

IoTやAzure Cognitive ServicesのAIを活用したデモ環境・ソリューション作成を担当。

担当記事一覧