letschatty 0.4.280__py3-none-any.whl → 0.4.343__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 (92) hide show
  1. letschatty/models/ai_microservices/__init__.py +3 -3
  2. letschatty/models/ai_microservices/expected_output.py +35 -1
  3. letschatty/models/ai_microservices/lambda_events.py +85 -45
  4. letschatty/models/ai_microservices/lambda_invokation_types.py +6 -3
  5. letschatty/models/analytics/events/__init__.py +2 -3
  6. letschatty/models/analytics/events/chat_based_events/chat_funnel.py +69 -13
  7. letschatty/models/analytics/events/company_based_events/asset_events.py +9 -2
  8. letschatty/models/analytics/events/event_type_to_classes.py +6 -3
  9. letschatty/models/analytics/events/event_types.py +13 -50
  10. letschatty/models/chat/chat.py +14 -2
  11. letschatty/models/chat/chat_with_assets.py +6 -1
  12. letschatty/models/chat/client.py +0 -2
  13. letschatty/models/chat/continuous_conversation.py +1 -1
  14. letschatty/models/company/CRM/funnel.py +365 -33
  15. letschatty/models/company/__init__.py +3 -1
  16. letschatty/models/company/assets/ai_agents_v2/ai_agent_message_draft.py +58 -0
  17. letschatty/models/company/assets/ai_agents_v2/ai_agents_decision_output.py +1 -1
  18. letschatty/models/company/assets/ai_agents_v2/chain_of_thought_in_chat.py +5 -3
  19. letschatty/models/company/assets/ai_agents_v2/chatty_ai_agent.py +46 -2
  20. letschatty/models/company/assets/ai_agents_v2/chatty_ai_agent_in_chat.py +93 -1
  21. letschatty/models/company/assets/ai_agents_v2/pre_qualify_config.py +111 -0
  22. letschatty/models/company/assets/ai_agents_v2/statuses.py +33 -0
  23. letschatty/models/company/assets/assignment/__init__.py +14 -0
  24. letschatty/models/company/assets/assignment/assignment_assets.py +75 -0
  25. letschatty/models/company/assets/automation.py +10 -19
  26. letschatty/models/company/assets/chat_assets.py +12 -2
  27. letschatty/models/company/assets/company_assets.py +3 -0
  28. letschatty/models/company/assets/launch/__init__.py +12 -0
  29. letschatty/models/company/assets/launch/launch.py +128 -0
  30. letschatty/models/company/assets/launch/scheduled_communication.py +44 -0
  31. letschatty/models/company/assets/launch/subscription.py +63 -0
  32. letschatty/models/company/assets/sale.py +3 -3
  33. letschatty/models/company/assets/users/user.py +5 -1
  34. letschatty/models/company/company_messaging_settgins.py +2 -1
  35. letschatty/models/company/form_field.py +182 -12
  36. letschatty/models/data_base/collection_interface.py +29 -101
  37. letschatty/models/data_base/mongo_connection.py +9 -92
  38. letschatty/models/messages/chatty_messages/schema/chatty_content/content_document.py +4 -2
  39. letschatty/models/messages/chatty_messages/schema/chatty_content/content_media.py +4 -3
  40. letschatty/models/utils/custom_exceptions/custom_exceptions.py +24 -13
  41. letschatty/services/ai_agents/smart_follow_up_service_v2.py +10 -0
  42. letschatty/services/chat/chat_service.py +79 -14
  43. letschatty/services/chatty_assets/__init__.py +0 -12
  44. letschatty/services/chatty_assets/asset_service.py +13 -190
  45. letschatty/services/chatty_assets/base_container.py +2 -3
  46. letschatty/services/chatty_assets/base_container_with_collection.py +26 -35
  47. letschatty/services/continuous_conversation_service/continuous_conversation_helper.py +0 -11
  48. letschatty/services/events/events_manager.py +1 -218
  49. letschatty/services/factories/analytics/events_factory.py +6 -66
  50. letschatty/services/factories/lambda_ai_orchestrartor/lambda_events_factory.py +8 -23
  51. letschatty/services/users/user_factory.py +14 -8
  52. letschatty/services/validators/analytics_validator.py +11 -0
  53. {letschatty-0.4.280.dist-info → letschatty-0.4.343.dist-info}/METADATA +1 -1
  54. {letschatty-0.4.280.dist-info → letschatty-0.4.343.dist-info}/RECORD +56 -83
  55. {letschatty-0.4.280.dist-info → letschatty-0.4.343.dist-info}/WHEEL +1 -1
  56. letschatty/models/analytics/events/chat_based_events/ai_agent_execution_event.py +0 -71
  57. letschatty/services/chatty_assets/assets_collections.py +0 -137
  58. letschatty/services/chatty_assets/collections/__init__.py +0 -38
  59. letschatty/services/chatty_assets/collections/ai_agent_collection.py +0 -19
  60. letschatty/services/chatty_assets/collections/ai_agent_in_chat_collection.py +0 -32
  61. letschatty/services/chatty_assets/collections/ai_component_collection.py +0 -21
  62. letschatty/services/chatty_assets/collections/chain_of_thought_collection.py +0 -30
  63. letschatty/services/chatty_assets/collections/chat_collection.py +0 -21
  64. letschatty/services/chatty_assets/collections/contact_point_collection.py +0 -21
  65. letschatty/services/chatty_assets/collections/fast_answer_collection.py +0 -21
  66. letschatty/services/chatty_assets/collections/filter_criteria_collection.py +0 -18
  67. letschatty/services/chatty_assets/collections/flow_collection.py +0 -20
  68. letschatty/services/chatty_assets/collections/product_collection.py +0 -20
  69. letschatty/services/chatty_assets/collections/sale_collection.py +0 -20
  70. letschatty/services/chatty_assets/collections/source_collection.py +0 -21
  71. letschatty/services/chatty_assets/collections/tag_collection.py +0 -19
  72. letschatty/services/chatty_assets/collections/topic_collection.py +0 -21
  73. letschatty/services/chatty_assets/collections/user_collection.py +0 -20
  74. letschatty/services/chatty_assets/example_usage.py +0 -44
  75. letschatty/services/chatty_assets/services/__init__.py +0 -37
  76. letschatty/services/chatty_assets/services/ai_agent_in_chat_service.py +0 -73
  77. letschatty/services/chatty_assets/services/ai_agent_service.py +0 -23
  78. letschatty/services/chatty_assets/services/chain_of_thought_service.py +0 -70
  79. letschatty/services/chatty_assets/services/chat_service.py +0 -25
  80. letschatty/services/chatty_assets/services/contact_point_service.py +0 -29
  81. letschatty/services/chatty_assets/services/fast_answer_service.py +0 -32
  82. letschatty/services/chatty_assets/services/filter_criteria_service.py +0 -30
  83. letschatty/services/chatty_assets/services/flow_service.py +0 -25
  84. letschatty/services/chatty_assets/services/product_service.py +0 -30
  85. letschatty/services/chatty_assets/services/sale_service.py +0 -25
  86. letschatty/services/chatty_assets/services/source_service.py +0 -28
  87. letschatty/services/chatty_assets/services/tag_service.py +0 -32
  88. letschatty/services/chatty_assets/services/topic_service.py +0 -31
  89. letschatty/services/chatty_assets/services/user_service.py +0 -32
  90. letschatty/services/events/__init__.py +0 -6
  91. letschatty/services/factories/analytics/ai_agent_event_factory.py +0 -161
  92. {letschatty-0.4.280.dist-info → letschatty-0.4.343.dist-info}/LICENSE +0 -0
@@ -1,14 +1,8 @@
1
1
  from ..base_models.singleton import SingletonMeta
2
2
  from pymongo import MongoClient
3
- from motor.motor_asyncio import AsyncIOMotorClient
4
3
  from typing import Optional
5
- from urllib.parse import quote_plus
6
4
  import os
7
5
  import atexit
8
- import asyncio
9
- import logging
10
-
11
- logger = logging.getLogger(__name__)
12
6
 
13
7
  class MongoConnection(metaclass=SingletonMeta):
14
8
  def __init__(
@@ -16,103 +10,26 @@ class MongoConnection(metaclass=SingletonMeta):
16
10
  username: Optional[str] = None,
17
11
  password: Optional[str] = None,
18
12
  uri_base: Optional[str] = None,
19
- instance: Optional[str] = None,
20
- verify_on_init: bool = True
13
+ instance: Optional[str] = None
21
14
  ):
22
15
  self.username = username or os.getenv('MONGO_USERNAME')
23
16
  self.password = password or os.getenv('MONGO_PASSWORD')
24
17
  self.uri_base = uri_base or os.getenv('MONGO_URI_BASE')
25
18
  self.instance = instance or os.getenv('MONGO_INSTANCE_COMPONENT')
26
-
19
+
27
20
  if not all([self.username, self.password, self.uri_base, self.instance]):
28
21
  raise ValueError("Missing required MongoDB connection parameters")
29
-
30
- # URL-encode username and password to handle special characters per RFC 3986
31
- encoded_username = quote_plus(self.username)
32
- encoded_password = quote_plus(self.password)
33
-
34
- uri = f"{self.uri_base}://{encoded_username}:{encoded_password}@{self.instance}.mongodb.net"
35
-
36
- # Sync client (existing)
22
+
23
+ uri = f"{self.uri_base}://{self.username}:{self.password}@{self.instance}.mongodb.net"
37
24
  self.client = MongoClient(uri)
38
-
39
- # NEW: Async client for async operations
40
- # Don't pass io_loop - Motor will automatically use the current event loop
41
- # This is important for Lambda where the event loop changes between invocations
42
- self.async_client = AsyncIOMotorClient(uri)
43
-
44
- # Verify connections if requested
45
- if verify_on_init:
46
- try:
47
- # Try to get running loop
48
- loop = asyncio.get_running_loop()
49
- # If we get here, there's a running loop
50
- logger.warning(
51
- "Event loop is already running. Skipping connection verification in __init__. "
52
- "Call verify_connection_async() from async context to verify connection."
53
- )
54
- self._connection_verified = False
55
- except RuntimeError:
56
- # No running loop, safe to use run_until_complete
57
- try:
58
- # Test sync client
59
- self.client.admin.command('ping')
60
-
61
- # Test async client in sync context
62
- loop = asyncio.new_event_loop()
63
- asyncio.set_event_loop(loop)
64
- loop.run_until_complete(self.async_client.admin.command('ping'))
65
- self._connection_verified = True
66
- loop.close()
67
- except Exception as e:
68
- self.client.close()
69
- self.async_client.close()
70
- raise ConnectionError(f"Failed to connect to MongoDB: {str(e)}")
71
- else:
72
- self._connection_verified = False
73
-
74
- atexit.register(self.close)
75
-
76
- def _ensure_async_client_loop(self):
77
- """Ensure async client is using the current event loop (for Lambda compatibility)"""
78
25
  try:
79
- current_loop = asyncio.get_running_loop()
80
- # Check if client's loop is closed or different
81
- client_loop = getattr(self.async_client, '_io_loop', None)
82
- if client_loop is not None:
83
- try:
84
- # Try to check if the loop is closed
85
- if client_loop.is_closed():
86
- # Recreate client with current loop
87
- logger.warning("Async client's event loop is closed, recreating client")
88
- old_client = self.async_client
89
- uri = f"{self.uri_base}://{quote_plus(self.username)}:{quote_plus(self.password)}@{self.instance}.mongodb.net"
90
- self.async_client = AsyncIOMotorClient(uri)
91
- try:
92
- old_client.close()
93
- except:
94
- pass
95
- except AttributeError:
96
- # _io_loop might not exist in newer Motor versions
97
- pass
98
- except RuntimeError:
99
- # No running loop, which is fine - Motor will handle it
100
- pass
101
-
102
- async def verify_connection_async(self) -> bool:
103
- """Verify MongoDB connection asynchronously. Safe to call from async context."""
104
- try:
105
- # Ensure we're using the current event loop
106
- self._ensure_async_client_loop()
107
- await self.async_client.admin.command('ping')
108
- self._connection_verified = True
109
- return True
26
+ self.client.admin.command('ping')
110
27
  except Exception as e:
111
- logger.error(f"Failed to verify MongoDB connection: {e}")
112
- raise ConnectionError(f"Failed to verify MongoDB connection: {e}")
28
+ self.client.close()
29
+ raise ConnectionError(f"Failed to connect to MongoDB: {str(e)}")
113
30
 
31
+ atexit.register(self.close)
32
+
114
33
  def close(self) -> None:
115
34
  if hasattr(self, 'client'):
116
35
  self.client.close()
117
- if hasattr(self, 'async_client'):
118
- self.async_client.close()
@@ -1,5 +1,6 @@
1
1
  from pydantic import BaseModel, Field, model_validator, ValidationInfo
2
2
  from typing import Optional
3
+ from urllib.parse import urlparse, unquote
3
4
  from .content_media import ChattyContentMedia
4
5
 
5
6
  class ChattyContentDocument(ChattyContentMedia):
@@ -8,5 +9,6 @@ class ChattyContentDocument(ChattyContentMedia):
8
9
  @model_validator(mode='before')
9
10
  def validate_filename(cls, data: dict, info: ValidationInfo):
10
11
  if not data.get("filename") and data.get("url"):
11
- data["filename"] = data["url"].split("/")[-1]
12
- return data
12
+ parsed = urlparse(data["url"])
13
+ data["filename"] = unquote(parsed.path.split("/")[-1])
14
+ return data
@@ -1,5 +1,6 @@
1
1
  from pydantic import BaseModel, Field, field_validator, HttpUrl
2
2
  from typing import Optional
3
+ from urllib.parse import quote
3
4
  class ChattyContentMedia(BaseModel):
4
5
  id: Optional[str] = Field(description="Unique identifier for the image. Also known as media_id", default="")
5
6
  url: str = Field(description="URL of the media from S3")
@@ -11,9 +12,9 @@ class ChattyContentMedia(BaseModel):
11
12
  def validate_url(cls, v):
12
13
  if not v:
13
14
  raise ValueError("URL is required")
14
- HttpUrl(v)
15
- return v
15
+ encoded = quote(str(v), safe=":/?&=%#")
16
+ HttpUrl(encoded)
17
+ return encoded
16
18
 
17
19
  def get_body_or_caption(self) -> str:
18
20
  return self.caption
19
-
@@ -5,7 +5,6 @@ import json
5
5
  from datetime import timedelta
6
6
 
7
7
  from letschatty.models.utils.definitions import Area
8
- from pydantic_core.core_schema import custom_error_schema
9
8
  logger = logging.getLogger("logger")
10
9
 
11
10
  class Context(BaseModel):
@@ -60,6 +59,30 @@ class WhatsAppPayloadValidationError(Exception):
60
59
  def __init__(self, message="WhatsApp payload validation error", status_code=400, **context_data):
61
60
  super().__init__(message, status_code=status_code, **context_data)
62
61
 
62
+
63
+ class ChatWithActiveContinuousConversation(CustomException):
64
+ """
65
+ Raised when an AI agent is triggered on a chat that has an active Continuous Conversation.
66
+ """
67
+ def __init__(self, message="Chat has an active continuous conversation", status_code=400, **context_data):
68
+ super().__init__(message, status_code=status_code, **context_data)
69
+
70
+
71
+ class ChattyAIModeOff(CustomException):
72
+ """
73
+ Raised when an AI agent is in OFF mode and cannot be triggered.
74
+ """
75
+ def __init__(self, message="Chatty AI agent is OFF", status_code=400, **context_data):
76
+ super().__init__(message, status_code=status_code, **context_data)
77
+
78
+
79
+ class MissingAIAgentInChat(CustomException):
80
+ """
81
+ Raised when a chat has no AI agent assigned but one is required for the operation.
82
+ """
83
+ def __init__(self, message="AI agent not assigned to chat", status_code=404, **context_data):
84
+ super().__init__(message, status_code=status_code, **context_data)
85
+
63
86
  class UnsuportedChannel(CustomException):
64
87
  def __init__(self, message="Channel not supported", status_code=400, **context_data):
65
88
  super().__init__(message, status_code=status_code, **context_data)
@@ -206,16 +229,4 @@ class OpenAIError(CustomException):
206
229
 
207
230
  class NewerAvailableMessageToBeProcessedByAiAgent(CustomException):
208
231
  def __init__(self, message="Duplicated incoming message call for ai agent", status_code=400, **context_data):
209
- super().__init__(message, status_code=status_code, **context_data)
210
-
211
- class MissingAIAgentInChat(CustomException):
212
- def __init__(self, message="Missing AI agent in chat", status_code=400, **context_data):
213
- super().__init__(message, status_code=status_code, **context_data)
214
-
215
- class ChattyAIModeOff(CustomException):
216
- def __init__(self, message="Chatty AI agent is in OFF mode", status_code=400, **context_data):
217
- super().__init__(message, status_code=status_code, **context_data)
218
-
219
- class ChatWithActiveContinuousConversation(CustomException):
220
- def __init__(self, message="Chat has active continuous conversation", status_code=400, **context_data):
221
232
  super().__init__(message, status_code=status_code, **context_data)
@@ -1,5 +1,6 @@
1
1
  from typing import List
2
2
  from letschatty.models.chat.chat import Chat, FlowStateAssignedToChat
3
+ from letschatty.models.chat.flow_link_state import StateTrigger
3
4
  from letschatty.models.company.assets.ai_agents_v2.follow_up_strategy import FollowUpStrategy
4
5
  from letschatty.models.company.assets.ai_agents_v2.ai_agents_decision_output import SmartFollowUpDecision, SmartFollowUpDecisionAction
5
6
  from letschatty.services.chat.chat_service import ChatService
@@ -85,5 +86,14 @@ class SmartFollowUpService:
85
86
  SmartFollowUpService.validate_next_call_time_or_default(decision, smart_follow_up_state.consecutive_count + 1, follow_up_strategy)
86
87
  smart_follow_up_state.next_call = decision.next_call_time_value
87
88
  ChatService.update_workflow_link(chat=chat, workflow_id=smart_follow_up_state.flow_id, workflow_link=smart_follow_up_state, execution_context=execution_context)
89
+ elif decision.action == SmartFollowUpDecisionAction.POSTPONE_DELTA_TIME:
90
+ logger.debug(f"Postponing smart follow up for chat {chat.id} by delta time {decision.next_call_time_value}")
91
+ SmartFollowUpService.validate_next_call_time_or_default(decision, smart_follow_up_state.consecutive_count + 1, follow_up_strategy)
92
+ smart_follow_up_state.next_call = decision.next_call_time_value
93
+ ChatService.update_workflow_link(chat=chat, workflow_id=smart_follow_up_state.flow_id, workflow_link=smart_follow_up_state, execution_context=execution_context)
94
+ elif decision.action == SmartFollowUpDecisionAction.POSTPONE_TILL_UPDATE:
95
+ logger.debug(f"Postponing smart follow up for chat {chat.id} till update")
96
+ smart_follow_up_state.trigger = StateTrigger.CHAT_UPDATE
97
+ ChatService.update_workflow_link(chat=chat, workflow_id=smart_follow_up_state.flow_id, workflow_link=smart_follow_up_state, execution_context=execution_context)
88
98
  else:
89
99
  raise ValueError(f"Invalid action: {decision.action}")
@@ -28,9 +28,10 @@ from ...models.chat.scheduled_messages import ScheduledMessageStatus
28
28
  from ...models.utils.types.identifier import StrObjectId
29
29
  from ...models.utils.custom_exceptions.custom_exceptions import AssetAlreadyAssigned, MessageNotFoundError, NotFoundError, MessageAlreadyInChat, MetaErrorNotification, ChatAlreadyAssigned, AlreadyCompleted, ErrorToMantainSafety
30
30
  from ..factories.messages.central_notification_factory import CentralNotificationFactory
31
+ from ..factories.messages.chatty_message_factory import from_message_draft
31
32
  from ...models.messages.chatty_messages.base.message_draft import ChattyContentAudio, MessageDraft
32
33
  from ...models.messages.chatty_messages.schema.chatty_content.content_central import CentralNotificationStatus
33
- from ...models.messages.chatty_messages.schema import ChattyContext
34
+ from ...models.messages.chatty_messages.schema import ChattyContext, ChattyContentText
34
35
  from ...models.utils.types.message_types import MessageType
35
36
  from .conversation_topics_service import ConversationTopicsService
36
37
  import logging
@@ -211,6 +212,43 @@ class ChatService:
211
212
  ChatService.add_central_notification_from_text(chat=chat, body=f"Agente de IA {chatty_ai_agent.name} actualizado en el chat {chat.id} por {execution_context.executor.name}", subtype=MessageSubtype.CHATTY_AI_AGENT_UPDATED)
212
213
  return chat.chatty_ai_agent
213
214
 
215
+ @staticmethod
216
+ def escalate_chatty_ai_agent(
217
+ chat: Chat,
218
+ execution_context: ExecutionContext,
219
+ message: Optional[str] = None,
220
+ reason: Optional[str] = None
221
+ ) -> None:
222
+ """
223
+ Mark the chat's AI agent as requiring human intervention and add a central notification.
224
+ """
225
+ if chat.chatty_ai_agent and not chat.chatty_ai_agent.requires_human_intervention:
226
+ chat.chatty_ai_agent.requires_human_intervention = True
227
+ execution_context.set_event_time(datetime.now(tz=ZoneInfo("UTC")))
228
+ body = "El chat fue escalado a un agente humano"
229
+ if reason:
230
+ body = f"{body}. Motivo: {reason}"
231
+ ChatService.add_central_notification_from_text(
232
+ chat=chat,
233
+ body=body,
234
+ subtype=MessageSubtype.CHATTY_AI_AGENT_NOTIFICATION,
235
+ content_status=CentralNotificationStatus.WARNING,
236
+ context=ChattyContext(chain_of_thought_id=execution_context.chain_of_thought_id)
237
+ )
238
+ if message:
239
+ outgoing_context = ChattyContext(chain_of_thought_id=execution_context.chain_of_thought_id)
240
+ outgoing_message = from_message_draft(
241
+ MessageDraft(
242
+ type=MessageType.TEXT,
243
+ content=ChattyContentText(body=message),
244
+ context=outgoing_context,
245
+ subtype=MessageSubtype.NONE,
246
+ is_incoming_message=False
247
+ ),
248
+ sent_by=execution_context.executor.id
249
+ )
250
+ ChatService.add_message(chat=chat, message=outgoing_message)
251
+
214
252
  @staticmethod
215
253
  def add_workflow_link(chat : Chat, link : LinkItem, flow:FlowPreview, execution_context: ExecutionContext, description: str, last_incoming_message_id: Optional[str] = None, next_call: Optional[datetime] = None) -> FlowStateAssignedToChat:
216
254
  """
@@ -266,36 +304,46 @@ class ChatService:
266
304
  return next((state for state in chat.flow_states if state.is_smart_follow_up), None)
267
305
 
268
306
  @staticmethod
269
- def create_sale(chat : Chat, execution_context: ExecutionContext, sale : Sale, product : Product) -> SaleAssignedToChat:
307
+ def create_sale(
308
+ chat: Chat,
309
+ execution_context: ExecutionContext,
310
+ sale: Sale,
311
+ product: Optional[Product],
312
+ product_ids: Optional[List[StrObjectId]] = None,
313
+ product_label: Optional[str] = None
314
+ ) -> SaleAssignedToChat:
270
315
  """
271
316
  Add a sale to the chat.
272
317
  """
273
318
  if next((sale for sale in chat.client.sales if sale.asset_id == sale.id), None) is not None:
274
319
  raise AssetAlreadyAssigned(f"Sale with id {sale.id} already assigned to chat {chat.id}")
320
+ label = product_label or (product.name if product else "multiples productos")
321
+ assigned_product_ids = product_ids or ([product.id] if product else [])
275
322
  assigned_asset = SaleAssignedToChat(
276
323
  asset_type=ChatAssetType.SALE,
277
324
  asset_id=sale.id,
278
325
  assigned_at=sale.created_at,
279
326
  assigned_by=execution_context.executor.id,
280
- product_id=product.id
327
+ product_id=product.id if product else None,
328
+ product_ids=assigned_product_ids
281
329
  )
282
330
  execution_context.set_event_time(assigned_asset.assigned_at)
283
331
  bisect.insort(chat.client.sales, assigned_asset)
284
- ChatService.create_highlight(chat=chat, execution_context=execution_context, highlight_data=HighlightRequestData(title=f"🛍️ Venta de {product.name}", description=f"Venta de {product.name} creada por {execution_context.executor.name}", starred=False, subtype=MessageSubtype.SALE_ADDED))
285
- ChatService.add_central_notification_from_text(chat=chat, body=f"Venta de {product.name} agregada al chat {chat.id} por {execution_context.executor.name}", subtype=MessageSubtype.SALE_ADDED)
332
+ ChatService.create_highlight(chat=chat, execution_context=execution_context, highlight_data=HighlightRequestData(title=f"🛍️ Venta de {label}", description=f"Venta de {label} creada por {execution_context.executor.name}", starred=False, subtype=MessageSubtype.SALE_ADDED))
333
+ ChatService.add_central_notification_from_text(chat=chat, body=f"Venta de {label} agregada al chat {chat.id} por {execution_context.executor.name}", subtype=MessageSubtype.SALE_ADDED)
286
334
  return assigned_asset
287
335
 
288
336
  @staticmethod
289
- def update_sale(chat : Chat, execution_context: ExecutionContext, sale : Sale, product : Product) -> Sale:
337
+ def update_sale(chat: Chat, execution_context: ExecutionContext, sale: Sale, product_label: str) -> Sale:
290
338
  """
291
339
  Update a sale for the chat.
292
340
  """
293
- ChatService.create_highlight(chat=chat, execution_context=execution_context, highlight_data=HighlightRequestData(title=f"🛍️ Venta actualizada de {product.name}", description=f"Venta de {product.name} actualizada por {execution_context.executor.name}", starred=False, subtype=MessageSubtype.SALE_UPDATED))
294
- ChatService.add_central_notification_from_text(chat=chat, body=f"Venta de {product.name} actualizada en el chat {chat.id} por {execution_context.executor.name}", subtype=MessageSubtype.SALE_UPDATED)
341
+ ChatService.create_highlight(chat=chat, execution_context=execution_context, highlight_data=HighlightRequestData(title=f"🛍️ Venta actualizada de {product_label}", description=f"Venta de {product_label} actualizada por {execution_context.executor.name}", starred=False, subtype=MessageSubtype.SALE_UPDATED))
342
+ ChatService.add_central_notification_from_text(chat=chat, body=f"Venta de {product_label} actualizada en el chat {chat.id} por {execution_context.executor.name}", subtype=MessageSubtype.SALE_UPDATED)
295
343
  return sale
296
344
 
297
345
  @staticmethod
298
- def delete_sale(chat : Chat, execution_context: ExecutionContext, sale_id : StrObjectId, product : Product) -> SaleAssignedToChat:
346
+ def delete_sale(chat: Chat, execution_context: ExecutionContext, sale_id: StrObjectId, product_label: str) -> SaleAssignedToChat:
299
347
  """
300
348
  Logically remove a sale from the chat.
301
349
  """
@@ -303,8 +351,8 @@ class ChatService:
303
351
  assigned_asset_to_remove = next(sale for sale in chat.client.sales if sale.asset_id == sale_id)
304
352
  chat.client.sales.remove(assigned_asset_to_remove)
305
353
  execution_context.set_event_time(datetime.now(tz=ZoneInfo("UTC")))
306
- ChatService.create_highlight(chat=chat, execution_context=execution_context, highlight_data=HighlightRequestData(title=f"🛍️ Venta eliminada de {product.name}", description=f"Venta de {product.name} eliminada por {execution_context.executor.name}", starred=False, subtype=MessageSubtype.SALE_DELETED))
307
- ChatService.add_central_notification_from_text(chat=chat, body=f"Venta de {product.name} eliminada del chat {chat.id} por {execution_context.executor.name}", subtype=MessageSubtype.SALE_DELETED)
354
+ ChatService.create_highlight(chat=chat, execution_context=execution_context, highlight_data=HighlightRequestData(title=f"🛍️ Venta eliminada de {product_label}", description=f"Venta de {product_label} eliminada por {execution_context.executor.name}", starred=False, subtype=MessageSubtype.SALE_DELETED))
355
+ ChatService.add_central_notification_from_text(chat=chat, body=f"Venta de {product_label} eliminada del chat {chat.id} por {execution_context.executor.name}", subtype=MessageSubtype.SALE_DELETED)
308
356
  return assigned_asset_to_remove
309
357
  except StopIteration:
310
358
  raise NotFoundError(message=f"Sale with id {sale_id} not found in chat {chat.id}")
@@ -663,7 +711,10 @@ class ChatService:
663
711
  if client_data.external_id is not None:
664
712
  chat.client.external_id = client_data.external_id
665
713
  if client_data.lead_form_data is not None:
666
- chat.client.lead_form_data = client_data.lead_form_data
714
+ # Merge with existing lead_form_data instead of replacing
715
+ if chat.client.lead_form_data is None:
716
+ chat.client.lead_form_data = {}
717
+ chat.client.lead_form_data.update(client_data.lead_form_data)
667
718
  execution_context.set_event_time(datetime.now(tz=ZoneInfo("UTC")))
668
719
  ChatService.add_central_notification_from_text(chat=chat, body=f"La info del cliente fue actualizada por {execution_context.executor.name}", subtype=MessageSubtype.CLIENT_INFO_UPDATED)
669
720
  return chat
@@ -846,6 +897,13 @@ class ChatService:
846
897
  chat.client.email = collected_data.email
847
898
  updated_fields.append("email")
848
899
 
900
+ if collected_data.phone:
901
+ if chat.client.lead_form_data is None:
902
+ chat.client.lead_form_data = {}
903
+ if chat.client.lead_form_data.get("phone") != collected_data.phone:
904
+ chat.client.lead_form_data["phone"] = collected_data.phone
905
+ updated_fields.append("phone")
906
+
849
907
  if collected_data.document_id and chat.client.document_id != collected_data.document_id:
850
908
  chat.client.document_id = collected_data.document_id
851
909
  updated_fields.append("document_id")
@@ -863,11 +921,18 @@ class ChatService:
863
921
  execution_context.set_event_time(datetime.now(tz=ZoneInfo("UTC")))
864
922
  logger.info(f"Updated collected data for chat {chat.id}: {', '.join(updated_fields)}")
865
923
 
924
+ field_label_map = {
925
+ "name": "nombre",
926
+ "email": "email",
927
+ "phone": "telefono",
928
+ "document_id": "dni",
929
+ }
930
+ display_fields = [field_label_map.get(field, field) for field in updated_fields]
866
931
  ChatService.add_central_notification_from_text(
867
932
  chat=chat,
868
- body=f"Collected customer data: {', '.join(updated_fields)}",
933
+ body=f"Datos del cliente recopilados: {', '.join(display_fields)}",
869
934
  subtype=MessageSubtype.CLIENT_INFO_UPDATED,
870
935
  context=ChattyContext(chain_of_thought_id=execution_context.chain_of_thought_id)
871
936
  )
872
937
 
873
- return chat.client.lead_form_data
938
+ return chat.client.lead_form_data
@@ -1,14 +1,2 @@
1
1
  from .base_container import ChattyAssetBaseContainer
2
2
  from .base_container_with_collection import ChattyAssetContainerWithCollection
3
- from .assets_collections import AssetsCollections
4
- from .services import (
5
- ProductService,
6
- TagService,
7
- UserService,
8
- ChatService,
9
- SourceService,
10
- FlowService,
11
- SaleService,
12
- ContactPointService,
13
- AiAgentService
14
- )