django-unicom 25.2.27.dev3__tar.gz → 25.2.27.dev4__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 (147) hide show
  1. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/PKG-INFO +1 -1
  2. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/django_unicom.egg-info/PKG-INFO +1 -1
  3. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/django_unicom.egg-info/SOURCES.txt +1 -0
  4. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/_version.py +2 -2
  5. django_unicom-25.2.27.dev4/unicom/migrations/0012_message_response_to_tool_call_and_more.py +25 -0
  6. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/models/message.py +100 -6
  7. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/models/message_template.py +0 -4
  8. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/models/request.py +1 -0
  9. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/models/tool_call.py +8 -0
  10. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/email/IMAP_thread_manager.py +0 -2
  11. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/email/listen_to_IMAP.py +2 -8
  12. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/email/send_email_message.py +1 -4
  13. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/html_inline_images.py +0 -4
  14. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/telegram/send_telegram_message.py +1 -4
  15. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/.cursor/rules/match_codebase_style.mdc +0 -0
  16. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/.cursor/rules/preserve_comments_and_irrelevant_code.mdc +0 -0
  17. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/.cursor/rules/require_context_before_changes.mdc +0 -0
  18. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/Dockerfile +0 -0
  19. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/MANIFEST.in +0 -0
  20. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/Makefile +0 -0
  21. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/README.md +0 -0
  22. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/conftest.py +0 -0
  23. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/django_unicom.egg-info/dependency_links.txt +0 -0
  24. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/django_unicom.egg-info/requires.txt +0 -0
  25. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/django_unicom.egg-info/top_level.txt +0 -0
  26. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/docker-compose.yaml +0 -0
  27. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/entrypoint.sh +0 -0
  28. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/manage.py +0 -0
  29. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/pyproject.toml +0 -0
  30. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/pytest.ini +0 -0
  31. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/requirements.txt +0 -0
  32. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/setup.cfg +0 -0
  33. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/setup.py +0 -0
  34. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/test_dkim_security.py +0 -0
  35. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/tests/__init__.py +0 -0
  36. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/tests/test_email_authentication.py +0 -0
  37. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/tests/test_email_live.py +0 -0
  38. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/tests/test_email_request_processing.py +0 -0
  39. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/tests/test_telegram_live.py +0 -0
  40. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/tests/utils.py +0 -0
  41. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/__init__.py +0 -0
  42. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/admin/__init__.py +0 -0
  43. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/admin/account_admin.py +0 -0
  44. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/admin/channel_admin.py +0 -0
  45. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/admin/chat_admin.py +0 -0
  46. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/admin/draft_message_admin.py +0 -0
  47. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/admin/email_inline_image_admin.py +0 -0
  48. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/admin/filters.py +0 -0
  49. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/admin/member_admin.py +0 -0
  50. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/admin/message_admin.py +0 -0
  51. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/admin/message_template_admin.py +0 -0
  52. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/admin/request_admin.py +0 -0
  53. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/apps.py +0 -0
  54. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/management/__init__.py +0 -0
  55. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/management/commands/__init__.py +0 -0
  56. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/management/commands/mail_inbox_listen.py +0 -0
  57. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/management/commands/run_as_llm_chat.py +0 -0
  58. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/management/commands/send_scheduled_messages.py +0 -0
  59. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/management/commands/start_imap_listeners.py +0 -0
  60. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/migrations/0001_initial.py +0 -0
  61. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/migrations/0002_emailinlineimage.py +0 -0
  62. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/migrations/0003_alter_emailinlineimage_email_message.py +0 -0
  63. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/migrations/0004_messagetemplateinlineimage.py +0 -0
  64. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/migrations/0005_emailinlineimage_hash_and_more.py +0 -0
  65. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/migrations/0006_channel_created_at_channel_created_by_and_more.py +0 -0
  66. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/migrations/0007_message_imap_uid.py +0 -0
  67. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/migrations/0008_add_all_members_group.py +0 -0
  68. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/migrations/0009_add_tool_call_types.py +0 -0
  69. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/migrations/0010_request_initial_request_request_llm_calls_count_and_more.py +0 -0
  70. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/migrations/0011_toolcall_add_message_reference.py +0 -0
  71. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/migrations/__init__.py +0 -0
  72. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/models/__init__.py +0 -0
  73. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/models/account.py +0 -0
  74. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/models/account_chat.py +0 -0
  75. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/models/channel.py +0 -0
  76. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/models/chat.py +0 -0
  77. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/models/constants.py +0 -0
  78. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/models/draft_message.py +0 -0
  79. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/models/fields.py +0 -0
  80. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/models/member.py +0 -0
  81. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/models/member_group.py +0 -0
  82. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/models/request_category.py +0 -0
  83. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/models/update.py +0 -0
  84. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/__init__.py +0 -0
  85. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/chat_summary.py +0 -0
  86. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/crossplatform/__init__.py +0 -0
  87. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/crossplatform/reply_to_message.py +0 -0
  88. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/crossplatform/scheduler.py +0 -0
  89. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/crossplatform/send_message.py +0 -0
  90. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/decode_base64_image.py +0 -0
  91. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/email/__init__.py +0 -0
  92. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/email/email_tracking.py +0 -0
  93. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/email/quote_filter.py +0 -0
  94. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/email/replace_cid_images_with_base64.py +0 -0
  95. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/email/save_email_message.py +0 -0
  96. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/email/validate_email_config.py +0 -0
  97. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/get_public_origin.py +0 -0
  98. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/internal/__init__.py +0 -0
  99. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/internal/generate_text_message_data.py +0 -0
  100. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/internal/save_internal_message.py +0 -0
  101. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/internal/send_internal_message.py +0 -0
  102. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/llm/README.md +0 -0
  103. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/llm/__init__.py +0 -0
  104. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/llm/tool_calls.py +0 -0
  105. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/telegram/__init__.py +0 -0
  106. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/telegram/download_file.py +0 -0
  107. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/telegram/escape_markdown.py +0 -0
  108. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/telegram/get_file_path.py +0 -0
  109. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/telegram/save_telegram_message.py +0 -0
  110. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/telegram/set_telegram_webhook.py +0 -0
  111. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/telegram/start_typing_in_telegram.py +0 -0
  112. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/telegram/stop_typing_in_telegram.py +0 -0
  113. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/whatsapp/__init__.py +0 -0
  114. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/whatsapp/get_template.py +0 -0
  115. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/whatsapp/save_whatsapp_message.py +0 -0
  116. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/whatsapp/save_whatsapp_message_status.py +0 -0
  117. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/services/whatsapp/send_whatsapp_message.py +0 -0
  118. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/signals.py +0 -0
  119. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/static/unicom/css/bootstrap_scoped.css +0 -0
  120. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/static/unicom/css/draft_message_mobile.css +0 -0
  121. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/static/unicom/js/channel_config.js +0 -0
  122. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/static/unicom/js/tinymce_ai_template.js +0 -0
  123. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/static/unicom/js/tinymce_init.js +0 -0
  124. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/templates/admin/unicom/chat/change_list.html +0 -0
  125. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/templates/admin/unicom/chat/compose.html +0 -0
  126. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/templates/admin/unicom/chat_history.html +0 -0
  127. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/templates/admin/unicom/forms/email_message_form.html +0 -0
  128. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/templates/admin/unicom/forms/text_message_form.html +0 -0
  129. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/templates/admin/unicom/includes/ai_template_modal.html +0 -0
  130. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/templates/admin/unicom/includes/loading_indicators.html +0 -0
  131. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/templates/admin/unicom/includes/message_actions_menu.html +0 -0
  132. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/templates/admin/unicom/messagetemplate/change_form.html +0 -0
  133. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/templates/code_templates/category_processor.py +0 -0
  134. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/urls.py +0 -0
  135. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/views/__init__.py +0 -0
  136. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/views/chat_history_view.py +0 -0
  137. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/views/compose_view.py +0 -0
  138. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/views/email_tracking.py +0 -0
  139. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/views/inline_image.py +0 -0
  140. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/views/message_template.py +0 -0
  141. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/views/telegram_webhook.py +0 -0
  142. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom/views/whatsapp_webhook.py +0 -0
  143. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom_project/__init__.py +0 -0
  144. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom_project/asgi.py +0 -0
  145. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom_project/settings.py +0 -0
  146. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/unicom_project/urls.py +0 -0
  147. {django_unicom-25.2.27.dev3 → django_unicom-25.2.27.dev4}/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.dev3
3
+ Version: 25.2.27.dev4
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.dev3
3
+ Version: 25.2.27.dev4
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
@@ -58,6 +58,7 @@ unicom/migrations/0008_add_all_members_group.py
58
58
  unicom/migrations/0009_add_tool_call_types.py
59
59
  unicom/migrations/0010_request_initial_request_request_llm_calls_count_and_more.py
60
60
  unicom/migrations/0011_toolcall_add_message_reference.py
61
+ unicom/migrations/0012_message_response_to_tool_call_and_more.py
61
62
  unicom/migrations/__init__.py
62
63
  unicom/models/__init__.py
63
64
  unicom/models/account.py
@@ -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.dev3'
32
- __version_tuple__ = version_tuple = (25, 2, 27, 'dev3')
31
+ __version__ = version = '25.2.27.dev4'
32
+ __version_tuple__ = version_tuple = (25, 2, 27, 'dev4')
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -0,0 +1,25 @@
1
+ # Generated by Django 4.2.20 on 2025-09-27 14:19
2
+
3
+ from django.db import migrations, models
4
+ import django.db.models.deletion
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('unicom', '0011_toolcall_add_message_reference'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AddField(
15
+ model_name='message',
16
+ name='response_to_tool_call',
17
+ field=models.ForeignKey(blank=True, help_text='The ToolCall that this message is responding to (for assistant messages from tool responses)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='response_messages', to='unicom.toolcall'),
18
+ ),
19
+ migrations.AddField(
20
+ model_name='toolcall',
21
+ name='initial_user_message',
22
+ field=models.ForeignKey(default=1, help_text='The original user message that this tool call is responding to', on_delete=django.db.models.deletion.CASCADE, related_name='triggered_tool_calls', to='unicom.message'),
23
+ preserve_default=False,
24
+ ),
25
+ ]
@@ -68,6 +68,11 @@ class Message(models.Model):
68
68
  media = models.FileField(upload_to='media/', blank=True, null=True)
69
69
  reply_to_message = models.ForeignKey(
70
70
  'self', on_delete=models.SET_NULL, null=True, blank=True, related_name='replies')
71
+ response_to_tool_call = models.ForeignKey(
72
+ 'unicom.ToolCall', on_delete=models.SET_NULL, null=True, blank=True,
73
+ related_name='response_messages',
74
+ help_text="The ToolCall that this message is responding to (for assistant messages from tool responses)"
75
+ )
71
76
  timestamp = models.DateTimeField()
72
77
  time_sent = models.DateTimeField(null=True, blank=True)
73
78
  time_delivered = models.DateTimeField(null=True, blank=True)
@@ -100,7 +105,33 @@ class Message(models.Model):
100
105
  """
101
106
  Reply to this message with a dictionary containing the response.
102
107
  The dictionary can contain 'text', 'html', 'file_path', etc.
108
+
109
+ For tool response messages, this will send the reply to the original user message
110
+ while maintaining the chain by setting reply_to_message to this tool response.
103
111
  """
112
+ # Handle tool response messages - send to original user but maintain chain
113
+ if self.media_type == 'tool_response':
114
+ # Get the request using reverse lookup
115
+ request = self.request_set.first() # tool response message -> request
116
+ if request:
117
+ initial_request = request.initial_request or request
118
+ target_message = initial_request.message
119
+
120
+ # Get the ToolCall that this response came from
121
+ tool_call = request.tool_calls.first()
122
+
123
+ # Send reply to original user message
124
+ reply_msg = target_message.reply_with(msg_dict)
125
+
126
+ # Set the reply chain and tool call reference
127
+ reply_msg.reply_to_message = self
128
+ if tool_call:
129
+ reply_msg.response_to_tool_call = tool_call
130
+ reply_msg.save(update_fields=['reply_to_message', 'response_to_tool_call'])
131
+
132
+ return reply_msg
133
+
134
+ # Normal case - reply to this message directly
104
135
  from unicom.services.crossplatform.reply_to_message import reply_to_message
105
136
  return reply_to_message(self.channel, self, msg_dict)
106
137
 
@@ -177,6 +208,71 @@ class Message(models.Model):
177
208
  img_tag['src'] = f'data:{mime};base64,{b64}'
178
209
  return str(soup)
179
210
 
211
+ def _process_chain_with_tool_grouping(self, chain, msg_to_dict):
212
+ """
213
+ Process message chain with intelligent tool call grouping.
214
+ Groups parallel tool calls (same initial_user_message, unique call_id) into single LLM messages.
215
+ """
216
+ messages = []
217
+ i = 0
218
+
219
+ while i < len(chain):
220
+ msg = chain[i]
221
+
222
+ # Check if this is an assistant message responding to a tool call
223
+ if (msg.is_outgoing and msg.response_to_tool_call and
224
+ msg.response_to_tool_call.initial_user_message):
225
+
226
+ # This assistant message was created from a tool response
227
+ # Find all parallel tool calls from the same initial user message
228
+ initial_user_msg = msg.response_to_tool_call.initial_user_message
229
+
230
+ # Get all tool calls triggered by the same initial user message
231
+ # Group by unique call_id to handle periodic responses properly
232
+ tool_calls_dict = {}
233
+ for tool_call in initial_user_msg.triggered_tool_calls.all():
234
+ if tool_call.call_id not in tool_calls_dict:
235
+ tool_calls_dict[tool_call.call_id] = tool_call
236
+
237
+ # Create grouped tool call message
238
+ if tool_calls_dict:
239
+ tool_calls_list = []
240
+ for tool_call in tool_calls_dict.values():
241
+ arguments = tool_call.arguments
242
+ if isinstance(arguments, dict):
243
+ import json
244
+ arguments = json.dumps(arguments)
245
+
246
+ tool_calls_list.append({
247
+ "id": tool_call.call_id,
248
+ "type": "function",
249
+ "function": {
250
+ "name": tool_call.tool_name,
251
+ "arguments": arguments
252
+ }
253
+ })
254
+
255
+ # Create single assistant message with all parallel tool calls
256
+ grouped_msg = {
257
+ "role": "assistant",
258
+ "content": None,
259
+ "tool_calls": tool_calls_list
260
+ }
261
+ messages.append(grouped_msg)
262
+
263
+ # Add the current assistant message (response to tool calls)
264
+ messages.append(msg_to_dict(msg))
265
+
266
+ else:
267
+ # Regular message - just convert normally
268
+ # Skip tool_call and tool_response messages as they're handled above
269
+ if msg.media_type not in ['tool_call', 'tool_response']:
270
+ messages.append(msg_to_dict(msg))
271
+
272
+ i += 1
273
+
274
+ return messages
275
+
180
276
  def as_llm_chat(self, depth=10, mode="chat", system_instruction=None, multimodal=True):
181
277
  """
182
278
  Returns a list of dicts for LLM chat APIs (OpenAI, Gemini, etc), each with 'role' and 'content'.
@@ -338,8 +434,8 @@ class Message(models.Model):
338
434
  system_instruction=system_instruction,
339
435
  multimodal=multimodal)
340
436
 
341
- for m in selected:
342
- messages.append(msg_to_dict(m))
437
+ # Process selected messages with tool call grouping
438
+ messages = self._process_chain_with_tool_grouping(selected, msg_to_dict)
343
439
  elif mode == "thread":
344
440
  chain = []
345
441
  cur = self
@@ -375,8 +471,8 @@ class Message(models.Model):
375
471
  system_instruction=system_instruction,
376
472
  multimodal=multimodal)
377
473
 
378
- for m in reversed(chain):
379
- messages.append(msg_to_dict(m))
474
+ # Process chain with tool call grouping
475
+ messages = self._process_chain_with_tool_grouping(chain, msg_to_dict)
380
476
  else:
381
477
  raise ValueError(f"Unknown mode: {mode}")
382
478
  if system_instruction:
@@ -498,8 +594,6 @@ class Message(models.Model):
498
594
  m = re.match(r"data:(.*?);base64,(.*)", url)
499
595
  if m:
500
596
  mime, b64data = m.groups()
501
- if not isinstance(mime, str):
502
- raise ValueError(f"Expected mime to be string, got {type(mime)}: {mime}")
503
597
  ext = mime.split("/")[-1]
504
598
  image_file_name = f"media/{uuid.uuid4()}.{ext}"
505
599
  with open(image_file_name, "wb") as f:
@@ -108,11 +108,7 @@ 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}")
113
111
  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}")
116
112
  mime = header.split(':')[1]
117
113
  ext = mimetypes.guess_extension(mime) or '.png'
118
114
  data = base64.b64decode(b64data)
@@ -532,6 +532,7 @@ class Request(models.Model):
532
532
  arguments=arguments,
533
533
  request=self,
534
534
  tool_call_message=tool_call_msg, # Link to the tool call message
535
+ initial_user_message=self.message, # Link to the original user message
535
536
  status='PENDING'
536
537
  )
537
538
 
@@ -37,6 +37,14 @@ class ToolCall(models.Model):
37
37
  help_text="The tool_call message that this ToolCall object represents"
38
38
  )
39
39
 
40
+ # Reference to the initial user message that triggered this tool call
41
+ initial_user_message = models.ForeignKey(
42
+ 'unicom.Message',
43
+ on_delete=models.CASCADE,
44
+ related_name='triggered_tool_calls',
45
+ help_text="The original user message that this tool call is responding to"
46
+ )
47
+
40
48
  # Timestamps
41
49
  created_at = models.DateTimeField(auto_now_add=True, db_index=True)
42
50
  started_at = models.DateTimeField(null=True, blank=True)
@@ -66,8 +66,6 @@ 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()}")
71
69
  time.sleep(10)
72
70
  finally:
73
71
  # ensure no DB connections are leaked by this thread
@@ -41,8 +41,6 @@ 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()}")
46
44
 
47
45
  if mark_seen_on == 'on_save':
48
46
  if uids:
@@ -87,17 +85,13 @@ def listen_to_IMAP(channel):
87
85
  server.add_flags(uid, [SEEN])
88
86
  logger.debug(f"Incoming email - Message-ID: {msg.id}, In-Reply-To: {msg.raw.get('In-Reply-To') if msg.raw else 'None'}")
89
87
  logger.debug(f"Associated with chat: {msg.chat_id}")
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()}")
88
+ except Exception:
89
+ logger.error(f"Channel {channel.pk}: Failed to process UID {uid}")
94
90
  finally:
95
91
  connections.close_all()
96
92
 
97
93
  except Exception as e:
98
94
  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()}")
101
95
  time.sleep(3)
102
96
  finally:
103
97
  # Ensure we close all connections to avoid leaks
@@ -223,10 +223,7 @@ 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_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())
226
+ references.extend(parent.raw['References'].split())
230
227
  # Then add the parent's Message-ID
231
228
  references.append(params['reply_to_message_id'])
232
229
  else:
@@ -41,11 +41,7 @@ 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}")
46
44
  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}")
49
45
  mime = header.split(':')[1]
50
46
  ext = mimetypes.guess_extension(mime) or '.png'
51
47
  data = base64.b64decode(b64data)
@@ -125,10 +125,7 @@ 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
- 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])
128
+ byte_offset = int(ret['description'].split("byte offset")[1].split()[0])
132
129
  print(f"Byte offset: {byte_offset}")
133
130
  mentioned_char = params[text_field_key][byte_offset]
134
131
  print(f"Mentioned char that's causing the error: \"{mentioned_char}\"")