diff --git a/cookbook/amazon_personalize_how_to.ipynb b/cookbook/amazon_personalize_how_to.ipynb new file mode 100644 index 0000000000..7555e39d89 --- /dev/null +++ b/cookbook/amazon_personalize_how_to.ipynb @@ -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 = \"\"\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 tag. \n", + " All movies in the tag must be recommended. Give a summary of the movies and why the human should watch them. \n", + " Put the email between tags.\n", + "\n", + " \n", + " {result} \n", + " \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 tag. \n", + " All movies in the tag must be recommended. Give a summary of the movies and why the human should watch them. \n", + " Put the email between tags.\n", + "\n", + " \n", + " {result}\n", + " \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 = \"\"\n", + "metadata_column_names = [\n", + " \"\",\n", + " \"\",\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 +} diff --git a/libs/experimental/langchain_experimental/recommenders/__init__.py b/libs/experimental/langchain_experimental/recommenders/__init__.py new file mode 100644 index 0000000000..ec06f55418 --- /dev/null +++ b/libs/experimental/langchain_experimental/recommenders/__init__.py @@ -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"] diff --git a/libs/experimental/langchain_experimental/recommenders/amazon_personalize.py b/libs/experimental/langchain_experimental/recommenders/amazon_personalize.py new file mode 100644 index 0000000000..b2300f0a19 --- /dev/null +++ b/libs/experimental/langchain_experimental/recommenders/amazon_personalize.py @@ -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='' ) + """ + + 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='' )\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='' )\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) diff --git a/libs/experimental/langchain_experimental/recommenders/amazon_personalize_chain.py b/libs/experimental/langchain_experimental/recommenders/amazon_personalize_chain.py new file mode 100644 index 0000000000..4c187a8006 --- /dev/null +++ b/libs/experimental/langchain_experimental/recommenders/amazon_personalize_chain.py @@ -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 below. +Make correlation into the items in the list and provide a summary. + + {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"