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.
@@ -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 .FGARetriever import FGARetriever
2
+
3
+ __all__ = ["FGARetriever"]
@@ -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,4 @@
1
+ from auth0_ai.authorizers.fga_authorizer import (
2
+ FGAAuthorizer as FGAAuthorizer,
3
+ FGAAuthorizerOptions as FGAAuthorizerOptions
4
+ )
@@ -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
+ ![Release](https://img.shields.io/pypi/v/auth0-ai-langchain) ![Downloads](https://img.shields.io/pypi/dw/auth0-ai-langchain) [![License](https://img.shields.io/:license-APACHE%202.0-blue.svg?style=flat)](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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.1.3
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any