Skip to content

Commit 00eff8c

Browse files
Zizhong Zhangbaskaryan
andauthored
feat: Add PromptGuard integration (langchain-ai#9481)
Add PromptGuard integration ------- There are two approaches to integrate PromptGuard with a LangChain application. 1. PromptGuardLLMWrapper 2. functions that can be used in LangChain expression. ----- - Dependencies `promptguard` python package, which is a runtime requirement if you'd try out the demo. - @baskaryan @hwchase17 Thanks for the ideas and suggestions along the development process. --------- Co-authored-by: Bagatur <[email protected]>
1 parent 6c308aa commit 00eff8c

File tree

5 files changed

+516
-0
lines changed

5 files changed

+516
-0
lines changed
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# PromptGuard\n",
8+
"\n",
9+
"[PromptGuard](https://promptguard.readthedocs.io/en/latest/) is a service that enables applications to leverage the power of language models without compromising user privacy. Designed for composability and ease of integration into existing applications and services, PromptGuard is consumable via a simple Python library as well as through LangChain. Perhaps more importantly, PromptGuard leverages the power of [confidential computing](https://en.wikipedia.org/wiki/Confidential_computing) to ensure that even the PromptGuard service itself cannot access the data it is protecting.\n",
10+
" \n",
11+
"\n",
12+
"This notebook goes over how to use LangChain to interact with `PromptGuard`."
13+
]
14+
},
15+
{
16+
"cell_type": "code",
17+
"execution_count": null,
18+
"metadata": {},
19+
"outputs": [],
20+
"source": [
21+
"# install the promptguard and langchain packages\n",
22+
"! pip install promptguard langchain"
23+
]
24+
},
25+
{
26+
"cell_type": "markdown",
27+
"metadata": {},
28+
"source": [
29+
"Accessing the PromptGuard API requires an API key, which you can get by creating an account on [the PromptGuard website](https://promptguard.opaque.co/). Once you have an account, you can find your API key on [the API Keys page](https://promptguard.opaque.co/api-keys)."
30+
]
31+
},
32+
{
33+
"cell_type": "code",
34+
"execution_count": null,
35+
"metadata": {},
36+
"outputs": [],
37+
"source": [
38+
"import os\n",
39+
"\n",
40+
"# Set API keys\n",
41+
"\n",
42+
"os.environ['PROMPT_GUARD_API_KEY'] = \"<PROMPT_GUARD_API_KEY>\"\n",
43+
"os.environ['OPENAI_API_KEY'] = \"<OPENAI_API_KEY>\""
44+
]
45+
},
46+
{
47+
"cell_type": "markdown",
48+
"metadata": {},
49+
"source": [
50+
"# Use PromptGuardLLMWrapper\n",
51+
"\n",
52+
"Applying promptguard to your application could be as simple as wrapping your LLM using the PromptGuardLLMWrapper class by replace `llm=OpenAI()` with `llm=PromptGuardLLMWrapper(OpenAI())`."
53+
]
54+
},
55+
{
56+
"cell_type": "code",
57+
"execution_count": null,
58+
"metadata": {},
59+
"outputs": [],
60+
"source": [
61+
"import langchain\n",
62+
"from langchain import LLMChain, PromptTemplate\n",
63+
"from langchain.callbacks.stdout import StdOutCallbackHandler\n",
64+
"from langchain.llms import OpenAI\n",
65+
"from langchain.memory import ConversationBufferWindowMemory\n",
66+
"\n",
67+
"from langchain.llms import PromptGuardLLMWrapper\n",
68+
"\n",
69+
"langchain.verbose = True\n",
70+
"langchain.debug = True\n",
71+
"\n",
72+
"prompt_template = \"\"\"\n",
73+
"As an AI assistant, you will answer questions according to given context.\n",
74+
"\n",
75+
"Sensitive personal information in the question is masked for privacy.\n",
76+
"For instance, if the original text says \"Giana is good,\" it will be changed\n",
77+
"to \"PERSON_998 is good.\" \n",
78+
"\n",
79+
"Here's how to handle these changes:\n",
80+
"* Consider these masked phrases just as placeholders, but still refer to\n",
81+
"them in a relevant way when answering.\n",
82+
"* It's possible that different masked terms might mean the same thing.\n",
83+
"Stick with the given term and don't modify it.\n",
84+
"* All masked terms follow the \"TYPE_ID\" pattern.\n",
85+
"* Please don't invent new masked terms. For instance, if you see \"PERSON_998,\"\n",
86+
"don't come up with \"PERSON_997\" or \"PERSON_999\" unless they're already in the question.\n",
87+
"\n",
88+
"Conversation History: ```{history}```\n",
89+
"Context : ```During our recent meeting on February 23, 2023, at 10:30 AM,\n",
90+
"John Doe provided me with his personal details. His email is [email protected]\n",
91+
"and his contact number is 650-456-7890. He lives in New York City, USA, and\n",
92+
"belongs to the American nationality with Christian beliefs and a leaning towards\n",
93+
"the Democratic party. He mentioned that he recently made a transaction using his\n",
94+
"credit card 4111 1111 1111 1111 and transferred bitcoins to the wallet address\n",
95+
"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa. While discussing his European travels, he noted\n",
96+
"down his IBAN as GB29 NWBK 6016 1331 9268 19. Additionally, he provided his website\n",
97+
"as https://johndoeportfolio.com. John also discussed some of his US-specific details.\n",
98+
"He said his bank account number is 1234567890123456 and his drivers license is Y12345678.\n",
99+
"His ITIN is 987-65-4321, and he recently renewed his passport, the number for which is\n",
100+
"123456789. He emphasized not to share his SSN, which is 123-45-6789. Furthermore, he\n",
101+
"mentioned that he accesses his work files remotely through the IP 192.168.1.1 and has\n",
102+
"a medical license number MED-123456. ```\n",
103+
"Question: ```{question}```\n",
104+
"\n",
105+
"\"\"\"\n",
106+
"\n",
107+
"chain = LLMChain(\n",
108+
" prompt=PromptTemplate.from_template(prompt_template),\n",
109+
" llm=PromptGuardLLMWrapper(llm=OpenAI()),\n",
110+
" memory=ConversationBufferWindowMemory(k=2),\n",
111+
" verbose=True,\n",
112+
")\n",
113+
"\n",
114+
"\n",
115+
"print(\n",
116+
" chain.run(\n",
117+
" {\"question\": \"\"\"Write a message to remind John to do password reset for his website to stay secure.\"\"\"},\n",
118+
" callbacks=[StdOutCallbackHandler()],\n",
119+
" )\n",
120+
")"
121+
]
122+
},
123+
{
124+
"cell_type": "markdown",
125+
"metadata": {},
126+
"source": [
127+
"From the output, you can see the following context from user input has sensitive data.\n",
128+
"\n",
129+
"``` \n",
130+
"# Context from user input\n",
131+
"\n",
132+
"During our recent meeting on February 23, 2023, at 10:30 AM, John Doe provided me with his personal details. His email is [email protected] and his contact number is 650-456-7890. He lives in New York City, USA, and belongs to the American nationality with Christian beliefs and a leaning towards the Democratic party. He mentioned that he recently made a transaction using his credit card 4111 1111 1111 1111 and transferred bitcoins to the wallet address 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa. While discussing his European travels, he noted down his IBAN as GB29 NWBK 6016 1331 9268 19. Additionally, he provided his website as https://johndoeportfolio.com. John also discussed some of his US-specific details. He said his bank account number is 1234567890123456 and his drivers license is Y12345678. His ITIN is 987-65-4321, and he recently renewed his passport, the number for which is 123456789. He emphasized not to share his SSN, which is 669-45-6789. Furthermore, he mentioned that he accesses his work files remotely through the IP 192.168.1.1 and has a medical license number MED-123456.\n",
133+
"```\n",
134+
"\n",
135+
"PromptGuard will automatically detect the sensitive data and replace it with a placeholder. \n",
136+
"\n",
137+
"```\n",
138+
"# Context after PromptGuard\n",
139+
"\n",
140+
"During our recent meeting on DATE_TIME_3, at DATE_TIME_2, PERSON_3 provided me with his personal details. His email is EMAIL_ADDRESS_1 and his contact number is PHONE_NUMBER_1. He lives in LOCATION_3, LOCATION_2, and belongs to the NRP_3 nationality with NRP_2 beliefs and a leaning towards the Democratic party. He mentioned that he recently made a transaction using his credit card CREDIT_CARD_1 and transferred bitcoins to the wallet address CRYPTO_1. While discussing his NRP_1 travels, he noted down his IBAN as IBAN_CODE_1. Additionally, he provided his website as URL_1. PERSON_2 also discussed some of his LOCATION_1-specific details. He said his bank account number is US_BANK_NUMBER_1 and his drivers license is US_DRIVER_LICENSE_2. His ITIN is US_ITIN_1, and he recently renewed his passport, the number for which is DATE_TIME_1. He emphasized not to share his SSN, which is US_SSN_1. Furthermore, he mentioned that he accesses his work files remotely through the IP IP_ADDRESS_1 and has a medical license number MED-US_DRIVER_LICENSE_1.\n",
141+
"```\n",
142+
"\n",
143+
"Placeholder is used in the LLM response.\n",
144+
"\n",
145+
"```\n",
146+
"# response returned by LLM\n",
147+
"\n",
148+
"Hey PERSON_1, just wanted to remind you to do a password reset for your website URL_1 through your email EMAIL_ADDRESS_1. It's important to stay secure online, so don't forget to do it!\n",
149+
"```\n",
150+
"\n",
151+
"Response is desanitized by replacing the placeholder with the original sensitive data.\n",
152+
"\n",
153+
"```\n",
154+
"# desanitized LLM response from PromptGuard\n",
155+
"\n",
156+
"Hey John, just wanted to remind you to do a password reset for your website https://johndoeportfolio.com through your email [email protected]. It's important to stay secure online, so don't forget to do it!\n",
157+
"```"
158+
]
159+
},
160+
{
161+
"cell_type": "markdown",
162+
"metadata": {},
163+
"source": [
164+
"# Use PromptGuard in LangChain expression\n",
165+
"\n",
166+
"There are functions that can be used with LangChain expression as well if a drop-in replacement doesn't offer the flexibility you need. "
167+
]
168+
},
169+
{
170+
"cell_type": "code",
171+
"execution_count": null,
172+
"metadata": {},
173+
"outputs": [],
174+
"source": [
175+
"import langchain.utilities.promptguard as pgf\n",
176+
"from langchain.schema.runnable import RunnableMap\n",
177+
"from langchain.schema.output_parser import StrOutputParser\n",
178+
"\n",
179+
"\n",
180+
"prompt=PromptTemplate.from_template(prompt_template), \n",
181+
"llm = OpenAI()\n",
182+
"pg_chain = (\n",
183+
" pgf.sanitize\n",
184+
" | RunnableMap(\n",
185+
" {\n",
186+
" \"response\": (lambda x: x[\"sanitized_input\"])\n",
187+
" | prompt\n",
188+
" | llm\n",
189+
" | StrOutputParser(),\n",
190+
" \"secure_context\": lambda x: x[\"secure_context\"],\n",
191+
" }\n",
192+
" )\n",
193+
" | (lambda x: pgf.desanitize(x[\"response\"], x[\"secure_context\"]))\n",
194+
")\n",
195+
"\n",
196+
"pg_chain.invoke({\"question\": \"Write a text message to remind John to do password reset for his website through his email to stay secure.\", \"history\": \"\"})"
197+
]
198+
}
199+
],
200+
"metadata": {
201+
"kernelspec": {
202+
"display_name": "langchain",
203+
"language": "python",
204+
"name": "python3"
205+
},
206+
"language_info": {
207+
"name": "python",
208+
"version": "3.10.10"
209+
},
210+
"orig_nbformat": 4
211+
},
212+
"nbformat": 4,
213+
"nbformat_minor": 2
214+
}

libs/langchain/langchain/llms/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
from langchain.llms.pipelineai import PipelineAI
7070
from langchain.llms.predibase import Predibase
7171
from langchain.llms.predictionguard import PredictionGuard
72+
from langchain.llms.promptguard import PromptGuard
7273
from langchain.llms.promptlayer_openai import PromptLayerOpenAI, PromptLayerOpenAIChat
7374
from langchain.llms.replicate import Replicate
7475
from langchain.llms.rwkv import RWKV
@@ -141,6 +142,7 @@
141142
"PredictionGuard",
142143
"PromptLayerOpenAI",
143144
"PromptLayerOpenAIChat",
145+
"PromptGuard",
144146
"RWKV",
145147
"Replicate",
146148
"SagemakerEndpoint",
@@ -205,6 +207,7 @@
205207
"petals": Petals,
206208
"pipelineai": PipelineAI,
207209
"predibase": Predibase,
210+
"promptguard": PromptGuard,
208211
"replicate": Replicate,
209212
"rwkv": RWKV,
210213
"sagemaker_endpoint": SagemakerEndpoint,
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import logging
2+
from typing import Any, Dict, List, Optional
3+
4+
from langchain.callbacks.manager import CallbackManagerForLLMRun
5+
from langchain.llms.base import LLM
6+
from langchain.pydantic_v1 import Extra, root_validator
7+
from langchain.schema.language_model import BaseLanguageModel
8+
from langchain.utils import get_from_dict_or_env
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
class PromptGuard(LLM):
14+
"""An LLM wrapper that uses PromptGuard to sanitize prompts.
15+
16+
Wraps another LLM and sanitizes prompts before passing it to the LLM, then
17+
de-sanitizes the response.
18+
19+
To use, you should have the ``promptguard`` python package installed,
20+
and the environment variable ``PROMPTGUARD_API_KEY`` set with
21+
your API key, or pass it as a named parameter to the constructor.
22+
23+
Example:
24+
.. code-block:: python
25+
26+
from langchain.llms import PromptGuardLLM
27+
from langchain.chat_models import ChatOpenAI
28+
29+
prompt_guard_llm = PromptGuardLLM(base_llm=ChatOpenAI())
30+
"""
31+
32+
base_llm: BaseLanguageModel
33+
"""The base LLM to use."""
34+
35+
class Config:
36+
"""Configuration for this pydantic object."""
37+
38+
extra = Extra.forbid
39+
40+
@root_validator()
41+
def validate_environment(cls, values: Dict) -> Dict:
42+
"""Validates that the PromptGuard API key and the Python package exist."""
43+
try:
44+
import promptguard as pg
45+
except ImportError:
46+
raise ImportError(
47+
"Could not import the `promptguard` Python package, "
48+
"please install it with `pip install promptguard`."
49+
)
50+
if pg.__package__ is None:
51+
raise ValueError(
52+
"Could not properly import `promptguard`, "
53+
"promptguard.__package__ is None."
54+
)
55+
56+
api_key = get_from_dict_or_env(
57+
values, "promptguard_api_key", "PROMPTGUARD_API_KEY", default=""
58+
)
59+
if not api_key:
60+
raise ValueError(
61+
"Could not find PROMPTGUARD_API_KEY in the environment. "
62+
"Please set it to your PromptGuard API key."
63+
"You can get it by creating an account on the PromptGuard website: "
64+
"https://promptguard.opaque.co/ ."
65+
)
66+
return values
67+
68+
def _call(
69+
self,
70+
prompt: str,
71+
stop: Optional[List[str]] = None,
72+
run_manager: Optional[CallbackManagerForLLMRun] = None,
73+
**kwargs: Any,
74+
) -> str:
75+
"""Call base LLM with sanitization before and de-sanitization after.
76+
77+
Args:
78+
prompt: The prompt to pass into the model.
79+
80+
Returns:
81+
The string generated by the model.
82+
83+
Example:
84+
.. code-block:: python
85+
86+
response = prompt_guard_llm("Tell me a joke.")
87+
"""
88+
import promptguard as pg
89+
90+
_run_manager = run_manager or CallbackManagerForLLMRun.get_noop_manager()
91+
92+
# sanitize the prompt by replacing the sensitive information with a placeholder
93+
sanitize_response: pg.SanitizeResponse = pg.sanitize(prompt)
94+
sanitized_prompt_value_str = sanitize_response.sanitized_text
95+
96+
# TODO: Add in callbacks once child runs for LLMs are supported by LangSmith.
97+
# call the LLM with the sanitized prompt and get the response
98+
llm_response = self.base_llm.predict(
99+
sanitized_prompt_value_str,
100+
stop=stop,
101+
)
102+
103+
# desanitize the response by restoring the original sensitive information
104+
desanitize_response: pg.DesanitizeResponse = pg.desanitize(
105+
llm_response,
106+
secure_context=sanitize_response.secure_context,
107+
)
108+
return desanitize_response.desanitized_text
109+
110+
@property
111+
def _llm_type(self) -> str:
112+
"""Return type of LLM.
113+
114+
This is an override of the base class method.
115+
"""
116+
return "promptguard"

0 commit comments

Comments
 (0)