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,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,,
|