hyperpocket-langgraph 0.0.1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -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