hyperpocket-langgraph 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ from hyperpocket_langgraph.pocket_langgraph import PocketLanggraph
2
+
3
+ __all__ = ["PocketLanggraph"]
@@ -0,0 +1,221 @@
1
+ import copy
2
+ from typing import List, Any
3
+
4
+ from langchain_core.runnables import RunnableConfig
5
+ from langgraph.errors import NodeInterrupt
6
+ from pydantic import BaseModel
7
+
8
+ from hyperpocket.config import pocket_logger
9
+ from hyperpocket.pocket_main import ToolLike
10
+ from hyperpocket.prompts import pocket_extended_tool_description
11
+ from hyperpocket.server.server import PocketServerOperations
12
+
13
+ try:
14
+ from langchain_core.messages import AnyMessage, ToolMessage, AIMessage, RemoveMessage, SystemMessage
15
+ from langchain_core.tools import BaseTool, StructuredTool
16
+ except ImportError:
17
+ raise ImportError(
18
+ "You need to install langchain to use pocket langgraph."
19
+ )
20
+
21
+ try:
22
+ from langgraph.constants import START, END
23
+ from langgraph.graph import add_messages, StateGraph, MessagesState
24
+ from langgraph.graph.state import CompiledStateGraph
25
+ except ImportError:
26
+ raise ImportError(
27
+ "You need to install langgraph to use pocket langgraph"
28
+ )
29
+
30
+ from hyperpocket import Pocket, PocketAuth
31
+ from hyperpocket.tool import Tool as PocketTool
32
+
33
+
34
+ class PocketLanggraphBaseState(MessagesState):
35
+ pass
36
+
37
+
38
+ class PocketLanggraph(Pocket):
39
+ langgraph_tools: dict[str, BaseTool]
40
+
41
+ def __init__(self,
42
+ tools: List[ToolLike],
43
+ auth: PocketAuth = None):
44
+ super().__init__(tools, auth)
45
+ self.langgraph_tools = {}
46
+ for tool_name, tool_impl in self.tools.items():
47
+ self.langgraph_tools[tool_name] = self._get_langgraph_tool(tool_impl)
48
+
49
+ def get_tools(self):
50
+ return [v for v in self.langgraph_tools.values()]
51
+
52
+ def get_tool_node(self, should_interrupt: bool = False):
53
+ async def _tool_node(state: PocketLanggraphBaseState, config: RunnableConfig) -> dict:
54
+ thread_id = config.get("configurable", {}).get("thread_id", "default")
55
+ last_message = state['messages'][-1]
56
+ tool_calls = last_message.tool_calls
57
+
58
+ # 01. prepare
59
+ prepare_list = []
60
+ prepare_done_list = []
61
+ need_prepare = False
62
+ for tool_call in tool_calls:
63
+ pocket_logger.debug(f"prepare tool {tool_call}")
64
+ _tool_call = copy.deepcopy(tool_call)
65
+
66
+ tool_call_id = _tool_call['id']
67
+ tool_name = _tool_call['name']
68
+ tool_args = _tool_call['args']
69
+ body = tool_args.pop('body')
70
+ if isinstance(body, BaseModel):
71
+ body = body.model_dump()
72
+
73
+ prepare = await self.prepare_in_subprocess(tool_name, body=body, thread_id=thread_id,
74
+ profile=tool_args.get("profile", "default"))
75
+ need_prepare |= True if prepare else False
76
+
77
+ if prepare is None:
78
+ prepare_done_list.append(ToolMessage(content="prepare done", tool_call_id=tool_call_id))
79
+ else:
80
+ prepare_list.append(ToolMessage(content=prepare, tool_call_id=tool_call_id))
81
+
82
+ if need_prepare:
83
+ pocket_logger.debug(f"need prepare : {prepare_list}")
84
+ if should_interrupt: # interrupt
85
+ pocket_logger.debug(f"{last_message.name}({last_message.id}) is interrupt.")
86
+ result = "\n\t" + "\n\t".join(set(msg.content for msg in prepare_list))
87
+ raise NodeInterrupt(f'{result}\n\nThe tool execution interrupted. Please talk to me to resume.')
88
+
89
+ else: # multi turn
90
+ pocket_logger.debug(f"{last_message.name}({last_message.id}) is multi-turn")
91
+ return {"messages": prepare_done_list + prepare_list}
92
+
93
+ pocket_logger.debug(f"no need prepare {last_message.name}({last_message.id})")
94
+
95
+ # 02. authenticate and tool call
96
+ tool_messages = []
97
+ for tool_call in tool_calls:
98
+ pocket_logger.debug(f"authenticate and call {tool_call}")
99
+ _tool_call = copy.deepcopy(tool_call)
100
+
101
+ tool_call_id = _tool_call['id']
102
+ tool_name = _tool_call['name']
103
+ tool_args = _tool_call['args']
104
+ body = tool_args.pop('body')
105
+ if isinstance(body, BaseModel):
106
+ body = body.model_dump()
107
+
108
+ try:
109
+ auth = await self.authenticate_in_subprocess(
110
+ tool_name, body=body, thread_id=thread_id, profile=tool_args.get("profile", "default"))
111
+ except Exception as e:
112
+ pocket_logger.error(f"occur exception during authenticate. error : {e}")
113
+ tool_messages.append(
114
+ ToolMessage(content=f"occur exception during authenticate. error : {e}", tool_name=tool_name,
115
+ tool_call_id=tool_call_id))
116
+ continue
117
+
118
+ try:
119
+ result = await self.tool_call_in_subprocess(
120
+ tool_name, body=body, envs=auth, thread_id=thread_id,
121
+ profile=tool_args.get("profile", "default"))
122
+ except Exception as e:
123
+ pocket_logger.error(f"occur exception during tool calling. error : {e}")
124
+ tool_messages.append(
125
+ ToolMessage(content=f"occur exception during tool calling. error : {e}", tool_name=tool_name,
126
+ tool_call_id=tool_call_id))
127
+ continue
128
+
129
+ pocket_logger.debug(f"{tool_name} tool result : {result}")
130
+ tool_messages.append(ToolMessage(content=result, tool_name=tool_name, tool_call_id=tool_call_id))
131
+
132
+ return {"messages": tool_messages}
133
+
134
+ return _tool_node
135
+
136
+ async def prepare_in_subprocess(self,
137
+ tool_name: str,
138
+ body: Any,
139
+ thread_id: str = 'default',
140
+ profile: str = 'default',
141
+ *args, **kwargs):
142
+ prepare = await self.server.call_in_subprocess(
143
+ PocketServerOperations.PREPARE_AUTH,
144
+ args,
145
+ {
146
+ 'tool_name': tool_name,
147
+ 'body': body,
148
+ 'thread_id': thread_id,
149
+ 'profile': profile,
150
+ **kwargs,
151
+ },
152
+ )
153
+
154
+ return prepare
155
+
156
+ async def authenticate_in_subprocess(self,
157
+ tool_name: str,
158
+ body: Any,
159
+ thread_id: str = 'default',
160
+ profile: str = 'default',
161
+ *args, **kwargs):
162
+ credentials = await self.server.call_in_subprocess(
163
+ PocketServerOperations.AUTHENTICATE,
164
+ args,
165
+ {
166
+ 'tool_name': tool_name,
167
+ 'body': body,
168
+ 'thread_id': thread_id,
169
+ 'profile': profile,
170
+ **kwargs,
171
+ },
172
+ )
173
+
174
+ return credentials
175
+
176
+ async def tool_call_in_subprocess(self,
177
+ tool_name: str,
178
+ body: Any,
179
+ thread_id: str = 'default',
180
+ profile: str = 'default',
181
+ *args, **kwargs):
182
+ result = await self.server.call_in_subprocess(
183
+ PocketServerOperations.TOOL_CALL,
184
+ args,
185
+ {
186
+ 'tool_name': tool_name,
187
+ 'body': body,
188
+ 'thread_id': thread_id,
189
+ 'profile': profile,
190
+ **kwargs,
191
+ },
192
+ )
193
+
194
+ return result
195
+
196
+ def _get_langgraph_tool(self, pocket_tool: PocketTool) -> BaseTool:
197
+ def _invoke(body: Any, thread_id: str = 'default', profile: str = 'default', **kwargs) -> str:
198
+ if isinstance(body, BaseModel):
199
+ body = body.model_dump()
200
+ result, interrupted = self.invoke_with_state(pocket_tool.name, body, thread_id, profile, **kwargs)
201
+ say = result
202
+ if interrupted:
203
+ say = f'{say}\n\nThe tool execution interrupted. Please talk to me to resume.'
204
+ return say
205
+
206
+ async def _ainvoke(body: Any, thread_id: str = 'default', profile: str = 'default', **kwargs) -> str:
207
+ if isinstance(body, BaseModel):
208
+ body = body.model_dump()
209
+ result, interrupted = await self.ainvoke_with_state(pocket_tool.name, body, thread_id, profile, **kwargs)
210
+ say = result
211
+ if interrupted:
212
+ say = f'{say}\n\nThe tool execution interrupted. Please talk to me to resume.'
213
+ return say
214
+
215
+ return StructuredTool.from_function(
216
+ func=_invoke,
217
+ coroutine=_ainvoke,
218
+ name=pocket_tool.name,
219
+ description=pocket_extended_tool_description(pocket_tool.description),
220
+ args_schema=pocket_tool.schema_model(),
221
+ )
@@ -0,0 +1,201 @@
1
+ Metadata-Version: 2.1
2
+ Name: hyperpocket-langgraph
3
+ Version: 0.0.1
4
+ Summary:
5
+ Author: moon
6
+ Author-email: moon@vessl.ai
7
+ Requires-Python: >=3.11,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Requires-Dist: hyperpocket (==0.0.3)
13
+ Requires-Dist: langgraph (>=0.2.59,<0.3.0)
14
+ Description-Content-Type: text/markdown
15
+
16
+ # Langgraph extensions
17
+
18
+ - Provide compatible tools in LangGraph
19
+ - Offer various methods for controlling authentication flow
20
+ - [Multi-turn](#multi-turn)
21
+ - [Human-in-the-loop](#human-in-the-loop-auth)
22
+
23
+ ## Get Pocket Subgraph
24
+
25
+ ```python
26
+ import hyperpocket as pk
27
+ from pocket_langgraph import PocketLanggraph
28
+
29
+ pocket = PocketLanggraph(tools=[
30
+ *pk.curated_tools.SLACK, # SLACK = [slack_get_message, slack_post_message, ..]
31
+ *pk.curated_tools.LINEAR,
32
+ "https://github.com/my-org/some-awesome-tool",
33
+ ])
34
+
35
+ # get tool node from pocket
36
+ pocket_node = pocket.get_tool_node()
37
+ ```
38
+
39
+ ## Binding Pocket Tools
40
+
41
+ ```python
42
+ import os
43
+
44
+ from langchain_openai import ChatOpenAI
45
+
46
+ import hyperpocket as pk
47
+ from pocket_langgraph import PocketLanggraph
48
+
49
+ pocket = PocketLanggraph(tools=[
50
+ *pk.curated_tools.SLACK, # SLACK = [slack_get_message, slack_post_message, ..]
51
+ *pk.curated_tools.LINEAR,
52
+ "https://github.com/my-org/some-awesome-tool",
53
+ ])
54
+
55
+ # get tools from pocket to bind llm
56
+ tools = pocket.get_tools()
57
+ llm = ChatOpenAI(model="gpt-4o", api_key=os.getenv("OPENAI_API_KEY"))
58
+
59
+ # bind pocket tools with llm
60
+ llm_with_tools = llm.bind_tools(tools)
61
+ ```
62
+
63
+ ## Examples
64
+
65
+ ```python
66
+ import os
67
+ from typing import Annotated
68
+
69
+ from langchain_core.runnables import RunnableConfig
70
+ from langchain_openai import ChatOpenAI
71
+ from langgraph.checkpoint.memory import MemorySaver
72
+ from langgraph.graph import StateGraph, START, END
73
+ from langgraph.graph.message import add_messages
74
+ from langgraph.prebuilt import tools_condition
75
+ from typing_extensions import TypedDict
76
+
77
+ import hyperpocket as pk
78
+ from pocket_langgraph import PocketLanggraph
79
+
80
+ # Define pocket tools
81
+ pocket = PocketLanggraph(tools=[
82
+ *pk.curated_tools.SLACK, # SLACK = [slack_get_message, slack_post_message, ..]
83
+ *pk.curated_tools.LINEAR,
84
+ "https://github.com/my-org/some-awesome-tool",
85
+ ])
86
+
87
+ # Get Pocket ToolNode
88
+ pocket_node = pocket.get_tool_node()
89
+
90
+
91
+ # Define your own langgraph state
92
+ class State(TypedDict):
93
+ messages: Annotated[list, add_messages]
94
+
95
+
96
+ # Biding Pocket Tools
97
+ tools = pocket.get_tools()
98
+ llm = ChatOpenAI(model="gpt-4o", api_key=os.getenv("OPENAI_API_KEY"))
99
+ llm_with_tools = llm.bind_tools(tools)
100
+
101
+
102
+ # Make your langgraph with pocket nodes and bound llm
103
+ def chatbot(state: State):
104
+ return {"messages": [llm_with_tools.invoke(state["messages"])]}
105
+
106
+
107
+ graph_builder = StateGraph(State)
108
+ graph_builder.add_node("chatbot", chatbot)
109
+
110
+ # add pocket tool node
111
+ graph_builder.add_node("tools", pocket_node)
112
+
113
+ graph_builder.add_conditional_edges(
114
+ "chatbot",
115
+ tools_condition,
116
+ )
117
+
118
+ graph_builder.add_edge("tools", "chatbot")
119
+ graph_builder.add_edge(START, "chatbot")
120
+ graph_builder.add_edge("chatbot", END)
121
+
122
+ memory = MemorySaver()
123
+ graph = graph_builder.compile(checkpointer=memory)
124
+ config = RunnableConfig(
125
+ recursion_limit=30,
126
+ configurable={"thread_id": "1"}, # edit thread_id by user if needed
127
+ )
128
+ ```
129
+
130
+ ## Authentication Flow Control
131
+
132
+ In Pocket, you can configure whether authentication operates in a multi-turn manner or as human-in-the-loop
133
+
134
+ by setting the `should_interrupt` flag when calling `get_tool_node`
135
+
136
+ ### Multi-turn
137
+
138
+ Perform authentication in a multi-turn way
139
+
140
+ ```python
141
+ import hyperpocket as pk
142
+ from pocket_langgraph import PocketLanggraph
143
+
144
+ # Define pocket tools
145
+ pocket = PocketLanggraph(tools=[
146
+ *pk.curated_tools.SLACK, # SLACK = [slack_get_message, slack_post_message, ..]
147
+ *pk.curated_tools.LINEAR,
148
+ "https://github.com/my-org/some-awesome-tool",
149
+ ])
150
+
151
+ # Get Pocket ToolNode
152
+ pocket_node = pocket.get_tool_node(should_interrupt=False) # multi turn
153
+ ```
154
+
155
+ ### Human-in-the-loop Auth
156
+
157
+ ```python
158
+ import hyperpocket as pk
159
+ from pocket_langgraph import PocketLanggraph
160
+
161
+ # Define pocket tools
162
+ pocket = PocketLanggraph(tools=[
163
+ *pk.curated_tools.SLACK, # SLACK = [slack_get_message, slack_post_message, ..]
164
+ *pk.curated_tools.LINEAR,
165
+ "https://github.com/my-org/some-awesome-tool",
166
+ ])
167
+
168
+ # Get Pocket ToolNode
169
+ pocket_node = pocket.get_tool_node(should_interrupt=True) # Human-in-the-loop
170
+ ```
171
+
172
+ ### Differences between multi-turn and human-in-the-loop
173
+
174
+ The two approaches may not differ significantly in terms of user experience, but they are fundamentally distinct
175
+ internally.
176
+
177
+ In the case of the **multi-turn** way, the decision to return to an ongoing authentication(auth) flow is delegated to
178
+ the agent.
179
+
180
+ As a result, if the user want to cancel or modify the auth process midway, then **agent can adapt the
181
+ auth flow to incorporate those changes.**
182
+
183
+ <br>
184
+ However, with the human-in-the-loop way, it simply resumes from an interrupted point, proceeding directly with the
185
+ previous flow regardless of user input.
186
+
187
+ (Currently, users need to manually provide any input to move forward, but future updates aim to automate this transition
188
+ once the user's task is complete.)
189
+
190
+ Therefore, **in the human-in-the-loop way, users cannot modify or cancel the auth flow once it's resumed.**
191
+
192
+ <br>
193
+
194
+
195
+ That said, the multi-turn approach has its drawbacks as well. Since the agent determines whether to re-enter the auth
196
+ flow, there is a risk of incorrect decisions being made.
197
+
198
+ Conversely, the human-in-the-loop approach eliminates this risk by always continuing the previous flow as it is,
199
+ offering more predictable and controlled behavior for managing the auth flow.
200
+
201
+
@@ -0,0 +1,5 @@
1
+ hyperpocket_langgraph/__init__.py,sha256=0Az6FKsAvohOk8G4kUE99g6kyWz-Nf7dUuCuN3_pViY,97
2
+ hyperpocket_langgraph/pocket_langgraph.py,sha256=SqXZDdqvOKX66f0rRI8Ol-rNkL66yIXLFlToyorMvMs,9176
3
+ hyperpocket_langgraph-0.0.1.dist-info/METADATA,sha256=9zG8UMulVdv3cVcSMHiOXzauJov9Jldx44myu1PRQeg,5671
4
+ hyperpocket_langgraph-0.0.1.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
5
+ hyperpocket_langgraph-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any