Agent Village 〜 エージェントの里 〜 |
||||||||||||||||||||||||||||||||||
JADEチュートリアル - Round 02 :
|
||||||||||||||||||||||||||||||||||
![]() |
図2のシーケンス図はAUMLというUMLの拡張表記形式で記述されているのですが、細かいところを気にしなければ「だいたいどんな対話手順になっているか」というのが読み取れるかと思います。一応、この流れを順に追っていってみましょう。
InitiatorがParticipantに対して何らかのリクエスト・メッセージを送ります。このACLメッセージのprotocolスロットにはfipa-requestが、performativeスロットにはrequestが、それぞれ設定されていなければなりません。Participantが要求された処理の実行を却下する場合、Initiatorに対してrefuseメッセージ(performativeスロットにrefuseが設定されたACLメッセージ)が返され、このリクエスト・シーケンスは終了します。Participantは受け付けた処理に時間がかかる場合など、必要に応じてInitiatorに対してagreeメッセージを返します(「リクエストは受け付けたけどちょっと待ってね」といった感じ)。このagreeメッセージの返信は省略されることがあります。Participantは要求された処理を実行し、実行に成功した場合はinformメッセージを(実行に失敗した場合はfailureメッセージを)Initiatorに対して返信して、このリクエスト・シーケンスを終了します。
JADEには前述のREQUESTインタラクション・プロトコルを含む「REQUESTライクな」プロトコル群(fipa-request, fipa-query, fipa-request-when, fipa-brokering, fipa-recruiting)の手順制御をやってくれる便利なBehaviourが用意されています。リクエストを発する側のエージェント(Initiator)のためのBehaviourがAchieveREInitiator(またはSimpleAchieveREInitiator)で、リクエストを受け付ける側のエージェント(ParticipantまたはResponder)のためのBehaviourがAchieveREResponder(またはSimpleAchieveREResponder)です。頭に"Simple〜"と付いているモノはリクエスト先のエージェントが1つの場合(良くある普通のリクエスト)用の簡易版で、そうでないモノは同時に複数のエージェントに同じリクエストを投げる場合にも対応できるようになっています。ちなみに、"RE"は"Rational Effect"の略で、全体で「合理的な効果を成し遂げる振る舞い」といった感じの名前になっています。
コトバ(日本語)で説明しても解りにくいのでサンプル・コードを見ながらこれらのBehaviourの使い方を理解していきましょう。
SAMPLE_HOME\src\com\mamezou\nappa\sample\echoフォルダの下にEchoClient.javaファイルがありますから、これを適当なエディタなどで開いてみてください。これは前回の【Round01】で作成したEchoServerエージェントに対してリクエスト・メッセージを作って投げるエージェントで、簡単なGUIを備えています。
このソース・コード中でSimpleAchieveREInitiatorを使っている部分を抜粋してリスト1に示します。
/**
* EchoServerにメッセージを送る。
*
* @param message 送信するメッセージ文字列。
*/
protected void sendMessage( String message ) {
ACLMessage request = new ACLMessage( ACLMessage.REQUEST ) ; // 送信メッセージの作成
request.setProtocol( FIPANames.InteractionProtocol.FIPA_REQUEST ) ; // プロトコルの指定
request.addReceiver( new AID( DEFAULT_SERVER_AGENT_NAME ,
AID.ISLOCALNAME ) ) ; // メッセージ送信先の指定
// TODO メッセージ送信先のエージェント名を起動時の引数で指定できるようにしてみよう
request.setContent( message ) ; // メッセージ内容の設定
// メッセージ内容に日本語を入れて通すためのおまじない
Envelope envelope = request.getEnvelope() ;
if ( envelope == null ) {
request.setDefaultEnvelope() ;
envelope = request.getEnvelope() ;
}
envelope.setPayloadEncoding( "UTF-8" ) ;
// FIPA Request Interaction ProtocolのInitiatorを作成し自身に登録
this.addBehaviour( new SimpleAchieveREInitiator( this , request ) {
protected void handleInform( ACLMessage inform ) {
// (EchoServerから)INFORMを受け取ったときに呼び出される
gui.responseTextArea.setCaretPosition( gui.responseTextArea.getText().length() ) ;
gui.responseTextArea.append( "【" + inform.getSender().getName()
+ "】 " + inform.getContent() + "\n" ) ;
}
// TODO メッセージの送信に失敗したときのハンドラを追加してみよう
} ) ;
}
EchoClient#sendMessage(String)メソッドはGUIの「Send」ボタンが押される都度呼び出されるのですが、その中では毎回リクエスト・メッセージを作成し、そのリクエスト・メッセージをコンストラクタで引き渡したSimpleAchieveREInitiatorの匿名インナークラスのインスタンスを生成して#addBehaviour()でエージェントのタスク・リストに追加して終わっています。ここで追加されたBehaviourは(そのうち)エージェント・スレッドによって活性化され実行されることになります。このBehaviourはプロトコル手順を完了するかタイムアウトすると終了してエージェントのタスク・リストから自動的に消えます。
実際にメッセージを送信する処理や返答を待ち受ける部分などの厄介な処理はすべてSimpleAchieveREInitiatorで記述されているので、そちらにお任せしてしまえば十分です。ここでは、いくつか用意されているフック・メソッドのひとつである#handleInform()をオーバーライドして、informメッセージが返信されてきたらGUIにその結果を表示するようにしています。SimpleAchieveREInitiatorで用意されているフック・メソッド(ハンドラ・メソッド)の一覧を表1に示します(AchieveREInitiatorもほぼ同様のフック・メソッドを備えています)。
| メソッド名 | 解説 |
|---|---|
#handleAgree(ACLMessage message) |
agreeメッセージが返信されてきた時に呼び出される。 |
#handleAllResponses(Vector messages) |
すべての"responses"メッセージ(agree, refuse, not-understood)が集められた時かタイムアウトした時に呼び出される。 |
#handleAllResultNotifications(Vector messages) |
すべての"result notification"メッセージ(inform, failure)が集められた時かタイムアウトした時に呼び出される。 |
#handleFailure(ACLMessage message) |
failureメッセージが返信されてきた時に呼び出される。 |
#handleInform(ACLMessage message) |
informメッセージが返信されてきた時に呼び出される。 |
#handleNotUnderstood(ACLMessage message) |
not-understoodメッセージが返信されてきた時に呼び出される。 |
#handleOutOfSequence(ACLMessage message) |
プロトコル・ルールに従わないメッセージを受け取った時に呼び出される。 |
#handleRefuse(ACLMessage message) |
refuseメッセージが返信されてきた時に呼び出される。 |
EchoClientに合わせてEchoServer側もSimpleAchieveREResponderを使ってリクエストを受け付けるように改造します。SAMPLE_HOME\src\com\mamezou\nappa\sample\echoフォルダの下のEchoServer.javaファイルが改造後のソースなので、適当なエディタなどで開いてみてください。
このソース・コード中でSimpleAchieveREResponderを使っている部分を抜粋してリスト2に示します。
protected void setup() {
super.setup() ;
// REQUESTメッセージにマッチするメッセージ・テンプレートを作成する。
MessageTemplate template =
SimpleAchieveREResponder.createMessageTemplate( FIPANames.InteractionProtocol.FIPA_REQUEST ) ;
this.addBehaviour( new SimpleAchieveREResponder( this , template ) {
protected ACLMessage prepareResponse( ACLMessage request )
throws NotUnderstoodException , RefuseException {
// テンプレートに合致するメッセージが届いた時に呼び出されるハンドラ・メソッド。
String content = request.getContent() ;
Logger.getLogger( "com.mamezou.nappa.sample.echo" ).info(
request.getSender().getName()
+ " sends a request message with \"" + content
+ "\"." ) ;
// REQUESTメッセージに対応する返信メッセージを作成する(この場合はINFORM)。
ACLMessage reply = request.createReply() ;
reply.setPerformative( ACLMessage.INFORM ) ;
reply.setContent( "Echo: " + request.getContent() ) ;
// メッセージ内容に日本語を入れて通すためのおまじない
Envelope envelope = reply.getEnvelope() ;
if ( envelope == null ) {
reply.setDefaultEnvelope() ;
envelope = reply.getEnvelope() ;
}
envelope.setPayloadEncoding( "UTF-8" ) ;
// 作成したメッセージを返してあげればフレームワーク側でメッセージ送信や後始末をやってくれる。
return reply ;
}
// TODO 一度AGREEメッセージを返し、しかる後に結果をINFORMメッセージで返すようにしてみよう。
} ) ;
}
Responder側のBehaviour(SimpleAchieveREResponder)は常にクライアントからのリクエストを待ち受けている必要があるので、#setup()で生成してエージェントのタスク・リストに追加しておきます。このBehaviourはあるクライアントからのリクエスト処理手順を終えると自動的にリクエスト待ちの状態に戻ります。明示的にタスク・リストから削除しない限り、自動的に終了して消えるようなことはありません。
ここではEchoClientと同様にSimpleAchieveREResponderの匿名インナークラスのインスタンスを生成して自身のタスク・リストに登録していますが、それに先立ってメッセージ・テンプレートというオブジェクトを作ってコンストラクタに引き渡しています。このメッセージ・テンプレートは一種のフィルタ・パターンで、エージェントのメッセージ・キューに入っている特定のACLメッセージにマッチする条件を表現しています。このサンプルでは、protocolスロットに"fipa-request"が、performativeに"request"が設定されているACLメッセージにマッチするパターンを作っています(実際にこのパターンを作っているのはSimpleAchieveREResponder#createMessageTemplate()メソッドの中)。このようなメッセージ・テンプレートを指定することによって、SimpleAchieveREResponderはエージェントのメッセージ・キューに入ってきている様々なメッセージの中から自分が処理すべきメッセージを見つけ出すことができるわけです。
サンプル・コードでは、いくつか用意されているフック・メソッドのうちSimpleAchieveREResponder#prepareResponse()メソッドだけをオーバーライドして返答メッセージ(informメッセージ)を生成して返信しています。SimpleAchieveREResponderで用意されているフック・メソッドの一覧を表2に示します(AchieveREResponderもほぼ同様のフック・メソッドを備えています)。
| メソッド名 | 解説 |
|---|---|
#prepareResponse(ACLMessage request) |
メッセージ・テンプレートに合致したメッセージを受信した時に呼び出される応答メッセージ(agree, refuse, not-understood)準備用のフック・メソッド。この時点でいきなり返答メッセージ(inform)を返しても良い。デフォルトの実装ではnullを返す(応答メッセージを返信しない)。 |
#prepareResultNotification(ACLMessage request, ACLMessage response) |
返答メッセージ(inform, failure)準備用のフック・メソッド。Initiatorから送られたリクエスト・メッセージと事前に返信した応答メッセージ(応答メッセージを返信していない場合はnull)を引数に取り、リクエストされた何らかの処理を実行し、その結果を返答メッセージとして返す。デフォルトの実装ではnullを返す(Initiatorに返答メッセージを返信しない)ので、少なくとも#prepareResponse()か#prepareResultNotification()のどちらか一方をオーバーライドして返答メッセージを返すようにする必要がある。 |
では、実際にEchoServerとEchoClientを動かして動作を観察してみましょう。
カレント・ディレクトリを SAMPLE_HOME に移し、コマンド・プロンプトから図3のように "ant run-EchoServerAndClient" コマンドを実行してください。
SAMPLE_HOME\> ant run-EchoServerAndClientBuildfile: build.xml init: compile: resource: build: run-rma: [java] This is JADE 3.2 - 2004/07/26 13:41:05 [java] downloaded in Open Source, under LGPL restrictions, [java] at http://jade.cselt.it/ [java] http://hostname:7778/acc [java] Agent container Main-Container@JADE-IMTP://hostname is ready.
しばらくすると、【Round01】でも見た RMA (Remote Management Agent) の画面と一緒に図4のようなEchoClientのGUI画面が表示されます。
![]() |
EchoClientのGUI画面の下側のテキスト・フィールドに適当な文字列を入力して「Send」ボタンを押すと、メッセージがEchoServerに送信されて、その返答が上側のテキスト・フィールドに表示されます(図5)。
![]() |
このメッセージ送受信の様子はSnifferツールを使って視覚的に確認することができます(図6)。Snifferの使い方については【Round01】や【JADE - ツール - Sniffer】をご覧ください。
![]() |
図6のSnifferの画面表示で興味深いのは、対になるリクエストと返答メッセージが同じ色の矢印で表示されている点です。これは偶然なのでしょうか? この秘密はSnifferの画面でメッセージの矢印をダブル・クリックして各々のメッセージの内容をよく調べてみると判ります。
このサンプルでやり取りされた各々のメッセージのConversation-idスロットにはC28487985_1101209444125といった感じのIDが自動的に設定されています(このIDの管理やハンドリングはSimpleAchieveREInitiatorやSimpleAchieveREResponderが裏でやってくれています)。これは一連の対話(このサンプルではrequestインタラクション・プロトコルの一連の流れ)に対して割り振られたユニークなIDで、同じ対話に含まれる各々の発話(メッセージ)には同じConversation-idが振られます。これによって、エージェントはバラバラに送られてくるメッセージがどの対話シーケンスに含まれているものであるか特定することができます。
SnifferはこのようなFIPA仕様の特性を利用して、同じ対話に含まれるメッセージ線を同じ色で表示して見やすくしているわけです。
サンプル・コードにはEchoClient/EchoServerと似たようなPingAgent/PongAgentというペアのエージェントが用意されています。PongAgentの方がサーバ(Responder側)でPingAgentの方がクライアント(Initiator側)です。
PongAgentは自分が知っているキーワードでリクエストされると、それに対応する合言葉を返答してきます。たとえば、「ピン」と言えば「ポン」と答え、「山」と言えば「川」と答える…といった感じです。PongAgentがどんなキーワードを知っているかはソース・コードを覗いてみてください。PongAgentはリクエストされたキーワードに対して、自分の知っているキーワードであればagreeメッセージを返答し、そうでなければrefuse(却下)メッセージを返答します。その後(agreeだった場合)、あらためて対応する合言葉をinformメッセージとして返信してきます。この手順はFIPAのREQUESTインタラクション・プロトコルに合致しています。
では、実際にPingAgentとPongAgentを動かして動作を観察してみましょう。
カレント・ディレクトリを SAMPLE_HOME に移し、コマンド・プロンプトから図7のように "ant run-PingAndPongAgent" コマンドを実行してください。
SAMPLE_HOME\> ant run-PingAndPongAgentBuildfile: build.xml init: compile: resource: build: run-PingAndPongAgent: [java] This is JADE 3.2 - 2004/07/26 13:41:05 [java] downloaded in Open Source, under LGPL restrictions, [java] at http://jade.cselt.it/ [java] http://hostname:7778/acc [java] Agent container Main-Container@JADE-IMTP://hostname is ready.
しばらくすると、【Round01】でも見た RMA (Remote Management Agent) の画面と一緒に図8のようなPingAgentのGUI画面が表示されます。
![]() |
この画面はEchoClientのGUI画面とよく似ていて使い方もほとんど同じですが、画面の上側にサーバのエージェント名(グローバル名)とプラットフォームのアドレスを指定する欄が増えている点が少し違います。実際にメッセージを送る前に、「Agent GUID」と「Platform Address」の欄の「hostname」となっている箇所を実際のホスト名に書き換えてください。あとはEchoClientのGUIと同じような感じでSnifferなども併用しながらメッセージの送受信の様子を確かめながらいろいろ入力してみてください(図9)。
![]() |
さて、ここまでは1台のPCの上でサーバ(Responder)とクライアント(Initiator)を動作させてメッセージ送受信をやってきましたが、せっかくJADEのような分散環境を使っているので、そろそろプラットフォーム(ノード)を越えたメッセージ送受信を試してみましょう。ネットワークで繋がった2台以上のPCが使える方は是非試してみてください。
この例では、"ICHIYOU"というPCでPongAgent(サーバ(Responder)側)を、"KITSUNE-PRONOTE"というPCでPingAgent(クライアント(Initiator)側)を、それぞれ立ち上げてメッセージが送られる様子を確認してみます。
まず、サーバ側(この例では"ICHIYOU"というPC)でカレント・ディレクトリを SAMPLE_HOME に移し、コマンド・プロンプトから図10のように "ant run-PongAgent" コマンドを実行してください。
SAMPLE_HOME\> ant run-PongAgentBuildfile: build.xml init: compile: resource: build: run-PongAgent: [java] This is JADE 3.2 - 2004/07/26 13:41:05 [java] downloaded in Open Source, under LGPL restrictions, [java] at http://jade.cselt.it/ [java] http://hostname:7778/acc [java] Agent container Main-Container@JADE-IMTP://hostname is ready.
次に、クライアント側(この例では"KITSUNE-PRONOTE"というPC)でカレント・ディレクトリを SAMPLE_HOME に移し、コマンド・プロンプトから図11のように "ant run-PingAgent" コマンドを実行してください。
SAMPLE_HOME\> ant run-PingAgentBuildfile: build.xml init: compile: resource: build: run-PingAgent: [java] This is JADE 3.2 - 2004/07/26 13:41:05 [java] downloaded in Open Source, under LGPL restrictions, [java] at http://jade.cselt.it/ [java] http://hostname:7778/acc [java] Agent container Main-Container@JADE-IMTP://hostname is ready.
クライアント側にはPingAgentのGUI画面が開くので、この画面の上部の"Agent GUID"と"Platform Address"欄の中のホスト名の部分をサーバのホスト名に書き換え、あとはローカルでやっていたのと同様に画面下部のRequestの欄に適当な文字列を書き込んで「Send」ボタンを押すとメッセージの送受信が行われます(図12)。
![]() |
この時、クライアント側でSnifferを起動してPingAgentのメッセージを監視すると(図13)のように、また、サーバ側でもSnifferを起動してPongAgentのメッセージを監視すると(図14)のように、それぞれプラットフォームの外(Sniffer画面中でOtherと表示されているところ)から(または外へ)のメッセージ送受信が行われているのが観察できます。
![]() |
![]() |
コード中では、エージェントがローカルに居る(同じプラットフォーム上で動作している)かリモートに居る(別のプラットフォーム上で動作している)かはほとんど意識していません。唯一の違いはACLメッセージの宛先となるAID(AgentID)の作り方の部分で、相手のエージェントがローカルに居る場合はリスト3のようにエージェントのニックネームだけを指定してAIDを生成していたのに対して、リモートに居るエージェントを指定する場合はリスト4のように GUID ("Global Unique ID"の略で "エージェント名@ホスト名:1099/JADE" といった感じで大域的にユニークになるように付けられたエージェント名) とプラットフォームのアドレス(この例では HTTP MTP (Message Transport Protocol) を使っているので "http://ホスト名:7778/acc" といった感じのURLになる)を指定してグローバルなAIDを生成しています。
AID receiverAID = new AID( "PongAgent" , AID.ISLOCALNAME ) ;
AID receiverAID = new AID( "PongAgent@hostname:1099/JADE" , AID.ISGUID ) ; receiverAID.addAddresses( "http://hostname:7778/acc" ) ;
要は、相手エージェントのグローバルな名前とアドレスさえ判明すれば(※3)ホストを越えたリクエストの送受信ができてしまうのです。どうです? なんだかとってもお手軽じゃないですか?
正確には通信するプラットフォーム間でお互いにサポートするMTP(Message Transport Protocol)が合致している必要があります。この例では双方のプラットフォームが HTTP MTP (JADE 3.2 以降のデフォルトMTP) を使っているので特に問題なくメッセージの送受信が行われています。FIPA準拠のエージェント・プラットフォームでは複数種類のMTPをサポートする(プラグインする)ことができるような仕様になっています。
このチュートリアルでは、FIPA仕様で定義されているインタラクション・プロトコル群のうち、もっとも代表的なREQUESTインタラクション・プロトコルの手順を知り、それをJADE上のエージェント間でどうやって実現(利用)すれば良いのかサンプル・コードを見ながら具体的に学んできました。複数のエージェントの間で会話をさせてみると、だんだん面白いことができるようになってきますね。
次のチュートリアルでは、また別の(そして特徴的な)インタラクション・プロトコルであるSUBSCRIBEインタラクション・プロトコルを利用した具体例を取り上げていきます。このようなBehaviourを自由に使えるようになれば、JADEを活用した本格的なマルチ・エージェント・システムの開発が容易になります。
Copyright © 2004 by Mamezou, Co., Ltd.. All rights reserved.
JADE is a trademark of Telecom Italia Lab.
Java and all Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries.
All other products referred to herein are service names, trademarks or registered trademarks of the companies that own and market those products.