Add: Steam API tool (#14008)

- **Description:** Our PR is an integration of a Steam API Tool that
makes recommendations on steam games based on user's Steam profile and
provides information on games based on user provided queries.
- **Issue:** the issue # our PR implements:
https://github.com/langchain-ai/langchain/issues/12120
- **Dependencies:** python-steam-api library, steamspypi library and
decouple library
  - **Tag maintainer:** @baskaryan, @hwchase17 
  - **Twitter handle:** N/A

Hello langchain Maintainers,

We are a team of 4 University of Toronto students contributing to
langchain as part of our course [CSCD01 (link to course
page)](https://cscd01.com/work/open-source-project). We hope our changes
help the community. We have run make format, make lint and make test
locally before submitting the PR. To our knowledge, our changes do not
introduce any new errors.

Our PR integrates the python-steam-api, steamspypi and decouple
packages. We have added integration tests to test our python API
integration into langchain and an example notebook is also provided.

Our amazing team that contributed to this PR: @JohnY2002, @shenceyang,
@andrewqian2001 and @muntaqamahmood

Thank you in advance to all the maintainers for reviewing our PR!

---------

Co-authored-by: Shence <ysc1412799032@163.com>
Co-authored-by: JohnY2002 <johnyuan0526@gmail.com>
Co-authored-by: Andrew Qian <andrewqian2001@gmail.com>
Co-authored-by: Harrison Chase <hw.chase.17@gmail.com>
Co-authored-by: JohnY <94477598+JohnY2002@users.noreply.github.com>
This commit is contained in:
Muntaqa Mahmood 2023-12-04 15:27:38 -05:00 committed by GitHub
parent cd2028288e
commit 25f72944a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 419 additions and 0 deletions

View File

@ -0,0 +1,105 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Steam Game Recommendation & Game Details Tool"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"\n",
"from langchain.agents import AgentType, initialize_agent\n",
"from langchain.agents.agent_toolkits.steam.toolkit import SteamToolkit\n",
"from langchain.llms import OpenAI\n",
"from langchain.utilities.steam import SteamWebAPIWrapper"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"STEAM_KEY\"] = \"xyz\"\n",
"os.environ[\"STEAM_ID\"] = \"123\"\n",
"os.environ[\"OPENAI_API_KEY\"] = \"abc\""
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [],
"source": [
"llm = OpenAI(temperature=0)\n",
"Steam = SteamWebAPIWrapper()\n",
"toolkit = SteamToolkit.from_steam_api_wrapper(Steam)\n",
"agent = initialize_agent(\n",
" toolkit.get_tools(), llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"\n",
"\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n",
"\u001b[32;1m\u001b[1;3m I need to find the game details\n",
"Action: Get Games Details\n",
"Action Input: Terraria\u001b[0m\n",
"Observation: \u001b[36;1m\u001b[1;3mThe id is: 105600\n",
"The link is: https://store.steampowered.com/app/105600/Terraria/?snr=1_7_15__13\n",
"The price is: $9.99\n",
"The summary of the game is: Dig, Fight, Explore, Build: The very world is at your fingertips as you fight for survival, fortune, and glory. Will you delve deep into cavernous expanses in search of treasure and raw materials with which to craft ever-evolving gear, machinery, and aesthetics? Perhaps you will choose instead to seek out ever-greater foes to test your mettle in combat? Maybe you will decide to construct your own city to house the host of mysterious allies you may encounter along your travels? In the World of Terraria, the choice is yours!Blending elements of classic action games with the freedom of sandbox-style creativity, Terraria is a unique gaming experience where both the journey and the destination are completely in the players control. The Terraria adventure is truly as unique as the players themselves! Are you up for the monumental task of exploring, creating, and defending a world of your own? Key features: Sandbox Play Randomly generated worlds Free Content Updates \n",
"The supported languages of the game are: English, French, Italian, German, Spanish - Spain, Polish, Portuguese - Brazil, Russian, Simplified Chinese\n",
"\u001b[0m\n",
"Thought:\u001b[32;1m\u001b[1;3m I now know the final answer\n",
"Final Answer: Terraria is a game with an id of 105600, a link of https://store.steampowered.com/app/105600/Terraria/?snr=1_7_15__13, a price of $9.99, a summary of \"Dig, Fight, Explore, Build: The very world is at your fingertips as you fight for survival, fortune, and glory. Will you delve deep into cavernous expanses in search of treasure and raw materials with which to craft ever-evolving gear, machinery, and aesthetics? Perhaps you will choose instead to seek out ever-greater foes to test your mettle in combat? Maybe you will decide to construct your own city to house the host of mysterious allies you may encounter along your travels? In the World of Terraria, the choice is yours!Blending elements of classic action games with the freedom of sandbox-style creativity, Terraria is a unique gaming experience where both the journey and the destination are completely in the players control. The Terraria adventure is truly as unique as the players themselves! Are you up for the monumental task of exploring, creating, and defending a\u001b[0m\n",
"\n",
"\u001b[1m> Finished chain.\u001b[0m\n",
"{'input': 'can you give the information about the game Terraria', 'output': 'Terraria is a game with an id of 105600, a link of https://store.steampowered.com/app/105600/Terraria/?snr=1_7_15__13, a price of $9.99, a summary of \"Dig, Fight, Explore, Build: The very world is at your fingertips as you fight for survival, fortune, and glory. Will you delve deep into cavernous expanses in search of treasure and raw materials with which to craft ever-evolving gear, machinery, and aesthetics? Perhaps you will choose instead to seek out ever-greater foes to test your mettle in combat? Maybe you will decide to construct your own city to house the host of mysterious allies you may encounter along your travels? In the World of Terraria, the choice is yours!Blending elements of classic action games with the freedom of sandbox-style creativity, Terraria is a unique gaming experience where both the journey and the destination are completely in the players control. The Terraria adventure is truly as unique as the players themselves! Are you up for the monumental task of exploring, creating, and defending a'}\n"
]
}
],
"source": [
"out = agent(\"can you give the information about the game Terraria\")\n",
"print(out)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.1"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

@ -47,6 +47,7 @@ from langchain.agents.agent_toolkits.spark_sql.base import create_spark_sql_agen
from langchain.agents.agent_toolkits.spark_sql.toolkit import SparkSQLToolkit
from langchain.agents.agent_toolkits.sql.base import create_sql_agent
from langchain.agents.agent_toolkits.sql.toolkit import SQLDatabaseToolkit
from langchain.agents.agent_toolkits.steam.toolkit import SteamToolkit
from langchain.agents.agent_toolkits.vectorstore.base import (
create_vectorstore_agent,
create_vectorstore_router_agent,
@ -98,6 +99,7 @@ __all__ = [
"PlayWrightBrowserToolkit",
"PowerBIToolkit",
"SlackToolkit",
"SteamToolkit",
"SQLDatabaseToolkit",
"SparkSQLToolkit",
"VectorStoreInfo",

View File

@ -0,0 +1 @@
"""Steam Toolkit."""

View File

@ -0,0 +1,48 @@
"""Steam Toolkit."""
from typing import List
from langchain.agents.agent_toolkits.base import BaseToolkit
from langchain.tools import BaseTool
from langchain.tools.steam.prompt import (
STEAM_GET_GAMES_DETAILS,
STEAM_GET_RECOMMENDED_GAMES,
)
from langchain.tools.steam.tool import SteamWebAPIQueryRun
from langchain.utilities.steam import SteamWebAPIWrapper
class SteamToolkit(BaseToolkit):
"""Steam Toolkit."""
tools: List[BaseTool] = []
@classmethod
def from_steam_api_wrapper(
cls, steam_api_wrapper: SteamWebAPIWrapper
) -> "SteamToolkit":
operations: List[dict] = [
{
"mode": "get_games_details",
"name": "Get Games Details",
"description": STEAM_GET_GAMES_DETAILS,
},
{
"mode": "get_recommended_games",
"name": "Get Recommended Games",
"description": STEAM_GET_RECOMMENDED_GAMES,
},
]
tools = [
SteamWebAPIQueryRun(
name=action["name"],
description=action["description"],
mode=action["mode"],
api_wrapper=steam_api_wrapper,
)
for action in operations
]
return cls(tools=tools)
def get_tools(self) -> List[BaseTool]:
"""Get the tools in the toolkit."""
return self.tools

View File

@ -534,6 +534,12 @@ def _import_requests_tool_RequestsPutTool() -> Any:
return RequestsPutTool
def _import_steam_webapi_tool() -> Any:
from langchain.tools.steam.tool import SteamWebAPIQueryRun
return SteamWebAPIQueryRun
def _import_scenexplain_tool() -> Any:
from langchain.tools.scenexplain.tool import SceneXplainTool
@ -887,6 +893,8 @@ def __getattr__(name: str) -> Any:
return _import_requests_tool_RequestsPostTool()
elif name == "RequestsPutTool":
return _import_requests_tool_RequestsPutTool()
elif name == "SteamWebAPIQueryRun":
return _import_steam_webapi_tool()
elif name == "SceneXplainTool":
return _import_scenexplain_tool()
elif name == "SearxSearchResults":
@ -1044,6 +1052,7 @@ __all__ = [
"RequestsPatchTool",
"RequestsPostTool",
"RequestsPutTool",
"SteamWebAPIQueryRun",
"SceneXplainTool",
"SearxSearchResults",
"SearxSearchRun",

View File

@ -0,0 +1 @@
"""Steam API toolkit"""

View File

@ -0,0 +1,26 @@
STEAM_GET_GAMES_DETAILS = """
This tool is a wrapper around python-steam-api's steam.apps.search_games API and
steam.apps.get_app_details API, useful when you need to search for a game.
The input to this tool is a string specifying the name of the game you want to
search for. For example, to search for a game called "Counter-Strike: Global
Offensive", you would input "Counter-Strike: Global Offensive" as the game name.
This input will be passed into steam.apps.search_games to find the game id, link
and price, and then the game id will be passed into steam.apps.get_app_details to
get the detailed description and supported languages of the game. Finally the
results are combined and returned as a string.
"""
STEAM_GET_RECOMMENDED_GAMES = """
This tool is a wrapper around python-steam-api's steam.users.get_owned_games API
and steamspypi's steamspypi.download API, useful when you need to get a list of
recommended games. The input to this tool is a string specifying the steam id of
the user you want to get recommended games for. For example, to get recommended
games for a user with steam id 76561197960435530, you would input
"76561197960435530" as the steam id. This steamid is then utilized to form a
data_request sent to steamspypi's steamspypi.download to retrieve genres of user's
owned games. Then, calculates the frequency of each genre, identifying the most
popular one, and stored it in a dictionary. Subsequently, use steamspypi.download
to returns all games in this genre and return 5 most-played games that is not owned
by the user.
"""

View File

@ -0,0 +1,29 @@
"""Tool for Steam Web API"""
from typing import Optional
from langchain.callbacks.manager import CallbackManagerForToolRun
from langchain.tools.base import BaseTool
from langchain.utilities.steam import SteamWebAPIWrapper
class SteamWebAPIQueryRun(BaseTool):
"""Tool that searches the Steam Web API."""
mode: str
name: str = "Steam"
description: str = (
"A wrapper around Steam Web API."
"Steam Tool is useful for fetching User profiles and stats, Game data and more!"
"Input should be the User or Game you want to query."
)
api_wrapper: SteamWebAPIWrapper
def _run(
self,
query: str,
run_manager: Optional[CallbackManagerForToolRun] = None,
) -> str:
"""Use the Steam-WebAPI tool."""
return self.api_wrapper.run(self.mode, query)

View File

@ -218,6 +218,12 @@ def _import_sql_database() -> Any:
return SQLDatabase
def _import_steam_webapi() -> Any:
from langchain.utilities.steam import SteamWebAPIWrapper
return SteamWebAPIWrapper
def _import_stackexchange() -> Any:
from langchain.utilities.stackexchange import StackExchangeAPIWrapper
@ -327,6 +333,8 @@ def __getattr__(name: str) -> Any:
return _import_stackexchange()
elif name == "SQLDatabase":
return _import_sql_database()
elif name == "SteamWebAPIWrapper":
return _import_steam_webapi()
elif name == "TensorflowDatasets":
return _import_tensorflow_datasets()
elif name == "TwilioAPIWrapper":
@ -373,6 +381,7 @@ __all__ = [
"PythonREPL",
"Requests",
"RequestsWrapper",
"SteamWebAPIWrapper",
"SQLDatabase",
"SceneXplainAPIWrapper",
"SearchApiAPIWrapper",

View File

@ -0,0 +1,164 @@
"""Util that calls Steam-WebAPI."""
from typing import Any, List
from langchain.pydantic_v1 import BaseModel, Extra, root_validator
class SteamWebAPIWrapper(BaseModel):
"""Wrapper for Steam API."""
steam: Any # for python-steam-api
from langchain.tools.steam.prompt import (
STEAM_GET_GAMES_DETAILS,
STEAM_GET_RECOMMENDED_GAMES,
)
# operations: a list of dictionaries, each representing a specific operation that
# can be performed with the API
operations: List[dict] = [
{
"mode": "get_game_details",
"name": "Get Game Details",
"description": STEAM_GET_GAMES_DETAILS,
},
{
"mode": "get_recommended_games",
"name": "Get Recommended Games",
"description": STEAM_GET_RECOMMENDED_GAMES,
},
]
class Config:
"""Configuration for this pydantic object."""
extra = Extra.forbid
def get_operations(self) -> List[dict]:
"""Return a list of operations."""
return self.operations
@root_validator
def validate_environment(cls, values: dict) -> dict:
"""Validate api key and python package has been configured."""
# check if the python package is installed
try:
from steam import Steam
except ImportError:
raise ImportError("python-steam-api library is not installed. ")
try:
from decouple import config
except ImportError:
raise ImportError("decouple library is not installed. ")
# initialize the steam attribute for python-steam-api usage
KEY = config("STEAM_KEY")
steam = Steam(KEY)
values["steam"] = steam
return values
def parse_to_str(self, details: dict) -> str: # For later parsing
"""Parse the details result."""
result = ""
for key, value in details.items():
result += "The " + str(key) + " is: " + str(value) + "\n"
return result
def get_id_link_price(self, games: dict) -> dict:
"""The response may contain more than one game, so we need to choose the right
one and return the id."""
game_info = {}
for app in games["apps"]:
game_info["id"] = app["id"]
game_info["link"] = app["link"]
game_info["price"] = app["price"]
break
return game_info
def remove_html_tags(self, html_string: str) -> str:
from bs4 import BeautifulSoup
soup = BeautifulSoup(html_string, "html.parser")
return soup.get_text()
def details_of_games(self, name: str) -> str:
games = self.steam.apps.search_games(name)
info_partOne_dict = self.get_id_link_price(games)
info_partOne = self.parse_to_str(info_partOne_dict)
id = str(info_partOne_dict.get("id"))
info_dict = self.steam.apps.get_app_details(id)
data = info_dict.get(id).get("data")
detailed_description = data.get("detailed_description")
# detailed_description contains <li> <br> some other html tags, so we need to
# remove them
detailed_description = self.remove_html_tags(detailed_description)
supported_languages = info_dict.get(id).get("data").get("supported_languages")
info_partTwo = (
"The summary of the game is: "
+ detailed_description
+ "\n"
+ "The supported languages of the game are: "
+ supported_languages
+ "\n"
)
info = info_partOne + info_partTwo
return info
def get_steam_id(self, name: str) -> str:
user = self.steam.users.search_user(name)
steam_id = user["player"]["steamid"]
return steam_id
def get_users_games(self, steam_id: str) -> List[str]:
return self.steam.users.get_owned_games(steam_id, False, False)
def recommended_games(self, steam_id: str) -> str:
try:
import steamspypi
except ImportError:
raise ImportError("steamspypi library is not installed.")
users_games = self.get_users_games(steam_id)
result = {} # type: ignore
most_popular_genre = ""
most_popular_genre_count = 0
for game in users_games["games"]: # type: ignore
appid = game["appid"]
data_request = {"request": "appdetails", "appid": appid}
genreStore = steamspypi.download(data_request)
genreList = genreStore.get("genre", "").split(", ")
for genre in genreList:
if genre in result:
result[genre] += 1
else:
result[genre] = 1
if result[genre] > most_popular_genre_count:
most_popular_genre_count = result[genre]
most_popular_genre = genre
data_request = dict()
data_request["request"] = "genre"
data_request["genre"] = most_popular_genre
data = steamspypi.download(data_request)
sorted_data = sorted(
data.values(), key=lambda x: x.get("average_forever", 0), reverse=True
)
owned_games = [game["appid"] for game in users_games["games"]] # type: ignore
remaining_games = [
game for game in sorted_data if game["appid"] not in owned_games
]
top_5_popular_not_owned = [game["name"] for game in remaining_games[:5]]
return str(top_5_popular_not_owned)
def run(self, mode: str, game: str) -> str:
if mode == "get_games_details":
return self.details_of_games(game)
elif mode == "get_recommended_games":
return self.recommended_games(game)
else:
raise ValueError(f"Invalid mode {mode} for Steam API.")

View File

@ -0,0 +1,22 @@
import ast
from langchain.utilities.steam import SteamWebAPIWrapper
def test_get_game_details() -> None:
"""Test for getting game details on Steam"""
steam = SteamWebAPIWrapper()
output = steam.run("get_game_details", "Terraria")
assert "id" in output
assert "link" in output
assert "detailed description" in output
assert "supported languages" in output
assert "price" in output
def test_get_recommended_games() -> None:
"""Test for getting recommended games on Steam"""
steam = SteamWebAPIWrapper()
output = steam.run("get_recommended_games", "76561198362745711")
output = ast.literal_eval(output)
assert len(output) == 5

View File

@ -101,6 +101,7 @@ EXPECTED_ALL = [
"SleepTool",
"StackExchangeTool",
"StdInInquireTool",
"SteamWebAPIQueryRun",
"SteamshipImageGenerationTool",
"StructuredTool",
"Tool",

View File

@ -105,6 +105,7 @@ _EXPECTED = [
"StackExchangeTool",
"SteamshipImageGenerationTool",
"StructuredTool",
"SteamWebAPIQueryRun",
"Tool",
"VectorStoreQATool",
"VectorStoreQAWithSourcesTool",

View File

@ -38,6 +38,7 @@ EXPECTED_ALL = [
"SerpAPIWrapper",
"SparkSQL",
"StackExchangeAPIWrapper",
"SteamWebAPIWrapper",
"TensorflowDatasets",
"TextRequestsWrapper",
"TwilioAPIWrapper",