auth0-ai-langchain 0.2.0__py3-none-any.whl → 1.0.0b2__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.
Potentially problematic release.
This version of auth0-ai-langchain might be problematic. Click here for more details.
- auth0_ai_langchain/FGARetriever.py +3 -3
- auth0_ai_langchain/auth0_ai.py +101 -31
- auth0_ai_langchain/ciba/__init__.py +3 -0
- auth0_ai_langchain/ciba/ciba_authorizer.py +17 -0
- auth0_ai_langchain/ciba/graph_resumer.py +154 -0
- auth0_ai_langchain/federated_connections/__init__.py +9 -3
- auth0_ai_langchain/federated_connections/federated_connection_authorizer.py +14 -33
- auth0_ai_langchain/fga/__init__.py +4 -0
- auth0_ai_langchain/utils/interrupt.py +18 -1
- auth0_ai_langchain/utils/tool_wrapper.py +34 -0
- auth0_ai_langchain-1.0.0b2.dist-info/METADATA +352 -0
- auth0_ai_langchain-1.0.0b2.dist-info/RECORD +15 -0
- auth0_ai_langchain/ciba/ciba_graph/ciba_graph.py +0 -109
- auth0_ai_langchain/ciba/ciba_graph/initialize_ciba.py +0 -91
- auth0_ai_langchain/ciba/ciba_graph/initialize_hitl.py +0 -50
- auth0_ai_langchain/ciba/ciba_graph/types.py +0 -115
- auth0_ai_langchain/ciba/ciba_graph/utils.py +0 -17
- auth0_ai_langchain/ciba/ciba_poller_graph.py +0 -105
- auth0_ai_langchain/ciba/types.py +0 -8
- auth0_ai_langchain/fga/fga_authorizer.py +0 -3
- auth0_ai_langchain-0.2.0.dist-info/METADATA +0 -221
- auth0_ai_langchain-0.2.0.dist-info/RECORD +0 -19
- {auth0_ai_langchain-0.2.0.dist-info → auth0_ai_langchain-1.0.0b2.dist-info}/LICENSE +0 -0
- {auth0_ai_langchain-0.2.0.dist-info → auth0_ai_langchain-1.0.0b2.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: auth0-ai-langchain
|
|
3
|
+
Version: 1.0.0b2
|
|
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.0b2,<2.0.0)
|
|
15
|
+
Requires-Dist: langchain (>=0.3.25,<0.4.0)
|
|
16
|
+
Requires-Dist: langchain-core (>=0.3.59,<0.4.0)
|
|
17
|
+
Requires-Dist: langgraph (>=0.4.3,<0.5.0)
|
|
18
|
+
Requires-Dist: langgraph-sdk (>=0.1.66,<0.2.0)
|
|
19
|
+
Requires-Dist: openfga-sdk (>=0.9.4,<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 development and it is not intended to be used in production, and therefore has no official support.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install auth0-ai-langchain
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Async User Confirmation
|
|
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 User Confirmation](https://github.com/auth0-lab/auth0-ai-python/tree/main/examples/async-user-confirmation/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.ciba import get_ciba_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_user_confirmation = auth0_ai.with_async_user_confirmation(
|
|
55
|
+
scopes=["stock:trade"],
|
|
56
|
+
audience=os.getenv("AUDIENCE"),
|
|
57
|
+
binding_message=lambda ticker, qty: f"Authorize the purchase of {qty} {ticker}",
|
|
58
|
+
user_id=lambda *_, **__: ensure_config().get("configurable", {}).get("user_id"),
|
|
59
|
+
# Optional:
|
|
60
|
+
# store=InMemoryStore()
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def tool_function(ticker: str, qty: int) -> str:
|
|
64
|
+
credentials = get_ciba_credentials()
|
|
65
|
+
headers = {
|
|
66
|
+
"Authorization": f"{credentials["token_type"]} {credentials["access_token"]}",
|
|
67
|
+
# ...
|
|
68
|
+
}
|
|
69
|
+
# Call API
|
|
70
|
+
|
|
71
|
+
trade_tool = with_async_user_confirmation(
|
|
72
|
+
StructuredTool(
|
|
73
|
+
name="trade_tool",
|
|
74
|
+
description="Use this function to trade a stock",
|
|
75
|
+
func=trade_tool_function,
|
|
76
|
+
# ...
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
2. Handle interruptions properly. For example, if user is not enrolled to MFA, it will throw an interruption. See [Handling Interrupts](#handling-interrupts) section.
|
|
82
|
+
|
|
83
|
+
### CIBA with RAR (Rich Authorization Requests)
|
|
84
|
+
`Auth0AI` supports RAR (Rich Authorization Requests) for CIBA. This allows you to provide additional authorization parameters to be displayed during the user confirmation request.
|
|
85
|
+
|
|
86
|
+
When defining the tool authorizer, you can specify the `authorization_details` parameter to include detailed information about the authorization being requested:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
with_async_user_confirmation = auth0_ai.with_async_user_confirmation(
|
|
90
|
+
scopes=["stock:trade"],
|
|
91
|
+
audience=os.getenv("AUDIENCE"),
|
|
92
|
+
binding_message=lambda ticker, qty: f"Authorize the purchase of {qty} {ticker}",
|
|
93
|
+
authorization_details=lambda ticker, qty: [
|
|
94
|
+
{
|
|
95
|
+
"type": "trade_authorization",
|
|
96
|
+
"qty": qty,
|
|
97
|
+
"ticker": ticker,
|
|
98
|
+
"action": "buy"
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
user_id=lambda *_, **__: ensure_config().get("configurable", {}).get("user_id"),
|
|
102
|
+
# Optional:
|
|
103
|
+
# store=InMemoryStore()
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
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.
|
|
108
|
+
|
|
109
|
+
For more information on setting up RAR with CIBA, refer to:
|
|
110
|
+
- [Configure Rich Authorization Requests (RAR)](https://auth0.com/docs/get-started/apis/configure-rich-authorization-requests)
|
|
111
|
+
- [User Authorization with CIBA](https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-initiated-backchannel-authentication-flow/user-authorization-with-ciba)
|
|
112
|
+
|
|
113
|
+
## Authorization for Tools
|
|
114
|
+
|
|
115
|
+
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.
|
|
116
|
+
|
|
117
|
+
Full example of [Authorization for Tools](https://github.com/auth0-lab/auth0-ai-python/tree/main/examples/authorization-for-tools/langchain-examples).
|
|
118
|
+
|
|
119
|
+
1. Create an instance of FGA Authorizer:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from auth0_ai_langchain.fga import FGAAuthorizer
|
|
123
|
+
|
|
124
|
+
# If not provided, FGA settings will be read from env variables: `FGA_STORE_ID`, `FGA_CLIENT_ID`, `FGA_CLIENT_SECRET`, etc.
|
|
125
|
+
fga = FGAAuthorizer.create()
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
2. Define the FGA query (`build_query`) and, optionally, the `on_unauthorized` handler:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from langchain_core.runnables import ensure_config
|
|
132
|
+
|
|
133
|
+
async def build_fga_query(tool_input):
|
|
134
|
+
user_id = ensure_config().get("configurable",{}).get("user_id")
|
|
135
|
+
return {
|
|
136
|
+
"user": f"user:{user_id}",
|
|
137
|
+
"object": f"asset:{tool_input["ticker"]}",
|
|
138
|
+
"relation": "can_buy",
|
|
139
|
+
"context": {"current_time": datetime.now(timezone.utc).isoformat()}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
def on_unauthorized(tool_input):
|
|
143
|
+
return f"The user is not allowed to buy {tool_input["qty"]} shares of {tool_input["ticker"]}."
|
|
144
|
+
|
|
145
|
+
use_fga = fga(
|
|
146
|
+
build_query=build_fga_query,
|
|
147
|
+
on_unauthorized=on_unauthorized,
|
|
148
|
+
)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Note**: The parameters given to the `build_query` and `on_unauthorized` functions are the same as those provided to the tool function.
|
|
152
|
+
|
|
153
|
+
3. Wrap the tool:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from langchain_core.tools import StructuredTool
|
|
157
|
+
|
|
158
|
+
async def buy_tool_function(ticker: str, qty: int) -> str:
|
|
159
|
+
# TODO: implement buy operation
|
|
160
|
+
return f"Purchased {qty} shares of {ticker}"
|
|
161
|
+
|
|
162
|
+
func=use_fga(buy_tool_function)
|
|
163
|
+
|
|
164
|
+
buy_tool = StructuredTool(
|
|
165
|
+
func=func,
|
|
166
|
+
coroutine=func,
|
|
167
|
+
name="buy",
|
|
168
|
+
description="Use this function to buy stocks",
|
|
169
|
+
)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Calling APIs On User's Behalf
|
|
173
|
+
|
|
174
|
+
The `Auth0AI.with_federated_connection` function exchanges user's refresh token taken, by default, from the runnable configuration (`config.configurable._credentials.refresh_token`) for a Federated Connection API token.
|
|
175
|
+
|
|
176
|
+
Full Example of [Calling APIs On User's Behalf](https://github.com/auth0-lab/auth0-ai-python/tree/main/examples/calling-apis/langchain-examples).
|
|
177
|
+
|
|
178
|
+
1. Define a tool with the proper authorizer:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from auth0_ai_langchain.auth0_ai import Auth0AI
|
|
182
|
+
from auth0_ai_langchain.federated_connections import get_credentials_for_connection
|
|
183
|
+
from langchain_core.tools import StructuredTool
|
|
184
|
+
|
|
185
|
+
# If not provided, Auth0 settings will be read from env variables: `AUTH0_DOMAIN`, `AUTH0_CLIENT_ID`, and `AUTH0_CLIENT_SECRET`
|
|
186
|
+
auth0_ai = Auth0AI()
|
|
187
|
+
|
|
188
|
+
with_google_calendar_access = auth0_ai.with_federated_connection(
|
|
189
|
+
connection="google-oauth2",
|
|
190
|
+
scopes=["https://www.googleapis.com/auth/calendar.freebusy"],
|
|
191
|
+
# Optional:
|
|
192
|
+
# refresh_token=lambda *_, **__: ensure_config().get("configurable", {}).get("_credentials", {}).get("refresh_token"),
|
|
193
|
+
# store=InMemoryStore(),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def tool_function(date: datetime):
|
|
197
|
+
credentials = get_credentials_for_connection()
|
|
198
|
+
# Call Google API using credentials["access_token"]
|
|
199
|
+
|
|
200
|
+
check_calendar_tool = with_google_calendar_access(
|
|
201
|
+
StructuredTool(
|
|
202
|
+
name="check_user_calendar",
|
|
203
|
+
description="Use this function to check if the user is available on a certain date and time",
|
|
204
|
+
func=tool_function,
|
|
205
|
+
# ...
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
2. Add a node to your graph for your tools:
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
workflow = (
|
|
214
|
+
StateGraph(State)
|
|
215
|
+
.add_node(
|
|
216
|
+
"tools",
|
|
217
|
+
ToolNode(
|
|
218
|
+
[
|
|
219
|
+
check_calendar_tool,
|
|
220
|
+
# ...
|
|
221
|
+
],
|
|
222
|
+
# The error handler should be disabled to allow interruptions to be triggered from within tools.
|
|
223
|
+
handle_tool_errors=False
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
# ...
|
|
227
|
+
)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
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.
|
|
231
|
+
|
|
232
|
+
## RAG with FGA
|
|
233
|
+
|
|
234
|
+
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.
|
|
235
|
+
|
|
236
|
+
Full Example of [RAG Application](https://github.com/auth0-lab/auth0-ai-python/tree/main/examples/authorization-for-rag/langchain-examples).
|
|
237
|
+
|
|
238
|
+
Create a retriever instance using the `FGARetriever` class.
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
from langchain.vectorstores import VectorStoreIndex
|
|
242
|
+
from langchain.schema import Document
|
|
243
|
+
from auth0_ai_langchain import FGARetriever
|
|
244
|
+
from openfga_sdk.client.models import ClientCheckRequest
|
|
245
|
+
from openfga_sdk import ClientConfiguration
|
|
246
|
+
from openfga_sdk.credentials import CredentialConfiguration, Credentials
|
|
247
|
+
|
|
248
|
+
# Define some docs:
|
|
249
|
+
documents = [
|
|
250
|
+
Document(page_content="This is a public doc", metadata={"doc_id": "public-doc"}),
|
|
251
|
+
Document(page_content="This is a private doc", metadata={"doc_id": "private-doc"}),
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
# Create a vector store:
|
|
255
|
+
vector_store = VectorStoreIndex.from_documents(documents)
|
|
256
|
+
|
|
257
|
+
# Create a retriever:
|
|
258
|
+
base_retriever = vector_store.as_retriever()
|
|
259
|
+
|
|
260
|
+
# Create the FGA retriever wrapper.
|
|
261
|
+
# If not provided, FGA settings will be read from env variables: `FGA_STORE_ID`, `FGA_CLIENT_ID`, `FGA_CLIENT_SECRET`, etc.
|
|
262
|
+
retriever = FGARetriever(
|
|
263
|
+
base_retriever,
|
|
264
|
+
build_query=lambda node: ClientCheckRequest(
|
|
265
|
+
user=f'user:{user}',
|
|
266
|
+
object=f'doc:{node.metadata["doc_id"]}',
|
|
267
|
+
relation="viewer",
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Create a query engine:
|
|
272
|
+
query_engine = RetrieverQueryEngine.from_args(
|
|
273
|
+
retriever=retriever,
|
|
274
|
+
llm=OpenAI()
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Query:
|
|
278
|
+
response = query_engine.query("What is the forecast for ZEKO?")
|
|
279
|
+
|
|
280
|
+
print(response)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Handling Interrupts
|
|
284
|
+
|
|
285
|
+
`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.
|
|
286
|
+
|
|
287
|
+
It is important to disable error handling in your tools node as follows:
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
.add_node(
|
|
291
|
+
"tools",
|
|
292
|
+
ToolNode(
|
|
293
|
+
[
|
|
294
|
+
# your authorizer-wrapped tools
|
|
295
|
+
],
|
|
296
|
+
# Error handler should be disabled in order to trigger interruptions from within tools.
|
|
297
|
+
handle_tool_errors=False
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
From the client side of the graph you get the interrupts:
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
from auth0_ai_langchain.utils.interrupt import get_auth0_interrupts
|
|
306
|
+
|
|
307
|
+
# Get the langgraph thread:
|
|
308
|
+
thread = await client.threads.get(thread_id)
|
|
309
|
+
|
|
310
|
+
# Filter the auth0 interrupts:
|
|
311
|
+
auth0_interrupts = get_auth0_interrupts(thread)
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Then you can resume the thread by doing this:
|
|
315
|
+
|
|
316
|
+
```python
|
|
317
|
+
await client.runs.wait(thread_id, assistant_id)
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
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.
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
import os
|
|
324
|
+
from auth0_ai_langchain.ciba import GraphResumer
|
|
325
|
+
from langgraph_sdk import get_client
|
|
326
|
+
|
|
327
|
+
resumer = GraphResumer(
|
|
328
|
+
lang_graph=get_client(url=os.getenv("LANGGRAPH_API_URL")),
|
|
329
|
+
# optionally, you can filter by a specific graph:
|
|
330
|
+
filters={"graph_id": "conditional-trade"},
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
resumer \
|
|
334
|
+
.on_resume(lambda thread: print(f"Attempting to resume thread {thread['thread_id']} from interruption {thread['interruption_id']}")) \
|
|
335
|
+
.on_error(lambda err: print(f"Error in GraphResumer: {str(err)}"))
|
|
336
|
+
|
|
337
|
+
resumer.start()
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
<p align="center">
|
|
343
|
+
<picture>
|
|
344
|
+
<source media="(prefers-color-scheme: light)" srcset="https://cdn.auth0.com/website/sdks/logos/auth0_light_mode.png" width="150">
|
|
345
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.auth0.com/website/sdks/logos/auth0_dark_mode.png" width="150">
|
|
346
|
+
<img alt="Auth0 Logo" src="https://cdn.auth0.com/website/sdks/logos/auth0_light_mode.png" width="150">
|
|
347
|
+
</picture>
|
|
348
|
+
</p>
|
|
349
|
+
<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>
|
|
350
|
+
<p align="center">
|
|
351
|
+
This project is licensed under the Apache 2.0 license. See the <a href="https://github.com/auth0-lab/auth0-ai-python/blob/main/LICENSE"> LICENSE</a> file for more info.</p>
|
|
352
|
+
|
|
@@ -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/auth0_ai.py,sha256=rAHvGBBvPgMXhnmEySmNMp6cbrXrZk3c3cn9r0psfZc,4748
|
|
4
|
+
auth0_ai_langchain/ciba/__init__.py,sha256=X62HZB20XdhsgcaKld6rLm2BOSuiO5uU5v7ePQz27Mk,268
|
|
5
|
+
auth0_ai_langchain/ciba/ciba_authorizer.py,sha256=GRAB3NBnmoxAECrRjPNdA9N9uQ4pCEzP6dF8RUwlysM,766
|
|
6
|
+
auth0_ai_langchain/ciba/graph_resumer.py,sha256=EpdzzB_NccdggKA3x__Q3Yziejo7AJjR4aJ57TZmYPA,5474
|
|
7
|
+
auth0_ai_langchain/federated_connections/__init__.py,sha256=kHsfxMbbuYtcnubslUTVLZX2FybLlKRcoPCgYQCMIK4,509
|
|
8
|
+
auth0_ai_langchain/federated_connections/federated_connection_authorizer.py,sha256=o25oRGiTo9y5mpjDNEWWaFVAIFbhwaxC0pcRId-4oYE,1405
|
|
9
|
+
auth0_ai_langchain/fga/__init__.py,sha256=rgqTD4Gvz28jNdqhxTG5udbgyeUMsyvRj83fHBJdt4s,137
|
|
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.0b2.dist-info/LICENSE,sha256=Lu_2YH0oK8b_VVisAhNQ2WIdtwY8pSU2PLbll-y6Cj8,9792
|
|
13
|
+
auth0_ai_langchain-1.0.0b2.dist-info/METADATA,sha256=JyplSC6fkjFar5bkiaGwSD8etO7mmq5dLXHYsZL72Tk,13548
|
|
14
|
+
auth0_ai_langchain-1.0.0b2.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
|
15
|
+
auth0_ai_langchain-1.0.0b2.dist-info/RECORD,,
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
from typing import Awaitable, Hashable, List, Optional, Callable, Any, Union
|
|
2
|
-
from langchain_core.tools import StructuredTool
|
|
3
|
-
from langchain_core.tools.base import BaseTool
|
|
4
|
-
from langgraph.graph import StateGraph, END, START
|
|
5
|
-
from langchain_core.runnables import Runnable
|
|
6
|
-
from auth0_ai.authorizers.types import AuthorizerParams
|
|
7
|
-
from ..types import Auth0Nodes
|
|
8
|
-
from .initialize_ciba import initialize_ciba
|
|
9
|
-
from .initialize_hitl import initialize_hitl
|
|
10
|
-
from .types import CIBAGraphOptions, CIBAOptions, ProtectedTool, BaseState
|
|
11
|
-
|
|
12
|
-
class CIBAGraph():
|
|
13
|
-
def __init__(
|
|
14
|
-
self,
|
|
15
|
-
options: Optional[CIBAGraphOptions] = None,
|
|
16
|
-
authorizer_params: Optional[AuthorizerParams] = None,
|
|
17
|
-
):
|
|
18
|
-
self.options = options
|
|
19
|
-
self.authorizer_params = authorizer_params
|
|
20
|
-
self.tools: List[ProtectedTool] = []
|
|
21
|
-
self.graph: Optional[StateGraph] = None
|
|
22
|
-
|
|
23
|
-
def get_tools(self) -> List[ProtectedTool]:
|
|
24
|
-
return self.tools
|
|
25
|
-
|
|
26
|
-
def get_graph(self) -> Optional[StateGraph]:
|
|
27
|
-
return self.graph
|
|
28
|
-
|
|
29
|
-
def get_options(self) -> Optional[CIBAGraphOptions]:
|
|
30
|
-
return self.options
|
|
31
|
-
|
|
32
|
-
def get_authorizer_params(self) -> Optional[AuthorizerParams]:
|
|
33
|
-
return self.authorizer_params
|
|
34
|
-
|
|
35
|
-
def register_nodes(
|
|
36
|
-
self,
|
|
37
|
-
graph: StateGraph,
|
|
38
|
-
) -> StateGraph:
|
|
39
|
-
self.graph = graph
|
|
40
|
-
|
|
41
|
-
# Add CIBA HITL and CIBA nodes
|
|
42
|
-
self.graph.add_node(Auth0Nodes.AUTH0_CIBA_HITL.value, initialize_hitl(self))
|
|
43
|
-
self.graph.add_node(Auth0Nodes.AUTH0_CIBA.value, initialize_ciba(self))
|
|
44
|
-
self.graph.add_conditional_edges(
|
|
45
|
-
Auth0Nodes.AUTH0_CIBA.value,
|
|
46
|
-
lambda state: END if getattr(state, "auth0", {}).get("error") else Auth0Nodes.AUTH0_CIBA_HITL.value,
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
return graph
|
|
50
|
-
|
|
51
|
-
def protect_tool(
|
|
52
|
-
self,
|
|
53
|
-
tool: Union[BaseTool, Callable],
|
|
54
|
-
options: CIBAOptions,
|
|
55
|
-
) -> StructuredTool:
|
|
56
|
-
"""
|
|
57
|
-
Authorize Options to start CIBA flow.
|
|
58
|
-
|
|
59
|
-
Attributes:
|
|
60
|
-
tool (Union[BaseTool, Callable]): The tool to be protected.
|
|
61
|
-
options (CIBAOptions): The CIBA options.
|
|
62
|
-
"""
|
|
63
|
-
|
|
64
|
-
# Merge default options with tool-specific options
|
|
65
|
-
merged_options = {**self.options, **options.__dict__} if isinstance(self.options, dict) else {**vars(self.options), **vars(options)}
|
|
66
|
-
|
|
67
|
-
if merged_options["on_approve_go_to"] is None:
|
|
68
|
-
raise ValueError(f"[{tool.name}] on_approve_go_to is required")
|
|
69
|
-
|
|
70
|
-
if merged_options["on_reject_go_to"] is None:
|
|
71
|
-
raise ValueError(f"[{tool.name}] on_reject_go_to is required")
|
|
72
|
-
|
|
73
|
-
self.tools.append(ProtectedTool(tool_name=tool.name, options=merged_options))
|
|
74
|
-
|
|
75
|
-
return tool
|
|
76
|
-
|
|
77
|
-
def with_auth(self, path: Union[
|
|
78
|
-
Callable[..., Union[Hashable, list[Hashable]]],
|
|
79
|
-
Callable[..., Awaitable[Union[Hashable, list[Hashable]]]],
|
|
80
|
-
Runnable[Any, Union[Hashable, list[Hashable]]],
|
|
81
|
-
]):
|
|
82
|
-
"""
|
|
83
|
-
A wrapper for the callable that determines the next node or nodes using a protected tool.
|
|
84
|
-
|
|
85
|
-
Attributes:
|
|
86
|
-
path (Union[Callable[..., Union[Hashable, list[Hashable]]], Callable[..., Awaitable[Union[Hashable, list[Hashable]]]], Runnable[Any, Union[Hashable, list[Hashable]]]])): The callable that determines the next node or nodes using a protected tool.
|
|
87
|
-
"""
|
|
88
|
-
def wrapper(*args):
|
|
89
|
-
if not callable(path):
|
|
90
|
-
return START
|
|
91
|
-
|
|
92
|
-
state: BaseState = args[0]
|
|
93
|
-
messages = state.get("messages")
|
|
94
|
-
last_message = messages[-1] if messages else None
|
|
95
|
-
|
|
96
|
-
# Call default path if there are no tool calls
|
|
97
|
-
if not last_message or not hasattr(last_message, "tool_calls") or not last_message.tool_calls:
|
|
98
|
-
return path(*args)
|
|
99
|
-
|
|
100
|
-
tool_name = last_message.tool_calls[0]["name"]
|
|
101
|
-
tool = next((t for t in self.tools if t.tool_name == tool_name), None)
|
|
102
|
-
|
|
103
|
-
if tool:
|
|
104
|
-
return Auth0Nodes.AUTH0_CIBA.value
|
|
105
|
-
|
|
106
|
-
# Call default path if tool is not protected
|
|
107
|
-
return path(*args)
|
|
108
|
-
|
|
109
|
-
return wrapper
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from langgraph.types import Command
|
|
3
|
-
from langgraph_sdk import get_client
|
|
4
|
-
from langchain_core.runnables.config import RunnableConfig
|
|
5
|
-
from auth0_ai.authorizers.ciba_authorizer import CIBAAuthorizer
|
|
6
|
-
from ..types import Auth0Graphs, Auth0Nodes
|
|
7
|
-
from .types import ICIBAGraph, BaseState
|
|
8
|
-
from .utils import get_tool_definition
|
|
9
|
-
|
|
10
|
-
def initialize_ciba(ciba_graph: ICIBAGraph):
|
|
11
|
-
async def handler(state: BaseState, config: RunnableConfig):
|
|
12
|
-
try:
|
|
13
|
-
ciba_params = ciba_graph.get_options()
|
|
14
|
-
tools = ciba_graph.get_tools()
|
|
15
|
-
tool_definition = get_tool_definition(state, tools)
|
|
16
|
-
|
|
17
|
-
if not tool_definition:
|
|
18
|
-
return Command(resume=True)
|
|
19
|
-
|
|
20
|
-
graph = ciba_graph.get_graph()
|
|
21
|
-
metadata, tool = tool_definition["metadata"], tool_definition["tool"]
|
|
22
|
-
ciba_options = metadata.options
|
|
23
|
-
|
|
24
|
-
langgraph = get_client(url=os.getenv("LANGGRAPH_API_URL", "http://localhost:54367"))
|
|
25
|
-
|
|
26
|
-
# Check if CIBA Poller Graph exists
|
|
27
|
-
search_result = await langgraph.assistants.search(graph_id=Auth0Graphs.CIBA_POLLER.value)
|
|
28
|
-
if not search_result:
|
|
29
|
-
raise ValueError(
|
|
30
|
-
f"[{Auth0Nodes.AUTH0_CIBA}] \"{Auth0Graphs.CIBA_POLLER}\" does not exist. Make sure to register the graph in your \"langgraph.json\"."
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
if ciba_options["on_approve_go_to"] not in graph.nodes:
|
|
34
|
-
raise ValueError(f"[{Auth0Nodes.AUTH0_CIBA}] \"{ciba_options["on_approve_go_to"]}\" is not a valid node.")
|
|
35
|
-
|
|
36
|
-
if ciba_options["on_reject_go_to"] not in graph.nodes:
|
|
37
|
-
raise ValueError(f"[{Auth0Nodes.AUTH0_CIBA}] \"{ciba_options["on_reject_go_to"]}\" is not a valid node.")
|
|
38
|
-
|
|
39
|
-
scheduler = ciba_params.config["scheduler"]
|
|
40
|
-
on_resume_invoke = ciba_params.config["on_resume_invoke"]
|
|
41
|
-
audience = ciba_params.audience
|
|
42
|
-
|
|
43
|
-
if not scheduler:
|
|
44
|
-
raise ValueError(f"[{Auth0Nodes.AUTH0_CIBA}] \"scheduler\" must be a \"function\" or a \"string\".")
|
|
45
|
-
|
|
46
|
-
if not on_resume_invoke:
|
|
47
|
-
raise ValueError(f"[{Auth0Nodes.AUTH0_CIBA}] \"on_resume_invoke\" must be defined.")
|
|
48
|
-
|
|
49
|
-
user_id = config.get("configurable", {}).get("user_id")
|
|
50
|
-
thread_id = config.get("metadata", {}).get("thread_id")
|
|
51
|
-
|
|
52
|
-
ciba_response = await CIBAAuthorizer.start(
|
|
53
|
-
{
|
|
54
|
-
"user_id": user_id,
|
|
55
|
-
"scope": ciba_options["scope"] or "openid",
|
|
56
|
-
"audience": audience,
|
|
57
|
-
"binding_message": ciba_options["binding_message"],
|
|
58
|
-
},
|
|
59
|
-
ciba_graph.get_authorizer_params(),
|
|
60
|
-
tool["args"],
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
scheduler_params = {
|
|
64
|
-
"tool_id": tool["id"],
|
|
65
|
-
"user_id": user_id,
|
|
66
|
-
"ciba_graph_id": Auth0Graphs.CIBA_POLLER.value,
|
|
67
|
-
"thread_id": thread_id,
|
|
68
|
-
"ciba_response": ciba_response,
|
|
69
|
-
"on_resume_invoke": on_resume_invoke,
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if callable(scheduler):
|
|
73
|
-
# Use Custom Scheduler
|
|
74
|
-
await scheduler(scheduler_params)
|
|
75
|
-
elif isinstance(scheduler, str):
|
|
76
|
-
# Use Langgraph SDK to schedule the task
|
|
77
|
-
await langgraph.crons.create_for_thread(
|
|
78
|
-
thread_id,
|
|
79
|
-
scheduler_params["ciba_graph_id"],
|
|
80
|
-
schedule="*/1 * * * *", # Default to every minute
|
|
81
|
-
input=scheduler_params,
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
print("CIBA Task Scheduled")
|
|
85
|
-
except Exception as e:
|
|
86
|
-
print(e)
|
|
87
|
-
state["auth0"] = {"error": str(e)}
|
|
88
|
-
|
|
89
|
-
return state
|
|
90
|
-
|
|
91
|
-
return handler
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
from typing import Awaitable, Callable
|
|
2
|
-
from langchain_core.messages import ToolMessage, AIMessage, ToolCall
|
|
3
|
-
from langgraph.types import interrupt, Command
|
|
4
|
-
from .types import ICIBAGraph, BaseState
|
|
5
|
-
from .utils import get_tool_definition
|
|
6
|
-
from auth0_ai.authorizers.ciba_authorizer import CibaAuthorizerCheckResponse
|
|
7
|
-
|
|
8
|
-
def initialize_hitl(ciba_graph: ICIBAGraph) -> Callable[[BaseState], Awaitable[Command]]:
|
|
9
|
-
async def handler(state: BaseState) -> Command:
|
|
10
|
-
tools = ciba_graph.get_tools()
|
|
11
|
-
tool_definition = get_tool_definition(state, tools)
|
|
12
|
-
|
|
13
|
-
# if no tool calls, resume
|
|
14
|
-
if not tool_definition:
|
|
15
|
-
return Command(resume=True)
|
|
16
|
-
|
|
17
|
-
# wait for user approval
|
|
18
|
-
human_review = interrupt("A push notification has been sent to your device.")
|
|
19
|
-
|
|
20
|
-
metadata, tool, message = tool_definition["metadata"], tool_definition["tool"], tool_definition["message"]
|
|
21
|
-
|
|
22
|
-
if human_review["status"] == CibaAuthorizerCheckResponse.APPROVED.value:
|
|
23
|
-
updated_message = AIMessage(
|
|
24
|
-
id=message.id,
|
|
25
|
-
content="The user has approved the transaction",
|
|
26
|
-
tool_calls=[
|
|
27
|
-
ToolCall(
|
|
28
|
-
name=tool["name"],
|
|
29
|
-
args=tool["args"],
|
|
30
|
-
id=tool["id"],
|
|
31
|
-
)
|
|
32
|
-
],
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
return Command(
|
|
36
|
-
goto=metadata.options["on_approve_go_to"],
|
|
37
|
-
update={"messages": [updated_message]},
|
|
38
|
-
)
|
|
39
|
-
else:
|
|
40
|
-
tool_message = ToolMessage(
|
|
41
|
-
name=tool["name"],
|
|
42
|
-
content="The user has rejected the transaction.",
|
|
43
|
-
tool_call_id=tool["id"],
|
|
44
|
-
)
|
|
45
|
-
return Command(
|
|
46
|
-
goto=metadata.options["on_reject_go_to"],
|
|
47
|
-
update={"messages": [tool_message]},
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
return handler
|