experimental[minor]: Amazon Personalize support (#17436)

## Amazon Personalize support on Langchain

This PR is a successor to this PR -
https://github.com/langchain-ai/langchain/pull/13216

This PR introduces an integration with [Amazon
Personalize](https://aws.amazon.com/personalize/) to help you to
retrieve recommendations and use them in your natural language
applications. This integration provides two new components:

1. An `AmazonPersonalize` client, that provides a wrapper around the
Amazon Personalize API.
2. An `AmazonPersonalizeChain`, that provides a chain to pull in
recommendations using the client, and then generating the response in
natural language.

We have added this to langchain_experimental since there was feedback
from the previous PR about having this support in experimental rather
than the core or community extensions.

Here is some sample code to explain the usage.

```python

from langchain_experimental.recommenders import AmazonPersonalize
from langchain_experimental.recommenders import AmazonPersonalizeChain
from langchain.llms.bedrock import Bedrock

recommender_arn = "<insert_arn>"

client=AmazonPersonalize(
    credentials_profile_name="default",
    region_name="us-west-2",
    recommender_arn=recommender_arn
)
bedrock_llm = Bedrock(
    model_id="anthropic.claude-v2", 
    region_name="us-west-2"
)

chain = AmazonPersonalizeChain.from_llm(
    llm=bedrock_llm, 
    client=client
)
response = chain({'user_id': '1'})
```


Reviewer: @3coins
pull/17750/head
Pranav Agarwal 3 months ago committed by GitHub
parent 0d294760e7
commit 86ae48b781
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,284 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Amazon Personalize\n",
"\n",
"[Amazon Personalize](https://docs.aws.amazon.com/personalize/latest/dg/what-is-personalize.html) is a fully managed machine learning service that uses your data to generate item recommendations for your users. It can also generate user segments based on the users' affinity for certain items or item metadata.\n",
"\n",
"This notebook goes through how to use Amazon Personalize Chain. You need a Amazon Personalize campaign_arn or a recommender_arn before you get started with the below notebook.\n",
"\n",
"Following is a [tutorial](https://github.com/aws-samples/retail-demo-store/blob/master/workshop/1-Personalization/Lab-1-Introduction-and-data-preparation.ipynb) to setup a campaign_arn/recommender_arn on Amazon Personalize. Once the campaign_arn/recommender_arn is setup, you can use it in the langchain ecosystem. \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 1. Install Dependencies"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"scrolled": true
},
"outputs": [],
"source": [
"!pip install boto3"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 2. Sample Use-cases"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 2.1 [Use-case-1] Setup Amazon Personalize Client and retrieve recommendations"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from langchain_experimental.recommenders import AmazonPersonalize\n",
"\n",
"recommender_arn = \"<insert_arn>\"\n",
"\n",
"client = AmazonPersonalize(\n",
" credentials_profile_name=\"default\",\n",
" region_name=\"us-west-2\",\n",
" recommender_arn=recommender_arn,\n",
")\n",
"client.get_recommendations(user_id=\"1\")"
]
},
{
"cell_type": "markdown",
"metadata": {
"collapsed": false,
"jupyter": {
"outputs_hidden": false
}
},
"source": [
"### 2.2 [Use-case-2] Invoke Personalize Chain for summarizing results"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false,
"jupyter": {
"outputs_hidden": false
}
},
"outputs": [],
"source": [
"from langchain.llms.bedrock import Bedrock\n",
"from langchain_experimental.recommenders import AmazonPersonalizeChain\n",
"\n",
"bedrock_llm = Bedrock(model_id=\"anthropic.claude-v2\", region_name=\"us-west-2\")\n",
"\n",
"# Create personalize chain\n",
"# Use return_direct=True if you do not want summary\n",
"chain = AmazonPersonalizeChain.from_llm(\n",
" llm=bedrock_llm, client=client, return_direct=False\n",
")\n",
"response = chain({\"user_id\": \"1\"})\n",
"print(response)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 2.3 [Use-Case-3] Invoke Amazon Personalize Chain using your own prompt"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from langchain.prompts.prompt import PromptTemplate\n",
"\n",
"RANDOM_PROMPT_QUERY = \"\"\"\n",
"You are a skilled publicist. Write a high-converting marketing email advertising several movies available in a video-on-demand streaming platform next week, \n",
" given the movie and user information below. Your email will leverage the power of storytelling and persuasive language. \n",
" The movies to recommend and their information is contained in the <movie> tag. \n",
" All movies in the <movie> tag must be recommended. Give a summary of the movies and why the human should watch them. \n",
" Put the email between <email> tags.\n",
"\n",
" <movie>\n",
" {result} \n",
" </movie>\n",
"\n",
" Assistant:\n",
" \"\"\"\n",
"\n",
"RANDOM_PROMPT = PromptTemplate(input_variables=[\"result\"], template=RANDOM_PROMPT_QUERY)\n",
"\n",
"chain = AmazonPersonalizeChain.from_llm(\n",
" llm=bedrock_llm, client=client, return_direct=False, prompt_template=RANDOM_PROMPT\n",
")\n",
"chain.run({\"user_id\": \"1\", \"item_id\": \"234\"})"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 2.4 [Use-case-4] Invoke Amazon Personalize in a Sequential Chain "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from langchain.chains import LLMChain, SequentialChain\n",
"\n",
"RANDOM_PROMPT_QUERY_2 = \"\"\"\n",
"You are a skilled publicist. Write a high-converting marketing email advertising several movies available in a video-on-demand streaming platform next week, \n",
" given the movie and user information below. Your email will leverage the power of storytelling and persuasive language. \n",
" You want the email to impress the user, so make it appealing to them.\n",
" The movies to recommend and their information is contained in the <movie> tag. \n",
" All movies in the <movie> tag must be recommended. Give a summary of the movies and why the human should watch them. \n",
" Put the email between <email> tags.\n",
"\n",
" <movie>\n",
" {result}\n",
" </movie>\n",
"\n",
" Assistant:\n",
" \"\"\"\n",
"\n",
"RANDOM_PROMPT_2 = PromptTemplate(\n",
" input_variables=[\"result\"], template=RANDOM_PROMPT_QUERY_2\n",
")\n",
"personalize_chain_instance = AmazonPersonalizeChain.from_llm(\n",
" llm=bedrock_llm, client=client, return_direct=True\n",
")\n",
"random_chain_instance = LLMChain(llm=bedrock_llm, prompt=RANDOM_PROMPT_2)\n",
"overall_chain = SequentialChain(\n",
" chains=[personalize_chain_instance, random_chain_instance],\n",
" input_variables=[\"user_id\"],\n",
" verbose=True,\n",
")\n",
"overall_chain.run({\"user_id\": \"1\", \"item_id\": \"234\"})"
]
},
{
"cell_type": "markdown",
"metadata": {
"collapsed": false,
"jupyter": {
"outputs_hidden": false
}
},
"source": [
"### 2.5 [Use-case-5] Invoke Amazon Personalize and retrieve metadata "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false,
"jupyter": {
"outputs_hidden": false
}
},
"outputs": [],
"source": [
"recommender_arn = \"<insert_arn>\"\n",
"metadata_column_names = [\n",
" \"<insert metadataColumnName-1>\",\n",
" \"<insert metadataColumnName-2>\",\n",
"]\n",
"metadataMap = {\"ITEMS\": metadata_column_names}\n",
"\n",
"client = AmazonPersonalize(\n",
" credentials_profile_name=\"default\",\n",
" region_name=\"us-west-2\",\n",
" recommender_arn=recommender_arn,\n",
")\n",
"client.get_recommendations(user_id=\"1\", metadataColumns=metadataMap)"
]
},
{
"cell_type": "markdown",
"metadata": {
"collapsed": false,
"jupyter": {
"outputs_hidden": false
}
},
"source": [
"### 2.6 [Use-Case 6] Invoke Personalize Chain with returned metadata for summarizing results"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false,
"jupyter": {
"outputs_hidden": false
}
},
"outputs": [],
"source": [
"bedrock_llm = Bedrock(model_id=\"anthropic.claude-v2\", region_name=\"us-west-2\")\n",
"\n",
"# Create personalize chain\n",
"# Use return_direct=True if you do not want summary\n",
"chain = AmazonPersonalizeChain.from_llm(\n",
" llm=bedrock_llm, client=client, return_direct=False\n",
")\n",
"response = chain({\"user_id\": \"1\", \"metadata_columns\": metadataMap})\n",
"print(response)"
]
}
],
"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.11.7"
},
"vscode": {
"interpreter": {
"hash": "15e58ce194949b77a891bd4339ce3d86a9bd138e905926019517993f97db9e6c"
}
}
},
"nbformat": 4,
"nbformat_minor": 4
}

@ -0,0 +1,7 @@
"""Amazon Personalize primitives."""
from langchain_experimental.recommenders.amazon_personalize import AmazonPersonalize
from langchain_experimental.recommenders.amazon_personalize_chain import (
AmazonPersonalizeChain,
)
__all__ = ["AmazonPersonalize", "AmazonPersonalizeChain"]

@ -0,0 +1,195 @@
from typing import Any, List, Mapping, Optional, Sequence
class AmazonPersonalize:
"""Amazon Personalize Runtime wrapper for executing real-time operations:
https://docs.aws.amazon.com/personalize/latest/dg/API_Operations_Amazon_Personalize_Runtime.html
Args:
campaign_arn: str, Optional: The Amazon Resource Name (ARN) of the campaign
to use for getting recommendations.
recommender_arn: str, Optional: The Amazon Resource Name (ARN) of the
recommender to use to get recommendations
client: Optional: boto3 client
credentials_profile_name: str, Optional :AWS profile name
region_name: str, Optional: AWS region, e.g., us-west-2
Example:
.. code-block:: python
personalize_client = AmazonPersonalize (
campaignArn='<my-campaign-arn>' )
"""
def __init__(
self,
campaign_arn: Optional[str] = None,
recommender_arn: Optional[str] = None,
client: Optional[Any] = None,
credentials_profile_name: Optional[str] = None,
region_name: Optional[str] = None,
):
self.campaign_arn = campaign_arn
self.recommender_arn = recommender_arn
if campaign_arn and recommender_arn:
raise ValueError(
"Cannot initialize AmazonPersonalize with both "
"campaign_arn and recommender_arn."
)
if not campaign_arn and not recommender_arn:
raise ValueError(
"Cannot initialize AmazonPersonalize. Provide one of "
"campaign_arn or recommender_arn"
)
try:
if client is not None:
self.client = client
else:
import boto3
import botocore.config
if credentials_profile_name is not None:
session = boto3.Session(profile_name=credentials_profile_name)
else:
# use default credentials
session = boto3.Session()
client_params = {}
if region_name:
client_params["region_name"] = region_name
service = "personalize-runtime"
session_config = botocore.config.Config(user_agent_extra="langchain")
client_params["config"] = session_config
self.client = session.client(service, **client_params)
except ImportError:
raise ModuleNotFoundError(
"Could not import boto3 python package. "
"Please install it with `pip install boto3`."
)
def get_recommendations(
self,
user_id: Optional[str] = None,
item_id: Optional[str] = None,
filter_arn: Optional[str] = None,
filter_values: Optional[Mapping[str, str]] = None,
num_results: Optional[int] = 10,
context: Optional[Mapping[str, str]] = None,
promotions: Optional[Sequence[Mapping[str, Any]]] = None,
metadata_columns: Optional[Mapping[str, Sequence[str]]] = None,
**kwargs: Any,
) -> Mapping[str, Any]:
"""Get recommendations from Amazon Personalize:
https://docs.aws.amazon.com/personalize/latest/dg/API_RS_GetRecommendations.html
Args:
user_id: str, Optional: The user identifier
for which to retrieve recommendations
item_id: str, Optional: The item identifier
for which to retrieve recommendations
filter_arn: str, Optional: The ARN of the filter
to apply to the returned recommendations
filter_values: Mapping, Optional: The values
to use when filtering recommendations.
num_results: int, Optional: Default=10: The number of results to return
context: Mapping, Optional: The contextual metadata
to use when getting recommendations
promotions: Sequence, Optional: The promotions
to apply to the recommendation request.
metadata_columns: Mapping, Optional: The metadata Columns to be returned
as part of the response.
Returns:
response: Mapping[str, Any]: Returns an itemList and recommendationId.
Example:
.. code-block:: python
personalize_client = AmazonPersonalize(campaignArn='<my-campaign-arn>' )\n
response = personalize_client.get_recommendations(user_id="1")
"""
if not user_id and not item_id:
raise ValueError("One of user_id or item_id is required")
if filter_arn:
kwargs["filterArn"] = filter_arn
if filter_values:
kwargs["filterValues"] = filter_values
if user_id:
kwargs["userId"] = user_id
if num_results:
kwargs["numResults"] = num_results
if context:
kwargs["context"] = context
if promotions:
kwargs["promotions"] = promotions
if item_id:
kwargs["itemId"] = item_id
if metadata_columns:
kwargs["metadataColumns"] = metadata_columns
if self.campaign_arn:
kwargs["campaignArn"] = self.campaign_arn
if self.recommender_arn:
kwargs["recommenderArn"] = self.recommender_arn
return self.client.get_recommendations(**kwargs)
def get_personalized_ranking(
self,
user_id: str,
input_list: List[str],
filter_arn: Optional[str] = None,
filter_values: Optional[Mapping[str, str]] = None,
context: Optional[Mapping[str, str]] = None,
metadata_columns: Optional[Mapping[str, Sequence[str]]] = None,
**kwargs: Any,
) -> Mapping[str, Any]:
"""Re-ranks a list of recommended items for the given user.
https://docs.aws.amazon.com/personalize/latest/dg/API_RS_GetPersonalizedRanking.html
Args:
user_id: str, Required: The user identifier
for which to retrieve recommendations
input_list: List[str], Required: A list of items (by itemId) to rank
filter_arn: str, Optional: The ARN of the filter to apply
filter_values: Mapping, Optional: The values to use
when filtering recommendations.
context: Mapping, Optional: The contextual metadata
to use when getting recommendations
metadata_columns: Mapping, Optional: The metadata Columns to be returned
as part of the response.
Returns:
response: Mapping[str, Any]: Returns personalizedRanking
and recommendationId.
Example:
.. code-block:: python
personalize_client = AmazonPersonalize(campaignArn='<my-campaign-arn>' )\n
response = personalize_client.get_personalized_ranking(user_id="1",
input_list=["123,"256"])
"""
if filter_arn:
kwargs["filterArn"] = filter_arn
if filter_values:
kwargs["filterValues"] = filter_values
if user_id:
kwargs["userId"] = user_id
if input_list:
kwargs["inputList"] = input_list
if context:
kwargs["context"] = context
if metadata_columns:
kwargs["metadataColumns"] = metadata_columns
kwargs["campaignArn"] = self.campaign_arn
return self.client.get_personalized_ranking(kwargs)

@ -0,0 +1,192 @@
from __future__ import annotations
from typing import Any, Dict, List, Mapping, Optional, cast
from langchain.callbacks.manager import (
CallbackManagerForChainRun,
)
from langchain.chains import LLMChain
from langchain.chains.base import Chain
from langchain.prompts.prompt import PromptTemplate
from langchain.schema.language_model import BaseLanguageModel
from langchain_experimental.recommenders.amazon_personalize import AmazonPersonalize
SUMMARIZE_PROMPT_QUERY = """
Summarize the recommended items for a user from the items list in tag <result> below.
Make correlation into the items in the list and provide a summary.
<result>
{result}
</result>
"""
SUMMARIZE_PROMPT = PromptTemplate(
input_variables=["result"], template=SUMMARIZE_PROMPT_QUERY
)
INTERMEDIATE_STEPS_KEY = "intermediate_steps"
# Input Key Names to be used
USER_ID_INPUT_KEY = "user_id"
ITEM_ID_INPUT_KEY = "item_id"
INPUT_LIST_INPUT_KEY = "input_list"
FILTER_ARN_INPUT_KEY = "filter_arn"
FILTER_VALUES_INPUT_KEY = "filter_values"
CONTEXT_INPUT_KEY = "context"
PROMOTIONS_INPUT_KEY = "promotions"
METADATA_COLUMNS_INPUT_KEY = "metadata_columns"
RESULT_OUTPUT_KEY = "result"
class AmazonPersonalizeChain(Chain):
"""Amazon Personalize Chain for retrieving recommendations
from Amazon Personalize, and summarizing
the recommendations in natural language.
It will only return recommendations if return_direct=True.
Can also be used in sequential chains for working with
the output of Amazon Personalize.
Example:
.. code-block:: python
chain = PersonalizeChain.from_llm(llm=agent_llm, client=personalize_lg,
return_direct=True)\n
response = chain.run({'user_id':'1'})\n
response = chain.run({'user_id':'1', 'item_id':'234'})
"""
client: AmazonPersonalize
summarization_chain: LLMChain
return_direct: bool = False
return_intermediate_steps: bool = False
is_ranking_recipe: bool = False
@property
def input_keys(self) -> List[str]:
"""This returns an empty list since not there are optional
input_keys and none is required.
:meta private:
"""
return []
@property
def output_keys(self) -> List[str]:
"""Will always return result key.
:meta private:
"""
return [RESULT_OUTPUT_KEY]
@classmethod
def from_llm(
cls,
llm: BaseLanguageModel,
client: AmazonPersonalize,
prompt_template: PromptTemplate = SUMMARIZE_PROMPT,
is_ranking_recipe: bool = False,
**kwargs: Any,
) -> AmazonPersonalizeChain:
"""Initializes the Personalize Chain with LLMAgent, Personalize Client,
Prompts to be used
Args:
llm: BaseLanguageModel: The LLM to be used in the Chain
client: AmazonPersonalize: The client created to support
invoking AmazonPersonalize
prompt_template: PromptTemplate: The prompt template which can be
invoked with the output from Amazon Personalize
is_ranking_recipe: bool: default: False: specifies
if the trained recipe is USER_PERSONALIZED_RANKING
Example:
.. code-block:: python
chain = PersonalizeChain.from_llm(llm=agent_llm,
client=personalize_lg, return_direct=True)\n
response = chain.run({'user_id':'1'})\n
response = chain.run({'user_id':'1', 'item_id':'234'})
RANDOM_PROMPT_QUERY=" Summarize recommendations in {result}"
chain = PersonalizeChain.from_llm(llm=agent_llm,
client=personalize_lg, prompt_template=PROMPT_TEMPLATE)\n
"""
summarization_chain = LLMChain(llm=llm, prompt=prompt_template)
return cls(
summarization_chain=summarization_chain,
client=client,
is_ranking_recipe=is_ranking_recipe,
**kwargs,
)
def _call(
self,
inputs: Mapping[str, Any],
run_manager: Optional[CallbackManagerForChainRun] = None,
) -> Dict[str, Any]:
"""Retrieves recommendations by invoking Amazon Personalize,
and invokes an LLM using the default/overridden
prompt template with the output from Amazon Personalize
Args:
inputs: Mapping [str, Any] : Provide input identifiers in a map.
For example - {'user_id','1'} or
{'user_id':'1', 'item_id':'123'}. You can also pass the
filter_arn, filter_values as an
input.
"""
_run_manager = run_manager or CallbackManagerForChainRun.get_noop_manager()
callbacks = _run_manager.get_child()
user_id = inputs.get(USER_ID_INPUT_KEY)
item_id = inputs.get(ITEM_ID_INPUT_KEY)
input_list = inputs.get(INPUT_LIST_INPUT_KEY)
filter_arn = inputs.get(FILTER_ARN_INPUT_KEY)
filter_values = inputs.get(FILTER_VALUES_INPUT_KEY)
promotions = inputs.get(PROMOTIONS_INPUT_KEY)
context = inputs.get(CONTEXT_INPUT_KEY)
metadata_columns = inputs.get(METADATA_COLUMNS_INPUT_KEY)
intermediate_steps: List = []
intermediate_steps.append({"Calling Amazon Personalize"})
if self.is_ranking_recipe:
response = self.client.get_personalized_ranking(
user_id=str(user_id),
input_list=cast(List[str], input_list),
filter_arn=filter_arn,
filter_values=filter_values,
context=context,
metadata_columns=metadata_columns,
)
else:
response = self.client.get_recommendations(
user_id=user_id,
item_id=item_id,
filter_arn=filter_arn,
filter_values=filter_values,
context=context,
promotions=promotions,
metadata_columns=metadata_columns,
)
_run_manager.on_text("Call to Amazon Personalize complete \n")
if self.return_direct:
final_result = response
else:
result = self.summarization_chain(
{RESULT_OUTPUT_KEY: response}, callbacks=callbacks
)
final_result = result[self.summarization_chain.output_key]
intermediate_steps.append({"context": response})
chain_result: Dict[str, Any] = {RESULT_OUTPUT_KEY: final_result}
if self.return_intermediate_steps:
chain_result[INTERMEDIATE_STEPS_KEY] = intermediate_steps
return chain_result
@property
def _chain_type(self) -> str:
return "amazon_personalize_chain"
Loading…
Cancel
Save