auth0-ai-langchain 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- auth0_ai_langchain/FGARetriever.py +158 -0
- auth0_ai_langchain/__init__.py +3 -0
- auth0_ai_langchain/async_authorization/__init__.py +3 -0
- auth0_ai_langchain/async_authorization/async_authorizer.py +17 -0
- auth0_ai_langchain/async_authorization/graph_resumer.py +154 -0
- auth0_ai_langchain/auth0_ai.py +114 -0
- auth0_ai_langchain/fga/__init__.py +4 -0
- auth0_ai_langchain/token_vault/__init__.py +10 -0
- auth0_ai_langchain/token_vault/token_vault_authorizer.py +38 -0
- auth0_ai_langchain/utils/interrupt.py +30 -0
- auth0_ai_langchain/utils/tool_wrapper.py +34 -0
- auth0_ai_langchain-1.0.1.dist-info/LICENSE +176 -0
- auth0_ai_langchain-1.0.1.dist-info/METADATA +371 -0
- auth0_ai_langchain-1.0.1.dist-info/RECORD +15 -0
- auth0_ai_langchain-1.0.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from typing import Callable, Optional
|
|
4
|
+
from langchain_core.retrievers import BaseRetriever
|
|
5
|
+
from langchain_core.documents import Document
|
|
6
|
+
from openfga_sdk.client.client import ClientBatchCheckRequest
|
|
7
|
+
from pydantic import PrivateAttr
|
|
8
|
+
from openfga_sdk import ClientConfiguration, OpenFgaClient
|
|
9
|
+
from openfga_sdk.client.models import ClientBatchCheckItem
|
|
10
|
+
from openfga_sdk.sync import OpenFgaClient as OpenFgaClientSync
|
|
11
|
+
from openfga_sdk.credentials import CredentialConfiguration, Credentials
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FGARetriever(BaseRetriever):
|
|
15
|
+
"""
|
|
16
|
+
FGARetriever integrates with OpenFGA to filter documents based on fine-grained authorization (FGA).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
_retriever: BaseRetriever = PrivateAttr()
|
|
20
|
+
_fga_configuration: ClientConfiguration = PrivateAttr()
|
|
21
|
+
_query_builder: Callable[[Document], ClientBatchCheckItem] = PrivateAttr()
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
retriever: BaseRetriever,
|
|
26
|
+
build_query: Callable[[Document], ClientBatchCheckItem],
|
|
27
|
+
fga_configuration: Optional[ClientConfiguration] = None,
|
|
28
|
+
):
|
|
29
|
+
"""
|
|
30
|
+
Initialize the FGARetriever with the specified retriever, query builder, and configuration.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
retriever (BaseRetriever): The retriever used to fetch documents.
|
|
34
|
+
build_query (Callable[[Document], ClientBatchCheckItem]): Function to convert documents into FGA queries.
|
|
35
|
+
fga_configuration (ClientConfiguration, optional): Configuration for the OpenFGA client. If not provided, defaults to environment variables.
|
|
36
|
+
"""
|
|
37
|
+
super().__init__()
|
|
38
|
+
self._retriever = retriever
|
|
39
|
+
self._fga_configuration = fga_configuration or ClientConfiguration(
|
|
40
|
+
api_url=os.getenv("FGA_API_URL") or "https://api.us1.fga.dev",
|
|
41
|
+
store_id=os.getenv("FGA_STORE_ID"),
|
|
42
|
+
credentials=Credentials(
|
|
43
|
+
method="client_credentials",
|
|
44
|
+
configuration=CredentialConfiguration(
|
|
45
|
+
api_issuer=os.getenv("FGA_API_TOKEN_ISSUER") or "auth.fga.dev",
|
|
46
|
+
api_audience=os.getenv("FGA_API_AUDIENCE")
|
|
47
|
+
or "https://api.us1.fga.dev/",
|
|
48
|
+
client_id=os.getenv("FGA_CLIENT_ID"),
|
|
49
|
+
client_secret=os.getenv("FGA_CLIENT_SECRET"),
|
|
50
|
+
),
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
self._query_builder = build_query
|
|
54
|
+
|
|
55
|
+
async def _async_filter_FGA(self, docs: list[Document]) -> list[Document]:
|
|
56
|
+
"""
|
|
57
|
+
Asynchronously filter documents using OpenFGA.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
docs (List[Document]): List of documents to filter.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
List[Document]: Filtered list of documents authorized by FGA.
|
|
64
|
+
"""
|
|
65
|
+
async with OpenFgaClient(self._fga_configuration) as fga_client:
|
|
66
|
+
all_checks = [self._query_builder(doc) for doc in docs]
|
|
67
|
+
unique_checks = list(
|
|
68
|
+
{
|
|
69
|
+
(check.relation, check.object, check.user): check
|
|
70
|
+
for check in all_checks
|
|
71
|
+
}.values()
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
doc_to_obj = {doc: check.object for check, doc in zip(all_checks, docs)}
|
|
75
|
+
|
|
76
|
+
fga_response = await fga_client.batch_check(
|
|
77
|
+
ClientBatchCheckRequest(checks=unique_checks)
|
|
78
|
+
)
|
|
79
|
+
await fga_client.close()
|
|
80
|
+
|
|
81
|
+
permissions_map = {
|
|
82
|
+
result.request.object: result.allowed for result in fga_response.result
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return [
|
|
86
|
+
doc
|
|
87
|
+
for doc in docs
|
|
88
|
+
if doc_to_obj[doc] in permissions_map
|
|
89
|
+
and permissions_map[doc_to_obj[doc]]
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
async def _aget_relevant_documents(self, query, *, run_manager) -> list[Document]:
|
|
93
|
+
"""
|
|
94
|
+
Asynchronously retrieve relevant documents from the base retrieve and filter them using FGA.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
query (str): The query for retrieving documents.
|
|
98
|
+
run_manager (object, optional): Optional manager for tracking runs.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
List[Document]: Filtered and relevant documents.
|
|
102
|
+
"""
|
|
103
|
+
docs = await self._retriever._aget_relevant_documents(
|
|
104
|
+
query, run_manager=run_manager
|
|
105
|
+
)
|
|
106
|
+
docs = await self._async_filter_FGA(docs)
|
|
107
|
+
return docs
|
|
108
|
+
|
|
109
|
+
def _filter_FGA(self, docs: list[Document]) -> list[Document]:
|
|
110
|
+
"""
|
|
111
|
+
Synchronously filter documents using OpenFGA.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
docs (List[Document]): List of documents to filter.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
List[Document]: Filtered list of documents authorized by FGA.
|
|
118
|
+
"""
|
|
119
|
+
with OpenFgaClientSync(self._fga_configuration) as fga_client:
|
|
120
|
+
all_checks = [self._query_builder(doc) for doc in docs]
|
|
121
|
+
unique_checks = list(
|
|
122
|
+
{
|
|
123
|
+
(check.relation, check.object, check.user): check
|
|
124
|
+
for check in all_checks
|
|
125
|
+
}.values()
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
doc_to_obj = {doc.id: check.object for check, doc in zip(all_checks, docs)}
|
|
129
|
+
|
|
130
|
+
fga_response = fga_client.batch_check(
|
|
131
|
+
ClientBatchCheckRequest(checks=unique_checks)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
permissions_map = {
|
|
135
|
+
result.request.object: result.allowed for result in fga_response.result
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return [
|
|
139
|
+
doc
|
|
140
|
+
for doc in docs
|
|
141
|
+
if doc_to_obj[doc.id] in permissions_map
|
|
142
|
+
and permissions_map[doc_to_obj[doc.id]]
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
def _get_relevant_documents(self, query, *, run_manager) -> list[Document]:
|
|
146
|
+
"""
|
|
147
|
+
Retrieve relevant documents and filter them using FGA.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
query (str): The query for retrieving documents.
|
|
151
|
+
run_manager (object, optional): Optional manager for tracking runs.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
List[Document]: Filtered and relevant documents.
|
|
155
|
+
"""
|
|
156
|
+
docs = self._retriever._get_relevant_documents(query, run_manager=run_manager)
|
|
157
|
+
docs = self._filter_FGA(docs)
|
|
158
|
+
return docs
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
from auth0_ai.authorizers.async_authorization.async_authorizer_base import get_async_authorization_credentials as get_async_authorization_credentials
|
|
2
|
+
from auth0_ai_langchain.async_authorization.async_authorizer import AsyncAuthorizer as AsyncAuthorizer
|
|
3
|
+
from auth0_ai_langchain.async_authorization.graph_resumer import GraphResumer as GraphResumer
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from typing import Union
|
|
3
|
+
from auth0_ai.authorizers.async_authorization import AsyncAuthorizerBase
|
|
4
|
+
from auth0_ai.interrupts.async_authorization_interrupts import AuthorizationPendingInterrupt, AuthorizationPollingInterrupt
|
|
5
|
+
from auth0_ai_langchain.utils.interrupt import to_graph_interrupt
|
|
6
|
+
from auth0_ai_langchain.utils.tool_wrapper import tool_wrapper
|
|
7
|
+
from langchain_core.tools import BaseTool
|
|
8
|
+
|
|
9
|
+
class AsyncAuthorizer(AsyncAuthorizerBase, ABC):
|
|
10
|
+
def _handle_authorization_interrupts(self, err: Union[AuthorizationPendingInterrupt, AuthorizationPollingInterrupt]) -> None:
|
|
11
|
+
raise to_graph_interrupt(err)
|
|
12
|
+
|
|
13
|
+
def authorizer(self):
|
|
14
|
+
def wrap_tool(tool: BaseTool) -> BaseTool:
|
|
15
|
+
return tool_wrapper(tool, self.protect)
|
|
16
|
+
|
|
17
|
+
return wrap_tool
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from threading import Event
|
|
3
|
+
from typing import Callable, Optional, Dict, Any, List, TypedDict
|
|
4
|
+
from auth0_ai.authorizers.async_authorization import AsyncAuthorizationRequest
|
|
5
|
+
from auth0_ai.interrupts.async_authorization_interrupts import AsyncAuthorizationInterrupt, AuthorizationPendingInterrupt, AuthorizationPollingInterrupt
|
|
6
|
+
from auth0_ai_langchain.utils.interrupt import get_auth0_interrupts
|
|
7
|
+
from langgraph_sdk.client import LangGraphClient
|
|
8
|
+
from langgraph_sdk.schema import Thread, Interrupt
|
|
9
|
+
|
|
10
|
+
class WatchedThread(TypedDict):
|
|
11
|
+
thread_id: str
|
|
12
|
+
assistant_id: str
|
|
13
|
+
interruption_id: str
|
|
14
|
+
auth_request: AsyncAuthorizationRequest
|
|
15
|
+
config: Dict[str, Any]
|
|
16
|
+
last_run: float
|
|
17
|
+
|
|
18
|
+
class GraphResumerFilters(TypedDict):
|
|
19
|
+
graph_id: str
|
|
20
|
+
|
|
21
|
+
class GraphResumer:
|
|
22
|
+
def __init__(self, lang_graph: LangGraphClient, filters: Optional[GraphResumerFilters] = None):
|
|
23
|
+
self.lang_graph = lang_graph
|
|
24
|
+
self.filters = filters or {}
|
|
25
|
+
self.map: Dict[str, WatchedThread] = {}
|
|
26
|
+
self._stop_event = Event()
|
|
27
|
+
self._loop_task: Optional[asyncio.Task] = None
|
|
28
|
+
|
|
29
|
+
# Event callbacks
|
|
30
|
+
self._resume_callbacks: List[Callable[[WatchedThread], None]] = []
|
|
31
|
+
self._error_callbacks: List[Callable[[Exception], None]] = []
|
|
32
|
+
|
|
33
|
+
# Public API to register event callbacks
|
|
34
|
+
def on_resume(self, callback: Callable[[WatchedThread], None]) -> "GraphResumer":
|
|
35
|
+
self._resume_callbacks.append(callback)
|
|
36
|
+
return self
|
|
37
|
+
|
|
38
|
+
def on_error(self, callback: Callable[[Exception], None]) -> "GraphResumer":
|
|
39
|
+
self._error_callbacks.append(callback)
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
def _emit_resume(self, thread: WatchedThread) -> None:
|
|
43
|
+
for callback in self._resume_callbacks:
|
|
44
|
+
callback(thread)
|
|
45
|
+
|
|
46
|
+
def _emit_error(self, error: Exception) -> None:
|
|
47
|
+
for callback in self._error_callbacks:
|
|
48
|
+
callback(error)
|
|
49
|
+
|
|
50
|
+
async def _get_all_interrupted_threads(self) -> List[Thread]:
|
|
51
|
+
interrupted_threads: List[Thread] = []
|
|
52
|
+
offset = 0
|
|
53
|
+
|
|
54
|
+
while True:
|
|
55
|
+
page = await self.lang_graph.threads.search(
|
|
56
|
+
status="interrupted",
|
|
57
|
+
limit=100,
|
|
58
|
+
offset=offset,
|
|
59
|
+
metadata={"graph_id": self.filters["graph_id"]} if "graph_id" in self.filters else None
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if not page:
|
|
63
|
+
break
|
|
64
|
+
|
|
65
|
+
for t in page:
|
|
66
|
+
interrupt = self._get_first_interrupt(t)
|
|
67
|
+
if interrupt and AsyncAuthorizationInterrupt.is_interrupt(interrupt["value"]) and AsyncAuthorizationInterrupt.has_request_data(interrupt["value"]):
|
|
68
|
+
interrupted_threads.append(t)
|
|
69
|
+
|
|
70
|
+
offset += len(page)
|
|
71
|
+
if len(page) < 100:
|
|
72
|
+
break
|
|
73
|
+
|
|
74
|
+
return interrupted_threads
|
|
75
|
+
|
|
76
|
+
def _get_first_interrupt(self, thread: Thread) -> Optional[Interrupt]:
|
|
77
|
+
interrupts = thread["interrupts"]
|
|
78
|
+
if interrupts:
|
|
79
|
+
values = list(interrupts.values())
|
|
80
|
+
if values and values[0]:
|
|
81
|
+
return values[0][0]
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
def _get_hash_map_id(self, thread: Thread) -> str:
|
|
85
|
+
return f"{thread['thread_id']}:{next(iter(thread['interrupts']))}"
|
|
86
|
+
|
|
87
|
+
async def _resume_thread(self, t: WatchedThread):
|
|
88
|
+
self._emit_resume(t)
|
|
89
|
+
|
|
90
|
+
await self.lang_graph.runs.wait(t["thread_id"], t["assistant_id"], config=t["config"])
|
|
91
|
+
|
|
92
|
+
t["last_run"] = asyncio.get_event_loop().time() * 1000
|
|
93
|
+
|
|
94
|
+
async def loop(self):
|
|
95
|
+
all_threads = await self._get_all_interrupted_threads()
|
|
96
|
+
|
|
97
|
+
# Remove old interrupted threads
|
|
98
|
+
active_keys = {self._get_hash_map_id(t) for t in all_threads}
|
|
99
|
+
|
|
100
|
+
for key in list(self.map.keys()):
|
|
101
|
+
if key not in active_keys:
|
|
102
|
+
del self.map[key]
|
|
103
|
+
|
|
104
|
+
# Add new interrupted threads
|
|
105
|
+
for thread in all_threads:
|
|
106
|
+
interrupt = next(
|
|
107
|
+
(i for i in get_auth0_interrupts(thread)
|
|
108
|
+
if AuthorizationPendingInterrupt.is_interrupt(i["value"])
|
|
109
|
+
or AuthorizationPollingInterrupt.is_interrupt(i["value"])),
|
|
110
|
+
None
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if not interrupt or not interrupt["value"].get("_request"):
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
key = self._get_hash_map_id(thread)
|
|
117
|
+
if key not in self.map:
|
|
118
|
+
self.map[key] = {
|
|
119
|
+
"thread_id": thread["thread_id"],
|
|
120
|
+
"assistant_id": thread["metadata"].get("graph_id"),
|
|
121
|
+
"config": getattr(thread, "config", {}),
|
|
122
|
+
"interruption_id": next(iter(thread["interrupts"])),
|
|
123
|
+
"auth_request": interrupt["value"]["_request"],
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
threads_to_resume = [
|
|
127
|
+
t for t in self.map.values()
|
|
128
|
+
if "last_run" not in t or (t["last_run"] + t["auth_request"]["interval"] * 1000 < asyncio.get_event_loop().time() * 1000)
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
await asyncio.gather(*[
|
|
132
|
+
self._resume_thread(t) for t in threads_to_resume
|
|
133
|
+
])
|
|
134
|
+
|
|
135
|
+
def start(self):
|
|
136
|
+
if self._loop_task and not self._loop_task.done():
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
self._stop_event.clear()
|
|
140
|
+
|
|
141
|
+
async def _run_loop():
|
|
142
|
+
while not self._stop_event.is_set():
|
|
143
|
+
try:
|
|
144
|
+
await self.loop()
|
|
145
|
+
except Exception as e:
|
|
146
|
+
self._emit_error(e)
|
|
147
|
+
await asyncio.sleep(5)
|
|
148
|
+
|
|
149
|
+
self._loop_task = asyncio.create_task(_run_loop())
|
|
150
|
+
|
|
151
|
+
def stop(self):
|
|
152
|
+
self._stop_event.set()
|
|
153
|
+
if self._loop_task:
|
|
154
|
+
self._loop_task.cancel()
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from typing import Callable, Optional
|
|
2
|
+
from langchain_core.tools import BaseTool
|
|
3
|
+
from auth0_ai.authorizers.async_authorization import AsyncAuthorizerParams
|
|
4
|
+
from auth0_ai.authorizers.token_vault_authorizer import TokenVaultAuthorizerParams
|
|
5
|
+
from auth0_ai.authorizers.types import Auth0ClientParams
|
|
6
|
+
from auth0_ai_langchain.async_authorization.async_authorizer import AsyncAuthorizer
|
|
7
|
+
from auth0_ai_langchain.token_vault.token_vault_authorizer import TokenVaultAuthorizer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Auth0AI:
|
|
11
|
+
"""Provides decorators to secure LangChain tools using Auth0 authorization flows.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, auth0: Optional[Auth0ClientParams] = None):
|
|
15
|
+
"""Initializes the Auth0AI instance.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
auth0 (Optional[Auth0ClientParams]): Parameters for the Auth0 client.
|
|
19
|
+
If not provided, values will be automatically read from environment
|
|
20
|
+
variables: `AUTH0_DOMAIN`, `AUTH0_CLIENT_ID`, and `AUTH0_CLIENT_SECRET`.
|
|
21
|
+
"""
|
|
22
|
+
self.auth0 = auth0
|
|
23
|
+
|
|
24
|
+
def with_async_authorization(self, **params: AsyncAuthorizerParams) -> Callable[[BaseTool], BaseTool]:
|
|
25
|
+
"""Protects a tool with the CIBA (Client-Initiated Backchannel Authentication) flow.
|
|
26
|
+
|
|
27
|
+
Requires user confirmation via a second device (e.g., phone)
|
|
28
|
+
before allowing the tool to execute.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
**params: Parameters defined in `AsyncAuthorizerParams`.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Callable[[BaseTool], BaseTool]: A decorator to wrap a LangChain tool.
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
```python
|
|
38
|
+
import os
|
|
39
|
+
from auth0_ai_langchain.auth0_ai import Auth0AI
|
|
40
|
+
from auth0_ai_langchain.async_authorization import get_async_authorization_credentials
|
|
41
|
+
from langchain_core.runnables import ensure_config
|
|
42
|
+
from langchain_core.tools import StructuredTool
|
|
43
|
+
|
|
44
|
+
auth0_ai = Auth0AI()
|
|
45
|
+
|
|
46
|
+
with_async_authorization = auth0_ai.with_async_authorization(
|
|
47
|
+
scopes=["stock:trade"],
|
|
48
|
+
audience=os.getenv("AUDIENCE"),
|
|
49
|
+
requested_expiry=os.getenv("REQUESTED_EXPIRY"),
|
|
50
|
+
binding_message=lambda ticker, qty: f"Authorize the purchase of {qty} {ticker}",
|
|
51
|
+
user_id=lambda *_, **__: ensure_config().get("configurable", {}).get("user_id")
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def tool_function(ticker: str, qty: int) -> str:
|
|
55
|
+
credentials = get_async_authorization_credentials()
|
|
56
|
+
headers = {
|
|
57
|
+
"Authorization": f"{credentials['token_type']} {credentials['access_token']}",
|
|
58
|
+
# ...
|
|
59
|
+
}
|
|
60
|
+
# Call API
|
|
61
|
+
|
|
62
|
+
trade_tool = with_async_authorization(
|
|
63
|
+
StructuredTool(
|
|
64
|
+
name="trade_tool",
|
|
65
|
+
description="Use this function to trade a stock",
|
|
66
|
+
func=tool_function,
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
"""
|
|
71
|
+
authorizer = AsyncAuthorizer(AsyncAuthorizerParams(**params), self.auth0)
|
|
72
|
+
return authorizer.authorizer()
|
|
73
|
+
|
|
74
|
+
def with_token_vault(self, **params: TokenVaultAuthorizerParams) -> Callable[[BaseTool], BaseTool]:
|
|
75
|
+
"""Enables a tool to obtain an access token from a Token Vault identity provider (e.g., Google, Azure AD).
|
|
76
|
+
|
|
77
|
+
The token can then be used within the tool to call third-party APIs on behalf of the user.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
**params: Parameters defined in `TokenVaultAuthorizerParams`.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Callable[[BaseTool], BaseTool]: A decorator to wrap a LangChain tool.
|
|
84
|
+
|
|
85
|
+
Example:
|
|
86
|
+
```python
|
|
87
|
+
from auth0_ai_langchain.auth0_ai import Auth0AI
|
|
88
|
+
from auth0_ai_langchain.token_vault import get_credentials_from_token_vault
|
|
89
|
+
from langchain_core.tools import StructuredTool
|
|
90
|
+
from datetime import datetime
|
|
91
|
+
|
|
92
|
+
auth0_ai = Auth0AI()
|
|
93
|
+
|
|
94
|
+
with_google_calendar_access = auth0_ai.with_token_vault(
|
|
95
|
+
connection="google-oauth2",
|
|
96
|
+
scopes=["openid", "https://www.googleapis.com/auth/calendar.freebusy"]
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def tool_function(date: datetime):
|
|
100
|
+
credentials = get_credentials_from_token_vault()
|
|
101
|
+
# Call Google API using credentials["access_token"]
|
|
102
|
+
|
|
103
|
+
check_calendar_tool = with_google_calendar_access(
|
|
104
|
+
StructuredTool(
|
|
105
|
+
name="check_user_calendar",
|
|
106
|
+
description="Use this function to check if the user is available on a certain date and time",
|
|
107
|
+
func=tool_function,
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
"""
|
|
112
|
+
authorizer = TokenVaultAuthorizer(
|
|
113
|
+
TokenVaultAuthorizerParams(**params), self.auth0)
|
|
114
|
+
return authorizer.authorizer()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from auth0_ai.interrupts.token_vault_interrupt import (
|
|
2
|
+
TokenVaultError as TokenVaultError,
|
|
3
|
+
TokenVaultInterrupt as TokenVaultInterrupt
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
from auth0_ai.authorizers.token_vault_authorizer import (
|
|
7
|
+
get_credentials_from_token_vault as get_credentials_from_token_vault,
|
|
8
|
+
get_access_token_from_token_vault as get_access_token_from_token_vault
|
|
9
|
+
)
|
|
10
|
+
from .token_vault_authorizer import TokenVaultAuthorizer as TokenVaultAuthorizer
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
from abc import ABC
|
|
3
|
+
from auth0_ai.authorizers.token_vault_authorizer import TokenVaultAuthorizerBase, \
|
|
4
|
+
TokenVaultAuthorizerParams
|
|
5
|
+
from auth0_ai.authorizers.types import Auth0ClientParams
|
|
6
|
+
from auth0_ai.interrupts.token_vault_interrupt import TokenVaultInterrupt
|
|
7
|
+
from auth0_ai_langchain.utils.interrupt import to_graph_interrupt
|
|
8
|
+
from auth0_ai_langchain.utils.tool_wrapper import tool_wrapper
|
|
9
|
+
from langchain_core.tools import BaseTool
|
|
10
|
+
from langchain_core.runnables import ensure_config
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def default_get_refresh_token(*_, **__) -> str | None:
|
|
14
|
+
return ensure_config().get("configurable", {}).get("_credentials", {}).get("refresh_token")
|
|
15
|
+
|
|
16
|
+
class TokenVaultAuthorizer(TokenVaultAuthorizerBase, ABC):
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
params: TokenVaultAuthorizerParams,
|
|
20
|
+
auth0: Auth0ClientParams = None,
|
|
21
|
+
):
|
|
22
|
+
missing_refresh = params.refresh_token.value is None
|
|
23
|
+
missing_access_token = params.access_token.value is None
|
|
24
|
+
|
|
25
|
+
if missing_refresh and missing_access_token and callable(default_get_refresh_token):
|
|
26
|
+
params = copy.copy(params)
|
|
27
|
+
params.refresh_token.value = default_get_refresh_token
|
|
28
|
+
|
|
29
|
+
super().__init__(params, auth0)
|
|
30
|
+
|
|
31
|
+
def _handle_authorization_interrupts(self, err: TokenVaultInterrupt) -> None:
|
|
32
|
+
raise to_graph_interrupt(err)
|
|
33
|
+
|
|
34
|
+
def authorizer(self):
|
|
35
|
+
def wrap_tool(tool: BaseTool) -> BaseTool:
|
|
36
|
+
return tool_wrapper(tool, self.protect)
|
|
37
|
+
|
|
38
|
+
return wrap_tool
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
from auth0_ai.interrupts.auth0_interrupt import Auth0Interrupt
|
|
3
|
+
from langgraph.errors import GraphInterrupt
|
|
4
|
+
from langgraph.types import Interrupt
|
|
5
|
+
from langgraph_sdk.schema import Thread
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def to_graph_interrupt(interrupt: Auth0Interrupt) -> GraphInterrupt:
|
|
9
|
+
return GraphInterrupt([
|
|
10
|
+
Interrupt(
|
|
11
|
+
value=interrupt.to_json(),
|
|
12
|
+
when="during",
|
|
13
|
+
resumable=True,
|
|
14
|
+
ns=[f"auth0AI:{interrupt.name}:{interrupt.code}"]
|
|
15
|
+
)
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_auth0_interrupts(thread: Thread) -> List[Interrupt]:
|
|
20
|
+
result = []
|
|
21
|
+
|
|
22
|
+
if "interrupts" not in thread:
|
|
23
|
+
return result
|
|
24
|
+
|
|
25
|
+
for interrupt_list in thread["interrupts"].values():
|
|
26
|
+
for interrupt in interrupt_list:
|
|
27
|
+
if Auth0Interrupt.is_interrupt(interrupt["value"]):
|
|
28
|
+
result.append(interrupt)
|
|
29
|
+
|
|
30
|
+
return result
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from typing import Callable
|
|
2
|
+
from typing_extensions import Annotated
|
|
3
|
+
from pydantic import create_model
|
|
4
|
+
from langchain_core.tools import BaseTool, tool as create_tool, InjectedToolCallId
|
|
5
|
+
from langchain_core.runnables import RunnableConfig
|
|
6
|
+
|
|
7
|
+
def tool_wrapper(tool: BaseTool, protect_fn: Callable) -> BaseTool:
|
|
8
|
+
|
|
9
|
+
# Workaround: extend existing args_schema to be able to get the tool_call_id value
|
|
10
|
+
args_schema = create_model(
|
|
11
|
+
tool.args_schema.__name__ + "Extended",
|
|
12
|
+
__base__=tool.args_schema,
|
|
13
|
+
**{"tool_call_id": (Annotated[str, InjectedToolCallId])}
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
@create_tool(
|
|
17
|
+
tool.name,
|
|
18
|
+
description=tool.description,
|
|
19
|
+
args_schema=args_schema
|
|
20
|
+
)
|
|
21
|
+
async def wrapped_tool(config: RunnableConfig, tool_call_id: Annotated[str, InjectedToolCallId], **input):
|
|
22
|
+
async def execute_fn(*_, **__):
|
|
23
|
+
return await tool.ainvoke(input, config)
|
|
24
|
+
|
|
25
|
+
return await protect_fn(
|
|
26
|
+
lambda *_, **__: {
|
|
27
|
+
"thread_id": config.get("configurable", {}).get("thread_id"),
|
|
28
|
+
"tool_call_id": tool_call_id,
|
|
29
|
+
"tool_name": tool.name,
|
|
30
|
+
},
|
|
31
|
+
execute_fn,
|
|
32
|
+
)(**input)
|
|
33
|
+
|
|
34
|
+
return wrapped_tool
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: auth0-ai-langchain
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: This package is an SDK for building secure AI-powered applications using Auth0, Okta FGA and LangChain.
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Author: Auth0
|
|
7
|
+
Author-email: support@auth0.com
|
|
8
|
+
Requires-Python: >=3.11,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Requires-Dist: auth0-ai (>=1.0.2,<2.0.0)
|
|
15
|
+
Requires-Dist: langchain (>=0.3.26,<0.4.0)
|
|
16
|
+
Requires-Dist: langchain-core (>=0.3.69,<0.4.0)
|
|
17
|
+
Requires-Dist: langgraph (>=0.5.3,<0.6.0)
|
|
18
|
+
Requires-Dist: langgraph-sdk (>=0.1.73,<0.2.0)
|
|
19
|
+
Requires-Dist: openfga-sdk (>=0.9.5,<0.10.0)
|
|
20
|
+
Project-URL: Homepage, https://auth0.com
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# Auth0 AI for LangChain
|
|
24
|
+
|
|
25
|
+
`auth0-ai-langchain` is an SDK for building secure AI-powered applications using [Auth0](https://www.auth0.ai/), [Okta FGA](https://docs.fga.dev/) and [LangChain](https://python.langchain.com/docs/tutorials/).
|
|
26
|
+
|
|
27
|
+
  [](https://opensource.org/license/apache-2-0)
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
> ⚠️ **WARNING**: `auth0-ai-langchain` is currently **under heavy development**. We strictly follow [Semantic Versioning (SemVer)](https://semver.org/), meaning all **breaking changes will only occur in major versions**. However, please note that during this early phase, **major versions may be released frequently** as the API evolves. We recommend locking versions when using this in production.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install auth0-ai-langchain
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Async Authorization
|
|
38
|
+
|
|
39
|
+
`Auth0AI` uses CIBA (Client-Initiated Backchannel Authentication) to handle user confirmation asynchronously. This is useful when you need to confirm a user action before proceeding with a tool execution.
|
|
40
|
+
|
|
41
|
+
Full Example of [Async Authorization](https://github.com/auth0/auth0-ai-python/tree/main/examples/async-authorization/langchain-examples).
|
|
42
|
+
|
|
43
|
+
1. Define a tool with the proper authorizer specifying a function to resolve the user id:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from auth0_ai_langchain.auth0_ai import Auth0AI
|
|
47
|
+
from auth0_ai_langchain.async_authorization import get_async_authorization_credentials
|
|
48
|
+
from langchain_core.runnables import ensure_config
|
|
49
|
+
from langchain_core.tools import StructuredTool
|
|
50
|
+
|
|
51
|
+
# If not provided, Auth0 settings will be read from env variables: `AUTH0_DOMAIN`, `AUTH0_CLIENT_ID`, and `AUTH0_CLIENT_SECRET`
|
|
52
|
+
auth0_ai = Auth0AI()
|
|
53
|
+
|
|
54
|
+
with_async_authorization = auth0_ai.with_async_authorization(
|
|
55
|
+
scopes=["stock:trade"],
|
|
56
|
+
audience=os.getenv("AUDIENCE"),
|
|
57
|
+
requested_expiry=os.getenv("REQUESTED_EXPIRY"),
|
|
58
|
+
binding_message=lambda ticker, qty: f"Authorize the purchase of {qty} {ticker}",
|
|
59
|
+
user_id=lambda *_, **__: ensure_config().get("configurable", {}).get("user_id"),
|
|
60
|
+
# Optional:
|
|
61
|
+
# store=InMemoryStore()
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def tool_function(ticker: str, qty: int) -> str:
|
|
65
|
+
credentials = get_async_authorization_credentials()
|
|
66
|
+
headers = {
|
|
67
|
+
"Authorization": f"{credentials["token_type"]} {credentials["access_token"]}",
|
|
68
|
+
# ...
|
|
69
|
+
}
|
|
70
|
+
# Call API
|
|
71
|
+
|
|
72
|
+
trade_tool = with_async_authorization(
|
|
73
|
+
StructuredTool(
|
|
74
|
+
name="trade_tool",
|
|
75
|
+
description="Use this function to trade a stock",
|
|
76
|
+
func=trade_tool_function,
|
|
77
|
+
# ...
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
2. Handle interruptions properly. For example, if user is not enrolled to MFA, it will throw an interruption. See [Handling Interrupts](#handling-interrupts) section.
|
|
83
|
+
|
|
84
|
+
### Async Authorization with RAR (Rich Authorization Requests)
|
|
85
|
+
|
|
86
|
+
`Auth0AI` supports RAR (Rich Authorization Requests) for CIBA. This allows you to provide additional authorization parameters to be displayed during the user confirmation request.
|
|
87
|
+
|
|
88
|
+
When defining the tool authorizer, you can specify the `authorization_details` parameter to include detailed information about the authorization being requested:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
with_async_authorization = auth0_ai.with_async_authorization(
|
|
92
|
+
scopes=["stock:trade"],
|
|
93
|
+
audience=os.getenv("AUDIENCE"),
|
|
94
|
+
requested_expiry=os.getenv("REQUESTED_EXPIRY"),
|
|
95
|
+
binding_message=lambda ticker, qty: f"Authorize the purchase of {qty} {ticker}",
|
|
96
|
+
authorization_details=lambda ticker, qty: [
|
|
97
|
+
{
|
|
98
|
+
"type": "trade_authorization",
|
|
99
|
+
"qty": qty,
|
|
100
|
+
"ticker": ticker,
|
|
101
|
+
"action": "buy"
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
user_id=lambda *_, **__: ensure_config().get("configurable", {}).get("user_id"),
|
|
105
|
+
# Optional:
|
|
106
|
+
# store=InMemoryStore()
|
|
107
|
+
)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
To use RAR with CIBA, you need to [set up authorization details](https://auth0.com/docs/get-started/apis/configure-rich-authorization-requests) in your Auth0 tenant. This includes defining the authorization request parameters and their types. Additionally, the [Guardian SDK](https://auth0.com/docs/secure/multi-factor-authentication/auth0-guardian) is required to handle these authorization details in your authorizer app.
|
|
111
|
+
|
|
112
|
+
For more information on setting up RAR with CIBA, refer to:
|
|
113
|
+
|
|
114
|
+
- [Configure Rich Authorization Requests (RAR)](https://auth0.com/docs/get-started/apis/configure-rich-authorization-requests)
|
|
115
|
+
- [User Authorization with CIBA](https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-initiated-backchannel-authentication-flow/user-authorization-with-ciba)
|
|
116
|
+
|
|
117
|
+
## Authorization for Tools
|
|
118
|
+
|
|
119
|
+
The `FGAAuthorizer` can leverage Okta FGA to authorize tools executions. The `FGAAuthorizer.create` function can be used to create an authorizer that checks permissions before executing the tool.
|
|
120
|
+
|
|
121
|
+
Full example of [Authorization for Tools](https://github.com/auth0/auth0-ai-python/tree/main/examples/authorization-for-tools/langchain-examples).
|
|
122
|
+
|
|
123
|
+
1. Create an instance of FGA Authorizer:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from auth0_ai_langchain.fga import FGAAuthorizer
|
|
127
|
+
|
|
128
|
+
# If not provided, FGA settings will be read from env variables: `FGA_STORE_ID`, `FGA_CLIENT_ID`, `FGA_CLIENT_SECRET`, etc.
|
|
129
|
+
fga = FGAAuthorizer.create()
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
2. Define the FGA query (`build_query`) and, optionally, the `on_unauthorized` handler:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from langchain_core.runnables import ensure_config
|
|
136
|
+
|
|
137
|
+
async def build_fga_query(tool_input):
|
|
138
|
+
user_id = ensure_config().get("configurable",{}).get("user_id")
|
|
139
|
+
return {
|
|
140
|
+
"user": f"user:{user_id}",
|
|
141
|
+
"object": f"asset:{tool_input["ticker"]}",
|
|
142
|
+
"relation": "can_buy",
|
|
143
|
+
"context": {"current_time": datetime.now(timezone.utc).isoformat()}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
def on_unauthorized(tool_input):
|
|
147
|
+
return f"The user is not allowed to buy {tool_input["qty"]} shares of {tool_input["ticker"]}."
|
|
148
|
+
|
|
149
|
+
use_fga = fga(
|
|
150
|
+
build_query=build_fga_query,
|
|
151
|
+
on_unauthorized=on_unauthorized,
|
|
152
|
+
)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Note**: The parameters given to the `build_query` and `on_unauthorized` functions are the same as those provided to the tool function.
|
|
156
|
+
|
|
157
|
+
3. Wrap the tool:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from langchain_core.tools import StructuredTool
|
|
161
|
+
|
|
162
|
+
async def buy_tool_function(ticker: str, qty: int) -> str:
|
|
163
|
+
# TODO: implement buy operation
|
|
164
|
+
return f"Purchased {qty} shares of {ticker}"
|
|
165
|
+
|
|
166
|
+
func=use_fga(buy_tool_function)
|
|
167
|
+
|
|
168
|
+
buy_tool = StructuredTool(
|
|
169
|
+
func=func,
|
|
170
|
+
coroutine=func,
|
|
171
|
+
name="buy",
|
|
172
|
+
description="Use this function to buy stocks",
|
|
173
|
+
)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Calling APIs On User's Behalf
|
|
177
|
+
|
|
178
|
+
The `Auth0AI.with_token_vault` function exchanges user's refresh token taken, by default, from the runnable configuration (`config.configurable._credentials.refresh_token`) for a Token Vault access token that is valid to call a third-party API.
|
|
179
|
+
|
|
180
|
+
Full Example of [Calling APIs On User's Behalf](https://github.com/auth0/auth0-ai-python/tree/main/examples/calling-apis/langchain-examples).
|
|
181
|
+
|
|
182
|
+
### Basic Usage
|
|
183
|
+
|
|
184
|
+
1. Define a tool with the proper authorizer:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from auth0_ai_langchain.auth0_ai import Auth0AI
|
|
188
|
+
from auth0_ai_langchain.token_vault import get_credentials_from_token_vault
|
|
189
|
+
from langchain_core.tools import StructuredTool
|
|
190
|
+
|
|
191
|
+
# If not provided, Auth0 settings will be read from env variables: `AUTH0_DOMAIN`, `AUTH0_CLIENT_ID`, and `AUTH0_CLIENT_SECRET`
|
|
192
|
+
auth0_ai = Auth0AI()
|
|
193
|
+
|
|
194
|
+
with_google_calendar_access = auth0_ai.with_token_vault(
|
|
195
|
+
connection="google-oauth2",
|
|
196
|
+
scopes=["openid", "https://www.googleapis.com/auth/calendar.freebusy"],
|
|
197
|
+
# Optional:
|
|
198
|
+
# refresh_token=lambda *_, **__: ensure_config().get("configurable", {}).get("_credentials", {}).get("refresh_token"),
|
|
199
|
+
# authorization_params={"login_hint": "user@example.com", "ui_locales": "en"}
|
|
200
|
+
# store=InMemoryStore(),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def tool_function(date: datetime):
|
|
204
|
+
credentials = get_credentials_from_token_vault()
|
|
205
|
+
# Call Google API using credentials["access_token"]
|
|
206
|
+
|
|
207
|
+
check_calendar_tool = with_google_calendar_access(
|
|
208
|
+
StructuredTool(
|
|
209
|
+
name="check_user_calendar",
|
|
210
|
+
description="Use this function to check if the user is available on a certain date and time",
|
|
211
|
+
func=tool_function,
|
|
212
|
+
# ...
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
2. Add a node to your graph for your tools:
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
workflow = (
|
|
221
|
+
StateGraph(State)
|
|
222
|
+
.add_node(
|
|
223
|
+
"tools",
|
|
224
|
+
ToolNode(
|
|
225
|
+
[
|
|
226
|
+
check_calendar_tool,
|
|
227
|
+
# ...
|
|
228
|
+
],
|
|
229
|
+
# The error handler should be disabled to allow interruptions to be triggered from within tools.
|
|
230
|
+
handle_tool_errors=False
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
# ...
|
|
234
|
+
)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
3. Handle interruptions properly. For example, if the tool does not have access to user's Google Calendar, it will throw an interruption. See [Handling Interrupts](#handling-interrupts) section.
|
|
238
|
+
|
|
239
|
+
### Additional Authorization Parameters
|
|
240
|
+
|
|
241
|
+
The `authorization_params` parameter is optional and can be used to pass additional authorization parameters needed to connect an account (e.g., `login_hint`, `ui_locales`):
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
with_google_calendar_access = auth0_ai.with_token_vault(
|
|
245
|
+
connection="google-oauth2",
|
|
246
|
+
scopes=["openid", "https://www.googleapis.com/auth/calendar.freebusy"],
|
|
247
|
+
authorization_params={"login_hint": "user@example.com", "ui_locales": "en"}
|
|
248
|
+
)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## RAG with FGA
|
|
252
|
+
|
|
253
|
+
The `FGARetriever` can be used to filter documents based on access control checks defined in Okta FGA. This retriever performs batch checks on retrieved documents, returning only the ones that pass the specified access criteria.
|
|
254
|
+
|
|
255
|
+
Full Example of [RAG Application](https://github.com/auth0/auth0-ai-python/tree/main/examples/authorization-for-rag/langchain-examples).
|
|
256
|
+
|
|
257
|
+
Create a retriever instance using the `FGARetriever` class.
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
from langchain.vectorstores import VectorStoreIndex
|
|
261
|
+
from langchain.schema import Document
|
|
262
|
+
from auth0_ai_langchain import FGARetriever
|
|
263
|
+
from openfga_sdk.client.models import ClientCheckRequest
|
|
264
|
+
from openfga_sdk import ClientConfiguration
|
|
265
|
+
from openfga_sdk.credentials import CredentialConfiguration, Credentials
|
|
266
|
+
|
|
267
|
+
# Define some docs:
|
|
268
|
+
documents = [
|
|
269
|
+
Document(page_content="This is a public doc", metadata={"doc_id": "public-doc"}),
|
|
270
|
+
Document(page_content="This is a private doc", metadata={"doc_id": "private-doc"}),
|
|
271
|
+
]
|
|
272
|
+
|
|
273
|
+
# Create a vector store:
|
|
274
|
+
vector_store = VectorStoreIndex.from_documents(documents)
|
|
275
|
+
|
|
276
|
+
# Create a retriever:
|
|
277
|
+
base_retriever = vector_store.as_retriever()
|
|
278
|
+
|
|
279
|
+
# Create the FGA retriever wrapper.
|
|
280
|
+
# If not provided, FGA settings will be read from env variables: `FGA_STORE_ID`, `FGA_CLIENT_ID`, `FGA_CLIENT_SECRET`, etc.
|
|
281
|
+
retriever = FGARetriever(
|
|
282
|
+
base_retriever,
|
|
283
|
+
build_query=lambda node: ClientCheckRequest(
|
|
284
|
+
user=f'user:{user}',
|
|
285
|
+
object=f'doc:{node.metadata["doc_id"]}',
|
|
286
|
+
relation="viewer",
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Create a query engine:
|
|
291
|
+
query_engine = RetrieverQueryEngine.from_args(
|
|
292
|
+
retriever=retriever,
|
|
293
|
+
llm=OpenAI()
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
# Query:
|
|
297
|
+
response = query_engine.query("What is the forecast for ZEKO?")
|
|
298
|
+
|
|
299
|
+
print(response)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Handling Interrupts
|
|
303
|
+
|
|
304
|
+
`Auth0AI` uses interrupts extensively and will never block a graph. Whenever an authorizer requires user interaction, the graph throws a `GraphInterrupt` exception with data that allows the client to resume the flow.
|
|
305
|
+
|
|
306
|
+
It is important to disable error handling in your tools node as follows:
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
.add_node(
|
|
310
|
+
"tools",
|
|
311
|
+
ToolNode(
|
|
312
|
+
[
|
|
313
|
+
# your authorizer-wrapped tools
|
|
314
|
+
],
|
|
315
|
+
# Error handler should be disabled in order to trigger interruptions from within tools.
|
|
316
|
+
handle_tool_errors=False
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
From the client side of the graph you get the interrupts:
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
from auth0_ai_langchain.utils.interrupt import get_auth0_interrupts
|
|
325
|
+
|
|
326
|
+
# Get the langgraph thread:
|
|
327
|
+
thread = await client.threads.get(thread_id)
|
|
328
|
+
|
|
329
|
+
# Filter the auth0 interrupts:
|
|
330
|
+
auth0_interrupts = get_auth0_interrupts(thread)
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Then you can resume the thread by doing this:
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
await client.runs.wait(thread_id, assistant_id)
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
For the specific case of **CIBA (Client-Initiated Backchannel Authorization)** you might attach a `GraphResumer` instance that watches for interrupted threads in the `"Authorization Pending"` state and attempts to resume them automatically, respecting Auth0's polling interval.
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
import os
|
|
343
|
+
from auth0_ai_langchain.async_authorization import GraphResumer
|
|
344
|
+
from langgraph_sdk import get_client
|
|
345
|
+
|
|
346
|
+
resumer = GraphResumer(
|
|
347
|
+
lang_graph=get_client(url=os.getenv("LANGGRAPH_API_URL")),
|
|
348
|
+
# optionally, you can filter by a specific graph:
|
|
349
|
+
filters={"graph_id": "conditional-trade"},
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
resumer \
|
|
353
|
+
.on_resume(lambda thread: print(f"Attempting to resume thread {thread['thread_id']} from interruption {thread['interruption_id']}")) \
|
|
354
|
+
.on_error(lambda err: print(f"Error in GraphResumer: {str(err)}"))
|
|
355
|
+
|
|
356
|
+
resumer.start()
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
<p align="center">
|
|
362
|
+
<picture>
|
|
363
|
+
<source media="(prefers-color-scheme: light)" srcset="https://cdn.auth0.com/website/sdks/logos/auth0_light_mode.png" width="150">
|
|
364
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.auth0.com/website/sdks/logos/auth0_dark_mode.png" width="150">
|
|
365
|
+
<img alt="Auth0 Logo" src="https://cdn.auth0.com/website/sdks/logos/auth0_light_mode.png" width="150">
|
|
366
|
+
</picture>
|
|
367
|
+
</p>
|
|
368
|
+
<p align="center">Auth0 is an easy to implement, adaptable authentication and authorization platform. To learn more checkout <a href="https://auth0.com/why-auth0">Why Auth0?</a></p>
|
|
369
|
+
<p align="center">
|
|
370
|
+
This project is licensed under the Apache 2.0 license. See the <a href="https://github.com/auth0/auth0-ai-python/blob/main/LICENSE"> LICENSE</a> file for more info.</p>
|
|
371
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
auth0_ai_langchain/FGARetriever.py,sha256=SQwxo2aDtQhwQtYmszoKw3BH-U5QVnvPAgVw9EDzKVM,6002
|
|
2
|
+
auth0_ai_langchain/__init__.py,sha256=I331Kz-q97ZU7TfXaOR5UBbJamGEJ15twbf2HP1iCHs,67
|
|
3
|
+
auth0_ai_langchain/async_authorization/__init__.py,sha256=ZZW3HLnRRuCr9rfcJCdqSSLdAs710gtX5TVnq2LcnT8,346
|
|
4
|
+
auth0_ai_langchain/async_authorization/async_authorizer.py,sha256=fS3q2hqefN4ewxyVw-VhuC2DAFeVYcNdIbEjhT0bFs8,799
|
|
5
|
+
auth0_ai_langchain/async_authorization/graph_resumer.py,sha256=mTCcsKiTcxB9QKaS8S4MKJUtXJqY6Y2DLcHrfFKQ7y0,5548
|
|
6
|
+
auth0_ai_langchain/auth0_ai.py,sha256=fnlIJscocm0MRTbcFeNS7Vxy_zOJdSmmMuhjcwYeiDE,4784
|
|
7
|
+
auth0_ai_langchain/fga/__init__.py,sha256=rgqTD4Gvz28jNdqhxTG5udbgyeUMsyvRj83fHBJdt4s,137
|
|
8
|
+
auth0_ai_langchain/token_vault/__init__.py,sha256=Wr4QAAPUcaSDP3wOj46vWsPTSt0SNaHyP5w60wUy5eI,436
|
|
9
|
+
auth0_ai_langchain/token_vault/token_vault_authorizer.py,sha256=KrgXdLFbNtGqELemeT7vjxhH2xj6KuETZzwZ-EkWBFM,1487
|
|
10
|
+
auth0_ai_langchain/utils/interrupt.py,sha256=DZ1b9OAkg3SQru9mSaQGBC6UY0ODz7QSskS9RlVyEGw,860
|
|
11
|
+
auth0_ai_langchain/utils/tool_wrapper.py,sha256=dHjcqykT2aohdFOm0mLZ9U6bXB6NHjfABb3aXef5174,1210
|
|
12
|
+
auth0_ai_langchain-1.0.1.dist-info/LICENSE,sha256=Lu_2YH0oK8b_VVisAhNQ2WIdtwY8pSU2PLbll-y6Cj8,9792
|
|
13
|
+
auth0_ai_langchain-1.0.1.dist-info/METADATA,sha256=3Cb-8K5O8HKnskAGxuXSGEhyK4f2uhC5CWtWe7jzre4,14511
|
|
14
|
+
auth0_ai_langchain-1.0.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
15
|
+
auth0_ai_langchain-1.0.1.dist-info/RECORD,,
|