乗換案内を自然言語で問い合わせ出来るBedockエージェント

Agents for Amazon Bedrockに対して自然言語で乗換案内を問い合わせ出来るようにします。

例）
Q:
たかし君は3日後の18時に名古屋駅に到着している必要があります。たかし君の家は千葉駅にあります。何時に出発すれば良いですか？

A:
たかし君は2023年12月23日（土）の15時20分に千葉駅を出発すれば、名古屋駅に18時に到着できます。


内部的には、あるLambda関数（Action group）の実行結果を次のLambda関数（Action group）に引き継いで実行し、最後にLLMでまとめるということをやっています。

<img width="30%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3573242/dfd847d4-f800-3507-af12-d1f6f938bc8f.png"> <img width="30%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3573242/2c84018d-f9f5-7f75-0ca7-44441ed429ca.png"> <img width="30%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3573242/da3e53e5-5aa7-fc96-2eab-78cecf5ce638.png">

# 作ったもの概要

「今」や「今日」という情報が必要なので、以前作った以下のAgentに対して、現在日時（JST）を取得するLambda関数（Action group）を追加しています。
また、乗換案内のLambda関数に、日時や検索モード（出発、到着、始発、終電）を渡せるように変更しています。

https://qiita.com/cyberBOSE/items/e8c2d0207d03b6934a1c

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3573242/1da31a7d-ad94-ff1b-e13a-6112133ce432.png)

# 現在日時取得Lambda関数

相当単純ですが以下のLambda関数を追加します。

```Python
import datetime

def lambda_handler(event, context):

    # 現在日時（JST）の取得（ISO8601形式）
	now = str(datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=9))).isoformat())
	print(now)

	# agentの形式でreturn
	response_body = {"application/json": {"body": now}}
	action_response = {
		"actionGroup": event["actionGroup"],
		"apiPath": event["apiPath"],
		"httpMethod": event["httpMethod"],
		"httpStatusCode": 200,
		"responseBody": response_body,
	}
	api_response = {"messageVersion": "1.0", "response": action_response}
	return api_response
```

OpenAPIスキーマは以下の様に定義します。
```YAML
openapi: 3.0.0
info:
  title: Lambda
  version: 1.0.0
paths:
  /search:
    get:
      summary: datetime
      description: 今の日時をISO8601形式で返します
      operationId: now
      responses:
        "200":
          description: 取得成功
          content:
            application/json:
              schema:
                type: object
                properties:
                  body:
                    type: string
```

# 乗換案内Lambda関数の改修

日時、検索モードを引数で受け取れるようにします。
かなりベタにYahoo乗換案内のパラメータをそのままもらうようにしています。

```Python
import requests
from bs4 import BeautifulSoup
import json

def lambda_handler(event, context):

	print(event) # パラメータの出力（確認用）
	
	# パラメータの取得
	parameters = event.get('parameters')
	dep_station = next(p for p in parameters if p['name'] == 'dep_station')['value']
	dest_station = next(p for p in parameters if p['name'] == 'dest_station')['value']
	yyyy = next(p for p in parameters if p['name'] == 'yyyy')['value']
	mm = next(p for p in parameters if p['name'] == 'mm')['value']
	dd = next(p for p in parameters if p['name'] == 'dd')['value']
	H = next(p for p in parameters if p['name'] == 'H')['value']
	M = next(p for p in parameters if p['name'] == 'M')['value']
	type = next(p for p in parameters if p['name'] == 'type')['value']
	
	# Yahoo乗換案内のURL設定
	url = "https://transit.yahoo.co.jp/search/print?from="+dep_station+"&to="+dest_station+"&y="+yyyy+"&m="+mm+"&d="+dd+"&hh="+H+"&m1="+M[:1]+"&m2="+M[1:]+"&type="+type+"&ticket=ic&expkind=1&userpass=1&ws=3&s=0&al=1&shin=1&ex=1&hb=1&lb=1&sr=1"
	print(url) # 確認用
	
    # 乗換案内のページからメイン部分のみ取得
	page = requests.get(url).text
	body = str(BeautifulSoup(page, 'html.parser').select('#srline'))

	# agentの形式でreturn
	response_body = {"application/json": {"body": body}}
	action_response = {
		"actionGroup": event["actionGroup"],
		"apiPath": event["apiPath"],
		"httpMethod": event["httpMethod"],
		"httpStatusCode": 200,
		"responseBody": response_body,
	}
	api_response = {"messageVersion": "1.0", "response": action_response}
	return api_response
```

OpenAPIスキーマにも引数を追加して以下の様にします。
引数に何を渡せばよいかはLLMに考えてもらいます。

```YAML
openapi: 3.0.0
info:
  title: Lambda
  version: 1.0.0
paths:
  /search:
    get:
      summary: Transit Search
      description: 乗換案内を検索します。事前に現在の日時を取得してから呼び出してください。日時の指定が無い場合は今日の日付、今の時刻を使用してください
      operationId: search
      parameters:
        - name: dep_station
          in: path
          description: 出発駅（日本語で指定）
          required: true
          schema:
            type: string
        - name: dest_station
          in: path
          description: 到着駅（日本語で指定）
          required: true
          schema:
            type: string
        - name: yyyy
          in: path
          description: 出発時刻で検索する場合は出発する年、到着時刻で検索する場合は到着したい年（year）
          required: true
          schema:
            type: string
            minLength: 4
            maxLength: 4
        - name: mm
          in: path
          description: 出発時刻で検索する場合は出発する月、到着時刻で検索する場合は到着したい月（month）
          required: true
          schema:
            type: string
            minLength: 2
            maxLength: 2
        - name: dd
          in: path
          description: 出発時刻で検索する場合は出発する日、到着時刻で検索する場合は到着したい日（day）
          required: true
          schema:
            type: string
            minLength: 2
            maxLength: 2
        - name: H
          in: path
          description: 出発時刻で検索する場合は出発する時刻、到着時刻で検索する場合は到着したい時刻（hourの24時間表記）。始発または終電を検索する場合は現在時刻
          required: true
          schema:
            type: string
            minLength: 2
            maxLength: 2
        - name: M
          in: path
          description: 出発時刻で検索する場合は出発する時刻、到着時刻で検索する場合は到着したい時刻の分（minute）。始発または終電を検索する場合は現在時刻
          required: true
          schema:
            type: string
            minLength: 2
            maxLength: 2
        - name: type
          in: path
          description: 検索方法。1:出発時刻を指定して検索、4:到着時刻を指定して検索、2:終電を検索、3:始発を検索
          required: true
          schema:
            type: string
            minLength: 1
            maxLength: 1
      responses:
        "200":
          description: 検索成功
          content:
            application/json:
              schema:
                type: object
                properties:
                  body:
                    type: string
```

# Agentの設定
元のままでも良いのですが、乗換案内がぶっきらぼう過ぎる時があるので、やや細かく指示を追加しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3573242/86688b23-0713-0d97-26ff-fd4956f40313.png)

# 動作確認

「今から30分後」と問い合わせた例で動作を確認します。

<img width="30%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3573242/dfd847d4-f800-3507-af12-d1f6f938bc8f.png">

ログを見てみます。まずはStep2

```JSON
{
    "completion": " To answer this question, 
    I will:\n\n
    1. Call GET::now::now function to get the current date and time\n\n
    2. Add 30 minutes to the current time \n\n
    3. Call GET::transit_search2::search function to search for the route from Shinjuku Station to Yokohama Station, departing at the time calculated in step 2\n\n4. Return the search results including route, transfer stations, travel time and fares.\n\n
    I have double checked and made sure that I have been provided the GET::now::now and GET::transit_search2::search functions.\n</scratchpad>\n\n
    <function_call>GET::now::now()",
    "stop_reason": "stop_sequence",
    "stop": "</function_call>"
}
```
現在日時を取得するLambda関数（now）を呼んで、30分足しましょうとなってます。
このあとLambda関数で以下の現在日時を取得します。
```
2023-12-20T13:57:34.827791+09:00
```
13時57分が取得されています。

これを受けてStep3に移ります。
```JSON
{
    "completion": "<scratchpad>\n
    1. The current date and time is 2023-12-20 13:57:34\n
    2. Adding 30 minutes gives the departure time as 2023-12-20 14:27\n
    3. I will use this departure time to call the GET::transit_search2::search function\n</scratchpad>\n
    <function_call>GET::transit_search2::search(mm=\"12\", dd=\"20\", yyyy=\"2023\", H=\"14\", M=\"27\", type=\"1\", dest_station=\"横浜駅\", dep_station=\"新宿駅\")",
    "stop_reason": "stop_sequence",
    "stop": "</function_call>"
}
```
乗換案内に対して、以下の引数で呼び出すべしとなっています。
```
mm=\"12\", （月） 
dd=\"20\",  （日）
yyyy=\"2023\",  （年）
H=\"14\",  （hour）
M=\"27\",  （minute）
type=\"1\",  （出発時刻で検索）
dest_station=\"横浜駅\", 
dep_station=\"新宿駅\"
```
一つ目のLambda関数の実行結果を踏まえて、LLMが30分を足して引数を作ってくれています。
この後、乗換案内のLambda関数が呼び出されて、以下が取得されます。
```HTML
<div class=\"elmRouteDetail\" id=\"srline\">
    <div class=\"routeSummary\">
        <ul class=\"summary\">
            <li class=\"time\"><span>14:30<!-- -->発→<span
                        class=\"mark\">15:00<!-- -->着</span></span>30分<!-- -->(乗車<!-- -->30分<!-- -->)</li>
            <li class=\"transfer\">乗換：<span class=\"mark\">0<!-- -->回</span></li>
            <li class=\"fare\"><img alt=\"IC優先\" height=\"13\"
                    src=\"https://s.yimg.jp/images/transit/pc/v2/img/print/icnIc.png\"
                    width=\"19\" /><span>IC優先：</span>571<!-- -->円</li>
            <li class=\"priority\"><img alt=\"早優先\" height=\"18\"
                    src=\"https://s.yimg.jp/images/transit/pc/v2/img/print/icnPriTime.png\" width=\"20\" /><img
                    alt=\"楽優先\" height=\"18\" src=\"https://s.yimg.jp/images/transit/pc/v2/img/print/icnPriTrans.png\"
                    width=\"20\" /></li>
            <li class=\"distance\">35.5km</li>
            <li class=\"pass\">定期券 <!-- -->通勤<!-- -->：<!-- -->1か月 17,310円 / 3か月 49,330円 / 6か月 83,160円</li>
        </ul>
    </div>
    <div class=\"routeDetail\">
        <div class=\"station\">
            <ul class=\"time\">
                <li>14:30</li>
            </ul>
            <p class=\"icon\"><img alt=\"出発駅\" height=\"20\"
                    src=\"https://s.yimg.jp/images/transit/pc/v2/img/print/icnStaDep.png\" width=\"20\" /></p>
            <dl>
                <dt>新宿</dt>
            </dl>
        </div>
        <div class=\"fareSection\">
            <div class=\"access\">
                <ul class=\"info\">
                    <li class=\"transport\"><span class=\"line\" style=\"border-left-color:#ff5000\">[line]</span>
                        <div><span class=\"icon\" style=\"width:20px;height:18px\"><img alt=\"電車\" height=\"18\"
                                    src=\"https://s.yimg.jp/images/transit/pc/v2/img/print/icnTrain.png\"
                                    width=\"20\" /></span>ＪＲ湘南新宿ライン快速・国府津行<span class=\"platform\">[発] <span
                                    class=\"num\">1</span>番線<!-- --> → <!-- -->[着] <span class=\"num\">9</span>番線</span>
                        </div>
                    </li>
                    <li class=\"stop\"><span class=\"stopNum\">5<!-- -->駅</span></li>
                </ul>
            </div>
            <p class=\"fare\"><span>571円</span></p>
        </div>
        <div class=\"station\">
            <ul class=\"time\">
                <li>15:00</li>
            </ul>
            <p class=\"icon\"><img alt=\"到着駅\" height=\"20\"
                    src=\"https://s.yimg.jp/images/transit/pc/v2/img/print/icnStaArr.png\" width=\"20\" /></p>
            <dl>
                <dt>横浜</dt>
            </dl>
        </div>
    </div>
</div>
```
最後にStep4で問い合わせ内容とHTMLを解釈して整形します。
```JSON
{
    "completion": "<answer>\n新宿駅から30分後の14時27分に出発して横浜駅に向かう方法は、\n
    - 新宿駅発14時30分のJR湘南新宿ライン快速・国府津行に乗車\n
    - 横浜駅に15時00分に到着\n
    - 所要時間は30分\n
    - 乗換はなし\n
    - 運賃はIC優先で571円\n
    です。\n",
    "stop_reason": "stop_sequence",
    "stop": "</answer>"
}
```

他のケースも同様で、
1. 日時取得のLambda関数で現在の日時を取得
1. LLMが問い合わせ内容を考慮して引数を考えて乗換案内のLambda関数を呼び出し
1. LLMが問い合わせ内容を考慮して整形して回答

という流れで処理がされます。

基本形はこれで動くので、例えば最寄り駅や徒歩の時間を予めどこかのプロンプト（AgentのInstructionあたり？）に与えておくなどすれば、個人にカスタマイズした乗換案内なんてのも作れそうです。

という感じで、特別なことをしなくても、OpenAPIスキーマを丁寧に書けば、前段のLambda関数の実行結果を後段のLambda関数に渡してくれることが分かりました。

https://qiita.com/cyberBOSE/items/fe66c2049aaa04568d61