hyperpocket-langgraph 0.1.10__tar.gz → 0.2.0__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {hyperpocket_langgraph-0.1.10 → hyperpocket_langgraph-0.2.0}/PKG-INFO +1 -1
- {hyperpocket_langgraph-0.1.10 → hyperpocket_langgraph-0.2.0}/hyperpocket_langgraph/__init__.py +1 -1
- {hyperpocket_langgraph-0.1.10 → hyperpocket_langgraph-0.2.0}/hyperpocket_langgraph/pocket_langgraph.py +84 -43
- {hyperpocket_langgraph-0.1.10 → hyperpocket_langgraph-0.2.0}/pyproject.toml +4 -23
- {hyperpocket_langgraph-0.1.10 → hyperpocket_langgraph-0.2.0}/tests/test_pocket_langgraph_no_profile.py +21 -22
- {hyperpocket_langgraph-0.1.10 → hyperpocket_langgraph-0.2.0}/tests/test_pocket_langgraph_use_profile.py +24 -22
- {hyperpocket_langgraph-0.1.10 → hyperpocket_langgraph-0.2.0}/.gitignore +0 -0
- {hyperpocket_langgraph-0.1.10 → hyperpocket_langgraph-0.2.0}/README.md +0 -0
- {hyperpocket_langgraph-0.1.10 → hyperpocket_langgraph-0.2.0}/__init__.py +0 -0
- {hyperpocket_langgraph-0.1.10 → hyperpocket_langgraph-0.2.0}/tests/__init__.py +0 -0
- {hyperpocket_langgraph-0.1.10 → hyperpocket_langgraph-0.2.0}/uv.lock +0 -0
@@ -1,26 +1,21 @@
|
|
1
1
|
import copy
|
2
2
|
from typing import Optional
|
3
3
|
|
4
|
+
from hyperpocket.config import pocket_logger
|
4
5
|
from langchain_core.runnables import RunnableConfig
|
5
6
|
from langgraph.errors import NodeInterrupt
|
6
7
|
from pydantic import BaseModel
|
7
8
|
|
8
|
-
from hyperpocket.config import pocket_logger
|
9
|
-
|
10
9
|
try:
|
11
10
|
from langchain_core.messages import ToolMessage
|
12
11
|
from langchain_core.tools import BaseTool, StructuredTool
|
13
12
|
except ImportError:
|
14
|
-
raise ImportError(
|
15
|
-
"You need to install langchain to use pocket langgraph."
|
16
|
-
)
|
13
|
+
raise ImportError("You need to install langchain to use pocket langgraph.")
|
17
14
|
|
18
15
|
try:
|
19
16
|
from langgraph.graph import MessagesState
|
20
17
|
except ImportError:
|
21
|
-
raise ImportError(
|
22
|
-
"You need to install langgraph to use pocket langgraph"
|
23
|
-
)
|
18
|
+
raise ImportError("You need to install langgraph to use pocket langgraph")
|
24
19
|
|
25
20
|
from hyperpocket import Pocket
|
26
21
|
from hyperpocket.tool import Tool as PocketTool
|
@@ -34,16 +29,22 @@ class PocketLanggraph(Pocket):
|
|
34
29
|
def get_tools(self, use_profile: Optional[bool] = None):
|
35
30
|
if use_profile is not None:
|
36
31
|
self.use_profile = use_profile
|
37
|
-
return [
|
38
|
-
|
39
|
-
|
40
|
-
|
32
|
+
return [
|
33
|
+
self._get_langgraph_tool(tool_impl)
|
34
|
+
for tool_impl in self.core.tools.values()
|
35
|
+
]
|
36
|
+
|
37
|
+
def get_tool_node(
|
38
|
+
self, should_interrupt: bool = False, use_profile: Optional[bool] = None
|
39
|
+
):
|
41
40
|
if use_profile is not None:
|
42
41
|
self.use_profile = use_profile
|
43
42
|
|
44
|
-
async def _tool_node(
|
43
|
+
async def _tool_node(
|
44
|
+
state: PocketLanggraphBaseState, config: RunnableConfig
|
45
|
+
) -> dict:
|
45
46
|
thread_id = config.get("configurable", {}).get("thread_id", "default")
|
46
|
-
last_message = state[
|
47
|
+
last_message = state["messages"][-1]
|
47
48
|
tool_calls = last_message.tool_calls
|
48
49
|
|
49
50
|
# 01. prepare
|
@@ -54,9 +55,9 @@ class PocketLanggraph(Pocket):
|
|
54
55
|
pocket_logger.debug(f"prepare tool {tool_call}")
|
55
56
|
_tool_call = copy.deepcopy(tool_call)
|
56
57
|
|
57
|
-
tool_call_id = _tool_call[
|
58
|
-
tool_name = _tool_call[
|
59
|
-
tool_args = _tool_call[
|
58
|
+
tool_call_id = _tool_call["id"]
|
59
|
+
tool_name = _tool_call["name"]
|
60
|
+
tool_args = _tool_call["args"]
|
60
61
|
|
61
62
|
if self.use_profile:
|
62
63
|
body = tool_args.pop("body")
|
@@ -68,27 +69,42 @@ class PocketLanggraph(Pocket):
|
|
68
69
|
if isinstance(body, BaseModel):
|
69
70
|
body = body.model_dump()
|
70
71
|
|
71
|
-
prepare = await self.prepare_in_subprocess(
|
72
|
-
|
72
|
+
prepare = await self.prepare_in_subprocess(
|
73
|
+
tool_name, body=body, thread_id=thread_id, profile=profile
|
74
|
+
)
|
73
75
|
need_prepare |= True if prepare else False
|
74
76
|
|
75
77
|
if prepare is None:
|
76
|
-
prepare_done_list.append(
|
78
|
+
prepare_done_list.append(
|
79
|
+
ToolMessage(content="prepare done", tool_call_id=tool_call_id)
|
80
|
+
)
|
77
81
|
else:
|
78
|
-
prepare_list.append(
|
82
|
+
prepare_list.append(
|
83
|
+
ToolMessage(content=prepare, tool_call_id=tool_call_id)
|
84
|
+
)
|
79
85
|
|
80
86
|
if need_prepare:
|
81
87
|
pocket_logger.debug(f"need prepare : {prepare_list}")
|
82
88
|
if should_interrupt: # interrupt
|
83
|
-
pocket_logger.debug(
|
84
|
-
|
85
|
-
|
89
|
+
pocket_logger.debug(
|
90
|
+
f"{last_message.name}({last_message.id}) is interrupt."
|
91
|
+
)
|
92
|
+
result = "\n\t" + "\n\t".join(
|
93
|
+
set(msg.content for msg in prepare_list)
|
94
|
+
)
|
95
|
+
raise NodeInterrupt(
|
96
|
+
f"{result}\n\nThe tool execution interrupted. Please talk to me to resume."
|
97
|
+
)
|
86
98
|
|
87
99
|
else: # multi turn
|
88
|
-
pocket_logger.debug(
|
100
|
+
pocket_logger.debug(
|
101
|
+
f"{last_message.name}({last_message.id}) is multi-turn"
|
102
|
+
)
|
89
103
|
return {"messages": prepare_done_list + prepare_list}
|
90
104
|
|
91
|
-
pocket_logger.debug(
|
105
|
+
pocket_logger.debug(
|
106
|
+
f"no need prepare {last_message.name}({last_message.id})"
|
107
|
+
)
|
92
108
|
|
93
109
|
# 02. authenticate and tool call
|
94
110
|
tool_messages = []
|
@@ -96,9 +112,9 @@ class PocketLanggraph(Pocket):
|
|
96
112
|
pocket_logger.debug(f"authenticate and call {tool_call}")
|
97
113
|
_tool_call = copy.deepcopy(tool_call)
|
98
114
|
|
99
|
-
tool_call_id = _tool_call[
|
100
|
-
tool_name = _tool_call[
|
101
|
-
tool_args = _tool_call[
|
115
|
+
tool_call_id = _tool_call["id"]
|
116
|
+
tool_name = _tool_call["name"]
|
117
|
+
tool_args = _tool_call["args"]
|
102
118
|
if self.use_profile:
|
103
119
|
body = tool_args.pop("body")
|
104
120
|
profile = tool_args.pop("profile", "default")
|
@@ -111,27 +127,48 @@ class PocketLanggraph(Pocket):
|
|
111
127
|
|
112
128
|
try:
|
113
129
|
auth = await self.authenticate_in_subprocess(
|
114
|
-
tool_name, body=body, thread_id=thread_id, profile=profile
|
130
|
+
tool_name, body=body, thread_id=thread_id, profile=profile
|
131
|
+
)
|
115
132
|
except Exception as e:
|
116
|
-
pocket_logger.error(
|
133
|
+
pocket_logger.error(
|
134
|
+
f"occur exception during authenticate. error : {e}"
|
135
|
+
)
|
117
136
|
tool_messages.append(
|
118
|
-
ToolMessage(
|
119
|
-
|
137
|
+
ToolMessage(
|
138
|
+
content=f"occur exception during authenticate. error : {e}",
|
139
|
+
tool_name=tool_name,
|
140
|
+
tool_call_id=tool_call_id,
|
141
|
+
)
|
142
|
+
)
|
120
143
|
continue
|
121
144
|
|
122
145
|
try:
|
123
146
|
result = await self.tool_call_in_subprocess(
|
124
|
-
tool_name,
|
125
|
-
|
147
|
+
tool_name,
|
148
|
+
body=body,
|
149
|
+
envs=auth,
|
150
|
+
thread_id=thread_id,
|
151
|
+
profile=tool_args.get("profile", "default"),
|
152
|
+
)
|
126
153
|
except Exception as e:
|
127
|
-
pocket_logger.error(
|
154
|
+
pocket_logger.error(
|
155
|
+
f"occur exception during tool calling. error : {e}"
|
156
|
+
)
|
128
157
|
tool_messages.append(
|
129
|
-
ToolMessage(
|
130
|
-
|
158
|
+
ToolMessage(
|
159
|
+
content=f"occur exception during tool calling. error : {e}",
|
160
|
+
tool_name=tool_name,
|
161
|
+
tool_call_id=tool_call_id,
|
162
|
+
)
|
163
|
+
)
|
131
164
|
continue
|
132
165
|
|
133
166
|
pocket_logger.debug(f"{tool_name} tool result : {result}")
|
134
|
-
tool_messages.append(
|
167
|
+
tool_messages.append(
|
168
|
+
ToolMessage(
|
169
|
+
content=result, tool_name=tool_name, tool_call_id=tool_call_id
|
170
|
+
)
|
171
|
+
)
|
135
172
|
|
136
173
|
return {"messages": tool_messages}
|
137
174
|
|
@@ -151,10 +188,12 @@ class PocketLanggraph(Pocket):
|
|
151
188
|
if isinstance(body, BaseModel):
|
152
189
|
body = body.model_dump()
|
153
190
|
|
154
|
-
result, interrupted = self.invoke_with_state(
|
191
|
+
result, interrupted = self.invoke_with_state(
|
192
|
+
pocket_tool.name, body, thread_id, profile, **kwargs
|
193
|
+
)
|
155
194
|
say = result
|
156
195
|
if interrupted:
|
157
|
-
say = f
|
196
|
+
say = f"{say}\n\nThe tool execution interrupted. Please talk to me to resume."
|
158
197
|
return say
|
159
198
|
|
160
199
|
async def _ainvoke(**kwargs) -> str:
|
@@ -170,10 +209,12 @@ class PocketLanggraph(Pocket):
|
|
170
209
|
if isinstance(body, BaseModel):
|
171
210
|
body = body.model_dump()
|
172
211
|
|
173
|
-
result, interrupted = await self.ainvoke_with_state(
|
212
|
+
result, interrupted = await self.ainvoke_with_state(
|
213
|
+
pocket_tool.name, body, thread_id, profile, **kwargs
|
214
|
+
)
|
174
215
|
say = result
|
175
216
|
if interrupted:
|
176
|
-
say = f
|
217
|
+
say = f"{say}\n\nThe tool execution interrupted. Please talk to me to resume."
|
177
218
|
return say
|
178
219
|
|
179
220
|
return StructuredTool.from_function(
|
@@ -1,7 +1,7 @@
|
|
1
1
|
|
2
2
|
[project]
|
3
3
|
name = "hyperpocket-langgraph"
|
4
|
-
version = "0.
|
4
|
+
version = "0.2.0"
|
5
5
|
description = ""
|
6
6
|
authors = [{ name = "Hyperpocket Team", email = "hyperpocket@vessl.ai" }]
|
7
7
|
requires-python = ">=3.10"
|
@@ -12,31 +12,12 @@ dependencies = ["langgraph>=0.2.59", "hyperpocket>=0.0.3"]
|
|
12
12
|
hyperpocket = { path = "../../hyperpocket", editable = true }
|
13
13
|
|
14
14
|
[dependency-groups]
|
15
|
-
dev = [
|
16
|
-
|
17
|
-
"ruff>=0.8.6",
|
18
|
-
]
|
19
|
-
test = [
|
20
|
-
"langchain-openai>=0.3.1",
|
21
|
-
]
|
15
|
+
dev = ["pytest>=8.3.4", "ruff>=0.8.6"]
|
16
|
+
test = ["langchain-openai>=0.3.1"]
|
22
17
|
|
23
18
|
[build-system]
|
24
19
|
requires = ["hatchling"]
|
25
20
|
build-backend = "hatchling.build"
|
26
21
|
|
27
|
-
[tool.ruff.lint]
|
28
|
-
select = [
|
29
|
-
"E", # pycodestyle errors,
|
30
|
-
"F", # pyflakes errors,
|
31
|
-
"I", # isort errors,
|
32
|
-
]
|
33
|
-
ignore = [
|
34
|
-
"E501", # line too long, handled by formatting
|
35
|
-
]
|
36
|
-
|
37
22
|
[tool.ruff]
|
38
|
-
|
39
|
-
target-version = "py310"
|
40
|
-
|
41
|
-
[tool.ruff.lint.per-file-ignores]
|
42
|
-
"__init__.py" = ["F401"]
|
23
|
+
extend = "../../../.ruff.toml"
|
@@ -1,20 +1,18 @@
|
|
1
1
|
import ast
|
2
2
|
from unittest.async_case import IsolatedAsyncioTestCase
|
3
3
|
|
4
|
+
from hyperpocket.config import config, secret
|
5
|
+
from hyperpocket.tool import from_git
|
4
6
|
from langchain_openai import ChatOpenAI
|
5
|
-
from langgraph.constants import
|
7
|
+
from langgraph.constants import END, START
|
6
8
|
from langgraph.graph import MessagesState, StateGraph
|
7
9
|
from langgraph.prebuilt import tools_condition
|
8
10
|
from pydantic import BaseModel
|
9
11
|
|
10
|
-
from hyperpocket.config import config, secret
|
11
|
-
from hyperpocket.tool import from_git
|
12
12
|
from hyperpocket_langgraph import PocketLanggraph
|
13
13
|
|
14
14
|
|
15
15
|
class TestPocketLanggraphNoProfile(IsolatedAsyncioTestCase):
|
16
|
-
|
17
|
-
|
18
16
|
async def asyncSetUp(self):
|
19
17
|
config.public_server_port = "https"
|
20
18
|
config.public_hostname = "localhost"
|
@@ -24,15 +22,21 @@ class TestPocketLanggraphNoProfile(IsolatedAsyncioTestCase):
|
|
24
22
|
|
25
23
|
self.pocket = PocketLanggraph(
|
26
24
|
tools=[
|
27
|
-
from_git(
|
25
|
+
from_git(
|
26
|
+
"https://github.com/vessl-ai/hyperawesometools",
|
27
|
+
"main",
|
28
|
+
"managed-tools/none/simple-echo-tool",
|
29
|
+
),
|
28
30
|
self.add,
|
29
|
-
self.sub_pydantic_args
|
31
|
+
self.sub_pydantic_args,
|
30
32
|
],
|
31
|
-
use_profile=False
|
33
|
+
use_profile=False,
|
32
34
|
)
|
33
35
|
tools = self.pocket.get_tools()
|
34
36
|
tool_node = self.pocket.get_tool_node()
|
35
|
-
self.llm = ChatOpenAI(
|
37
|
+
self.llm = ChatOpenAI(
|
38
|
+
model="gpt-4o", api_key=secret["OPENAI_API_KEY"]
|
39
|
+
).bind_tools(tools=tools)
|
36
40
|
|
37
41
|
def chatbot(state: MessagesState):
|
38
42
|
return {"messages": [self.llm.invoke(state["messages"])]}
|
@@ -52,7 +56,6 @@ class TestPocketLanggraphNoProfile(IsolatedAsyncioTestCase):
|
|
52
56
|
async def asyncTearDown(self):
|
53
57
|
self.pocket._teardown_server()
|
54
58
|
|
55
|
-
|
56
59
|
async def test_function_tool_no_profile(self):
|
57
60
|
# when
|
58
61
|
response = await self.graph.ainvoke({"messages": [("user", "add 1, 2")]})
|
@@ -62,10 +65,7 @@ class TestPocketLanggraphNoProfile(IsolatedAsyncioTestCase):
|
|
62
65
|
|
63
66
|
# then
|
64
67
|
self.assertEqual(tool_call.tool_calls[0]["name"], "add")
|
65
|
-
self.assertEqual(tool_call.tool_calls[0]["args"], {
|
66
|
-
'a': 1,
|
67
|
-
'b': 2
|
68
|
-
})
|
68
|
+
self.assertEqual(tool_call.tool_calls[0]["args"], {"a": 1, "b": 2})
|
69
69
|
self.assertEqual(tool_result.content, "3")
|
70
70
|
|
71
71
|
async def test_pydantic_function_tool_no_profile(self):
|
@@ -77,15 +77,16 @@ class TestPocketLanggraphNoProfile(IsolatedAsyncioTestCase):
|
|
77
77
|
|
78
78
|
# then
|
79
79
|
self.assertEqual(tool_call.tool_calls[0]["name"], "sub_pydantic_args")
|
80
|
-
self.assertEqual(
|
81
|
-
|
82
|
-
|
83
|
-
})
|
80
|
+
self.assertEqual(
|
81
|
+
tool_call.tool_calls[0]["args"], {"a": {"first": 1}, "b": {"second": 2}}
|
82
|
+
)
|
84
83
|
self.assertEqual(tool_result.content, "-1")
|
85
84
|
|
86
85
|
async def test_wasm_tool_no_profile(self):
|
87
86
|
# when
|
88
|
-
response = await self.graph.ainvoke(
|
87
|
+
response = await self.graph.ainvoke(
|
88
|
+
{"messages": [("user", "echo 'hello world'")]}
|
89
|
+
)
|
89
90
|
|
90
91
|
tool_call = response["messages"][1]
|
91
92
|
tool_result = response["messages"][2]
|
@@ -94,9 +95,7 @@ class TestPocketLanggraphNoProfile(IsolatedAsyncioTestCase):
|
|
94
95
|
|
95
96
|
# then
|
96
97
|
self.assertEqual(tool_call.tool_calls[0]["name"], "simple_echo_text")
|
97
|
-
self.assertEqual(tool_call.tool_calls[0]["args"], {
|
98
|
-
'text': 'hello world'
|
99
|
-
})
|
98
|
+
self.assertEqual(tool_call.tool_calls[0]["args"], {"text": "hello world"})
|
100
99
|
self.assertTrue(output["stdout"].startswith("echo message : hello world"))
|
101
100
|
|
102
101
|
@staticmethod
|
@@ -1,20 +1,18 @@
|
|
1
1
|
import ast
|
2
2
|
from unittest.async_case import IsolatedAsyncioTestCase
|
3
3
|
|
4
|
+
from hyperpocket.config import config, secret
|
5
|
+
from hyperpocket.tool import from_git
|
4
6
|
from langchain_openai import ChatOpenAI
|
5
|
-
from langgraph.constants import
|
7
|
+
from langgraph.constants import END, START
|
6
8
|
from langgraph.graph import MessagesState, StateGraph
|
7
9
|
from langgraph.prebuilt import tools_condition
|
8
10
|
from pydantic import BaseModel
|
9
11
|
|
10
|
-
from hyperpocket.config import config, secret
|
11
|
-
from hyperpocket.tool import from_git
|
12
12
|
from hyperpocket_langgraph import PocketLanggraph
|
13
13
|
|
14
14
|
|
15
15
|
class TestPocketLanggraphUseProfile(IsolatedAsyncioTestCase):
|
16
|
-
|
17
|
-
|
18
16
|
async def asyncSetUp(self):
|
19
17
|
config.public_server_port = "https"
|
20
18
|
config.public_hostname = "localhost"
|
@@ -24,15 +22,21 @@ class TestPocketLanggraphUseProfile(IsolatedAsyncioTestCase):
|
|
24
22
|
|
25
23
|
self.pocket = PocketLanggraph(
|
26
24
|
tools=[
|
27
|
-
from_git(
|
25
|
+
from_git(
|
26
|
+
"https://github.com/vessl-ai/hyperawesometools",
|
27
|
+
"main",
|
28
|
+
"managed-tools/none/simple-echo-tool",
|
29
|
+
),
|
28
30
|
self.add,
|
29
|
-
self.sub_pydantic_args
|
31
|
+
self.sub_pydantic_args,
|
30
32
|
],
|
31
|
-
use_profile=True
|
33
|
+
use_profile=True,
|
32
34
|
)
|
33
35
|
tools = self.pocket.get_tools()
|
34
36
|
tool_node = self.pocket.get_tool_node()
|
35
|
-
self.llm = ChatOpenAI(
|
37
|
+
self.llm = ChatOpenAI(
|
38
|
+
model="gpt-4o", api_key=secret["OPENAI_API_KEY"]
|
39
|
+
).bind_tools(tools=tools)
|
36
40
|
|
37
41
|
def chatbot(state: MessagesState):
|
38
42
|
return {"messages": [self.llm.invoke(state["messages"])]}
|
@@ -52,7 +56,6 @@ class TestPocketLanggraphUseProfile(IsolatedAsyncioTestCase):
|
|
52
56
|
async def asyncTearDown(self):
|
53
57
|
self.pocket._teardown_server()
|
54
58
|
|
55
|
-
|
56
59
|
async def test_function_tool_use_profile(self):
|
57
60
|
# when
|
58
61
|
response = await self.graph.ainvoke({"messages": [("user", "add 1, 2")]})
|
@@ -62,10 +65,7 @@ class TestPocketLanggraphUseProfile(IsolatedAsyncioTestCase):
|
|
62
65
|
|
63
66
|
# then
|
64
67
|
self.assertEqual(tool_call.tool_calls[0]["name"], "add")
|
65
|
-
self.assertEqual(tool_call.tool_calls[0]["args"]["body"], {
|
66
|
-
'a': 1,
|
67
|
-
'b': 2
|
68
|
-
})
|
68
|
+
self.assertEqual(tool_call.tool_calls[0]["args"]["body"], {"a": 1, "b": 2})
|
69
69
|
self.assertEqual(tool_result.content, "3")
|
70
70
|
|
71
71
|
async def test_pydantic_function_tool_use_profile(self):
|
@@ -77,15 +77,17 @@ class TestPocketLanggraphUseProfile(IsolatedAsyncioTestCase):
|
|
77
77
|
|
78
78
|
# then
|
79
79
|
self.assertEqual(tool_call.tool_calls[0]["name"], "sub_pydantic_args")
|
80
|
-
self.assertEqual(
|
81
|
-
|
82
|
-
|
83
|
-
|
80
|
+
self.assertEqual(
|
81
|
+
tool_call.tool_calls[0]["args"]["body"],
|
82
|
+
{"a": {"first": 1}, "b": {"second": 2}},
|
83
|
+
)
|
84
84
|
self.assertEqual(tool_result.content, "-1")
|
85
85
|
|
86
86
|
async def test_wasm_tool_use_profile(self):
|
87
87
|
# when
|
88
|
-
response = await self.graph.ainvoke(
|
88
|
+
response = await self.graph.ainvoke(
|
89
|
+
{"messages": [("user", "echo 'hello world'")]}
|
90
|
+
)
|
89
91
|
|
90
92
|
tool_call = response["messages"][1]
|
91
93
|
tool_result = response["messages"][2]
|
@@ -94,9 +96,9 @@ class TestPocketLanggraphUseProfile(IsolatedAsyncioTestCase):
|
|
94
96
|
|
95
97
|
# then
|
96
98
|
self.assertEqual(tool_call.tool_calls[0]["name"], "simple_echo_text")
|
97
|
-
self.assertEqual(
|
98
|
-
|
99
|
-
|
99
|
+
self.assertEqual(
|
100
|
+
tool_call.tool_calls[0]["args"]["body"], {"text": "hello world"}
|
101
|
+
)
|
100
102
|
self.assertTrue(output["stdout"].startswith("echo message : hello world"))
|
101
103
|
|
102
104
|
@staticmethod
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|