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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any