lollms-client 0.29.2__py3-none-any.whl → 0.31.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.
Potentially problematic release.
This version of lollms-client might be problematic. Click here for more details.
- lollms_client/__init__.py +1 -1
- lollms_client/llm_bindings/ollama/__init__.py +19 -0
- lollms_client/lollms_core.py +141 -50
- lollms_client/lollms_discussion.py +479 -58
- lollms_client/lollms_llm_binding.py +17 -0
- lollms_client/lollms_utilities.py +136 -0
- {lollms_client-0.29.2.dist-info → lollms_client-0.31.0.dist-info}/METADATA +61 -222
- {lollms_client-0.29.2.dist-info → lollms_client-0.31.0.dist-info}/RECORD +12 -11
- {lollms_client-0.29.2.dist-info → lollms_client-0.31.0.dist-info}/top_level.txt +1 -0
- test/test_lollms_discussion.py +368 -0
- {lollms_client-0.29.2.dist-info → lollms_client-0.31.0.dist-info}/WHEEL +0 -0
- {lollms_client-0.29.2.dist-info → lollms_client-0.31.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# test_lollms_discussion.py
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from unittest.mock import MagicMock, patch
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import base64
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
# Assuming your class definitions are in a file named 'lollms_discussion_module.py'
|
|
10
|
+
# Adjust the import path as necessary.
|
|
11
|
+
from lollms_client import LollmsDataManager, LollmsDiscussion
|
|
12
|
+
|
|
13
|
+
# --- Test Fixtures and Helpers ---
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def mock_lollms_client():
|
|
17
|
+
"""Creates a mock LollmsClient that simulates its core functions."""
|
|
18
|
+
client = MagicMock()
|
|
19
|
+
# Simulate token counting with a simple word count
|
|
20
|
+
client.count_tokens.side_effect = lambda text: len(str(text).split())
|
|
21
|
+
# Simulate image token counting with a fixed value
|
|
22
|
+
client.count_image_tokens.return_value = 75
|
|
23
|
+
# Simulate structured content generation
|
|
24
|
+
client.generate_structured_content.return_value = {"title": "A Mocked Title"}
|
|
25
|
+
# Simulate text generation for summary and memory
|
|
26
|
+
client.generate_text.return_value = "This is a mock summary."
|
|
27
|
+
# Simulate chat generation
|
|
28
|
+
client.chat.return_value = "This is a mock chat response."
|
|
29
|
+
client.remove_thinking_blocks.return_value = "This is a mock chat response."
|
|
30
|
+
return client
|
|
31
|
+
|
|
32
|
+
@pytest.fixture
|
|
33
|
+
def db_manager(tmp_path):
|
|
34
|
+
"""Creates a LollmsDataManager with a temporary database."""
|
|
35
|
+
db_file = tmp_path / "test_discussions.db"
|
|
36
|
+
return LollmsDataManager(f'sqlite:///{db_file}')
|
|
37
|
+
|
|
38
|
+
def create_dummy_image_b64(text="Test"):
|
|
39
|
+
"""Generates a valid base64 encoded dummy PNG image string."""
|
|
40
|
+
try:
|
|
41
|
+
from PIL import Image, ImageDraw
|
|
42
|
+
img = Image.new('RGB', (60, 30), color = 'red')
|
|
43
|
+
d = ImageDraw.Draw(img)
|
|
44
|
+
d.text((10,10), text, fill='white')
|
|
45
|
+
|
|
46
|
+
# In-memory saving to avoid disk I/O in test
|
|
47
|
+
from io import BytesIO
|
|
48
|
+
buffered = BytesIO()
|
|
49
|
+
img.save(buffered, format="PNG")
|
|
50
|
+
return base64.b64encode(buffered.getvalue()).decode('utf-8')
|
|
51
|
+
except ImportError:
|
|
52
|
+
# Fallback if Pillow is not installed
|
|
53
|
+
# This is a 1x1 transparent pixel
|
|
54
|
+
return "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
|
|
55
|
+
|
|
56
|
+
@pytest.fixture
|
|
57
|
+
def dummy_image_b64_1():
|
|
58
|
+
return create_dummy_image_b64("Image 1")
|
|
59
|
+
|
|
60
|
+
@pytest.fixture
|
|
61
|
+
def dummy_image_b64_2():
|
|
62
|
+
return create_dummy_image_b64("Image 2")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# --- Test Class ---
|
|
66
|
+
|
|
67
|
+
class TestLollmsDiscussion:
|
|
68
|
+
|
|
69
|
+
def test_creation_in_memory(self, mock_lollms_client):
|
|
70
|
+
"""Tests that an in-memory discussion can be created."""
|
|
71
|
+
disc = LollmsDiscussion.create_new(lollms_client=mock_lollms_client)
|
|
72
|
+
assert disc.id is not None
|
|
73
|
+
assert not disc._is_db_backed
|
|
74
|
+
|
|
75
|
+
def test_creation_db_backed(self, mock_lollms_client, db_manager):
|
|
76
|
+
"""Tests that a database-backed discussion can be created."""
|
|
77
|
+
metadata = {"title": "DB Test"}
|
|
78
|
+
disc = LollmsDiscussion.create_new(
|
|
79
|
+
lollms_client=mock_lollms_client,
|
|
80
|
+
db_manager=db_manager,
|
|
81
|
+
discussion_metadata=metadata
|
|
82
|
+
)
|
|
83
|
+
assert disc._is_db_backed
|
|
84
|
+
assert disc.metadata["title"] == "DB Test"
|
|
85
|
+
|
|
86
|
+
def test_add_and_get_message(self, mock_lollms_client):
|
|
87
|
+
"""Tests basic message addition and retrieval."""
|
|
88
|
+
disc = LollmsDiscussion.create_new(lollms_client=mock_lollms_client)
|
|
89
|
+
disc.add_message(sender="user", content="Hello, world!")
|
|
90
|
+
|
|
91
|
+
assert disc.active_branch_id is not None
|
|
92
|
+
messages = disc.get_messages()
|
|
93
|
+
assert len(messages) == 1
|
|
94
|
+
assert messages[0].sender == "user"
|
|
95
|
+
assert messages[0].content == "Hello, world!"
|
|
96
|
+
|
|
97
|
+
def test_data_zones_and_export(self, mock_lollms_client):
|
|
98
|
+
"""Tests that data zones are correctly included in exports."""
|
|
99
|
+
disc = LollmsDiscussion.create_new(lollms_client=mock_lollms_client)
|
|
100
|
+
disc.system_prompt = "You are a tester."
|
|
101
|
+
disc.memory = "Remember this."
|
|
102
|
+
disc.user_data_zone = "User is a dev."
|
|
103
|
+
disc.add_message(sender="user", content="Test prompt.")
|
|
104
|
+
|
|
105
|
+
exported_md = disc.export("markdown")
|
|
106
|
+
assert "You are a tester." in exported_md
|
|
107
|
+
assert "-- Memory --" in exported_md
|
|
108
|
+
assert "Remember this." in exported_md
|
|
109
|
+
assert "-- User Data Zone --" in exported_md
|
|
110
|
+
assert "User is a dev." in exported_md
|
|
111
|
+
assert "**User**: Test prompt." in exported_md
|
|
112
|
+
|
|
113
|
+
def test_message_image_activation(self, mock_lollms_client, dummy_image_b64_1, dummy_image_b64_2):
|
|
114
|
+
"""Tests activating and deactivating images on a specific message."""
|
|
115
|
+
disc = LollmsDiscussion.create_new(lollms_client=mock_lollms_client)
|
|
116
|
+
msg = disc.add_message(sender="user", content="Look", images=[dummy_image_b64_1, dummy_image_b64_2])
|
|
117
|
+
|
|
118
|
+
# Initially, both images should be active
|
|
119
|
+
assert len(msg.get_active_images()) == 2
|
|
120
|
+
|
|
121
|
+
# Deactivate the first image
|
|
122
|
+
msg.toggle_image_activation(0, active=False)
|
|
123
|
+
assert len(msg.get_active_images()) == 1
|
|
124
|
+
all_imgs = msg.get_all_images()
|
|
125
|
+
assert not all_imgs[0]["active"]
|
|
126
|
+
assert all_imgs[1]["active"]
|
|
127
|
+
|
|
128
|
+
# Toggle the first image back on
|
|
129
|
+
msg.toggle_image_activation(0)
|
|
130
|
+
assert len(msg.get_active_images()) == 2
|
|
131
|
+
assert msg.get_all_images()[0]["active"]
|
|
132
|
+
|
|
133
|
+
def test_discussion_image_management(self, mock_lollms_client, dummy_image_b64_1, dummy_image_b64_2):
|
|
134
|
+
"""Tests adding, toggling, and retrieving discussion-level images."""
|
|
135
|
+
disc = LollmsDiscussion.create_new(lollms_client=mock_lollms_client)
|
|
136
|
+
|
|
137
|
+
# Add two images
|
|
138
|
+
disc.add_discussion_image(dummy_image_b64_1)
|
|
139
|
+
disc.add_discussion_image(dummy_image_b64_2)
|
|
140
|
+
|
|
141
|
+
# Check initial state
|
|
142
|
+
assert len(disc.get_discussion_images()) == 2
|
|
143
|
+
assert disc.get_discussion_images()[0]["active"]
|
|
144
|
+
assert disc.get_discussion_images()[1]["active"]
|
|
145
|
+
|
|
146
|
+
# Deactivate the second image
|
|
147
|
+
disc.toggle_discussion_image_activation(1, active=False)
|
|
148
|
+
|
|
149
|
+
# Verify changes
|
|
150
|
+
all_disc_imgs = disc.get_discussion_images()
|
|
151
|
+
assert len(all_disc_imgs) == 2
|
|
152
|
+
assert all_disc_imgs[0]["active"]
|
|
153
|
+
assert not all_disc_imgs[1]["active"]
|
|
154
|
+
|
|
155
|
+
def test_export_with_multimodal_system_prompt(self, mock_lollms_client, dummy_image_b64_1):
|
|
156
|
+
"""Tests that active discussion images are included in the system prompt for export."""
|
|
157
|
+
disc = LollmsDiscussion.create_new(lollms_client=mock_lollms_client)
|
|
158
|
+
disc.system_prompt = "Analyze this image."
|
|
159
|
+
disc.add_discussion_image(dummy_image_b64_1) # This one is active
|
|
160
|
+
disc.add_discussion_image(create_dummy_image_b64("Inactive"))
|
|
161
|
+
disc.toggle_discussion_image_activation(1, active=False) # This one is inactive
|
|
162
|
+
|
|
163
|
+
disc.add_message(sender="user", content="What do you see?")
|
|
164
|
+
|
|
165
|
+
openai_export = disc.export("openai_chat")
|
|
166
|
+
|
|
167
|
+
system_message = openai_export[0]
|
|
168
|
+
assert system_message["role"] == "system"
|
|
169
|
+
|
|
170
|
+
content_parts = system_message["content"]
|
|
171
|
+
assert len(content_parts) == 2
|
|
172
|
+
|
|
173
|
+
text_part = next(p for p in content_parts if p["type"] == "text")
|
|
174
|
+
image_part = next(p for p in content_parts if p["type"] == "image_url")
|
|
175
|
+
|
|
176
|
+
assert text_part["text"] == "Analyze this image."
|
|
177
|
+
assert image_part["image_url"]["url"].endswith(dummy_image_b64_1)
|
|
178
|
+
|
|
179
|
+
def test_auto_title_updates_metadata(self, mock_lollms_client, db_manager):
|
|
180
|
+
"""Tests that auto_title calls the LLM and updates metadata."""
|
|
181
|
+
disc = LollmsDiscussion.create_new(lollms_client=mock_lollms_client, db_manager=db_manager)
|
|
182
|
+
disc.add_message(sender="user", content="This is a test to generate a title.")
|
|
183
|
+
|
|
184
|
+
title = disc.auto_title()
|
|
185
|
+
|
|
186
|
+
mock_lollms_client.generate_structured_content.assert_called_once()
|
|
187
|
+
assert title == "A Mocked Title"
|
|
188
|
+
assert disc.metadata['title'] == "A Mocked Title"
|
|
189
|
+
|
|
190
|
+
def test_delete_branch(self, mock_lollms_client, db_manager):
|
|
191
|
+
"""Tests deleting a message and its descendants."""
|
|
192
|
+
disc = LollmsDiscussion.create_new(lollms_client=mock_lollms_client, db_manager=db_manager)
|
|
193
|
+
|
|
194
|
+
msg_a = disc.add_message(sender="user", content="A")
|
|
195
|
+
msg_b = disc.add_message(sender="user", content="B", parent_id=msg_a.id)
|
|
196
|
+
msg_c = disc.add_message(sender="user", content="C", parent_id=msg_b.id)
|
|
197
|
+
|
|
198
|
+
assert disc.active_branch_id == msg_c.id
|
|
199
|
+
assert len(disc.get_messages()) == 3
|
|
200
|
+
|
|
201
|
+
disc.delete_branch(msg_b.id)
|
|
202
|
+
|
|
203
|
+
assert disc.active_branch_id == msg_a.id
|
|
204
|
+
|
|
205
|
+
remaining_messages = disc.get_messages()
|
|
206
|
+
assert len(remaining_messages) == 1
|
|
207
|
+
assert remaining_messages[0].id == msg_a.id
|
|
208
|
+
|
|
209
|
+
def test_branching_and_switching(self, mock_lollms_client):
|
|
210
|
+
"""Tests creating and switching between conversational branches."""
|
|
211
|
+
disc = LollmsDiscussion.create_new(lollms_client=mock_lollms_client)
|
|
212
|
+
|
|
213
|
+
root_msg = disc.add_message(sender="user", content="Hello")
|
|
214
|
+
ai_msg_1 = disc.add_message(sender="assistant", content="Hi there!")
|
|
215
|
+
|
|
216
|
+
disc.switch_to_branch(root_msg.id)
|
|
217
|
+
ai_msg_2 = disc.add_message(sender="assistant", content="Greetings!")
|
|
218
|
+
|
|
219
|
+
assert len(disc._message_index) == 3
|
|
220
|
+
assert disc.active_branch_id == ai_msg_2.id
|
|
221
|
+
|
|
222
|
+
branch_1 = disc.get_branch(ai_msg_1.id)
|
|
223
|
+
assert [m.id for m in branch_1] == [root_msg.id, ai_msg_1.id]
|
|
224
|
+
|
|
225
|
+
branch_2 = disc.get_branch(ai_msg_2.id)
|
|
226
|
+
assert [m.id for m in branch_2] == [root_msg.id, ai_msg_2.id]
|
|
227
|
+
|
|
228
|
+
def test_regenerate_branch(self, mock_lollms_client, db_manager):
|
|
229
|
+
"""Tests that regeneration deletes the old AI message and creates a new one."""
|
|
230
|
+
disc = LollmsDiscussion.create_new(lollms_client=mock_lollms_client, db_manager=db_manager)
|
|
231
|
+
|
|
232
|
+
user_msg = disc.add_message(sender="user", content="Tell me a joke.")
|
|
233
|
+
ai_msg = disc.add_message(sender="assistant", content="Why did the scarecrow win an award?")
|
|
234
|
+
|
|
235
|
+
original_ai_id = ai_msg.id
|
|
236
|
+
|
|
237
|
+
result = disc.regenerate_branch()
|
|
238
|
+
new_ai_msg = result["ai_message"]
|
|
239
|
+
|
|
240
|
+
assert new_ai_msg.id != original_ai_id
|
|
241
|
+
assert new_ai_msg.content == "This is a mock chat response."
|
|
242
|
+
|
|
243
|
+
messages = disc.get_messages(new_ai_msg.id)
|
|
244
|
+
assert len(messages) == 2
|
|
245
|
+
assert original_ai_id not in [m.id for m in messages]
|
|
246
|
+
|
|
247
|
+
assert original_ai_id in disc._messages_to_delete_from_db
|
|
248
|
+
|
|
249
|
+
def test_summarize_and_prune(self, mock_lollms_client):
|
|
250
|
+
"""Tests that the discussion is pruned when the token limit is exceeded."""
|
|
251
|
+
disc = LollmsDiscussion.create_new(lollms_client=mock_lollms_client)
|
|
252
|
+
disc.max_context_size = 20
|
|
253
|
+
|
|
254
|
+
disc.add_message(sender="user", content="This is the first long message to establish history.")
|
|
255
|
+
disc.add_message(sender="assistant", content="I see. This is the second long message.")
|
|
256
|
+
m3 = disc.add_message(sender="user", content="Third message.")
|
|
257
|
+
disc.add_message(sender="assistant", content="Fourth message.")
|
|
258
|
+
disc.add_message(sender="user", content="Fifth message.")
|
|
259
|
+
disc.add_message(sender="assistant", content="Sixth message.")
|
|
260
|
+
|
|
261
|
+
disc.summarize_and_prune(max_tokens=20, preserve_last_n=4)
|
|
262
|
+
|
|
263
|
+
mock_lollms_client.generate_text.assert_called_once()
|
|
264
|
+
assert "This is a mock summary." in disc.pruning_summary
|
|
265
|
+
assert disc.pruning_point_id == m3.id
|
|
266
|
+
|
|
267
|
+
def test_memorize_updates_memory_zone(self, mock_lollms_client):
|
|
268
|
+
"""Tests that memorize calls the LLM and appends to the memory data zone."""
|
|
269
|
+
disc = LollmsDiscussion.create_new(lollms_client=mock_lollms_client)
|
|
270
|
+
disc.add_message(sender="user", content="My favorite color is blue.")
|
|
271
|
+
|
|
272
|
+
disc.memorize()
|
|
273
|
+
|
|
274
|
+
mock_lollms_client.generate_text.assert_called_once()
|
|
275
|
+
assert "This is a mock summary." in disc.memory
|
|
276
|
+
assert "Memory entry from" in disc.memory
|
|
277
|
+
|
|
278
|
+
def test_get_context_status(self, mock_lollms_client, dummy_image_b64_1, dummy_image_b64_2):
|
|
279
|
+
"""Tests the token calculation in get_context_status."""
|
|
280
|
+
disc = LollmsDiscussion.create_new(lollms_client=mock_lollms_client)
|
|
281
|
+
disc.system_prompt = "You are a helpful AI."
|
|
282
|
+
disc.add_discussion_image(dummy_image_b64_1) # 1 active discussion image
|
|
283
|
+
|
|
284
|
+
msg = disc.add_message(sender="user", content="Hello there!", images=[dummy_image_b64_1, dummy_image_b64_2])
|
|
285
|
+
msg.toggle_image_activation(1, active=False) # 1 active message image
|
|
286
|
+
|
|
287
|
+
status = disc.get_context_status()
|
|
288
|
+
|
|
289
|
+
# System context: "!@>system:\nYou are a helpful AI.\n" -> 6 tokens
|
|
290
|
+
system_tokens = status["zones"]["system_context"]["tokens"]
|
|
291
|
+
assert system_tokens == 6
|
|
292
|
+
|
|
293
|
+
# Discussion Images: 1 active discussion image -> 75 tokens
|
|
294
|
+
discussion_image_zone = status["zones"]["discussion_images"]
|
|
295
|
+
assert discussion_image_zone["tokens"] == 75
|
|
296
|
+
|
|
297
|
+
# History:
|
|
298
|
+
# - Text: "!@>user:\nHello there!\n(2 image(s) attached)\n" -> 6 tokens
|
|
299
|
+
# - Image: 1 active message image -> 75 tokens
|
|
300
|
+
history_zone = status["zones"]["message_history"]
|
|
301
|
+
assert history_zone["breakdown"]["text_tokens"] == 6
|
|
302
|
+
assert history_zone["breakdown"]["image_tokens"] == 75
|
|
303
|
+
assert history_zone["tokens"] == 81 # 6 + 75
|
|
304
|
+
|
|
305
|
+
# Total should be sum of system, discussion images, and history
|
|
306
|
+
assert status["current_tokens"] == 6 + 75 + 81 # 162
|
|
307
|
+
|
|
308
|
+
# --- NEW PERSISTENCE AND SEPARATION TESTS ---
|
|
309
|
+
|
|
310
|
+
def test_image_separation_and_context(self, mock_lollms_client, dummy_image_b64_1, dummy_image_b64_2):
|
|
311
|
+
"""Tests that discussion and message images are distinct but aggregated correctly."""
|
|
312
|
+
disc = LollmsDiscussion.create_new(lollms_client=mock_lollms_client)
|
|
313
|
+
|
|
314
|
+
# Add one image to the discussion, one to the message
|
|
315
|
+
disc.add_discussion_image(dummy_image_b64_1)
|
|
316
|
+
msg = disc.add_message(sender="user", content="Check these out", images=[dummy_image_b64_2])
|
|
317
|
+
|
|
318
|
+
# Test separation
|
|
319
|
+
assert len(disc.get_discussion_images()) == 1
|
|
320
|
+
assert disc.get_discussion_images()[0]['data'] == dummy_image_b64_1
|
|
321
|
+
assert len(msg.get_all_images()) == 1
|
|
322
|
+
assert msg.get_all_images()[0]['data'] == dummy_image_b64_2
|
|
323
|
+
|
|
324
|
+
# Test aggregation for context
|
|
325
|
+
all_active_images = disc.get_active_images()
|
|
326
|
+
assert len(all_active_images) == 2
|
|
327
|
+
assert dummy_image_b64_1 in all_active_images
|
|
328
|
+
assert dummy_image_b64_2 in all_active_images
|
|
329
|
+
|
|
330
|
+
def test_full_multimodal_persistence(self, mock_lollms_client, db_manager, dummy_image_b64_1, dummy_image_b64_2):
|
|
331
|
+
"""End-to-end test for persisting both discussion and message images and their states."""
|
|
332
|
+
disc_id = None
|
|
333
|
+
|
|
334
|
+
# --- Scope 1: Create and Save ---
|
|
335
|
+
with db_manager.get_session():
|
|
336
|
+
disc = LollmsDiscussion.create_new(lollms_client=mock_lollms_client, db_manager=db_manager)
|
|
337
|
+
disc_id = disc.id
|
|
338
|
+
|
|
339
|
+
# Add and deactivate a discussion image
|
|
340
|
+
disc.add_discussion_image(dummy_image_b64_1)
|
|
341
|
+
disc.toggle_discussion_image_activation(0, active=False)
|
|
342
|
+
|
|
343
|
+
# Add and deactivate a message image
|
|
344
|
+
msg = disc.add_message(sender="user", content="Multimodal test", images=[dummy_image_b64_2])
|
|
345
|
+
msg.toggle_image_activation(0, active=False)
|
|
346
|
+
|
|
347
|
+
disc.commit()
|
|
348
|
+
disc.close()
|
|
349
|
+
|
|
350
|
+
# --- Scope 2: Reload and Verify ---
|
|
351
|
+
reloaded_disc = db_manager.get_discussion(mock_lollms_client, disc_id)
|
|
352
|
+
assert reloaded_disc is not None
|
|
353
|
+
|
|
354
|
+
# Verify discussion image state
|
|
355
|
+
reloaded_disc_imgs = reloaded_disc.get_discussion_images()
|
|
356
|
+
assert len(reloaded_disc_imgs) == 1
|
|
357
|
+
assert reloaded_disc_imgs[0]['data'] == dummy_image_b64_1
|
|
358
|
+
assert not reloaded_disc_imgs[0]['active'] # Must be inactive
|
|
359
|
+
|
|
360
|
+
# Verify message image state
|
|
361
|
+
reloaded_msg = reloaded_disc.get_messages()[0]
|
|
362
|
+
reloaded_msg_imgs = reloaded_msg.get_all_images()
|
|
363
|
+
assert len(reloaded_msg_imgs) == 1
|
|
364
|
+
assert reloaded_msg_imgs[0]['data'] == dummy_image_b64_2
|
|
365
|
+
assert not reloaded_msg_imgs[0]['active'] # Must be inactive
|
|
366
|
+
|
|
367
|
+
# Verify that get_active_images returns nothing
|
|
368
|
+
assert len(reloaded_disc.get_active_images()) == 0
|
|
File without changes
|
|
File without changes
|