langgraph-plainid 1.0.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.
- langgraph_plainid/__init__.py +23 -0
- langgraph_plainid/models/__init__.py +0 -0
- langgraph_plainid/models/state/__init__.py +0 -0
- langgraph_plainid/models/state/agent_state.py +35 -0
- langgraph_plainid/nodes/__init__.py +0 -0
- langgraph_plainid/nodes/anonymizer_node.py +56 -0
- langgraph_plainid/nodes/base_node.py +112 -0
- langgraph_plainid/nodes/categorization_node.py +49 -0
- langgraph_plainid/nodes/retrieval_node.py +56 -0
- langgraph_plainid-1.0.0.dist-info/METADATA +250 -0
- langgraph_plainid-1.0.0.dist-info/RECORD +12 -0
- langgraph_plainid-1.0.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from langgraph_plainid.models.state.agent_state import (
|
|
2
|
+
AgentState,
|
|
3
|
+
AnonymizationState,
|
|
4
|
+
CategorizationState,
|
|
5
|
+
ErrorDetails,
|
|
6
|
+
RetrievalState,
|
|
7
|
+
)
|
|
8
|
+
from langgraph_plainid.nodes.anonymizer_node import AnonymizerNode
|
|
9
|
+
from langgraph_plainid.nodes.base_node import BaseNode
|
|
10
|
+
from langgraph_plainid.nodes.categorization_node import CategorizationNode
|
|
11
|
+
from langgraph_plainid.nodes.retrieval_node import RetrievalNode
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"AgentState",
|
|
15
|
+
"AnonymizationState",
|
|
16
|
+
"AnonymizerNode",
|
|
17
|
+
"BaseNode",
|
|
18
|
+
"CategorizationNode",
|
|
19
|
+
"CategorizationState",
|
|
20
|
+
"ErrorDetails",
|
|
21
|
+
"RetrievalNode",
|
|
22
|
+
"RetrievalState",
|
|
23
|
+
]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Optional, TypedDict
|
|
2
|
+
|
|
3
|
+
from langchain_core.documents import Document
|
|
4
|
+
|
|
5
|
+
from core_plainid.models.context.request_context import RequestContext
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ErrorDetails(TypedDict):
|
|
9
|
+
error_message: Optional[str]
|
|
10
|
+
error: Exception
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AnonymizationState(TypedDict):
|
|
14
|
+
query: str
|
|
15
|
+
output_text: Optional[str]
|
|
16
|
+
error_details: Optional[ErrorDetails]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RetrievalState(TypedDict):
|
|
20
|
+
query: str
|
|
21
|
+
resource_types: list[str]
|
|
22
|
+
retrieved_documents: Optional[list[Document]]
|
|
23
|
+
error_details: Optional[ErrorDetails]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CategorizationState(TypedDict):
|
|
27
|
+
query: str
|
|
28
|
+
error_details: Optional[ErrorDetails]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AgentState(TypedDict):
|
|
32
|
+
request_context: RequestContext
|
|
33
|
+
anonymization: Optional[AnonymizationState]
|
|
34
|
+
retrieval: Optional[RetrievalState]
|
|
35
|
+
categorization: Optional[CategorizationState]
|
|
File without changes
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
from core_plainid.anonymization.base_anonymizer import BaseAnonymizer
|
|
4
|
+
|
|
5
|
+
from langgraph_plainid.models.state.agent_state import AgentState
|
|
6
|
+
from langgraph_plainid.nodes.base_node import BaseNode
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AnonymizerNode(BaseNode):
|
|
10
|
+
"""LangGraph node that anonymizes text via a PlainID anonymizer.
|
|
11
|
+
|
|
12
|
+
Reads the query from ``state["anonymization"]["query"]`` and writes the
|
|
13
|
+
result to ``state["anonymization"]["output_text"]``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
anonymizer: BaseAnonymizer,
|
|
19
|
+
next_node: Optional[str] = None,
|
|
20
|
+
next_node_on_error: Optional[str] = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
super().__init__(
|
|
23
|
+
substate_key="anonymization",
|
|
24
|
+
next_node=next_node,
|
|
25
|
+
next_node_on_error=next_node_on_error,
|
|
26
|
+
)
|
|
27
|
+
self._anonymizer = anonymizer
|
|
28
|
+
|
|
29
|
+
def _execute(self, state: AgentState) -> dict[str, Any]:
|
|
30
|
+
anonymization = state["anonymization"]
|
|
31
|
+
request_context = state.get("request_context")
|
|
32
|
+
|
|
33
|
+
output_text = self._anonymizer.anonymize(
|
|
34
|
+
anonymization["query"],
|
|
35
|
+
request_context=request_context,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return self._build_updated_state(output_text)
|
|
39
|
+
|
|
40
|
+
async def _aexecute(self, state: AgentState) -> dict[str, Any]:
|
|
41
|
+
anonymization = state["anonymization"]
|
|
42
|
+
request_context = state.get("request_context")
|
|
43
|
+
|
|
44
|
+
output_text = await self._anonymizer.aanonymize(
|
|
45
|
+
anonymization["query"],
|
|
46
|
+
request_context=request_context,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
return self._build_updated_state(output_text)
|
|
50
|
+
|
|
51
|
+
def _build_updated_state(self, output_text: str) -> dict[str, Any]:
|
|
52
|
+
return {
|
|
53
|
+
"anonymization": {
|
|
54
|
+
"output_text": output_text,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from abc import abstractmethod
|
|
3
|
+
from typing import Any, Optional, Union
|
|
4
|
+
|
|
5
|
+
from langchain_core.runnables import Runnable, RunnableConfig
|
|
6
|
+
from langgraph.types import Command
|
|
7
|
+
|
|
8
|
+
from core_plainid.utils.validation_utils import validate_not_blank
|
|
9
|
+
from langgraph_plainid.models.state.agent_state import AgentState, ErrorDetails
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseNode(Runnable[AgentState, Union[dict[str, Any], Command]]):
|
|
15
|
+
"""Abstract base for LangGraph nodes that execute a PlainID operation.
|
|
16
|
+
|
|
17
|
+
Handles routing to the next node via :class:`Command`, and routes to
|
|
18
|
+
an error-handler node when ``next_node_on_error`` is configured.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
substate_key: str,
|
|
24
|
+
next_node: Optional[str] = None,
|
|
25
|
+
next_node_on_error: Optional[str] = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""
|
|
28
|
+
Args:
|
|
29
|
+
substate_key: Key in :class:`AgentState` that this node reads/writes.
|
|
30
|
+
next_node: Node to route to on success. If ``None``, the updated
|
|
31
|
+
state dict is returned directly (graph edge routing applies).
|
|
32
|
+
next_node_on_error: Node to route to on error. If ``None``,
|
|
33
|
+
the exception propagates to the calling context.
|
|
34
|
+
"""
|
|
35
|
+
validate_not_blank(substate_key=substate_key)
|
|
36
|
+
|
|
37
|
+
self._substate_key = substate_key
|
|
38
|
+
self._next_node = next_node
|
|
39
|
+
self._next_node_on_error = next_node_on_error
|
|
40
|
+
|
|
41
|
+
def invoke(
|
|
42
|
+
self,
|
|
43
|
+
input: AgentState,
|
|
44
|
+
config: Optional[RunnableConfig] = None,
|
|
45
|
+
**kwargs: Any,
|
|
46
|
+
) -> Union[dict[str, Any], Command]:
|
|
47
|
+
"""Execute the node synchronously.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
input: The current agent state.
|
|
51
|
+
config: Optional LangChain runnable config.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Updated state dict or a :class:`Command` that routes to the next node.
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
updated_state = self._execute(input)
|
|
58
|
+
except Exception as e:
|
|
59
|
+
return self._handle_error(e)
|
|
60
|
+
|
|
61
|
+
return self._build_result(updated_state)
|
|
62
|
+
|
|
63
|
+
async def ainvoke(
|
|
64
|
+
self,
|
|
65
|
+
input: AgentState,
|
|
66
|
+
config: Optional[RunnableConfig] = None,
|
|
67
|
+
**kwargs: Any,
|
|
68
|
+
) -> Union[dict[str, Any], Command]:
|
|
69
|
+
"""Async version of :meth:`invoke`."""
|
|
70
|
+
try:
|
|
71
|
+
updated_state = await self._aexecute(input)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
return self._handle_error(e)
|
|
74
|
+
|
|
75
|
+
return self._build_result(updated_state)
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
def _execute(self, state: AgentState) -> dict[str, Any]:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
@abstractmethod
|
|
82
|
+
async def _aexecute(self, state: AgentState) -> dict[str, Any]:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
def _build_result(
|
|
86
|
+
self, updated_state: dict[str, Any]
|
|
87
|
+
) -> Union[dict[str, Any], Command]:
|
|
88
|
+
if self._next_node is not None:
|
|
89
|
+
|
|
90
|
+
return Command(update=updated_state, goto=self._next_node)
|
|
91
|
+
|
|
92
|
+
return updated_state
|
|
93
|
+
|
|
94
|
+
def _handle_error(self, error: Exception) -> Union[dict[str, Any], Command]:
|
|
95
|
+
logger.error("Error in node '%s': %s", self._substate_key, error)
|
|
96
|
+
|
|
97
|
+
if self._next_node_on_error is not None:
|
|
98
|
+
error_state = {
|
|
99
|
+
self._substate_key: {
|
|
100
|
+
"error_details": self._create_error_details(error),
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return Command(update=error_state, goto=self._next_node_on_error)
|
|
105
|
+
|
|
106
|
+
raise error
|
|
107
|
+
|
|
108
|
+
def _create_error_details(self, error: Exception) -> ErrorDetails:
|
|
109
|
+
return ErrorDetails(
|
|
110
|
+
error_message=str(error),
|
|
111
|
+
error=error,
|
|
112
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
from core_plainid.categorization.base_categorizer import BaseCategorizer
|
|
4
|
+
|
|
5
|
+
from langgraph_plainid.models.state.agent_state import AgentState
|
|
6
|
+
from langgraph_plainid.nodes.base_node import BaseNode
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CategorizationNode(BaseNode):
|
|
10
|
+
"""LangGraph node that categorizes prompts via a PlainID categorizer.
|
|
11
|
+
|
|
12
|
+
Reads the query from ``state["categorization"]["query"]`` and delegates
|
|
13
|
+
to the wrapped categorizer.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
categorizer: BaseCategorizer,
|
|
19
|
+
next_node: Optional[str] = None,
|
|
20
|
+
next_node_on_error: Optional[str] = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
super().__init__(
|
|
23
|
+
substate_key="categorization",
|
|
24
|
+
next_node=next_node,
|
|
25
|
+
next_node_on_error=next_node_on_error,
|
|
26
|
+
)
|
|
27
|
+
self._categorizer = categorizer
|
|
28
|
+
|
|
29
|
+
def _execute(self, state: AgentState) -> dict[str, Any]:
|
|
30
|
+
categorization = state["categorization"]
|
|
31
|
+
request_context = state.get("request_context")
|
|
32
|
+
|
|
33
|
+
self._categorizer.categorize(
|
|
34
|
+
categorization["query"],
|
|
35
|
+
request_context=request_context,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return {}
|
|
39
|
+
|
|
40
|
+
async def _aexecute(self, state: AgentState) -> dict[str, Any]:
|
|
41
|
+
categorization = state["categorization"]
|
|
42
|
+
request_context = state.get("request_context")
|
|
43
|
+
|
|
44
|
+
await self._categorizer.acategorize(
|
|
45
|
+
categorization["query"],
|
|
46
|
+
request_context=request_context,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
return {}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
from core_plainid.retrieval.base_retriever import BaseRetriever
|
|
4
|
+
|
|
5
|
+
from langgraph_plainid.models.state.agent_state import AgentState
|
|
6
|
+
from langgraph_plainid.nodes.base_node import BaseNode
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RetrievalNode(BaseNode):
|
|
10
|
+
"""LangGraph node that retrieves documents via PlainID-authorized filters.
|
|
11
|
+
|
|
12
|
+
Reads the query from ``state["retrieval"]["query"]`` and writes the
|
|
13
|
+
result to ``state["retrieval"]["retrieved_documents"]``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
retriever: BaseRetriever,
|
|
19
|
+
next_node: Optional[str] = None,
|
|
20
|
+
next_node_on_error: Optional[str] = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
super().__init__(
|
|
23
|
+
substate_key="retrieval",
|
|
24
|
+
next_node=next_node,
|
|
25
|
+
next_node_on_error=next_node_on_error,
|
|
26
|
+
)
|
|
27
|
+
self._retriever = retriever
|
|
28
|
+
|
|
29
|
+
def _execute(self, state: AgentState) -> dict[str, Any]:
|
|
30
|
+
retrieval = state["retrieval"]
|
|
31
|
+
request_context = state.get("request_context")
|
|
32
|
+
|
|
33
|
+
documents = self._retriever.retrieve(
|
|
34
|
+
retrieval["query"],
|
|
35
|
+
request_context=request_context,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return self._build_updated_state(documents)
|
|
39
|
+
|
|
40
|
+
async def _aexecute(self, state: AgentState) -> dict[str, Any]:
|
|
41
|
+
retrieval = state["retrieval"]
|
|
42
|
+
request_context = state.get("request_context")
|
|
43
|
+
|
|
44
|
+
documents = await self._retriever.aretrieve(
|
|
45
|
+
retrieval["query"],
|
|
46
|
+
request_context=request_context,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
return self._build_updated_state(documents)
|
|
50
|
+
|
|
51
|
+
def _build_updated_state(self, documents: list) -> dict[str, Any]:
|
|
52
|
+
return {
|
|
53
|
+
"retrieval": {
|
|
54
|
+
"retrieved_documents": documents,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: langgraph-plainid
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: LangGraph integration for PlainID authorization
|
|
5
|
+
Author: PlainID
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Security
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: <=3.12,>=3.10
|
|
19
|
+
Requires-Dist: core-plainid<2.0.0,>=1.0.0
|
|
20
|
+
Requires-Dist: langchain-plainid<2.0.0,>=1.0.0
|
|
21
|
+
Requires-Dist: langgraph<2.0.0,>=1.1.2
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# langgraph-plainid
|
|
25
|
+
|
|
26
|
+
[PlainID](https://www.plainid.com/) authorization integration for [LangGraph](https://langchain-ai.github.io/langgraph/). Provides LangGraph nodes for prompt categorization, text anonymization, and policy-based document retrieval that can be composed into stateful agent graphs.
|
|
27
|
+
|
|
28
|
+
This library depends on [core-plainid](https://pypi.org/project/core-plainid/) and [langchain-plainid](https://pypi.org/project/langchain-plainid/) for the underlying authorization components. Please refer to the **core-plainid README** for setting up permissions, categorizers, anonymizers, classifiers, and PlainID rulesets, and the **langchain-plainid README** for retrieval and vector store configuration.
|
|
29
|
+
|
|
30
|
+
All nodes fully support both **synchronous** and **asynchronous** execution. The examples below use the async API; replace `ainvoke` with `invoke` for synchronous usage.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install langgraph-plainid
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`core-plainid` and `langchain-plainid` are installed automatically as dependencies.
|
|
39
|
+
|
|
40
|
+
## Agent State
|
|
41
|
+
|
|
42
|
+
All nodes operate on a shared `AgentState` TypedDict that flows through the graph. The state contains the `request_context` for identity information and optional sub-states for each node type. Alternatively, `request_context` can be provided at construction time to the underlying components (e.g. `PlainIDPermissionsProvider`, `FilterDirectiveProvider`) — see the core-plainid README for details.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from langgraph_plainid.models.state.agent_state import AgentState
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
| Field | Type | Description |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `request_context` | `RequestContext` | Identity context (entity ID, type, additional identities) |
|
|
51
|
+
| `categorization` | `CategorizationState` | Sub-state with `query` and optional `error_details` |
|
|
52
|
+
| `anonymization` | `AnonymizationState` | Sub-state with `query`, optional `output_text`, and `error_details` |
|
|
53
|
+
| `retrieval` | `RetrievalState` | Sub-state with `query`, `resource_types`, optional `retrieved_documents`, and `error_details` |
|
|
54
|
+
|
|
55
|
+
Multiple identities (e.g. a User and an AI Agent) are supported for agentic scenarios through the `additional_identities` field in `RequestContext` — see the core-plainid README for details.
|
|
56
|
+
|
|
57
|
+
## Base Node
|
|
58
|
+
|
|
59
|
+
All PlainID nodes extend `BaseNode`, which provides common behavior:
|
|
60
|
+
|
|
61
|
+
- **`next_node`** — optional name of the next node to route to via LangGraph `Command`. If not set, the node returns the updated state directly and graph edges determine the flow.
|
|
62
|
+
- **`next_node_on_error`** — optional name of a node to route to when an error occurs. If set, errors are caught and routed as `ErrorDetails` in the sub-state. If not set, exceptions propagate normally.
|
|
63
|
+
|
|
64
|
+
## Categorization Node
|
|
65
|
+
|
|
66
|
+
The `CategorizationNode` classifies the input prompt against PlainID policies. If the categories are not allowed, a `PlainIDCategorizerException` is raised (or routed to the error handler node if configured).
|
|
67
|
+
|
|
68
|
+
For setting up the categorizer, classifier providers, and the PlainID `Prompt_Control` ruleset, see the **Category Filtering** section in the core-plainid README.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from core_plainid.categorization.categorizer import Categorizer
|
|
72
|
+
from core_plainid.utils.plainid_permissions_provider import PlainIDPermissionsProvider
|
|
73
|
+
from langgraph_plainid.nodes.categorization_node import CategorizationNode
|
|
74
|
+
|
|
75
|
+
permissions_provider = PlainIDPermissionsProvider(
|
|
76
|
+
base_url="https://platform-product.us1.plainid.io",
|
|
77
|
+
client_id="your_client_id",
|
|
78
|
+
client_secret="your_client_secret",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
categorizer = Categorizer(
|
|
82
|
+
classifier_provider=classifier,
|
|
83
|
+
permissions_provider=permissions_provider,
|
|
84
|
+
all_categories=["contract", "HR", "finance"],
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
categorization_node = CategorizationNode(
|
|
88
|
+
categorizer=categorizer,
|
|
89
|
+
next_node="anonymizer",
|
|
90
|
+
next_node_on_error="error_handler",
|
|
91
|
+
)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The node reads its input from `state["categorization"]["query"]`.
|
|
95
|
+
|
|
96
|
+
## Anonymization Node
|
|
97
|
+
|
|
98
|
+
The `AnonymizerNode` detects and anonymizes PII in the input text based on PlainID policies. The anonymized text is written to `state["anonymization"]["output_text"]`.
|
|
99
|
+
|
|
100
|
+
For setting up the anonymizer, encryption key, AHDS, and the PlainID `Output_Control` ruleset, see the **Anonymization** section in the core-plainid README.
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from core_plainid.anonymization.presidio_anonymizer import PresidioAnonymizer
|
|
104
|
+
from core_plainid.utils.plainid_permissions_provider import PlainIDPermissionsProvider
|
|
105
|
+
from langgraph_plainid.nodes.anonymizer_node import AnonymizerNode
|
|
106
|
+
|
|
107
|
+
permissions_provider = PlainIDPermissionsProvider(
|
|
108
|
+
base_url="https://platform-product.us1.plainid.io",
|
|
109
|
+
client_id="your_client_id",
|
|
110
|
+
client_secret="your_client_secret",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
anonymizer = PresidioAnonymizer(
|
|
114
|
+
permissions_provider=permissions_provider,
|
|
115
|
+
encrypt_key="your_16_char_key!",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
anonymizer_node = AnonymizerNode(
|
|
119
|
+
anonymizer=anonymizer,
|
|
120
|
+
next_node="retrieval",
|
|
121
|
+
next_node_on_error="error_handler",
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The node reads its input from `state["anonymization"]["query"]` and writes the result to `state["anonymization"]["output_text"]`.
|
|
126
|
+
|
|
127
|
+
## Retrieval Node
|
|
128
|
+
|
|
129
|
+
The `RetrievalNode` retrieves documents from vector stores with PlainID-enforced filters. The retrieved documents are written to `state["retrieval"]["retrieved_documents"]`.
|
|
130
|
+
|
|
131
|
+
For setting up the retriever, filter provider, and vector store configuration, see the **Retrieval** section in the langchain-plainid README.
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from langchain_plainid.retrieval.filter_directive_provider import FilterDirectiveProvider
|
|
135
|
+
from langchain_plainid.retrieval.multi_store_retriever import MultiStoreRetriever
|
|
136
|
+
from langgraph_plainid.nodes.retrieval_node import RetrievalNode
|
|
137
|
+
|
|
138
|
+
filter_provider = FilterDirectiveProvider(
|
|
139
|
+
base_url="https://platform-product.us1.plainid.io",
|
|
140
|
+
client_id="your_client_id",
|
|
141
|
+
client_secret="your_client_secret",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
retriever = MultiStoreRetriever(
|
|
145
|
+
filter_provider=filter_provider,
|
|
146
|
+
resource_types=["customer"],
|
|
147
|
+
vector_stores=[customer_store],
|
|
148
|
+
k=4,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
retrieval_node = RetrievalNode(
|
|
152
|
+
retriever=retriever,
|
|
153
|
+
next_node_on_error="error_handler",
|
|
154
|
+
)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The node reads its input from `state["retrieval"]["query"]` and writes the result to `state["retrieval"]["retrieved_documents"]`.
|
|
158
|
+
|
|
159
|
+
## Building a Graph
|
|
160
|
+
|
|
161
|
+
Nodes are composed into a LangGraph `StateGraph` to define the agent's execution flow. Here is an example graph that categorizes a prompt, anonymizes it, and then retrieves documents:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from langgraph.graph import END, START, StateGraph
|
|
165
|
+
from core_plainid.models.context.request_context import RequestContext
|
|
166
|
+
from langgraph_plainid.models.state.agent_state import AgentState
|
|
167
|
+
|
|
168
|
+
graph = StateGraph(AgentState)
|
|
169
|
+
|
|
170
|
+
graph.add_node("categorization", categorization_node)
|
|
171
|
+
graph.add_node("anonymizer", anonymizer_node)
|
|
172
|
+
graph.add_node("retrieval", retrieval_node)
|
|
173
|
+
|
|
174
|
+
graph.add_edge(START, "categorization")
|
|
175
|
+
graph.add_edge("categorization", "anonymizer")
|
|
176
|
+
graph.add_edge("anonymizer", "retrieval")
|
|
177
|
+
graph.add_edge("retrieval", END)
|
|
178
|
+
|
|
179
|
+
app = graph.compile()
|
|
180
|
+
|
|
181
|
+
request_context = RequestContext(
|
|
182
|
+
entity_id="your_entity_id",
|
|
183
|
+
entity_id_type="your_entity_type",
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
result = await app.ainvoke({
|
|
187
|
+
"request_context": request_context,
|
|
188
|
+
"categorization": {"query": "What is John Smith's contract status?"},
|
|
189
|
+
"anonymization": {"query": "What is John Smith's contract status?"},
|
|
190
|
+
"retrieval": {"query": "What is John Smith's contract status?"},
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
print(result["anonymization"]["output_text"]) # anonymized text
|
|
194
|
+
print(result["retrieval"]["retrieved_documents"]) # retrieved documents
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Using Command-Based Routing
|
|
198
|
+
|
|
199
|
+
Instead of defining edges between all nodes, you can use the `next_node` parameter to let nodes route to the next step via LangGraph `Command`:
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
categorization_node = CategorizationNode(
|
|
203
|
+
categorizer=categorizer,
|
|
204
|
+
next_node="anonymizer",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
anonymizer_node = AnonymizerNode(
|
|
208
|
+
anonymizer=anonymizer,
|
|
209
|
+
next_node="retrieval",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
retrieval_node = RetrievalNode(retriever=retriever)
|
|
213
|
+
|
|
214
|
+
graph = StateGraph(AgentState)
|
|
215
|
+
|
|
216
|
+
graph.add_node("categorization", categorization_node)
|
|
217
|
+
graph.add_node("anonymizer", anonymizer_node)
|
|
218
|
+
graph.add_node("retrieval", retrieval_node)
|
|
219
|
+
|
|
220
|
+
graph.add_edge(START, "categorization")
|
|
221
|
+
graph.add_edge("retrieval", END)
|
|
222
|
+
|
|
223
|
+
app = graph.compile()
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Error Handling
|
|
227
|
+
|
|
228
|
+
When `next_node_on_error` is set, errors are caught and the graph routes to the specified error handler node. The error details are available in the sub-state:
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
def error_handler(state: AgentState) -> dict:
|
|
232
|
+
for key in ["categorization", "anonymization", "retrieval"]:
|
|
233
|
+
sub_state = state.get(key)
|
|
234
|
+
|
|
235
|
+
if sub_state and sub_state.get("error_details"):
|
|
236
|
+
error_details = sub_state["error_details"]
|
|
237
|
+
|
|
238
|
+
print(f"Error in {key}: {error_details['error_message']}")
|
|
239
|
+
print(f"Exception: {error_details['error']}")
|
|
240
|
+
|
|
241
|
+
return {}
|
|
242
|
+
|
|
243
|
+
graph.add_node("error_handler", error_handler)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
If `next_node_on_error` is not set, exceptions propagate normally and can be caught in the calling context of `invoke` / `ainvoke`.
|
|
247
|
+
|
|
248
|
+
## Exceptions
|
|
249
|
+
|
|
250
|
+
All exceptions are defined in the `core-plainid` library. See the **Exceptions** section in the core-plainid README for the full list.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
langgraph_plainid/__init__.py,sha256=9cW8m3y1Kz_HABi3hluIa8T4b-FlgfthCQXLT8Wy57o,637
|
|
2
|
+
langgraph_plainid/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
langgraph_plainid/models/state/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
langgraph_plainid/models/state/agent_state.py,sha256=-wWItepXBdx3JBAhYyTvFrpTRrthKwu04rP7VluW6eI,846
|
|
5
|
+
langgraph_plainid/nodes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
langgraph_plainid/nodes/anonymizer_node.py,sha256=oJKeZY40oO9lZMjnjVKnn9jsfED6uhQ2KNawKh4Cv9s,1771
|
|
7
|
+
langgraph_plainid/nodes/base_node.py,sha256=AkTFnN-L5VXLwykEUk26HEDWhe06M6L989EGC2IBDeA,3574
|
|
8
|
+
langgraph_plainid/nodes/categorization_node.py,sha256=9u0tZjJ7l3EBT_BSwGlpaVdEW9uyPq9fLDyqjuTL03A,1484
|
|
9
|
+
langgraph_plainid/nodes/retrieval_node.py,sha256=Q1gEC42jANl4VIysE3obC_TnhcwIN4ylAzF6hknYHZ4,1731
|
|
10
|
+
langgraph_plainid-1.0.0.dist-info/METADATA,sha256=xQONXETIEX5tRFExTeg03VTVx6PmCt6Y9l8X3wRmXU0,10047
|
|
11
|
+
langgraph_plainid-1.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
12
|
+
langgraph_plainid-1.0.0.dist-info/RECORD,,
|