0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【実装】LangGraphを使用した天気エージェント機能

0
Posted at

概要

  • LangGraphとモデルを使用して天気エージェントを作成しました。
  • 最終にモデルからかえってきた回答をjson形式にして出力実装しています。

前置き

  • 今回はgoogle AI studioからAPIkeyを取得して実装しています。(無料枠)

0.ライブラリーの読み込み

from typing import Annotated, Sequence, TypedDict, List
from langchain_core.messages import BaseMessage, AIMessage
from langgraph.graph.message import add_messages
from langchain_core.tools import tool
from geopy.geocoders import Nominatim
from pydantic import BaseModel, Field
from datetime import datetime
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_core.messages import ToolMessage
from langchain_core.runnables import RunnableConfig
from langgraph.graph import StateGraph, END
from langchain_core.messages import ToolMessage, AIMessage
import requests
import os
import json
import pandas as pd
from dotenv import load_dotenv

1.初期設定

  • google AI studioからAPIkeyを「.env」ファイルに入力します。
  • その値(APIKey)を読み込みます
##########################################################
# 1. 初期設定
##########################################################
load_dotenv()
api_key=os.environ['GOOGLE_AI_ST_API']

2.agentの状態管理を設定

  • AgentState は「グラフ内で保持される状態(メッセージ履歴+ステップ数)」を定義します。
    • messages → AI・ユーザー・Tool のすべてのメッセージ履歴
    • number_of_steps → 何ステップ実行したかのカウンター
##########################################################
# 2. agent状態管理(LangGraph)
##########################################################
class AgentState(TypedDict):
  messages: Annotated[Sequence[BaseMessage], add_messages]
  number_of_steps: int

3.レスポンスの型定義

  • Pydanticを使用してレスポンスの型を定義します
##########################################################
# 3. Pydantic 出力モデル
##########################################################
class WeatherItem(BaseModel):
  date: str = Field(description="日時(YYYY-MM-DDTHH:MM)")
  temperature: str = Field(description="気温(℃)")
  weather: str = Field(description="天気")
  advice: str = Field(description="天候に応じたアドバイス")
class WeatherAnswer(BaseModel):
  answer: List[WeatherItem]

4.天気コードマップの定義

  • Open-Meteoはオープンソースの天気APIで天気の情報を取得します。
  • 天気コードを事前に設定します。
##########################################################
# 4. 天気コードマップ
##########################################################
WEATHER_CODE_MAP = {
  0: "快晴", 1: "晴れ", 2: "薄曇り", 3: "曇り",
  45: "", 48: "霧氷",
  51: "霧雨", 53: "霧雨", 55: "霧雨",
  61: "小雨", 63: "", 65: "強い雨",
  71: "小雪", 73: "", 75: "強い雪",
  80: "にわか雨", 81: "", 82: "激しいにわか雨",
  85: "にわか雪", 86: "大雪",
}

5.ツールの作成(天気情報取得ツール)

##########################################################
# 5. 天気情報取得ツール
##########################################################
## geopy の Nominatim(OpenStreetMap の地名検索 API)を使う為の初期化
geolocator = Nominatim(user_agent="weather-app")
## Tool に渡す 引数の型 を Pydantic で定義。
class SearchInput(BaseModel):
  location: str = Field(description="都道府県(例:東京)")
  date: str = Field(description="日付(yyyy-mm-dd)")
@tool("get_weather_forecast", args_schema=SearchInput, return_direct=True)
def get_weather_forecast(location: str, date: str):
    """Open-Meteo から天気予報を取得するツール"""
    ## 地名から緯度経度に変換
    loc = geolocator.geocode(location)
    if loc is None:
        return {"error": "Location not found"}
    ## Open-Meteo APIを呼び出す
    response = requests.get(
        "https://api.open-meteo.com/v1/forecast",
        params={
            "latitude": loc.latitude,
            "longitude": loc.longitude,
            "hourly": "temperature_2m,weathercode",
            "start_date": date,
            "end_date": date,
        }
    )
    ## Open-Meteo APIからのレスポンス処理
    data = response.json()
    times = data["hourly"]["time"]
    temps = data["hourly"]["temperature_2m"]
    codes = data["hourly"]["weathercode"]
    result = {}
    ## 天気コードより日本語に変換し整形
    for t, temp, code in zip(times, temps, codes):
        result[t] = {
            "temperature": temp,
            "weather": WEATHER_CODE_MAP.get(code, f"不明({code})"),
        }
    return result
## Graph 内でツールを使うための登録
## toolsリストとtool本体の辞書型を作成
tools = [get_weather_forecast]
tools_by_name = {tool.name: tool for tool in tools}

6.Geminiモデルの定義とツールのテスト

  • Geminiモデルの設定を行います。
  • 作成したToolsをモデルと関連付けます
  • 実際にユーザーの問いに対して適切にToolに引数を渡せるかテスト実装を行います。
##########################################################
# 6. Gemini モデルの定義
##########################################################
llm = ChatGoogleGenerativeAI(
    model="gemini-2.5-flash",
    google_api_key=api_key,
    temperature=0.8,
)
# ツールをモデルに関連付ける
model = llm.bind_tools(tools)
# 構造化出力用のモデル(ツール呼び出し後用)
structured_model = llm.with_structured_output(WeatherAnswer)
# モデルがツールに紐づいているかテスト
today_str = datetime.now().strftime("%Y-%m-%d %H") + ":00"
res=model.invoke(f"{today_str}の沖縄県の天気はどうですか?")
fc = res.additional_kwargs["function_call"]
print("Function name:", fc["name"])
# print("Arguments:", fc["arguments"])
# arguments を JSON としてパース
args = json.loads(fc["arguments"])
print("Arguments:", args)
  • テスト実装の結果
Function name: get_weather_forecast
Arguments: {'location': '沖縄県', 'date': '2025-11-23'}

※ 本来はエージェントは下記の引数を渡しています。
※ 上記の実装結果は確認為に変換処理を加えています

Function name: get_weather_forecast
Arguments: {"location": "\u6c96\u7e04\u770c", "date": "2025-11-23"}

7.ツール結果をJSON形式に変換する関数を作成

  • 天気ツール(get_weather_forecast)の返り値を、最終的に返すべきPydanticモデル(WeatherAnswer)に変換するための関数です。
##########################################################
# 7. ツール結果 → JSON の最終形式に変換
##########################################################
def convert_to_weather_answer(result: dict) -> WeatherAnswer:
  items = []
  for t, data in result.items():
    items.append(
      WeatherItem(
        date=t,
        temperature=f"{data['temperature']}",
        weather=data["weather"],
        advice="取得した天気のデータに応じてアドバイスをしてください",
      )
    )
  return WeatherAnswer(answer=items)

8.LangGraphノードの定義

以下の関数を定義します。

  • call_tool:LLM からのtool_callを受け取ってツール実行する関数
  • call_model:LLMを読んだりツール結果をJSON化にして出力する関数
##########################################################
# 8. Graph ノード定義
##########################################################
def call_tool(state: AgentState):
  ## LLM の最後のメッセージを確認
  last = state["messages"][-1]
  outputs = []
  ## LLMが要求しているtool_callを順に実行
  for tc in last.tool_calls:
    tool_result = tools_by_name[tc["name"]].invoke(tc["args"])
    ## LLM に返す「ツールの結果メッセージ」を生成
    outputs.append(
      ToolMessage(
        content=tool_result,
        name=tc["name"],
        tool_call_id=tc["id"]
      )
    )
  ## 次の状態としてmessagesを返す
  return {
      "messages": outputs,
      "number_of_steps": state["number_of_steps"] + 1
  }
def call_model(state: AgentState, config: RunnableConfig):
  # 状況に応じて処理を分岐処理を行います
  messages = state["messages"]
  # ▼ツール呼び出し(function_call)を含む → Gemini に渡す
  if any(hasattr(m, "tool_calls") and m.tool_calls for m in messages):
    response = model.invoke(messages, config)
    return {
      "messages": [response],
      "number_of_steps": state["number_of_steps"] + 1
    }
  # ▼ToolMessage(ツールの結果)→ ここが最終出力
  tool_messages = [m for m in messages if isinstance(m, ToolMessage)]
  if tool_messages:
    tool_content = tool_messages[-1].content
    answer = convert_to_weather_answer(tool_content)
    return {
      "messages": [AIMessage(content=json.dumps(answer.model_dump(), ensure_ascii=False))],
      "number_of_steps": state["number_of_steps"] + 1
    }
  # ▼初回(通常 LLM)
  response = model.invoke(messages, config)
  return {
    "messages": [response],
    "number_of_steps": state["number_of_steps"] + 1
  }

9.LangGraph の「分岐条件」(ルーティング条件) を定義

  • グラフが次にどのノード(call_tool or 終了)へ進むかを決める“信号機(ルーター)” のような役割を定義します。
##########################################################
# 9. グラフ遷移条件
##########################################################
def should_continue(state: AgentState):
  last = state["messages"][-1]
  if hasattr(last, "tool_calls") and last.tool_calls:
    return "continue"
  return "end"

10LangGraphを構築

  • 上記の定義した内容をLangGraphに構築処理を行います。
##########################################################
# 10. LangGraph 構築
##########################################################
## StateGraphのインスタンスを作成
workflow = StateGraph(AgentState)
## GrphにNodeを追加
workflow.add_node("llm", call_model)
workflow.add_node("tools", call_tool)
# 始点を定義
workflow.set_entry_point("llm")
# ループ処理を定義
workflow.add_conditional_edges(
  "llm",
  should_continue,
  {"continue": "tools", "end": END},
)
# Edgeを追加
workflow.add_edge("tools", "llm")
## NodeとEdgeで構成したGraphをコンパイル
graph = workflow.compile()
## Graphを可視化(mermaid)
img_bytes = graph.get_graph().draw_mermaid_png()
with open("./weather_tools.png", "wb") as f:
  f.write(img_bytes)
  • Graphを可視化

weather_tools.png

11.LangGraph実行と結果を取得

##########################################################
# 11. 実行
##########################################################
today = datetime.now().strftime("%Y-%m-%d")
user_msg = ("user", f"{today} の 青森県 の天気は?")
result = graph.invoke({
  "messages": [user_msg],
  "number_of_steps": 0
})
# result は LangGraph の結果
messages = result["messages"]
ai_message = result["messages"][-1]

12.実行結果からjson形式に成形

  • 残念ながらLangGraph実行結果からそのまま想定通りのJSON形式になりませんでした。
  • そのため、想定しているJSON形式に整形する処理を行います。

【ToolMessage(天気データ)を抽出】

###########################################################
# 12. ToolMessage(天気データ)を抽出
###########################################################
tool_msg = next(m for m in messages if isinstance(m, ToolMessage))
# content が文字列(シングルクォート)なので eval で dict に変換
weather_raw = tool_msg.content
if isinstance(weather_raw, str):
  weather_raw = eval(weather_raw)
  • whether_rowの出力例
// weather_raw の例:
{'2025-11-23T00:00': {'temperature': 20.8, 'weather': '快晴'}, ... }

【pandasのDataFrameを使ってデータを整形】

###########################################################
# 13. pandas DataFrame に変換
###########################################################
df = pd.DataFrame([
  {
    "datetime": dt,
    "temperature": data["temperature"],
    "weather": data["weather"]
  }
  for dt, data in weather_raw.items()
])

###########################################################
# 14. 最頻値(天気)・気温の平均・日付を取得
###########################################################
most_common_weather = df["weather"].mode()[0]
avg_temp = round(df["temperature"].mean(), 1)
date_str = df["datetime"].iloc[0].split("T")[0]

【最後のAIMessageのアドバイスを抽出】

###########################################################
# 15.AIMessage のアドバイスを抽出
###########################################################
def extract_text_from_message(msg):
  content = msg.content
  # string の場合はそのまま
  if isinstance(content, str):
    return content
  # dict の場合
  if isinstance(content, dict):
    return content.get("text", json.dumps(content, ensure_ascii=False))
  # list の場合
  if isinstance(content, list):
    texts = []
    for c in content:
      if isinstance(c, dict):
        if "text" in c:
          texts.append(c["text"])
    return "\n".join(texts)
  return str(content)
advice_text = extract_text_from_message(ai_message)

【最終形態のJSON形式にして出力】

###########################################################
# 16 最終 JSON を整形
###########################################################
final_json = {
    "answer": {
        "date": date_str,
        "weather": most_common_weather,
        "temperature": f"{avg_temp}",
        "advice": advice_text
    }
}
with open("./res_wether.json", "w", encoding="utf-8") as f:
    json.dump(final_json, f, ensure_ascii=False, indent=2)
  • jsonの出力例
{
  "answer": {
    "date": "2025-11-23",
    "weather": "晴れ",
    "temperature": "9.0℃",
    "advice": "青森県の2025-11-23の天気は以下の通りです。\n\n* 00:00: 気温8.9℃、晴れ\n* 01:00: 気温10.2℃、晴れ\n* 02:00: 気温11.3℃、薄曇り\n* 03:00: 気温10.1℃、霧雨\n* 04:00: 気温10.9℃、小雨\n* 05:00: 気温11.6℃、霧雨\n* 06:00: 気温11.4℃、霧雨\n* 07:00: 気温10.4℃、薄曇り\n* 08:00: 気温9.1℃、晴れ\n* 09:00: 気温9.1℃、晴れ\n* 10:00: 気温9.1℃、晴れ\n* 11:00: 気温9.1℃、晴れ\n* 12:00: 気温9.4℃、晴れ\n* 13:00: 気温9.4℃、晴れ\n* 14:00: 気温8.2℃、晴れ\n* 15:00: 気温7.8℃、快晴\n* 16:00: 気温7.5℃、晴れ\n* 17:00: 気温7.4℃、晴れ\n* 18:00: 気温7.3℃、晴れ\n* 19:00: 気温7.3℃、晴れ\n* 20:00: 気温7.2℃、晴れ\n* 21:00: 気温7.2℃、薄曇り\n* 22:00: 気温7.4℃、薄曇り\n* 23:00: 気温8.1℃、薄曇り"
  }
}

全体のコード(GitHub)

後記

  • 最後まで読んで頂きありがとうございます。
  • LangGraphを使用して天気エージェントを実装しました。
  • 基礎のサンプルコードを基にAIとバイブコーディングしながら実装を進めました。
  • これを基礎に様々なAPI情報取得からモデルによるコメントや要約などの出力までのAIエージェントを作成していきたいと思います。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?