django-unicom 25.2.27.dev0__tar.gz → 25.2.27.dev2__tar.gz

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 (146) hide show
  1. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/PKG-INFO +1 -1
  2. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/django_unicom.egg-info/PKG-INFO +1 -1
  3. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/_version.py +2 -2
  4. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/models/message.py +53 -2
  5. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/models/message_template.py +4 -0
  6. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/email/IMAP_thread_manager.py +2 -0
  7. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/email/listen_to_IMAP.py +8 -2
  8. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/email/send_email_message.py +4 -1
  9. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/html_inline_images.py +4 -0
  10. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/telegram/send_telegram_message.py +4 -1
  11. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/.cursor/rules/match_codebase_style.mdc +0 -0
  12. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/.cursor/rules/preserve_comments_and_irrelevant_code.mdc +0 -0
  13. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/.cursor/rules/require_context_before_changes.mdc +0 -0
  14. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/Dockerfile +0 -0
  15. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/MANIFEST.in +0 -0
  16. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/Makefile +0 -0
  17. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/README.md +0 -0
  18. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/conftest.py +0 -0
  19. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/django_unicom.egg-info/SOURCES.txt +0 -0
  20. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/django_unicom.egg-info/dependency_links.txt +0 -0
  21. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/django_unicom.egg-info/requires.txt +0 -0
  22. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/django_unicom.egg-info/top_level.txt +0 -0
  23. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/docker-compose.yaml +0 -0
  24. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/entrypoint.sh +0 -0
  25. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/manage.py +0 -0
  26. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/pyproject.toml +0 -0
  27. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/pytest.ini +0 -0
  28. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/requirements.txt +0 -0
  29. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/setup.cfg +0 -0
  30. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/setup.py +0 -0
  31. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/test_dkim_security.py +0 -0
  32. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/tests/__init__.py +0 -0
  33. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/tests/test_email_authentication.py +0 -0
  34. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/tests/test_email_live.py +0 -0
  35. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/tests/test_email_request_processing.py +0 -0
  36. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/tests/test_telegram_live.py +0 -0
  37. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/tests/utils.py +0 -0
  38. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/__init__.py +0 -0
  39. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/admin/__init__.py +0 -0
  40. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/admin/account_admin.py +0 -0
  41. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/admin/channel_admin.py +0 -0
  42. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/admin/chat_admin.py +0 -0
  43. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/admin/draft_message_admin.py +0 -0
  44. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/admin/email_inline_image_admin.py +0 -0
  45. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/admin/filters.py +0 -0
  46. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/admin/member_admin.py +0 -0
  47. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/admin/message_admin.py +0 -0
  48. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/admin/message_template_admin.py +0 -0
  49. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/admin/request_admin.py +0 -0
  50. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/apps.py +0 -0
  51. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/management/__init__.py +0 -0
  52. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/management/commands/__init__.py +0 -0
  53. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/management/commands/mail_inbox_listen.py +0 -0
  54. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/management/commands/run_as_llm_chat.py +0 -0
  55. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/management/commands/send_scheduled_messages.py +0 -0
  56. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/management/commands/start_imap_listeners.py +0 -0
  57. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/migrations/0001_initial.py +0 -0
  58. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/migrations/0002_emailinlineimage.py +0 -0
  59. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/migrations/0003_alter_emailinlineimage_email_message.py +0 -0
  60. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/migrations/0004_messagetemplateinlineimage.py +0 -0
  61. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/migrations/0005_emailinlineimage_hash_and_more.py +0 -0
  62. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/migrations/0006_channel_created_at_channel_created_by_and_more.py +0 -0
  63. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/migrations/0007_message_imap_uid.py +0 -0
  64. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/migrations/0008_add_all_members_group.py +0 -0
  65. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/migrations/0009_add_tool_call_types.py +0 -0
  66. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/migrations/0010_request_initial_request_request_llm_calls_count_and_more.py +0 -0
  67. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/migrations/0011_toolcall_add_message_reference.py +0 -0
  68. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/migrations/__init__.py +0 -0
  69. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/models/__init__.py +0 -0
  70. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/models/account.py +0 -0
  71. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/models/account_chat.py +0 -0
  72. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/models/channel.py +0 -0
  73. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/models/chat.py +0 -0
  74. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/models/constants.py +0 -0
  75. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/models/draft_message.py +0 -0
  76. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/models/fields.py +0 -0
  77. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/models/member.py +0 -0
  78. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/models/member_group.py +0 -0
  79. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/models/request.py +0 -0
  80. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/models/request_category.py +0 -0
  81. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/models/tool_call.py +0 -0
  82. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/models/update.py +0 -0
  83. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/__init__.py +0 -0
  84. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/chat_summary.py +0 -0
  85. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/crossplatform/__init__.py +0 -0
  86. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/crossplatform/reply_to_message.py +0 -0
  87. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/crossplatform/scheduler.py +0 -0
  88. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/crossplatform/send_message.py +0 -0
  89. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/decode_base64_image.py +0 -0
  90. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/email/__init__.py +0 -0
  91. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/email/email_tracking.py +0 -0
  92. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/email/quote_filter.py +0 -0
  93. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/email/replace_cid_images_with_base64.py +0 -0
  94. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/email/save_email_message.py +0 -0
  95. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/email/validate_email_config.py +0 -0
  96. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/get_public_origin.py +0 -0
  97. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/internal/__init__.py +0 -0
  98. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/internal/generate_text_message_data.py +0 -0
  99. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/internal/save_internal_message.py +0 -0
  100. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/internal/send_internal_message.py +0 -0
  101. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/llm/README.md +0 -0
  102. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/llm/__init__.py +0 -0
  103. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/llm/tool_calls.py +0 -0
  104. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/telegram/__init__.py +0 -0
  105. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/telegram/download_file.py +0 -0
  106. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/telegram/escape_markdown.py +0 -0
  107. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/telegram/get_file_path.py +0 -0
  108. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/telegram/save_telegram_message.py +0 -0
  109. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/telegram/set_telegram_webhook.py +0 -0
  110. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/telegram/start_typing_in_telegram.py +0 -0
  111. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/telegram/stop_typing_in_telegram.py +0 -0
  112. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/whatsapp/__init__.py +0 -0
  113. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/whatsapp/get_template.py +0 -0
  114. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/whatsapp/save_whatsapp_message.py +0 -0
  115. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/whatsapp/save_whatsapp_message_status.py +0 -0
  116. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/services/whatsapp/send_whatsapp_message.py +0 -0
  117. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/signals.py +0 -0
  118. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/static/unicom/css/bootstrap_scoped.css +0 -0
  119. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/static/unicom/css/draft_message_mobile.css +0 -0
  120. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/static/unicom/js/channel_config.js +0 -0
  121. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/static/unicom/js/tinymce_ai_template.js +0 -0
  122. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/static/unicom/js/tinymce_init.js +0 -0
  123. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/templates/admin/unicom/chat/change_list.html +0 -0
  124. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/templates/admin/unicom/chat/compose.html +0 -0
  125. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/templates/admin/unicom/chat_history.html +0 -0
  126. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/templates/admin/unicom/forms/email_message_form.html +0 -0
  127. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/templates/admin/unicom/forms/text_message_form.html +0 -0
  128. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/templates/admin/unicom/includes/ai_template_modal.html +0 -0
  129. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/templates/admin/unicom/includes/loading_indicators.html +0 -0
  130. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/templates/admin/unicom/includes/message_actions_menu.html +0 -0
  131. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/templates/admin/unicom/messagetemplate/change_form.html +0 -0
  132. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/templates/code_templates/category_processor.py +0 -0
  133. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/urls.py +0 -0
  134. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/views/__init__.py +0 -0
  135. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/views/chat_history_view.py +0 -0
  136. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/views/compose_view.py +0 -0
  137. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/views/email_tracking.py +0 -0
  138. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/views/inline_image.py +0 -0
  139. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/views/message_template.py +0 -0
  140. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/views/telegram_webhook.py +0 -0
  141. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom/views/whatsapp_webhook.py +0 -0
  142. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom_project/__init__.py +0 -0
  143. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom_project/asgi.py +0 -0
  144. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom_project/settings.py +0 -0
  145. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom_project/urls.py +0 -0
  146. {django_unicom-25.2.27.dev0 → django_unicom-25.2.27.dev2}/unicom_project/wsgi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-unicom
3
- Version: 25.2.27.dev0
3
+ Version: 25.2.27.dev2
4
4
  Summary: Unified communication layer for Django (Telegram, WhatsApp, Email)
5
5
  Home-page: https://github.com/meena-erian/unicom
6
6
  Author: Meena (Menas) Erian
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-unicom
3
- Version: 25.2.27.dev0
3
+ Version: 25.2.27.dev2
4
4
  Summary: Unified communication layer for Django (Telegram, WhatsApp, Email)
5
5
  Home-page: https://github.com/meena-erian/unicom
6
6
  Author: Meena (Menas) Erian
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '25.2.27.dev0'
32
- __version_tuple__ = version_tuple = (25, 2, 27, 'dev0')
31
+ __version__ = version = '25.2.27.dev2'
32
+ __version_tuple__ = version_tuple = (25, 2, 27, 'dev2')
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -284,8 +284,8 @@ class Message(models.Model):
284
284
  import json
285
285
  arguments = json.dumps(arguments)
286
286
  d = {
287
- "role": role,
288
- "content": msg.text or "",
287
+ "role": "assistant", # Tool calls are always from assistant
288
+ "content": None, # Tool calls should have null content
289
289
  "tool_calls": [{
290
290
  "id": tool_call_data.get('id', f"call_{msg.id}"),
291
291
  "type": "function",
@@ -316,6 +316,28 @@ class Message(models.Model):
316
316
  idx = list(qs.values_list("id", flat=True)).index(self.id)
317
317
  start = max(0, idx - depth + 1)
318
318
  selected = list(qs[start:idx+1])
319
+
320
+ # Handle user interruption for tool response messages in chat mode
321
+ if self.media_type == "tool_response":
322
+ # Find any user message that came chronologically after any tool call in our range
323
+ tool_call_timestamps = [m.timestamp for m in selected if m.media_type == "tool_call"]
324
+ if tool_call_timestamps:
325
+ latest_tool_call_time = max(tool_call_timestamps)
326
+ # Look for user messages after the latest tool call but before this tool response
327
+ user_interrupt = self.chat.messages.filter(
328
+ is_outgoing=False,
329
+ timestamp__gt=latest_tool_call_time,
330
+ timestamp__lt=self.timestamp
331
+ ).exclude(
332
+ media_type__in=['tool_call', 'tool_response']
333
+ ).order_by('-timestamp').first()
334
+
335
+ if user_interrupt:
336
+ # Found user interrupt - get conversation from that user message
337
+ return user_interrupt.as_llm_chat(depth=depth, mode=mode,
338
+ system_instruction=system_instruction,
339
+ multimodal=multimodal)
340
+
319
341
  for m in selected:
320
342
  messages.append(msg_to_dict(m))
321
343
  elif mode == "thread":
@@ -326,6 +348,33 @@ class Message(models.Model):
326
348
  break
327
349
  chain.append(cur)
328
350
  cur = cur.reply_to_message
351
+
352
+ # Handle user interruption for tool response messages in thread mode
353
+ if self.media_type == "tool_response":
354
+ # Find any user message that came chronologically after any tool call in our chain
355
+ # AND that replies to one of the messages in our chain
356
+ tool_call_timestamps = [m.timestamp for m in chain if m.media_type == "tool_call"]
357
+ if tool_call_timestamps:
358
+ latest_tool_call_time = max(tool_call_timestamps)
359
+ chain_message_ids = [m.id for m in chain]
360
+
361
+ # Look for user messages after the latest tool call, before this tool response,
362
+ # that reply to any message in our chain
363
+ user_interrupt = self.chat.messages.filter(
364
+ is_outgoing=False,
365
+ timestamp__gt=latest_tool_call_time,
366
+ timestamp__lt=self.timestamp,
367
+ reply_to_message_id__in=chain_message_ids
368
+ ).exclude(
369
+ media_type__in=['tool_call', 'tool_response']
370
+ ).order_by('-timestamp').first()
371
+
372
+ if user_interrupt:
373
+ # Found user interrupt - get conversation from that user message
374
+ return user_interrupt.as_llm_chat(depth=depth, mode=mode,
375
+ system_instruction=system_instruction,
376
+ multimodal=multimodal)
377
+
329
378
  for m in reversed(chain):
330
379
  messages.append(msg_to_dict(m))
331
380
  else:
@@ -449,6 +498,8 @@ class Message(models.Model):
449
498
  m = re.match(r"data:(.*?);base64,(.*)", url)
450
499
  if m:
451
500
  mime, b64data = m.groups()
501
+ if not isinstance(mime, str):
502
+ raise ValueError(f"Expected mime to be string, got {type(mime)}: {mime}")
452
503
  ext = mime.split("/")[-1]
453
504
  image_file_name = f"media/{uuid.uuid4()}.{ext}"
454
505
  with open(image_file_name, "wb") as f:
@@ -108,7 +108,11 @@ class MessageTemplate(models.Model):
108
108
  for img in soup.find_all('img'):
109
109
  src = img.get('src', '')
110
110
  if src.startswith('data:image/') and ';base64,' in src:
111
+ if not isinstance(src, str):
112
+ raise ValueError(f"Expected src to be string, got {type(src)}: {src}")
111
113
  header, b64data = src.split(';base64,', 1)
114
+ if not isinstance(header, str):
115
+ raise ValueError(f"Expected header to be string, got {type(header)}: {header}")
112
116
  mime = header.split(':')[1]
113
117
  ext = mimetypes.guess_extension(mime) or '.png'
114
118
  data = base64.b64decode(b64data)
@@ -66,6 +66,8 @@ class IMAPThreadManager:
66
66
  channel.listen_to_IMAP()
67
67
  except Exception as e:
68
68
  logger.exception(f"Listener for Channel {channel.pk} crashed: {e}")
69
+ import traceback
70
+ logger.error(f"Full traceback: {traceback.format_exc()}")
69
71
  time.sleep(10)
70
72
  finally:
71
73
  # ensure no DB connections are leaked by this thread
@@ -41,6 +41,8 @@ def listen_to_IMAP(channel):
41
41
  # logger.info(f"Channel {channel.pk}: Found email {msg.id} (uid={uid})")
42
42
  except Exception as e:
43
43
  logger.error(f"Channel {channel.pk}: Failed to process UID {uid}: {e}")
44
+ import traceback
45
+ logger.error(f"Full traceback: {traceback.format_exc()}")
44
46
 
45
47
  if mark_seen_on == 'on_save':
46
48
  if uids:
@@ -85,13 +87,17 @@ def listen_to_IMAP(channel):
85
87
  server.add_flags(uid, [SEEN])
86
88
  logger.debug(f"Incoming email - Message-ID: {msg.id}, In-Reply-To: {msg.raw.get('In-Reply-To') if msg.raw else 'None'}")
87
89
  logger.debug(f"Associated with chat: {msg.chat_id}")
88
- except Exception:
89
- logger.error(f"Channel {channel.pk}: Failed to process UID {uid}")
90
+ except Exception as e:
91
+ logger.error(f"Channel {channel.pk}: Failed to process UID {uid}: {e}")
92
+ import traceback
93
+ logger.error(f"Full traceback: {traceback.format_exc()}")
90
94
  finally:
91
95
  connections.close_all()
92
96
 
93
97
  except Exception as e:
94
98
  logger.error(f"Channel {channel.pk}: Fatal IMAP error: {e}, reconnecting in 30s…")
99
+ import traceback
100
+ logger.error(f"Full traceback: {traceback.format_exc()}")
95
101
  time.sleep(3)
96
102
  finally:
97
103
  # Ensure we close all connections to avoid leaks
@@ -223,7 +223,10 @@ def send_email_message(channel: Channel, params: dict, user: User=None):
223
223
  if parent:
224
224
  # First add any existing References from parent
225
225
  if parent.raw and 'References' in parent.raw:
226
- references.extend(parent.raw['References'].split())
226
+ references_header = parent.raw['References']
227
+ if not isinstance(references_header, str):
228
+ raise ValueError(f"Expected References header to be string, got {type(references_header)}: {references_header}")
229
+ references.extend((references_header or '').split())
227
230
  # Then add the parent's Message-ID
228
231
  references.append(params['reply_to_message_id'])
229
232
  else:
@@ -41,7 +41,11 @@ def html_base64_images_to_shortlinks(html: str) -> tuple[str, list[int]]:
41
41
  for img in soup.find_all('img'):
42
42
  src = img.get('src', '')
43
43
  if src.startswith('data:image/') and ';base64,' in src:
44
+ if not isinstance(src, str):
45
+ raise ValueError(f"Expected src to be string, got {type(src)}: {src}")
44
46
  header, b64data = src.split(';base64,', 1)
47
+ if not isinstance(header, str):
48
+ raise ValueError(f"Expected header to be string, got {type(header)}: {header}")
45
49
  mime = header.split(':')[1]
46
50
  ext = mimetypes.guess_extension(mime) or '.png'
47
51
  data = base64.b64decode(b64data)
@@ -125,7 +125,10 @@ def send_telegram_message(channel: Channel, params: dict, user: User=None, retry
125
125
  params[text_field_key] = params[text_field_key][:4095 - len(cropping_footer)] + cropping_footer
126
126
  elif "Can't find end of the entity starting at byte offset" in ret.get('description', ''):
127
127
  # Extract mentioned byte offset from error message
128
- byte_offset = int(ret['description'].split("byte offset")[1].split()[0])
128
+ description = ret['description']
129
+ if not isinstance(description, str):
130
+ raise ValueError(f"Expected description to be string, got {type(description)}: {description}")
131
+ byte_offset = int(description.split("byte offset")[1].split()[0])
129
132
  print(f"Byte offset: {byte_offset}")
130
133
  mentioned_char = params[text_field_key][byte_offset]
131
134
  print(f"Mentioned char that's causing the error: \"{mentioned_char}\"")