auth0-ai-langchain 0.1.0__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 +158 -0
- auth0_ai_langchain/__init__.py +3 -0
- auth0_ai_langchain/auth0_ai.py +43 -0
- auth0_ai_langchain/ciba/__init__.py +0 -0
- auth0_ai_langchain/ciba/ciba_graph/ciba_graph.py +109 -0
- auth0_ai_langchain/ciba/ciba_graph/initialize_ciba.py +91 -0
- auth0_ai_langchain/ciba/ciba_graph/initialize_hitl.py +50 -0
- auth0_ai_langchain/ciba/ciba_graph/types.py +115 -0
- auth0_ai_langchain/ciba/ciba_graph/utils.py +17 -0
- auth0_ai_langchain/ciba/ciba_poller_graph.py +94 -0
- auth0_ai_langchain/ciba/types.py +8 -0
- auth0_ai_langchain/federated_connections/__init__.py +3 -0
- auth0_ai_langchain/federated_connections/federated_connection_authorizer.py +52 -0
- auth0_ai_langchain/fga/fga_authorizer.py +3 -0
- auth0_ai_langchain/utils/interrupt.py +13 -0
- auth0_ai_langchain-0.1.0.dist-info/LICENSE +176 -0
- auth0_ai_langchain-0.1.0.dist-info/METADATA +227 -0
- auth0_ai_langchain-0.1.0.dist-info/RECORD +19 -0
- auth0_ai_langchain-0.1.0.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 (Optional[ClientConfiguration]): 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 (Optional[object]): 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 (Optional[object]): 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,43 @@
|
|
|
1
|
+
from typing import Callable, Optional
|
|
2
|
+
from langchain_core.runnables.config import RunnableConfig
|
|
3
|
+
from langchain_core.tools import BaseTool
|
|
4
|
+
from auth0_ai.credentials import Credential
|
|
5
|
+
from auth0_ai.authorizers.types import AuthorizerParams
|
|
6
|
+
from auth0_ai.authorizers.federated_connection_authorizer import FederatedConnectionAuthorizerParams
|
|
7
|
+
from .federated_connections.federated_connection_authorizer import FederatedConnectionAuthorizer
|
|
8
|
+
from .ciba.ciba_graph.ciba_graph import CIBAGraph
|
|
9
|
+
from .ciba.ciba_graph.types import CIBAGraphOptions
|
|
10
|
+
|
|
11
|
+
def get_access_token(config: RunnableConfig) -> Credential:
|
|
12
|
+
"""
|
|
13
|
+
Fetch the access token obtained during the CIBA flow.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
config(RunnableConfig): LangGraph runnable configuration instance.
|
|
17
|
+
"""
|
|
18
|
+
return config.get("configurable", {}).get("_credentials", {}).get("access_token")
|
|
19
|
+
|
|
20
|
+
class Auth0AI():
|
|
21
|
+
def __init__(self, config: Optional[AuthorizerParams] = None):
|
|
22
|
+
self._graph: Optional[CIBAGraph] = None
|
|
23
|
+
self.config = config
|
|
24
|
+
|
|
25
|
+
def with_async_user_confirmation(self, **options: CIBAGraphOptions) -> CIBAGraph:
|
|
26
|
+
"""
|
|
27
|
+
Initializes and registers a state graph for conditional trade operations using CIBA.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
options (Optional[CIBAGraphOptions]): The base CIBA options.
|
|
31
|
+
"""
|
|
32
|
+
self._graph = CIBAGraph(CIBAGraphOptions(**options), self.config)
|
|
33
|
+
return self._graph
|
|
34
|
+
|
|
35
|
+
def with_federated_connection(self, **options: FederatedConnectionAuthorizerParams) -> Callable[[BaseTool], BaseTool]:
|
|
36
|
+
"""
|
|
37
|
+
Protects a tool execution with the Federated Connection authorizer.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
options (FederatedConnectionAuthorizerParams): The Federated Connections authorizer options.
|
|
41
|
+
"""
|
|
42
|
+
authorizer = FederatedConnectionAuthorizer(FederatedConnectionAuthorizerParams(**options), self.config)
|
|
43
|
+
return authorizer.authorizer()
|
|
File without changes
|
|
@@ -0,0 +1,109 @@
|
|
|
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
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from typing import Optional, List, Callable, Union, Awaitable, TypedDict
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from langgraph.graph import StateGraph
|
|
4
|
+
from langchain_core.messages import AIMessage, ToolMessage
|
|
5
|
+
from auth0_ai.authorizers.types import AuthorizerParams
|
|
6
|
+
from auth0_ai.authorizers.ciba_authorizer import AuthorizeResponse
|
|
7
|
+
|
|
8
|
+
class Auth0State(TypedDict):
|
|
9
|
+
error: str
|
|
10
|
+
|
|
11
|
+
class BaseState(TypedDict):
|
|
12
|
+
task_id: str
|
|
13
|
+
messages: List[Union[AIMessage, ToolMessage]]
|
|
14
|
+
auth0: Optional[Auth0State] = None
|
|
15
|
+
|
|
16
|
+
class SchedulerParams:
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
user_id: str,
|
|
20
|
+
thread_id: str,
|
|
21
|
+
ciba_graph_id: str,
|
|
22
|
+
ciba_response: AuthorizeResponse,
|
|
23
|
+
tool_id: Optional[str] = None,
|
|
24
|
+
on_resume_invoke: str = "",
|
|
25
|
+
):
|
|
26
|
+
self.user_id = user_id
|
|
27
|
+
self.thread_id = thread_id
|
|
28
|
+
self.tool_id = tool_id
|
|
29
|
+
self.on_resume_invoke = on_resume_invoke
|
|
30
|
+
self.ciba_graph_id = ciba_graph_id
|
|
31
|
+
self.ciba_response = ciba_response
|
|
32
|
+
|
|
33
|
+
class CIBAOptions():
|
|
34
|
+
"""
|
|
35
|
+
The CIBA options.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
binding_message (Union[str, Callable[..., Awaitable[str]]]): A human-readable string to display to the user, or a function that resolves it.
|
|
39
|
+
scope (Optional[str]): Space-separated list of OIDC and custom API scopes.
|
|
40
|
+
on_approve_go_to (Optional[str]): A node name to redirect the flow after user approval.
|
|
41
|
+
on_reject_go_to (Optional[str]): A node name to redirect the flow after user rejection.
|
|
42
|
+
audience (Optional[str]): Unique identifier of the audience for an issued token.
|
|
43
|
+
request_expiry (Optional[int]): To configure a custom expiry time in seconds for CIBA request, pass a number between 1 and 300.
|
|
44
|
+
"""
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
binding_message: Union[str, Callable[..., Awaitable[str]]],
|
|
48
|
+
scope: Optional[str] = None,
|
|
49
|
+
on_approve_go_to: Optional[str] = None,
|
|
50
|
+
on_reject_go_to: Optional[str] = None,
|
|
51
|
+
audience: Optional[str] = None,
|
|
52
|
+
request_expiry: Optional[int] = None,
|
|
53
|
+
):
|
|
54
|
+
self.binding_message = binding_message
|
|
55
|
+
self.scope = scope
|
|
56
|
+
self.on_approve_go_to = on_approve_go_to
|
|
57
|
+
self.on_reject_go_to = on_reject_go_to
|
|
58
|
+
self.audience = audience
|
|
59
|
+
self.request_expiry = request_expiry
|
|
60
|
+
|
|
61
|
+
class ProtectedTool():
|
|
62
|
+
def __init__(self, tool_name: str, options: CIBAOptions):
|
|
63
|
+
self.tool_name = tool_name
|
|
64
|
+
self.options = options
|
|
65
|
+
|
|
66
|
+
class CIBAGraphOptionsConfig:
|
|
67
|
+
def __init__(self, on_resume_invoke: str, scheduler: Union[str, Callable[[SchedulerParams], Awaitable[None]]]):
|
|
68
|
+
self.on_resume_invoke = on_resume_invoke
|
|
69
|
+
self.scheduler = scheduler
|
|
70
|
+
|
|
71
|
+
class CIBAGraphOptions():
|
|
72
|
+
"""
|
|
73
|
+
The base CIBA options.
|
|
74
|
+
|
|
75
|
+
Attributes:
|
|
76
|
+
config (CIBAGraphOptionsConfig): Configuration options.
|
|
77
|
+
scope (Optional[str]): Space-separated list of OIDC and custom API scopes.
|
|
78
|
+
on_approve_go_to (Optional[str]): A node name to redirect the flow after user approval.
|
|
79
|
+
on_reject_go_to (Optional[str]): A node name to redirect the flow after user rejection.
|
|
80
|
+
audience (Optional[str]): Unique identifier of the audience for an issued token.
|
|
81
|
+
request_expiry (Optional[int]): To configure a custom expiry time in seconds for CIBA request, pass a number between 1 and 300.
|
|
82
|
+
"""
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
config: CIBAGraphOptionsConfig,
|
|
86
|
+
scope: Optional[str] = None,
|
|
87
|
+
on_approve_go_to: Optional[str] = None,
|
|
88
|
+
on_reject_go_to: Optional[str] = None,
|
|
89
|
+
audience: Optional[str] = None,
|
|
90
|
+
request_expiry: Optional[int] = None,
|
|
91
|
+
|
|
92
|
+
):
|
|
93
|
+
self.config = config
|
|
94
|
+
self.scope = scope
|
|
95
|
+
self.on_approve_go_to = on_approve_go_to
|
|
96
|
+
self.on_reject_go_to = on_reject_go_to
|
|
97
|
+
self.audience = audience
|
|
98
|
+
self.request_expiry = request_expiry
|
|
99
|
+
|
|
100
|
+
class ICIBAGraph(ABC):
|
|
101
|
+
@abstractmethod
|
|
102
|
+
def get_tools(self) -> List[ProtectedTool]:
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
@abstractmethod
|
|
106
|
+
def get_graph(self) -> StateGraph:
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
@abstractmethod
|
|
110
|
+
def get_authorizer_params(self) -> Optional[AuthorizerParams]:
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
@abstractmethod
|
|
114
|
+
def get_options(self) -> Optional[CIBAGraphOptions]:
|
|
115
|
+
pass
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Optional, List
|
|
2
|
+
from .types import ProtectedTool, BaseState
|
|
3
|
+
|
|
4
|
+
def get_tool_definition(state: BaseState, tools: List[ProtectedTool]) -> Optional[dict]:
|
|
5
|
+
message = state["messages"][-1]
|
|
6
|
+
|
|
7
|
+
if not hasattr(message, "tool_calls") or not message.tool_calls:
|
|
8
|
+
return None
|
|
9
|
+
|
|
10
|
+
tool_calls = message.tool_calls
|
|
11
|
+
tool = tool_calls[-1]
|
|
12
|
+
metadata = next((t for t in tools if t.tool_name == tool["name"]), None)
|
|
13
|
+
|
|
14
|
+
if not metadata:
|
|
15
|
+
return None
|
|
16
|
+
|
|
17
|
+
return {"metadata": metadata, "tool": tool, "message": message}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from langgraph.graph import StateGraph, END, START
|
|
3
|
+
from typing import Awaitable, Callable, Union, Optional, TypedDict
|
|
4
|
+
from langgraph_sdk import get_client
|
|
5
|
+
from langgraph_sdk.schema import Command
|
|
6
|
+
from auth0_ai.authorizers.ciba_authorizer import CIBAAuthorizer, CibaAuthorizerCheckResponse, AuthorizeResponse
|
|
7
|
+
from auth0_ai.credentials import Credentials
|
|
8
|
+
from auth0_ai.token_response import TokenResponse
|
|
9
|
+
from langchain_auth0_ai.ciba.types import Auth0Graphs
|
|
10
|
+
|
|
11
|
+
class State(TypedDict):
|
|
12
|
+
ciba_response: AuthorizeResponse
|
|
13
|
+
on_resume_invoke: str
|
|
14
|
+
thread_id: str
|
|
15
|
+
user_id: str
|
|
16
|
+
|
|
17
|
+
# Internal
|
|
18
|
+
task_id: str
|
|
19
|
+
tool_id: str
|
|
20
|
+
status: CibaAuthorizerCheckResponse
|
|
21
|
+
token_response: Optional[TokenResponse]
|
|
22
|
+
|
|
23
|
+
def ciba_poller_graph(on_stop_scheduler: Union[str, Callable[[State], Awaitable[None]]]):
|
|
24
|
+
"""
|
|
25
|
+
A LangGraph graph to monitor the status of a CIBA transaction.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
on_stop_scheduler (Union[str, Callable[[State], Awaitable[None]]]): A graph name to redirect the flow, or a function to execute when the CIBA transaction expires.
|
|
29
|
+
"""
|
|
30
|
+
async def check_status(state: State):
|
|
31
|
+
try:
|
|
32
|
+
res = await CIBAAuthorizer.check(state["ciba_response"]["auth_req_id"])
|
|
33
|
+
state["token_response"] = res.get("token")
|
|
34
|
+
state["status"] = res.get("status")
|
|
35
|
+
except Exception as e:
|
|
36
|
+
print(f"Error in check_status: {e}")
|
|
37
|
+
return state
|
|
38
|
+
|
|
39
|
+
async def stop_scheduler(state: State):
|
|
40
|
+
try:
|
|
41
|
+
if isinstance(on_stop_scheduler, str):
|
|
42
|
+
langgraph = get_client(url=os.getenv("LANGGRAPH_API_URL", "http://localhost:54367"))
|
|
43
|
+
await langgraph.crons.create_for_thread(state.thread_id, Auth0Graphs.CIBA_POLLER.value)
|
|
44
|
+
elif callable(on_stop_scheduler):
|
|
45
|
+
await on_stop_scheduler(state)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
print(f"Error in stop_scheduler: {e}")
|
|
48
|
+
return state
|
|
49
|
+
|
|
50
|
+
async def resume_agent(state: State):
|
|
51
|
+
langgraph = get_client(url=os.getenv("LANGGRAPH_API_URL", "http://localhost:54367"))
|
|
52
|
+
_credentials: Credentials = None
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
if state["status"] == CibaAuthorizerCheckResponse.APPROVED:
|
|
56
|
+
_credentials = {
|
|
57
|
+
"access_token": {
|
|
58
|
+
"type": state["token_response"].get("token_type", "Bearer"),
|
|
59
|
+
"value": state["token_response"].get("access_token"),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await langgraph.runs.wait(
|
|
64
|
+
state["thread_id"],
|
|
65
|
+
state["on_resume_invoke"],
|
|
66
|
+
config={
|
|
67
|
+
"configurable": {"_credentials": _credentials} # this is only for this run / thread_id
|
|
68
|
+
},
|
|
69
|
+
command=Command(resume={"status": state["status"]})
|
|
70
|
+
)
|
|
71
|
+
except Exception as e:
|
|
72
|
+
print(f"Error in resume_agent: {e}")
|
|
73
|
+
|
|
74
|
+
return state
|
|
75
|
+
|
|
76
|
+
async def should_continue(state: State):
|
|
77
|
+
status = state.get("status")
|
|
78
|
+
if status == CibaAuthorizerCheckResponse.PENDING:
|
|
79
|
+
return END
|
|
80
|
+
elif status == CibaAuthorizerCheckResponse.EXPIRED:
|
|
81
|
+
return "stop_scheduler"
|
|
82
|
+
elif status in [CibaAuthorizerCheckResponse.APPROVED, CibaAuthorizerCheckResponse.REJECTED]:
|
|
83
|
+
return "resume_agent"
|
|
84
|
+
return END
|
|
85
|
+
|
|
86
|
+
state_graph = StateGraph(State)
|
|
87
|
+
state_graph.add_node("check_status", check_status)
|
|
88
|
+
state_graph.add_node("stop_scheduler", stop_scheduler)
|
|
89
|
+
state_graph.add_node("resume_agent", resume_agent)
|
|
90
|
+
state_graph.add_edge(START, "check_status")
|
|
91
|
+
state_graph.add_edge("resume_agent", "stop_scheduler")
|
|
92
|
+
state_graph.add_conditional_edges("check_status", should_continue)
|
|
93
|
+
|
|
94
|
+
return state_graph
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
from auth0_ai.interrupts.federated_connection_interrupt import FederatedConnectionError as FederatedConnectionError
|
|
2
|
+
from auth0_ai.authorizers.federated_connection_authorizer import get_access_token_for_connection as get_access_token_for_connection
|
|
3
|
+
from .federated_connection_authorizer import FederatedConnectionAuthorizer as FederatedConnectionAuthorizer
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
from abc import ABC
|
|
3
|
+
from auth0_ai.authorizers.federated_connection_authorizer import FederatedConnectionAuthorizerBase, FederatedConnectionAuthorizerParams
|
|
4
|
+
from auth0_ai.authorizers.types import AuthorizerParams
|
|
5
|
+
from auth0_ai.interrupts.federated_connection_interrupt import FederatedConnectionInterrupt
|
|
6
|
+
from langchain_core.tools import BaseTool, tool
|
|
7
|
+
from langchain_core.runnables import ensure_config
|
|
8
|
+
from ..utils.interrupt import to_graph_interrupt
|
|
9
|
+
|
|
10
|
+
async def get_refresh_token(*_args, **_kwargs) -> str | None:
|
|
11
|
+
return ensure_config().get("configurable", {}).get("_credentials", {}).get("refresh_token")
|
|
12
|
+
|
|
13
|
+
class FederatedConnectionAuthorizer(FederatedConnectionAuthorizerBase, ABC):
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
options: FederatedConnectionAuthorizerParams,
|
|
17
|
+
config: AuthorizerParams = None,
|
|
18
|
+
):
|
|
19
|
+
if options.refresh_token.value is None:
|
|
20
|
+
options = copy.copy(options)
|
|
21
|
+
options.refresh_token.value = get_refresh_token
|
|
22
|
+
|
|
23
|
+
super().__init__(options, config)
|
|
24
|
+
|
|
25
|
+
def _handle_authorization_interrupts(self, err: FederatedConnectionInterrupt) -> None:
|
|
26
|
+
raise to_graph_interrupt(err)
|
|
27
|
+
|
|
28
|
+
def authorizer(self):
|
|
29
|
+
def wrapped_tool(t: BaseTool) -> BaseTool:
|
|
30
|
+
async def execute_fn(*_args, **kwargs):
|
|
31
|
+
return await t.ainvoke(input=kwargs)
|
|
32
|
+
|
|
33
|
+
tool_fn = self.protect(
|
|
34
|
+
lambda *_args, **_kwargs: {
|
|
35
|
+
"thread_id": ensure_config().get("configurable", {}).get("thread_id"),
|
|
36
|
+
"checkpoint_ns": ensure_config().get("configurable", {}).get("checkpoint_ns"),
|
|
37
|
+
"run_id": ensure_config().get("configurable", {}).get("run_id"),
|
|
38
|
+
"tool_call_id": ensure_config().get("configurable", {}).get("tool_call_id"), # TODO: review this
|
|
39
|
+
},
|
|
40
|
+
execute_fn
|
|
41
|
+
)
|
|
42
|
+
tool_fn.__name__ = t.name
|
|
43
|
+
|
|
44
|
+
return tool(
|
|
45
|
+
tool_fn,
|
|
46
|
+
description=t.description,
|
|
47
|
+
return_direct=t.return_direct,
|
|
48
|
+
args_schema=t.args_schema,
|
|
49
|
+
response_format=t.response_format,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return wrapped_tool
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from auth0_ai.interrupts.auth0_interrupt import Auth0Interrupt
|
|
2
|
+
from langgraph.errors import GraphInterrupt
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def to_graph_interrupt(interrupt: Auth0Interrupt) -> GraphInterrupt:
|
|
6
|
+
return GraphInterrupt([
|
|
7
|
+
{
|
|
8
|
+
"value": interrupt,
|
|
9
|
+
"when": "during",
|
|
10
|
+
"resumable": True,
|
|
11
|
+
"ns": [f"auth0AI:{interrupt.__class__.__name__}:{interrupt.code}"]
|
|
12
|
+
}
|
|
13
|
+
])
|
|
@@ -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,227 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: auth0-ai-langchain
|
|
3
|
+
Version: 0.1.0
|
|
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 (>=0.1.0,<0.2.0)
|
|
15
|
+
Requires-Dist: langchain (>=0.3.20,<0.4.0)
|
|
16
|
+
Requires-Dist: langchain-core (>=0.3.43,<0.4.0)
|
|
17
|
+
Requires-Dist: langgraph (>=0.3.25,<0.4.0)
|
|
18
|
+
Requires-Dist: langgraph-sdk (>=0.1.55,<0.2.0)
|
|
19
|
+
Requires-Dist: openfga-sdk (>=0.9.0,<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](../../examples/async-user-confirmation/langchain-examples/).
|
|
42
|
+
|
|
43
|
+
## Authorization for Tools
|
|
44
|
+
|
|
45
|
+
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.
|
|
46
|
+
|
|
47
|
+
Full example of [Authorization for Tools](../../examples/authorization-for-tools/langchain-examples/).
|
|
48
|
+
|
|
49
|
+
1. Create an instance of FGA Authorizer:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from langchain_auth0_ai.fga.fga_authorizer import FGAAuthorizer, FGAAuthorizerOptions
|
|
53
|
+
|
|
54
|
+
fga = FGAAuthorizer.create()
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Note**: Here, you can configure and specify your FGA credentials. By `default`, they are read from environment variables:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
FGA_STORE_ID="<fga-store-id>"
|
|
61
|
+
FGA_CLIENT_ID="<fga-client-id>"
|
|
62
|
+
FGA_CLIENT_SECRET="<fga-client-secret>"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
2. Define the FGA query (`build_query`) and, optionally, the `on_unauthorized` handler:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from langchain_core.runnables import ensure_config
|
|
69
|
+
|
|
70
|
+
async def build_fga_query(tool_input):
|
|
71
|
+
user_id = ensure_config().get("configurable",{}).get("user_id")
|
|
72
|
+
return {
|
|
73
|
+
"user": f"user:{user_id}",
|
|
74
|
+
"object": f"asset:{tool_input["ticker"]}",
|
|
75
|
+
"relation": "can_buy",
|
|
76
|
+
"context": {"current_time": datetime.now(timezone.utc).isoformat()}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
def on_unauthorized(tool_input):
|
|
80
|
+
return f"The user is not allowed to buy {tool_input["qty"]} shares of {tool_input["ticker"]}."
|
|
81
|
+
|
|
82
|
+
use_fga = fga(FGAAuthorizerOptions(
|
|
83
|
+
build_query=build_fga_query,
|
|
84
|
+
on_unauthorized=on_unauthorized,
|
|
85
|
+
))
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Note**: The parameters given to the `build_query` and `on_unauthorized` functions are the same as those provided to the tool function.
|
|
89
|
+
|
|
90
|
+
3. Wrap the tool:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from langchain_core.tools import StructuredTool
|
|
94
|
+
|
|
95
|
+
async def buy_tool_function(ticker: str, qty: int) -> str:
|
|
96
|
+
# TODO: implement buy operation
|
|
97
|
+
return f"Purchased {qty} shares of {ticker}"
|
|
98
|
+
|
|
99
|
+
func=use_fga(buy_tool_function)
|
|
100
|
+
|
|
101
|
+
buy_tool = StructuredTool(
|
|
102
|
+
func=func,
|
|
103
|
+
coroutine=func,
|
|
104
|
+
name="buy",
|
|
105
|
+
description="Use this function to buy stocks",
|
|
106
|
+
)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Calling APIs On User's Behalf
|
|
110
|
+
|
|
111
|
+
The `Auth0AI.with_federated_connection` function exchanges user's refresh token taken from the runnable configuration (`config.configurable._credentials.refresh_token`) for a Federated Connection API token.
|
|
112
|
+
|
|
113
|
+
Full Example of [Calling APIs On User's Behalf](../../examples/calling-apis/langchain-examples/).
|
|
114
|
+
|
|
115
|
+
1. Define a tool with the proper authorizer:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from langchain_auth0_ai.auth0_ai import Auth0AI
|
|
119
|
+
from langchain_auth0_ai.federated_connections import get_access_token_for_connection
|
|
120
|
+
from langchain_core.tools import StructuredTool
|
|
121
|
+
|
|
122
|
+
auth0_ai = Auth0AI()
|
|
123
|
+
|
|
124
|
+
with_google_calendar_access = auth0_ai.with_federated_connection(
|
|
125
|
+
connection="google-oauth2",
|
|
126
|
+
scopes=["https://www.googleapis.com/auth/calendar.freebusy"]
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def tool_function(date: datetime):
|
|
130
|
+
access_token = get_access_token_for_connection()
|
|
131
|
+
# Call Google API
|
|
132
|
+
|
|
133
|
+
check_calendar_tool = with_google_calendar_access(
|
|
134
|
+
StructuredTool(
|
|
135
|
+
name="check_user_calendar",
|
|
136
|
+
description="Use this function to check if the user is available on a certain date and time",
|
|
137
|
+
func=tool_function,
|
|
138
|
+
# ...
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
2. Add a node to your graph for your tools:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
workflow = (
|
|
147
|
+
StateGraph(State)
|
|
148
|
+
.add_node(
|
|
149
|
+
"tools",
|
|
150
|
+
ToolNode(
|
|
151
|
+
[
|
|
152
|
+
check_calendar_tool,
|
|
153
|
+
# ...
|
|
154
|
+
],
|
|
155
|
+
# The error handler should be disabled to allow interruptions to be triggered from within tools.
|
|
156
|
+
handle_tool_errors=False
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
# ...
|
|
160
|
+
)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
3. Handle interruptions properly. If the tool does not have access to user's Google Calendar, it will throw an interruption.
|
|
164
|
+
|
|
165
|
+
## RAG with FGA
|
|
166
|
+
|
|
167
|
+
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.
|
|
168
|
+
|
|
169
|
+
Full Example of [RAG Application](../../examples/authorization-for-rag/langchain-examples/).
|
|
170
|
+
|
|
171
|
+
Create a retriever instance using the `FGARetriever` class.
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from langchain.vectorstores import VectorStoreIndex
|
|
175
|
+
from langchain.schema import Document
|
|
176
|
+
from langchain_auth0_ai import FGARetriever
|
|
177
|
+
from openfga_sdk.client.models import ClientCheckRequest
|
|
178
|
+
from openfga_sdk import ClientConfiguration
|
|
179
|
+
from openfga_sdk.credentials import CredentialConfiguration, Credentials
|
|
180
|
+
|
|
181
|
+
# Define some docs:
|
|
182
|
+
documents = [
|
|
183
|
+
Document(page_content="This is a public doc", metadata={"doc_id": "public-doc"}),
|
|
184
|
+
Document(page_content="This is a private doc", metadata={"doc_id": "private-doc"}),
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
# Create a vector store:
|
|
188
|
+
vector_store = VectorStoreIndex.from_documents(documents)
|
|
189
|
+
|
|
190
|
+
# Create a retriever:
|
|
191
|
+
base_retriever = vector_store.as_retriever()
|
|
192
|
+
|
|
193
|
+
# Create the FGA retriever wrapper:
|
|
194
|
+
retriever = FGARetriever(
|
|
195
|
+
base_retriever,
|
|
196
|
+
build_query=lambda node: ClientCheckRequest(
|
|
197
|
+
user=f'user:{user}',
|
|
198
|
+
object=f'doc:{node.metadata["doc_id"]}',
|
|
199
|
+
relation="viewer",
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Create a query engine:
|
|
204
|
+
query_engine = RetrieverQueryEngine.from_args(
|
|
205
|
+
retriever=retriever,
|
|
206
|
+
llm=OpenAI()
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Query:
|
|
210
|
+
response = query_engine.query("What is the forecast for ZEKO?")
|
|
211
|
+
|
|
212
|
+
print(response)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
<p align="center">
|
|
218
|
+
<picture>
|
|
219
|
+
<source media="(prefers-color-scheme: light)" srcset="https://cdn.auth0.com/website/sdks/logos/auth0_light_mode.png" width="150">
|
|
220
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.auth0.com/website/sdks/logos/auth0_dark_mode.png" width="150">
|
|
221
|
+
<img alt="Auth0 Logo" src="https://cdn.auth0.com/website/sdks/logos/auth0_light_mode.png" width="150">
|
|
222
|
+
</picture>
|
|
223
|
+
</p>
|
|
224
|
+
<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>
|
|
225
|
+
<p align="center">
|
|
226
|
+
This project is licensed under the Apache 2.0 license. See the <a href="/LICENSE"> LICENSE</a> file for more info.</p>
|
|
227
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
auth0_ai_langchain/FGARetriever.py,sha256=6nQXRkbDLHZt9zYZJsS5iQljrogQVLW0aVwDIf6Mpac,6002
|
|
2
|
+
auth0_ai_langchain/__init__.py,sha256=I331Kz-q97ZU7TfXaOR5UBbJamGEJ15twbf2HP1iCHs,67
|
|
3
|
+
auth0_ai_langchain/auth0_ai.py,sha256=8NUV_80SxR8qQt_3RQGf0Oga178kChuROHuhz7rfOyU,1919
|
|
4
|
+
auth0_ai_langchain/ciba/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
auth0_ai_langchain/ciba/ciba_graph/ciba_graph.py,sha256=Wi7qXSMzvcfqdO8WsUJejmQzOVM469TFJCkH7eRlaR8,4115
|
|
6
|
+
auth0_ai_langchain/ciba/ciba_graph/initialize_ciba.py,sha256=a41KedBzxfLqG2AhvkFnemcEqWwQWVh1r-Oro-qJX-M,3752
|
|
7
|
+
auth0_ai_langchain/ciba/ciba_graph/initialize_hitl.py,sha256=CR3jMolZYYOBHx1AXb6yERBaZThMg677qGGo_vRy6I8,1901
|
|
8
|
+
auth0_ai_langchain/ciba/ciba_graph/types.py,sha256=NZS99vPOgJRc2O7mO5MWj6nAa4RSG1R5oSvzYhkz0RA,4234
|
|
9
|
+
auth0_ai_langchain/ciba/ciba_graph/utils.py,sha256=ZPAh0Gs7Hj59_xngg8M7yx1v52dTn2pNpMNRpFKCSII,560
|
|
10
|
+
auth0_ai_langchain/ciba/ciba_poller_graph.py,sha256=aKTOsnlZ-Uy6jAvEh2JBOYP0UGgZiDJ5DzDv7fXMRvM,3792
|
|
11
|
+
auth0_ai_langchain/ciba/types.py,sha256=gybqYEprklZwcMBgaWFooBsJ1GcNUK8ZWRvAX5PZWdE,177
|
|
12
|
+
auth0_ai_langchain/federated_connections/__init__.py,sha256=OJCWTnxYuaWjn8FyFGjCAF7m5Y4Eigkzx7a59atFFFg,356
|
|
13
|
+
auth0_ai_langchain/federated_connections/federated_connection_authorizer.py,sha256=ZLF4p7fPTrODOeHWIShfBTsReSy27u73rIp0L_Umhjg,2218
|
|
14
|
+
auth0_ai_langchain/fga/fga_authorizer.py,sha256=uDaGDSXaxQd1X-2w2zTvnfizMB-DtQ-1G6SIaDNBrho,137
|
|
15
|
+
auth0_ai_langchain/utils/interrupt.py,sha256=JoYJkigDEAPRHZtjo6gw6k3439E4i1O7F4_0ExkL_RE,405
|
|
16
|
+
auth0_ai_langchain-0.1.0.dist-info/LICENSE,sha256=Lu_2YH0oK8b_VVisAhNQ2WIdtwY8pSU2PLbll-y6Cj8,9792
|
|
17
|
+
auth0_ai_langchain-0.1.0.dist-info/METADATA,sha256=6HQKAj7PdTgh4u45LyBcoLv-iYFEOOefBLoJ3R8x53w,7926
|
|
18
|
+
auth0_ai_langchain-0.1.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
|
19
|
+
auth0_ai_langchain-0.1.0.dist-info/RECORD,,
|