mito-ai 0.1.45__py3-none-any.whl → 0.1.46__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.
Files changed (69) hide show
  1. mito_ai/__init__.py +10 -1
  2. mito_ai/_version.py +1 -1
  3. mito_ai/anthropic_client.py +90 -5
  4. mito_ai/chat_history/handlers.py +63 -0
  5. mito_ai/chat_history/urls.py +32 -0
  6. mito_ai/completions/handlers.py +18 -20
  7. mito_ai/constants.py +3 -0
  8. mito_ai/streamlit_conversion/agent_utils.py +148 -30
  9. mito_ai/streamlit_conversion/prompts/prompt_constants.py +147 -24
  10. mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +2 -1
  11. mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +2 -2
  12. mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +4 -3
  13. mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +50 -0
  14. mito_ai/streamlit_conversion/streamlit_agent_handler.py +101 -107
  15. mito_ai/streamlit_conversion/streamlit_system_prompt.py +1 -0
  16. mito_ai/streamlit_conversion/streamlit_utils.py +13 -10
  17. mito_ai/streamlit_conversion/validate_streamlit_app.py +77 -82
  18. mito_ai/streamlit_preview/handlers.py +3 -4
  19. mito_ai/streamlit_preview/utils.py +11 -7
  20. mito_ai/tests/chat_history/test_chat_history.py +211 -0
  21. mito_ai/tests/message_history/test_message_history_utils.py +43 -19
  22. mito_ai/tests/providers/test_anthropic_client.py +178 -6
  23. mito_ai/tests/streamlit_conversion/test_apply_patch_to_text.py +368 -0
  24. mito_ai/tests/streamlit_conversion/test_fix_diff_headers.py +533 -0
  25. mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +71 -74
  26. mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +16 -16
  27. mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +17 -14
  28. mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +2 -2
  29. mito_ai/tests/user/__init__.py +2 -0
  30. mito_ai/tests/user/test_user.py +120 -0
  31. mito_ai/user/handlers.py +33 -0
  32. mito_ai/user/urls.py +21 -0
  33. mito_ai/utils/anthropic_utils.py +8 -6
  34. mito_ai/utils/message_history_utils.py +4 -3
  35. mito_ai/utils/telemetry_utils.py +7 -4
  36. {mito_ai-0.1.45.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +1 -1
  37. {mito_ai-0.1.45.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
  38. {mito_ai-0.1.45.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +1 -1
  39. mito_ai-0.1.45.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.0c3368195d954d2ed033.js → mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.20f12766ecd3d430568e.js +955 -173
  40. mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.20f12766ecd3d430568e.js.map +1 -0
  41. mito_ai-0.1.45.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.684f82575fcc2e3b350c.js → mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.54126ab6511271265443.js +5 -5
  42. mito_ai-0.1.45.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.684f82575fcc2e3b350c.js.map → mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.54126ab6511271265443.js.map +1 -1
  43. {mito_ai-0.1.45.dist-info → mito_ai-0.1.46.dist-info}/METADATA +1 -1
  44. {mito_ai-0.1.45.dist-info → mito_ai-0.1.46.dist-info}/RECORD +68 -58
  45. mito_ai-0.1.45.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.0c3368195d954d2ed033.js.map +0 -1
  46. {mito_ai-0.1.45.data → mito_ai-0.1.46.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
  47. {mito_ai-0.1.45.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +0 -0
  48. {mito_ai-0.1.45.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +0 -0
  49. {mito_ai-0.1.45.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +0 -0
  50. {mito_ai-0.1.45.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
  51. {mito_ai-0.1.45.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +0 -0
  52. {mito_ai-0.1.45.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +0 -0
  53. {mito_ai-0.1.45.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_apis_signOut_mjs-node_module-75790d.688c25857e7b81b1740f.js +0 -0
  54. {mito_ai-0.1.45.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_apis_signOut_mjs-node_module-75790d.688c25857e7b81b1740f.js.map +0 -0
  55. {mito_ai-0.1.45.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js +0 -0
  56. {mito_ai-0.1.45.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js.map +0 -0
  57. {mito_ai-0.1.45.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +0 -0
  58. {mito_ai-0.1.45.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js.map +0 -0
  59. {mito_ai-0.1.45.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js +0 -0
  60. {mito_ai-0.1.45.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js.map +0 -0
  61. {mito_ai-0.1.45.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_react-dom_client_js-node_modules_aws-amplify_ui-react_dist_styles_css.b43d4249e4d3dac9ad7b.js +0 -0
  62. {mito_ai-0.1.45.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_react-dom_client_js-node_modules_aws-amplify_ui-react_dist_styles_css.b43d4249e4d3dac9ad7b.js.map +0 -0
  63. {mito_ai-0.1.45.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +0 -0
  64. {mito_ai-0.1.45.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +0 -0
  65. {mito_ai-0.1.45.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
  66. {mito_ai-0.1.45.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +0 -0
  67. {mito_ai-0.1.45.dist-info → mito_ai-0.1.46.dist-info}/WHEEL +0 -0
  68. {mito_ai-0.1.45.dist-info → mito_ai-0.1.46.dist-info}/entry_points.txt +0 -0
  69. {mito_ai-0.1.45.dist-info → mito_ai-0.1.46.dist-info}/licenses/LICENSE +0 -0
@@ -2,14 +2,14 @@
2
2
  # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
3
 
4
4
  import pytest
5
- from mito_ai.anthropic_client import get_anthropic_system_prompt_and_messages, extract_and_parse_anthropic_json_response, AnthropicClient
6
- from mito_ai.utils.anthropic_utils import get_anthropic_completion_function_params, FAST_ANTHROPIC_MODEL
5
+ from mito_ai.anthropic_client import get_anthropic_system_prompt_and_messages, get_anthropic_system_prompt_and_messages_with_caching, add_cache_control_to_message, extract_and_parse_anthropic_json_response, AnthropicClient
6
+ from mito_ai.utils.anthropic_utils import FAST_ANTHROPIC_MODEL
7
7
  from anthropic.types import Message, TextBlock, ToolUseBlock, Usage, ToolUseBlock, Message, Usage, TextBlock
8
8
  from openai.types.chat import ChatCompletionMessageParam, ChatCompletionUserMessageParam, ChatCompletionAssistantMessageParam, ChatCompletionSystemMessageParam
9
- from mito_ai.completions.models import MessageType, ResponseFormatInfo, AgentResponse
10
- from unittest.mock import MagicMock, patch
9
+ from mito_ai.completions.models import MessageType
10
+ from unittest.mock import patch
11
11
  import anthropic
12
- from typing import List, Dict, Any, cast, Union
12
+ from typing import List, Dict, cast
13
13
 
14
14
 
15
15
  # Dummy base64 image (1x1 PNG)
@@ -272,4 +272,176 @@ async def test_model_selection_based_on_message_type(message_type, expected_mode
272
272
  # Verify that create was called with the expected model
273
273
  mock_create.assert_called_once()
274
274
  call_args = mock_create.call_args
275
- assert call_args[1]['model'] == expected_model
275
+ assert call_args[1]['model'] == expected_model
276
+
277
+
278
+ # Caching Tests
279
+
280
+ @pytest.mark.parametrize("message,expected_role,expected_content_type,expected_content_length,expected_cache_control", [
281
+ # String content message
282
+ (
283
+ {"role": "user", "content": "Hello world"},
284
+ "user",
285
+ list,
286
+ 1,
287
+ True
288
+ ),
289
+ # List content message
290
+ (
291
+ {
292
+ "role": "user",
293
+ "content": [
294
+ {"type": "text", "text": "First part"},
295
+ {"type": "text", "text": "Second part"}
296
+ ]
297
+ },
298
+ "user",
299
+ list,
300
+ 2,
301
+ True
302
+ ),
303
+ # Empty content message
304
+ (
305
+ {"role": "user", "content": []},
306
+ "user",
307
+ list,
308
+ 0,
309
+ False
310
+ ),
311
+ # Assistant message with string content
312
+ (
313
+ {"role": "assistant", "content": "I can help you with that."},
314
+ "assistant",
315
+ list,
316
+ 1,
317
+ True
318
+ ),
319
+ ])
320
+ def test_add_cache_control_to_message(message, expected_role, expected_content_type, expected_content_length, expected_cache_control):
321
+ """Test adding cache control to different types of messages."""
322
+ result = add_cache_control_to_message(message)
323
+
324
+ assert result["role"] == expected_role
325
+ assert isinstance(result["content"], expected_content_type)
326
+ assert len(result["content"]) == expected_content_length
327
+
328
+ if expected_cache_control and expected_content_length > 0:
329
+ # Should have cache_control on the last content block
330
+ last_block = result["content"][-1]
331
+ assert last_block["cache_control"] == {"type": "ephemeral"}
332
+
333
+ # If there are multiple blocks, earlier blocks should not have cache_control
334
+ if expected_content_length > 1:
335
+ for i in range(expected_content_length - 1):
336
+ assert "cache_control" not in result["content"][i]
337
+ elif expected_content_length == 0:
338
+ # Empty content should return unchanged
339
+ assert result == message
340
+
341
+
342
+ @pytest.mark.parametrize("messages,expected_system_type,expected_system_content", [
343
+ # With system prompt
344
+ (
345
+ [
346
+ ChatCompletionSystemMessageParam(role="system", content="You are a helpful assistant."),
347
+ ChatCompletionUserMessageParam(role="user", content="Hello!")
348
+ ],
349
+ list,
350
+ "You are a helpful assistant.",
351
+ ),
352
+ # Without system prompt
353
+ (
354
+ [
355
+ ChatCompletionUserMessageParam(role="user", content="Hello!"),
356
+ ChatCompletionAssistantMessageParam(role="assistant", content="Hi there!")
357
+ ],
358
+ anthropic.Omit,
359
+ None,
360
+ ),
361
+ # Multiple system messages (should take last one)
362
+ (
363
+ [
364
+ ChatCompletionSystemMessageParam(role="system", content="First system message."),
365
+ ChatCompletionSystemMessageParam(role="system", content="Second system message."),
366
+ ChatCompletionUserMessageParam(role="user", content="Hello!"),
367
+ ChatCompletionUserMessageParam(role="user", content="Hello!"),
368
+ ChatCompletionUserMessageParam(role="user", content="Hello!")
369
+ ],
370
+ list,
371
+ "Second system message.",
372
+ ),
373
+ ])
374
+ def test_caching_system_prompt_scenarios(messages, expected_system_type, expected_system_content):
375
+ """Test caching with different system prompt scenarios."""
376
+ system_prompt, anthropic_messages = get_anthropic_system_prompt_and_messages_with_caching(messages)
377
+
378
+ # Check system prompt
379
+ assert isinstance(system_prompt, expected_system_type)
380
+ if expected_system_content:
381
+ assert system_prompt[0]["text"] == expected_system_content
382
+ assert system_prompt[0]["cache_control"] == {"type": "ephemeral"}
383
+
384
+
385
+ @pytest.mark.parametrize("message_count,expected_cache_boundary", [
386
+ (1, None), # 1 message, No cache boundary
387
+ (3, None), # 3 messages, No cache boundary
388
+ (5, 1), # 5 messages, cache at index 2
389
+ (10, 6), # 10 messages, cache at index 6
390
+ ])
391
+ def test_caching_conversation_history(message_count, expected_cache_boundary):
392
+ """Test that conversation history is cached at the keep_recent boundary for different message counts."""
393
+
394
+ # Create messages based on the parameter
395
+ messages: List[ChatCompletionMessageParam] = [
396
+ ChatCompletionSystemMessageParam(role="system", content="You are helpful.")
397
+ ]
398
+
399
+ # Add message pairs
400
+ for i in range(message_count):
401
+ messages.append(ChatCompletionUserMessageParam(role="user", content=f"Message {i+1}"))
402
+
403
+ system_prompt, anthropic_messages = get_anthropic_system_prompt_and_messages_with_caching(messages)
404
+
405
+ # System prompt should have cache control
406
+ assert isinstance(system_prompt, list)
407
+ assert system_prompt[0]["cache_control"] == {"type": "ephemeral"}
408
+
409
+ print(anthropic_messages)
410
+
411
+ if expected_cache_boundary is None:
412
+ # Verify no cache boundry
413
+ assert all("cache_control" not in str(message) for message in anthropic_messages)
414
+ else:
415
+ # Other messages should not have cache control
416
+ for i, message in enumerate(anthropic_messages):
417
+ if i == expected_cache_boundary:
418
+ assert anthropic_messages[expected_cache_boundary]["content"][0]["cache_control"] == {"type": "ephemeral"}
419
+ else:
420
+ assert "cache_control" not in str(message)
421
+
422
+ def test_caching_with_mixed_content():
423
+ """Test caching with mixed text and image content."""
424
+ messages: List[ChatCompletionMessageParam] = [
425
+ ChatCompletionSystemMessageParam(role="system", content="You are a helpful assistant."),
426
+ ChatCompletionUserMessageParam(role="user", content=[
427
+ {"type": "text", "text": "Here is an image:"},
428
+ {"type": "image_url", "image_url": {"url": DUMMY_IMAGE_DATA_URL}}
429
+ ])
430
+ ]
431
+ system_prompt, anthropic_messages = get_anthropic_system_prompt_and_messages_with_caching(messages)
432
+
433
+ # System prompt should have cache control
434
+ assert isinstance(system_prompt, list)
435
+ assert system_prompt[0]["cache_control"] == {"type": "ephemeral"}
436
+
437
+ # User message should NOT have cache control (only 1 message, so boundary is invalid)
438
+ user_message = anthropic_messages[0]
439
+ assert user_message["role"] == "user"
440
+ assert isinstance(user_message["content"], list)
441
+ assert len(user_message["content"]) == 2
442
+
443
+ # No content blocks should have cache control (too few messages to cache)
444
+ assert user_message["content"][0]["type"] == "text"
445
+ assert "cache_control" not in user_message["content"][0]
446
+ assert user_message["content"][1]["type"] == "image"
447
+ assert "cache_control" not in user_message["content"][1]
@@ -0,0 +1,368 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ import pytest
5
+ from mito_ai.streamlit_conversion.agent_utils import apply_patch_to_text
6
+
7
+
8
+ @pytest.mark.parametrize("original_text,diff,expected_result", [
9
+ # Test case 1: Simple line replacement
10
+ (
11
+ """import streamlit as st
12
+
13
+ st.markdown(\"\"\"
14
+ <style>
15
+ #MainMenu {visibility: hidden;}
16
+ .stAppDeployButton {display:none;}
17
+ footer {visibility: hidden;}
18
+ .stMainBlockContainer {padding: 2rem 1rem 2rem 1rem;}
19
+ </style>
20
+ \"\"\", unsafe_allow_html=True)
21
+
22
+ st.title("Simple Calculation")
23
+
24
+ x = 5
25
+ y = 10
26
+ result = x + y
27
+
28
+ st.write(f"x = {x}")
29
+ st.write(f"y = {y}")
30
+ st.write(f"x + y = {result}")""",
31
+ """--- a/app.py
32
+ +++ b/app.py
33
+ @@ -10,4 +10,4 @@
34
+ \"\"\", unsafe_allow_html=True)
35
+
36
+ -st.title("Simple Calculation")
37
+ +st.title("Math Examples")
38
+
39
+ x = 5""",
40
+ """import streamlit as st
41
+
42
+ st.markdown(\"\"\"
43
+ <style>
44
+ #MainMenu {visibility: hidden;}
45
+ .stAppDeployButton {display:none;}
46
+ footer {visibility: hidden;}
47
+ .stMainBlockContainer {padding: 2rem 1rem 2rem 1rem;}
48
+ </style>
49
+ \"\"\", unsafe_allow_html=True)
50
+
51
+ st.title("Math Examples")
52
+
53
+ x = 5
54
+ y = 10
55
+ result = x + y
56
+
57
+ st.write(f"x = {x}")
58
+ st.write(f"y = {y}")
59
+ st.write(f"x + y = {result}")"""
60
+ ),
61
+
62
+ # Test case 2: Add new lines
63
+ (
64
+ """import streamlit as st
65
+
66
+ st.title("My App")""",
67
+ """--- a/app.py
68
+ +++ b/app.py
69
+ @@ -1,3 +1,5 @@
70
+ import streamlit as st
71
+
72
+ +st.header("Welcome")
73
+ st.title("My App")
74
+ +st.write("This is a test app")""",
75
+ """import streamlit as st
76
+
77
+ st.header("Welcome")
78
+ st.title("My App")
79
+ st.write("This is a test app")"""
80
+ ),
81
+
82
+ # Test case 3: Remove lines
83
+ (
84
+ """import streamlit as st
85
+
86
+ st.header("Welcome")
87
+ st.title("My App")
88
+ st.write("This is a test app")""",
89
+ """--- a/app.py
90
+ +++ b/app.py
91
+ @@ -1,5 +1,3 @@
92
+ import streamlit as st
93
+
94
+ -st.header("Welcome")
95
+ st.title("My App")
96
+ -st.write("This is a test app")""",
97
+ """import streamlit as st
98
+
99
+ st.title("My App")
100
+ """
101
+ ),
102
+
103
+ # Test case 4: Single hunk with multiple changes
104
+ (
105
+ """import streamlit as st
106
+
107
+ st.title("Old Title")
108
+ x = 5
109
+ y = 10
110
+ st.write("Old message")""",
111
+ """--- a/app.py
112
+ +++ b/app.py
113
+ @@ -1,6 +1,6 @@
114
+ import streamlit as st
115
+
116
+ -st.title("Old Title")
117
+ +st.title("New Title")
118
+ x = 5
119
+ y = 10
120
+ -st.write("Old message")
121
+ +st.write("New message")""",
122
+ """import streamlit as st
123
+
124
+ st.title("New Title")
125
+ x = 5
126
+ y = 10
127
+ st.write("New message")"""
128
+ ),
129
+
130
+ # Test case 5: Empty diff
131
+ (
132
+ """import streamlit as st
133
+
134
+ st.title("My App")""",
135
+ "",
136
+ """import streamlit as st
137
+
138
+ st.title("My App")"""
139
+ ),
140
+
141
+ # Test case 6: Whitespace-only changes
142
+ (
143
+ """import streamlit as st
144
+ st.title("My App")""",
145
+ """--- a/app.py
146
+ +++ b/app.py
147
+ @@ -1,2 +1,2 @@
148
+ import streamlit as st
149
+ -st.title("My App")
150
+ +st.title("My App")""",
151
+ """import streamlit as st
152
+ st.title("My App")"""
153
+ ),
154
+
155
+ # Test case 7: Replace multiple consecutive lines
156
+ (
157
+ """import streamlit as st
158
+
159
+ st.title("My App")
160
+ st.write("Line 1")
161
+ st.write("Line 2")
162
+ st.write("Line 3")
163
+
164
+ x = 5""",
165
+ """--- a/app.py
166
+ +++ b/app.py
167
+ @@ -3,4 +3,2 @@
168
+ st.title("My App")
169
+ -st.write("Line 1")
170
+ -st.write("Line 2")
171
+ -st.write("Line 3")
172
+ +st.write("New content")
173
+
174
+ x = 5""",
175
+ """import streamlit as st
176
+
177
+ st.title("My App")
178
+ st.write("New content")
179
+
180
+ x = 5"""
181
+ ),
182
+
183
+ # Test case 8: Add lines at the beginning
184
+ (
185
+ """import streamlit as st
186
+
187
+ st.title("My App")""",
188
+ """--- a/app.py
189
+ +++ b/app.py
190
+ @@ -1,2 +1,3 @@
191
+ +import pandas as pd
192
+ import streamlit as st
193
+
194
+ st.title("My App")""",
195
+ """import pandas as pd
196
+ import streamlit as st
197
+
198
+ st.title("My App")"""
199
+ ),
200
+
201
+ # Test case 9: Add lines at the end
202
+ (
203
+ """import streamlit as st
204
+
205
+ st.title("My App")""",
206
+ """--- a/app.py
207
+ +++ b/app.py
208
+ @@ -3,1 +3,4 @@
209
+ st.title("My App")
210
+ +
211
+ +st.write("Footer content")
212
+ +st.write("More footer")""",
213
+ """import streamlit as st
214
+
215
+ st.title("My App")
216
+
217
+ st.write("Footer content")
218
+ st.write("More footer")"""
219
+ ),
220
+
221
+ # Test case 10: Complex replacement with context
222
+ (
223
+ """import streamlit as st
224
+
225
+ # This is a comment
226
+ st.title("Old Title")
227
+ # Another comment
228
+ x = 5
229
+ y = 10
230
+ # Final comment""",
231
+ """--- a/app.py
232
+ +++ b/app.py
233
+ @@ -2,4 +2,4 @@
234
+
235
+ # This is a comment
236
+ -st.title("Old Title")
237
+ +st.title("New Title")
238
+ # Another comment
239
+ x = 5""",
240
+ """import streamlit as st
241
+
242
+ # This is a comment
243
+ st.title("New Title")
244
+ # Another comment
245
+ x = 5
246
+ y = 10
247
+ # Final comment"""
248
+ ),
249
+
250
+ # Test case 11: Simple multiple hunks - title change and add footer
251
+ (
252
+ """import streamlit as st
253
+
254
+ st.title("Old App")
255
+ st.write("Hello World")""",
256
+ """--- a/app.py
257
+ +++ b/app.py
258
+ @@ -3,1 +3,1 @@
259
+ -st.title("Old App")
260
+ +st.title("New App")
261
+ @@ -4,1 +4,3 @@
262
+ st.write("Hello World")
263
+ +
264
+ +st.write("Footer text")""",
265
+ """import streamlit as st
266
+
267
+ st.title("New App")
268
+ st.write("Hello World")
269
+
270
+ st.write("Footer text")"""
271
+ ),
272
+
273
+ # Test case 12: Simple multiple hunks - remove and add lines
274
+ (
275
+ """import streamlit as st
276
+ st.write("Remove this")
277
+ st.title("My App")""",
278
+ """--- a/app.py
279
+ +++ b/app.py
280
+ @@ -2,1 +2,0 @@
281
+ -st.write("Remove this")
282
+ @@ -3,1 +3,2 @@
283
+ st.title("My App")
284
+ +st.write("Add this")""",
285
+ """import streamlit as st
286
+ st.title("My App")
287
+ st.write("Add this")"""
288
+ ),
289
+
290
+ # Test case 13: Multiple hunks with context lines
291
+ (
292
+ """import streamlit as st
293
+
294
+ st.title("Old Title")
295
+ st.write("Some content")
296
+
297
+ st.write("Old message")""",
298
+ """--- a/app.py
299
+ +++ b/app.py
300
+ @@ -3,1 +3,1 @@
301
+ -st.title("Old Title")
302
+ +st.title("New Title")
303
+ @@ -6,1 +6,1 @@
304
+ -st.write("Old message")
305
+ +st.write("New message")""",
306
+ """import streamlit as st
307
+
308
+ st.title("New Title")
309
+ st.write("Some content")
310
+
311
+ st.write("New message")"""
312
+ ),
313
+
314
+ # Test case 14: Multiple hunks - add imports and change content
315
+ (
316
+ """import streamlit as st
317
+
318
+ st.title("My App")
319
+ st.write("Hello")
320
+ st.write("World")""",
321
+ """--- a/app.py
322
+ +++ b/app.py
323
+ @@ -1,1 +1,3 @@
324
+ import streamlit as st
325
+ +import pandas as pd
326
+ +import numpy as np
327
+ @@ -4,2 +4,2 @@
328
+ st.write("Hello")
329
+ -st.write("World")
330
+ +st.write("World!")""",
331
+ """import streamlit as st
332
+ import pandas as pd
333
+ import numpy as np
334
+
335
+ st.title("My App")
336
+ st.write("Hello")
337
+ st.write("World!")"""
338
+ ),
339
+
340
+ # Test case 15: Add emoji to streamlit app title
341
+ (
342
+ """import streamlit as st
343
+
344
+ st.title("My App")
345
+ st.write("Welcome to my application")""",
346
+ """--- a/app.py
347
+ +++ b/app.py
348
+ @@ -3,1 +3,1 @@
349
+ -st.title("My App")
350
+ +st.title("🚀 My App")
351
+
352
+ st.write("Welcome to my application")""",
353
+ """import streamlit as st
354
+
355
+ st.title("🚀 My App")
356
+ st.write("Welcome to my application")"""
357
+ )
358
+ ])
359
+ def test_apply_patch_to_text(original_text, diff, expected_result):
360
+ """Test the apply_patch_to_text function with various diff scenarios."""
361
+ result = apply_patch_to_text(original_text, diff)
362
+
363
+ print(f"Original text: {repr(original_text)}")
364
+ print(f"Diff: {repr(diff)}")
365
+ print(f"Expected result: {repr(expected_result)}")
366
+ print(f"Result: {repr(result)}")
367
+
368
+ assert result == expected_result