letschatty 0.4.351__py3-none-any.whl → 0.4.353__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of letschatty might be problematic. Click here for more details.

Files changed (92) hide show
  1. letschatty/models/ai_microservices/__init__.py +4 -4
  2. letschatty/models/ai_microservices/expected_output.py +2 -29
  3. letschatty/models/ai_microservices/lambda_events.py +28 -155
  4. letschatty/models/ai_microservices/lambda_invokation_types.py +1 -4
  5. letschatty/models/ai_microservices/n8n_ai_agents_payload.py +1 -3
  6. letschatty/models/analytics/events/__init__.py +3 -3
  7. letschatty/models/analytics/events/chat_based_events/chat_client.py +19 -0
  8. letschatty/models/analytics/events/chat_based_events/chat_funnel.py +69 -13
  9. letschatty/models/analytics/events/company_based_events/asset_events.py +9 -2
  10. letschatty/models/analytics/events/event_type_to_classes.py +7 -3
  11. letschatty/models/analytics/events/event_types.py +11 -50
  12. letschatty/models/chat/chat.py +13 -2
  13. letschatty/models/chat/chat_with_assets.py +6 -1
  14. letschatty/models/chat/client.py +0 -2
  15. letschatty/models/chat/continuous_conversation.py +1 -1
  16. letschatty/models/company/CRM/funnel.py +365 -33
  17. letschatty/models/company/__init__.py +10 -1
  18. letschatty/models/company/assets/ai_agents_v2/ai_agents_decision_output.py +1 -1
  19. letschatty/models/company/assets/ai_agents_v2/chatty_ai_agent_in_chat.py +0 -4
  20. letschatty/models/company/assets/ai_agents_v2/chatty_ai_mode.py +2 -2
  21. letschatty/models/company/assets/ai_agents_v2/get_chat_with_prompt_response.py +0 -1
  22. letschatty/models/company/assets/ai_agents_v2/pre_qualify_config.py +1 -28
  23. letschatty/models/company/assets/automation.py +10 -19
  24. letschatty/models/company/assets/chat_assets.py +3 -2
  25. letschatty/models/company/assets/company_assets.py +2 -0
  26. letschatty/models/company/assets/sale.py +3 -3
  27. letschatty/models/company/empresa.py +4 -1
  28. letschatty/models/company/integrations/product_sync_status.py +28 -0
  29. letschatty/models/company/integrations/shopify/company_shopify_integration.py +62 -0
  30. letschatty/models/company/integrations/shopify/shopify_product_sync_status.py +18 -0
  31. letschatty/models/company/integrations/shopify/shopify_webhook_topics.py +40 -0
  32. letschatty/models/company/integrations/sync_status_enum.py +9 -0
  33. letschatty/models/company/integrations/tienda_nube/company_tienda_nube_integration.py +62 -0
  34. letschatty/models/company/integrations/tienda_nube/tienda_nube_product_sync_status.py +18 -0
  35. letschatty/models/company/integrations/tienda_nube/tienda_nube_webhook_topics.py +46 -0
  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 +1 -14
  41. letschatty/services/ai_agents/smart_follow_up_context_builder_v2.py +2 -5
  42. letschatty/services/chat/chat_service.py +47 -11
  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 +30 -66
  50. letschatty/services/factories/lambda_ai_orchestrartor/lambda_events_factory.py +8 -46
  51. letschatty/services/messages_helpers/get_caption_or_body_or_preview.py +4 -6
  52. letschatty/services/validators/analytics_validator.py +11 -0
  53. {letschatty-0.4.351.dist-info → letschatty-0.4.353.dist-info}/METADATA +1 -1
  54. {letschatty-0.4.351.dist-info → letschatty-0.4.353.dist-info}/RECORD +56 -83
  55. letschatty/models/analytics/events/chat_based_events/ai_agent_execution_event.py +0 -71
  56. letschatty/services/chatty_assets/assets_collections.py +0 -137
  57. letschatty/services/chatty_assets/collections/__init__.py +0 -38
  58. letschatty/services/chatty_assets/collections/ai_agent_collection.py +0 -19
  59. letschatty/services/chatty_assets/collections/ai_agent_in_chat_collection.py +0 -32
  60. letschatty/services/chatty_assets/collections/ai_component_collection.py +0 -21
  61. letschatty/services/chatty_assets/collections/chain_of_thought_collection.py +0 -30
  62. letschatty/services/chatty_assets/collections/chat_collection.py +0 -21
  63. letschatty/services/chatty_assets/collections/contact_point_collection.py +0 -21
  64. letschatty/services/chatty_assets/collections/fast_answer_collection.py +0 -21
  65. letschatty/services/chatty_assets/collections/filter_criteria_collection.py +0 -18
  66. letschatty/services/chatty_assets/collections/flow_collection.py +0 -20
  67. letschatty/services/chatty_assets/collections/product_collection.py +0 -20
  68. letschatty/services/chatty_assets/collections/sale_collection.py +0 -20
  69. letschatty/services/chatty_assets/collections/source_collection.py +0 -21
  70. letschatty/services/chatty_assets/collections/tag_collection.py +0 -19
  71. letschatty/services/chatty_assets/collections/topic_collection.py +0 -21
  72. letschatty/services/chatty_assets/collections/user_collection.py +0 -20
  73. letschatty/services/chatty_assets/example_usage.py +0 -44
  74. letschatty/services/chatty_assets/services/__init__.py +0 -37
  75. letschatty/services/chatty_assets/services/ai_agent_in_chat_service.py +0 -73
  76. letschatty/services/chatty_assets/services/ai_agent_service.py +0 -23
  77. letschatty/services/chatty_assets/services/chain_of_thought_service.py +0 -70
  78. letschatty/services/chatty_assets/services/chat_service.py +0 -25
  79. letschatty/services/chatty_assets/services/contact_point_service.py +0 -29
  80. letschatty/services/chatty_assets/services/fast_answer_service.py +0 -32
  81. letschatty/services/chatty_assets/services/filter_criteria_service.py +0 -30
  82. letschatty/services/chatty_assets/services/flow_service.py +0 -25
  83. letschatty/services/chatty_assets/services/product_service.py +0 -30
  84. letschatty/services/chatty_assets/services/sale_service.py +0 -25
  85. letschatty/services/chatty_assets/services/source_service.py +0 -28
  86. letschatty/services/chatty_assets/services/tag_service.py +0 -32
  87. letschatty/services/chatty_assets/services/topic_service.py +0 -31
  88. letschatty/services/chatty_assets/services/user_service.py +0 -32
  89. letschatty/services/events/__init__.py +0 -6
  90. letschatty/services/factories/analytics/ai_agent_event_factory.py +0 -161
  91. {letschatty-0.4.351.dist-info → letschatty-0.4.353.dist-info}/LICENSE +0 -0
  92. {letschatty-0.4.351.dist-info → letschatty-0.4.353.dist-info}/WHEEL +0 -0
@@ -64,8 +64,7 @@ class ChattyAssetContainerWithCollection(ChattyAssetBaseContainer[T, P], ABC):
64
64
  self.update_previews_thread()
65
65
  self.load_from_db_thread(company_id=None)
66
66
 
67
- # All methods are now async-only for better performance
68
- async def insert(self, item: T, execution_context: ExecutionContext) -> T:
67
+ def insert(self, item: T, execution_context: ExecutionContext) -> T:
69
68
  """
70
69
  Add an item to the container and insert it into the database collection.
71
70
 
@@ -78,12 +77,12 @@ class ChattyAssetContainerWithCollection(ChattyAssetBaseContainer[T, P], ABC):
78
77
  """
79
78
  logger.debug(f"{self.__class__.__name__} inserting item {item}")
80
79
  inserted_item = super().insert(item)
81
- await self.collection.insert(inserted_item)
80
+ self.collection.insert(inserted_item)
82
81
  execution_context.set_event_time(inserted_item.created_at)
83
82
  self.update_previews_thread()
84
83
  return inserted_item
85
84
 
86
- async def update(self, id: str, new_item: T, execution_context: ExecutionContext) -> T:
85
+ def update(self, id: str, new_item: T, execution_context: ExecutionContext) -> T:
87
86
  """
88
87
  Update an item in the container and in the database collection.
89
88
 
@@ -106,18 +105,18 @@ class ChattyAssetContainerWithCollection(ChattyAssetBaseContainer[T, P], ABC):
106
105
  if id != updated_item.id:
107
106
  logger.error(f"Item id {id} does not match updated item id {updated_item.id}")
108
107
  raise ValueError(f"Item id {id} does not match updated item id {updated_item.id}")
109
- await self.collection.update(updated_item)
108
+ self.collection.update(updated_item)
110
109
  execution_context.set_event_time(updated_item.updated_at)
111
110
  self.update_preview(updated_item)
112
111
  self.update_previews_thread()
113
112
  return updated_item
114
113
 
115
114
  except NotFoundError as e:
116
- outdated_item = await self.collection.get_by_id(id)
115
+ outdated_item = self.collection.get_by_id(id)
117
116
  if outdated_item:
118
117
  updated_item = outdated_item.update(new_item)
119
118
  self.items[id] = updated_item
120
- await self.collection.update(updated_item)
119
+ self.collection.update(updated_item)
121
120
  execution_context.set_event_time(updated_item.updated_at)
122
121
  self.update_previews_thread()
123
122
  return updated_item
@@ -126,7 +125,7 @@ class ChattyAssetContainerWithCollection(ChattyAssetBaseContainer[T, P], ABC):
126
125
  f"Item with id {id} not found in {self.__class__.__name__} nor in collection DB"
127
126
  )
128
127
 
129
- async def delete(self, id: str, execution_context: ExecutionContext,deletion_type : DeletionType = DeletionType.LOGICAL) -> T:
128
+ def delete(self, id: str, execution_context: ExecutionContext,deletion_type : DeletionType = DeletionType.LOGICAL) -> T:
130
129
  """
131
130
  Delete an item from the container and the collection.
132
131
 
@@ -143,16 +142,16 @@ class ChattyAssetContainerWithCollection(ChattyAssetBaseContainer[T, P], ABC):
143
142
  deleted_item = super().delete(id)
144
143
  self.delete_preview(id)
145
144
  execution_context.set_event_time(datetime.now(ZoneInfo("UTC")))
146
- await self.collection.delete(id, deletion_type)
145
+ self.collection.delete(id, deletion_type)
147
146
  return deleted_item
148
147
  except NotFoundError as e:
149
- await self.collection.delete(id, deletion_type)
148
+ self.collection.delete(id, deletion_type)
150
149
  self.delete_preview(id)
151
150
  self.update_previews_thread()
152
151
  execution_context.set_event_time(datetime.now(ZoneInfo("UTC")))
153
- return await self.collection.get_by_id(id)
152
+ return self.collection.get_by_id(id)
154
153
 
155
- async def get_by_id(self, id: str) -> T:
154
+ def get_by_id(self, id: str) -> T:
156
155
  """
157
156
  Get an item from the container.
158
157
 
@@ -175,7 +174,7 @@ class ChattyAssetContainerWithCollection(ChattyAssetBaseContainer[T, P], ABC):
175
174
  # #if they are supposed to be in memory, we raise an error since it shouldn't be in the collection DB
176
175
  # raise NotFoundError(f"Item with id {id} not found in {self.__class__.__name__} nor in collection DB")
177
176
  logger.debug(f"{self.__class__.__name__} getting item {id} not found in container, trying to get from collection")
178
- item = await self.collection.get_by_id(id)
177
+ item = self.collection.get_by_id(id)
179
178
  if item:
180
179
  if item.deleted_at is not None:
181
180
  return item
@@ -240,7 +239,7 @@ class ChattyAssetContainerWithCollection(ChattyAssetBaseContainer[T, P], ABC):
240
239
  logger.debug(f"Clearing previews cache of {self.__class__.__name__}")
241
240
  self.set_preview_items([])
242
241
 
243
- async def get_all(self, company_id: Optional[StrObjectId]) -> List[T]:
242
+ def get_all(self, company_id: Optional[StrObjectId]) -> List[T]:
244
243
  # Get items from memory
245
244
  logger.debug(f"{self.__class__.__name__} getting all items from memory and collection")
246
245
  memory_items = super().get_all(company_id=company_id)
@@ -248,7 +247,7 @@ class ChattyAssetContainerWithCollection(ChattyAssetBaseContainer[T, P], ABC):
248
247
  memory_ids = [ObjectId(item.id) for item in memory_items]
249
248
  # Build the query for collection items
250
249
  query = {"deleted_at": None, "_id": {"$nin": memory_ids}}
251
- collection_items = await self.collection.get_docs(query=query, company_id=company_id)
250
+ collection_items = self.collection.get_docs(query=query, company_id=company_id)
252
251
  all_items = memory_items + collection_items
253
252
  return sorted(all_items, key=lambda x: x.created_at, reverse=True)
254
253
 
@@ -262,8 +261,6 @@ class ChattyAssetContainerWithCollection(ChattyAssetBaseContainer[T, P], ABC):
262
261
  def update_previews_thread(self):
263
262
  """We start a thread to update the previews cache so it doesn't block the main thread"""
264
263
  # self.update_previews_cache()
265
- if not self.cache_config.keep_previews_always_in_memory:
266
- return
267
264
  thread = threading.Thread(target=self.update_previews_cache)
268
265
  thread.start()
269
266
 
@@ -278,13 +275,13 @@ class ChattyAssetContainerWithCollection(ChattyAssetBaseContainer[T, P], ABC):
278
275
  self.set_preview_items(collection_items)
279
276
  return collection_items
280
277
 
281
- async def get_by_query(self, query: dict, company_id: Optional[StrObjectId]) -> List[T]:
278
+ def get_by_query(self, query: dict, company_id: Optional[StrObjectId]) -> List[T]:
282
279
  logger.debug(f"{self.__class__.__name__} getting items by query {query} from collection")
283
- return await self.collection.get_docs(query=query, company_id=company_id)
280
+ return self.collection.get_docs(query=query, company_id=company_id)
284
281
 
285
- async def get_deleted(self, company_id: Optional[StrObjectId]) -> List[T]:
282
+ def get_deleted(self, company_id: Optional[StrObjectId]) -> List[T]:
286
283
  logger.debug(f"{self.__class__.__name__} getting deleted items from collection")
287
- return await self.collection.get_docs(query={"deleted_at": {"$ne": None}}, company_id=company_id)
284
+ return self.collection.get_docs(query={"deleted_at": {"$ne": None}}, company_id=company_id)
288
285
 
289
286
  def load_from_db_thread(self, company_id: Optional[StrObjectId]):
290
287
  """We start a thread to load the items from the database so it doesn't block the main thread"""
@@ -295,27 +292,21 @@ class ChattyAssetContainerWithCollection(ChattyAssetBaseContainer[T, P], ABC):
295
292
  thread.start()
296
293
 
297
294
  def load_from_db(self, company_id: Optional[StrObjectId]):
298
- """Pass company_id=None to load all items from the database. Uses sync client for background loading."""
295
+ """Pass company_id=None to load all items from the database."""
299
296
  logger.debug(f"{self.__class__.__name__} loading items from collection")
300
- # Background loading uses sync client (less critical, runs in thread)
301
- query: Dict[str, Any] = {"deleted_at": None}
302
- if company_id:
303
- query["company_id"] = company_id
304
- docs = list(self.collection.collection.find(filter=query))
305
- # Create instances once and reuse
306
- loaded_items = [self.collection.create_instance(doc) for doc in docs]
307
- self.items = {item.id: item for item in loaded_items}
308
-
309
- async def restore(self, id: str, execution_context: ExecutionContext) -> T:
297
+ # self.items = {item.id: item for item in self.collection.get_docs(query={}, company_id=company_id)}
298
+ self.items = {item.id: item for item in self.collection.get_docs(query={"deleted_at": None}, company_id=company_id)}
299
+
300
+ def restore(self, id: str, execution_context: ExecutionContext) -> T:
310
301
  logger.debug(f"{self.__class__.__name__} restoring item {id} with execution context {execution_context}")
311
302
  if id in self.items:
312
303
  raise ValueError(f"Item with id {id} already exists in {self.__class__.__name__}")
313
- restored_item = await self.collection.get_by_id(id)
304
+ restored_item = self.collection.get_by_id(id)
314
305
  if restored_item is None:
315
306
  raise NotFoundError(f"Item with id {id} not found in collection DB")
316
307
  restored_item.deleted_at = None
317
308
  restored_item.update_now()
318
309
  execution_context.set_event_time(restored_item.updated_at)
319
310
  self.items[id] = restored_item
320
- await self.collection.update(restored_item)
321
- return restored_item
311
+ self.collection.update(restored_item)
312
+ return restored_item
@@ -269,15 +269,4 @@ class ContinuousConversationHelper:
269
269
  central_notif_content = ChattyContentCentral(body=body, status=CentralNotificationStatus.WARNING, calls_to_action=[cta.value for cta in cc.calls_to_action])
270
270
  central_notif = CentralNotificationFactory.continuous_conversation_status(cc=cc, content=central_notif_content)
271
271
  ChatService.add_central_notification(central_notification=central_notif, chat=chat)
272
- return cc
273
-
274
- @staticmethod
275
- def handle_failed_template_cc(chat: Chat, cc: ContinuousConversation, error_details: str) -> ContinuousConversation:
276
- """This is for the handling of a failed template CC"""
277
- cc.set_status(status=ContinuousConversationStatus.FAILED)
278
- body=f"Continuous conversation failed to be sent: {error_details}"
279
- logger.debug(f"{body} | CC status: {cc.status} | CC id: {cc.id} | chat id: {chat.identifier}")
280
- central_notif_content = ChattyContentCentral(body=body, status=CentralNotificationStatus.ERROR, calls_to_action=[cta.value for cta in cc.calls_to_action])
281
- central_notif = CentralNotificationFactory.continuous_conversation_status(cc=cc, content=central_notif_content)
282
- ChatService.add_central_notification(central_notification=central_notif, chat=chat)
283
272
  return cc
@@ -1,219 +1,2 @@
1
- """
2
- Events Manager - Handles queuing and publishing events to EventBridge
1
+ from letschatty.models.base_models.singleton import SingletonMeta
3
2
 
4
- This is a generic implementation that can be configured for different environments.
5
- """
6
- from ...models.base_models.singleton import SingletonMeta
7
- from ...models.analytics.events.base import Event, EventType
8
- from typing import List, Optional, Callable
9
- import logging
10
- import boto3
11
- import queue
12
- import threading
13
- import time
14
- from datetime import datetime
15
- from zoneinfo import ZoneInfo
16
- import os
17
- import json
18
-
19
- logger = logging.getLogger("EventsManager")
20
-
21
-
22
- class EventsManager(metaclass=SingletonMeta):
23
- """
24
- Manages event queuing and publishing to AWS EventBridge.
25
-
26
- Can be configured via environment variables or init parameters.
27
- """
28
-
29
- def __init__(self,
30
- event_bus_name: Optional[str] = None,
31
- source: Optional[str] = None,
32
- publish_events: Optional[bool] = None,
33
- failed_events_callback: Optional[Callable] = None):
34
- """
35
- Initialize EventsManager.
36
-
37
- Args:
38
- event_bus_name: AWS EventBridge event bus name (or uses env var)
39
- source: Source identifier for events (or uses env var)
40
- publish_events: Whether to publish events (or uses env var)
41
- failed_events_callback: Optional callback for handling failed events
42
- """
43
- self.events_queue: queue.Queue[Event] = queue.Queue()
44
- self.eventbridge_client = boto3.client('events', region_name='us-east-1')
45
-
46
- # Configuration - prefer parameters, fall back to env vars
47
- self.event_bus_name = event_bus_name or os.getenv('CHATTY_EVENT_BUS_NAME', 'chatty-events')
48
- self.source = source or os.getenv('CHATTY_EVENT_SOURCE')
49
- if not self.source:
50
- raise ValueError("Source must be provided either as a parameter or through the CHATTY_EVENT_SOURCE environment variable.")
51
- self.publish_events = publish_events if publish_events is not None else os.getenv('PUBLISH_EVENTS_TO_EVENTBRIDGE', 'true').lower() == 'true'
52
-
53
- self.max_retries = 3
54
- self.thread_lock = threading.Lock()
55
- self.thread_running = False
56
- self.max_thread_runtime = 300
57
- self.failed_events_callback = failed_events_callback
58
-
59
- logger.debug(f"EventsManager initialized: bus={self.event_bus_name}, source={self.source}, publish={self.publish_events}")
60
-
61
- def queue_events(self, events: List[Event]):
62
- """Queue events and spawn a thread to publish them if one isn't already running"""
63
- if not self.publish_events:
64
- logger.debug("Event publishing disabled, skipping")
65
- return
66
-
67
- for event in events:
68
- logger.debug(f"Queueing event: {event.type.value} {event.company_id}")
69
- logger.debug(f"Event: {event.model_dump_json()}")
70
- self.events_queue.put(event)
71
-
72
- logger.debug(f"Queued {len(events)} events")
73
- if events:
74
- logger.debug(f"1° event: {events[0].model_dump_json()}")
75
-
76
- # Only start a new thread if one isn't already running
77
- with self.thread_lock:
78
- if not self.thread_running:
79
- logger.debug("Starting publisher thread")
80
- self.thread_running = True
81
- thread = threading.Thread(
82
- target=self._process_queue,
83
- daemon=True,
84
- name="EventBridge-Publisher"
85
- )
86
- thread.start()
87
- logger.debug("Started publisher thread")
88
- else:
89
- logger.debug("Publisher thread already running, using existing thread")
90
-
91
- def _process_queue(self):
92
- """Process all events in the queue and then terminate"""
93
- try:
94
- start_time = time.time()
95
- while not self.events_queue.empty():
96
- logger.debug("Processing queue")
97
- events_batch = []
98
- if time.time() - start_time > self.max_thread_runtime:
99
- logger.warning(f"Thread ran for more than {self.max_thread_runtime}s - terminating")
100
- break
101
-
102
- # Collect up to 10 events (EventBridge limit)
103
- for _ in range(10):
104
- try:
105
- event = self.events_queue.get(timeout=0.5)
106
- events_batch.append(event)
107
- self.events_queue.task_done()
108
- except queue.Empty:
109
- logger.debug("Queue is empty")
110
- break
111
-
112
- # Publish this batch
113
- if events_batch:
114
- self._publish_batch(events_batch)
115
-
116
- except Exception as e:
117
- logger.exception(f"Error in publisher thread: {str(e)}")
118
-
119
- finally:
120
- # Mark thread as completed
121
- with self.thread_lock:
122
- self.thread_running = False
123
-
124
- def _publish_batch(self, events: List[Event]):
125
- """Send a batch of events to EventBridge with retries"""
126
- if not events:
127
- return
128
-
129
- entries = []
130
- for event in events:
131
- entry = {
132
- 'Source': self.source,
133
- 'DetailType': event.type.value,
134
- 'Detail': json.dumps(event.model_dump_json()),
135
- 'EventBusName': self.event_bus_name
136
- }
137
- logger.debug(f"Appending event: {event.type.value}")
138
- entries.append(entry)
139
-
140
- for retry in range(self.max_retries):
141
- try:
142
- logger.debug(f"Sending {len(entries)} events to EventBridge")
143
- logger.debug(f"Entries: {entries}")
144
- response = self.eventbridge_client.put_events(Entries=entries)
145
- logger.debug(f"Response: {response}")
146
-
147
- if response.get('FailedEntryCount', 0) == 0:
148
- logger.info(f"Successfully published {len(events)} events")
149
- return
150
-
151
- # Handle partial failures
152
- failed_entries: List[dict] = []
153
- failed_events: List[Event] = []
154
-
155
- for i, result in enumerate(response.get('Entries', [])):
156
- if 'ErrorCode' in result:
157
- failed_entries.append(entries[i])
158
- failed_events.append(events[i])
159
- logger.error(f"Failed to publish event: {events[i].type.value}")
160
-
161
- if retry < self.max_retries - 1 and failed_entries:
162
- logger.info(f"Retrying {len(failed_entries)} events")
163
- entries = failed_entries
164
- events = failed_events
165
- else:
166
- # Store failed events via callback if provided
167
- if self.failed_events_callback and failed_events:
168
- failed_events_with_errors = []
169
- for i, event in enumerate(failed_events):
170
- result = response.get('Entries', [])[i]
171
- failed_event_data = {
172
- "event": event.model_dump_json(),
173
- "error_code": result.get('ErrorCode'),
174
- "error_message": result.get('ErrorMessage'),
175
- "retry_count": self.max_retries,
176
- "timestamp": datetime.now(ZoneInfo("UTC"))
177
- }
178
- failed_events_with_errors.append(failed_event_data)
179
-
180
- try:
181
- self.failed_events_callback(failed_events_with_errors)
182
- except Exception as e:
183
- logger.error(f"Error calling failed_events_callback: {e}")
184
-
185
- logger.error(f"Gave up on {len(failed_entries)} events after {self.max_retries} attempts")
186
- return
187
-
188
- except Exception as e:
189
- if retry < self.max_retries - 1:
190
- logger.warning(f"Error publishing events (attempt {retry+1}/{self.max_retries}): {str(e)}")
191
- time.sleep(0.5 * (2 ** retry)) # Exponential backoff
192
- else:
193
- logger.exception(f"Failed to publish events after {self.max_retries} attempts")
194
- return
195
-
196
- def flush(self):
197
- """Wait for all queued events to be processed"""
198
- # If no thread is running but we have events, start one
199
- with self.thread_lock:
200
- if not self.thread_running and not self.events_queue.empty():
201
- self.thread_running = True
202
- thread = threading.Thread(
203
- target=self._process_queue,
204
- daemon=True,
205
- name="EventBridge-Publisher"
206
- )
207
- thread.start()
208
-
209
- # Wait for queue to be empty
210
- try:
211
- self.events_queue.join()
212
- return True
213
- except Exception:
214
- logger.warning("Error waiting for events queue to complete")
215
- return False
216
-
217
-
218
- # Singleton instance
219
- events_manager = EventsManager()
@@ -1,7 +1,6 @@
1
1
  from typing import Optional, Dict, Any
2
2
  from datetime import datetime
3
3
  from ....models.analytics.events import *
4
- from ....models.analytics.events.chat_based_events.ai_agent_execution_event import AIAgentExecutionEvent
5
4
  from ....models.base_models.chatty_asset_model import ChattyAssetModel
6
5
  from ....models.company.assets.chat_assets import AssignedAssetToChat
7
6
  from ....models.company.empresa import EmpresaModel
@@ -322,7 +321,7 @@ class EventFactory:
322
321
  return events
323
322
 
324
323
  @staticmethod
325
- def funnel_stage_events(chat_with_assets: ChatWithAssets, company_info: EmpresaModel, trace_id: str,executor_type: ExecutorType, executor_id: StrObjectId, chat_funnel: ClientFunnel, funnel_transition: StageTransition, event_type: EventType, time: datetime, company_snapshot: Optional[CompanyChatsSnapshot] = None, agent_snapshot: Optional[AgentChatsSnapshot] = None) -> List[Event]:
324
+ def funnel_stage_events(chat_with_assets: ChatWithAssets, company_info: EmpresaModel, trace_id: str,executor_type: ExecutorType, executor_id: StrObjectId, chat_funnel: ChatFunnel, funnel_transition: StageTransition, event_type: EventType, time: datetime, company_snapshot: Optional[CompanyChatsSnapshot] = None, agent_snapshot: Optional[AgentChatsSnapshot] = None) -> List[Event]:
326
325
  events = []
327
326
  base_data = EventFactory._create_base_customer_event_data(
328
327
  chat_with_assets=chat_with_assets,
@@ -335,8 +334,10 @@ class EventFactory:
335
334
  funnel_data = FunnelEventData(
336
335
  **base_data.model_dump(),
337
336
  funnel_id=chat_funnel.funnel_id,
338
- funnel_stage_transition=funnel_transition,
339
- time_in_funnel_seconds=chat_funnel.time_in_funnel_seconds
337
+ chat_funnel_id=chat_funnel.id,
338
+ stage_transition=funnel_transition,
339
+ time_in_funnel_seconds=chat_funnel.time_in_funnel_seconds,
340
+ time_in_last_stage_seconds=chat_funnel.time_in_current_stage_seconds
340
341
  )
341
342
  event = ChatFunnelEvent(
342
343
  specversion=EventFactory.package_version(),
@@ -414,6 +415,30 @@ class EventFactory:
414
415
  events.append(event)
415
416
  return events
416
417
 
418
+ @staticmethod
419
+ def chat_client_updated_events(chat_with_assets: ChatWithAssets, company_info: EmpresaModel, trace_id: str, executor_type: ExecutorType, executor_id: StrObjectId, event_type: EventType, time: datetime, company_snapshot: Optional[CompanyChatsSnapshot] = None, agent_snapshot: Optional[AgentChatsSnapshot] = None) -> List[Event]:
420
+ events = []
421
+ base_data = EventFactory._create_base_customer_event_data(
422
+ chat_with_assets=chat_with_assets,
423
+ company_info=company_info,
424
+ executor_type=executor_type,
425
+ executor_id=executor_id,
426
+ company_snapshot=company_snapshot,
427
+ agent_snapshot=agent_snapshot
428
+ )
429
+ event = ChatClientEvent(
430
+ specversion=EventFactory.package_version(),
431
+ type=event_type,
432
+ time=time,
433
+ data=base_data,
434
+ source="chatty_api.webapp",
435
+ company_id=company_info.id,
436
+ frozen_company_name=company_info.frozen_name,
437
+ trace_id=trace_id
438
+ )
439
+ events.append(event)
440
+ return events
441
+
417
442
  @staticmethod
418
443
  def chat_created_events(chat_with_assets: ChatWithAssets, company_info: EmpresaModel, trace_id: str,executor_type: ExecutorType, executor_id: StrObjectId, event_type: EventType, time: datetime, contact_point_id: Optional[StrObjectId], created_from: ChatCreatedFrom, company_snapshot: Optional[CompanyChatsSnapshot] = None, agent_snapshot: Optional[AgentChatsSnapshot] = None) -> List[Event]:
419
444
  events = []
@@ -612,65 +637,4 @@ class EventFactory:
612
637
  trace_id=trace_id
613
638
  )
614
639
  events.append(event)
615
- return events
616
-
617
- @staticmethod
618
- def ai_agent_execution_event(
619
- event_type: EventType,
620
- chat_id: StrObjectId,
621
- company_id: StrObjectId,
622
- frozen_company_name: str,
623
- ai_agent_id: StrObjectId,
624
- chain_of_thought_id: StrObjectId,
625
- trigger: str,
626
- source: str = "chatty.api",
627
- decision_type: Optional[str] = None,
628
- error_message: Optional[str] = None,
629
- duration_ms: Optional[int] = None,
630
- user_rating: Optional[int] = None,
631
- metadata: Optional[Dict[str, Any]] = None,
632
- trace_id: Optional[str] = None
633
- ) -> AIAgentExecutionEvent:
634
- """
635
- Create an AI agent execution event using the modularized AIAgentEventFactory.
636
-
637
- This method delegates to AIAgentEventFactory to maintain modularity while
638
- keeping event creation centralized through EventsFactory.
639
-
640
- Args:
641
- event_type: The type of event (from EventType enum)
642
- chat_id: ID of the chat where the event occurred
643
- company_id: ID of the company
644
- frozen_company_name: Company name snapshot for analytics
645
- ai_agent_id: ID of the AI agent asset
646
- chain_of_thought_id: ID of the chain of thought execution
647
- trigger: What triggered the execution (USER_MESSAGE, FOLLOW_UP, etc.)
648
- source: Event source (e.g., 'chatty.api', 'chatty.lambda')
649
- decision_type: Type of decision if applicable
650
- error_message: Error message if this is an error event
651
- duration_ms: Duration of the operation in milliseconds
652
- user_rating: User rating (1-5 stars) if applicable
653
- metadata: Additional event-specific data
654
- trace_id: Trace ID for tracking event flows
655
-
656
- Returns:
657
- AIAgentExecutionEvent ready to be queued to EventBridge
658
- """
659
- from .ai_agent_event_factory import AIAgentEventFactory
660
-
661
- return AIAgentEventFactory.create_event(
662
- event_type=event_type,
663
- chat_id=chat_id,
664
- company_id=company_id,
665
- frozen_company_name=frozen_company_name,
666
- ai_agent_id=ai_agent_id,
667
- chain_of_thought_id=chain_of_thought_id,
668
- trigger=trigger,
669
- source=source,
670
- decision_type=decision_type,
671
- error_message=error_message,
672
- duration_ms=duration_ms,
673
- user_rating=user_rating,
674
- metadata=metadata,
675
- trace_id=trace_id
676
- )
640
+ return events
@@ -1,27 +1,8 @@
1
1
 
2
2
  from letschatty.models import StrObjectId
3
- from letschatty.models.ai_microservices.lambda_events import (
4
- DoubleCheckerForIncomingMessagesAnswerCallbackEvent,
5
- DoubleCheckerForIncomingMessagesAnswerEvent,
6
- FixBuggedAiAgentsCallsInChatsEvent,
7
- GetChainOfThoughtsByChatIdEvent,
8
- QualityTestEventData,
9
- QualityTestsForUpdatedAIComponentEvent,
10
- QualityTestsForUpdatedAIComponentEventData,
11
- CancelExecutionEvent,
12
- ManualTriggerEvent,
13
- AssignAIAgentToChatEvent,
14
- RemoveAIAgentFromChatEvent,
15
- SmartFollowUpDecisionOutputEvent,
16
- UpdateAIAgentModeInChatEvent,
17
- EscalateAIAgentInChatEvent,
18
- UnescalateAIAgentInChatEvent,
19
- GetChatWithPromptForIncomingMessageEvent,
20
- GetChatWithPromptForFollowUpEvent,
21
- UpdateAIAgentPrequalStatusInChatEvent,
22
- )
3
+ from letschatty.models.ai_microservices.lambda_events import DoubleCheckerForIncomingMessagesAnswerCallbackEvent, DoubleCheckerForIncomingMessagesAnswerEvent, FixBuggedAiAgentsCallsInChatsEvent, QualityTestEventData, QualityTestsForUpdatedAIComponentEvent, QualityTestsForUpdatedAIComponentEventData
23
4
  from letschatty.models.ai_microservices.lambda_invokation_types import InvokationType
24
- from letschatty.models.ai_microservices import AllQualityTestEvent, AllQualityTestEventData, QualityTestEvent, QualityTestInteractionCallbackEvent, SmartTaggingCallbackEvent, QualityTestCallbackEvent, LambdaAiEvent, SmartTaggingEvent, SmartTaggingPromptEvent
5
+ from letschatty.models.ai_microservices import AllQualityTestEvent, AllQualityTestEventData, FollowUpEvent, IncomingMessageEvent, QualityTestEvent, QualityTestInteractionCallbackEvent, SmartTaggingCallbackEvent, IncomingMessageCallbackEvent, QualityTestCallbackEvent, LambdaAiEvent, SmartTaggingEvent, SmartTaggingPromptEvent
25
6
  from letschatty.models.base_models.ai_agent_component import AiAgentComponent
26
7
  from letschatty.models.company.assets.ai_agents_v2.chat_example import ChatExample
27
8
  from letschatty.models.company.assets.ai_agents_v2.chat_example_test import ChatExampleTestCase
@@ -38,10 +19,16 @@ class LambdaEventFactory:
38
19
  return SmartTaggingEvent(**event_data)
39
20
  case InvokationType.SMART_TAGGING_CALLBACK:
40
21
  return SmartTaggingCallbackEvent(**event_data)
22
+ case InvokationType.INCOMING_MESSAGE:
23
+ return IncomingMessageEvent(**event_data)
41
24
  case InvokationType.SINGLE_QUALITY_TEST:
42
25
  return QualityTestEvent(**event_data)
43
26
  case InvokationType.ALL_QUALITY_TEST:
44
27
  return AllQualityTestEvent(**event_data)
28
+ case InvokationType.INCOMING_MESSAGE_CALLBACK:
29
+ return IncomingMessageCallbackEvent(**event_data)
30
+ case InvokationType.FOLLOW_UP:
31
+ return FollowUpEvent(**event_data)
45
32
  case InvokationType.SINGLE_QUALITY_TEST_CALLBACK:
46
33
  return QualityTestCallbackEvent(**event_data)
47
34
  case InvokationType.SMART_TAGGING_PROMPT:
@@ -54,31 +41,6 @@ class LambdaEventFactory:
54
41
  return DoubleCheckerForIncomingMessagesAnswerEvent(**event_data)
55
42
  case InvokationType.DOUBLE_CHECKER_FOR_INCOMING_MESSAGES_ANSWER_CALLBACK:
56
43
  return DoubleCheckerForIncomingMessagesAnswerCallbackEvent(**event_data)
57
- case InvokationType.MANUAL_TRIGGER:
58
- return ManualTriggerEvent(**event_data)
59
- case InvokationType.CANCEL_EXECUTION:
60
- return CancelExecutionEvent(**event_data)
61
- case InvokationType.ASSIGN_AI_AGENT_TO_CHAT:
62
- return AssignAIAgentToChatEvent(**event_data)
63
- case InvokationType.REMOVE_AI_AGENT_FROM_CHAT:
64
- return RemoveAIAgentFromChatEvent(**event_data)
65
- case InvokationType.UPDATE_AI_AGENT_MODE_IN_CHAT:
66
- return UpdateAIAgentModeInChatEvent(**event_data)
67
- case InvokationType.UPDATE_AI_AGENT_PREQUAL_STATUS_IN_CHAT:
68
- return UpdateAIAgentPrequalStatusInChatEvent(**event_data)
69
- case InvokationType.ESCALATE_AI_AGENT_IN_CHAT:
70
- return EscalateAIAgentInChatEvent(**event_data)
71
- case InvokationType.UNESCALATE_AI_AGENT_IN_CHAT:
72
- return UnescalateAIAgentInChatEvent(**event_data)
73
- case InvokationType.GET_CHAIN_OF_THOUGHTS_BY_CHAT_ID:
74
- return GetChainOfThoughtsByChatIdEvent(**event_data)
75
- case InvokationType.SMART_FOLLOW_UP_DECISION_OUTPUT:
76
- return SmartFollowUpDecisionOutputEvent(**event_data)
77
-
78
- case InvokationType.GET_CHAT_WITH_PROMPT_INCOMING_MESSAGE:
79
- return GetChatWithPromptForIncomingMessageEvent(**event_data)
80
- case InvokationType.GET_CHAT_WITH_PROMPT_FOLLOW_UP:
81
- return GetChatWithPromptForFollowUpEvent(**event_data)
82
44
  case _:
83
45
  raise ValueError(f"Invalid event type: {event_type}")
84
46
 
@@ -1,4 +1,3 @@
1
- from letschatty.models.messages.chatty_messages.button import ChattyContentButton
2
1
  from ...models.messages.chatty_messages.schema import ChattyContent, ChattyContentText, ChattyContentContacts, ChattyContentLocation, ChattyContentImage, ChattyContentVideo, ChattyContentAudio, ChattyContentDocument, ChattyContentSticker, ChattyContentReaction
3
2
  class MessageTextOrCaptionOrPreview:
4
3
  @staticmethod
@@ -6,9 +5,9 @@ class MessageTextOrCaptionOrPreview:
6
5
  if isinstance(message_content, ChattyContentText):
7
6
  return message_content.body
8
7
  elif isinstance(message_content, ChattyContentContacts):
9
- return f"👤 *Contacto recibido:* {message_content.contacts[0].full_name} \n📞 *Teléfono:* {message_content.contacts[0].phone_number}"
10
- elif isinstance(message_content, ChattyContentLocation):
11
- return f"📍 \nLatitud: {message_content.latitude} \nLongitud: {message_content.longitude}_"
8
+ return "👥 Mensaje de tipo contacto"
9
+ elif isinstance(message_content, ChattyContentLocation):
10
+ return "📍 Mensaje de tipo ubicación"
12
11
  elif isinstance(message_content, ChattyContentImage):
13
12
  return "🖼️ Mensaje de tipo imagen"
14
13
  elif isinstance(message_content, ChattyContentVideo):
@@ -23,7 +22,6 @@ class MessageTextOrCaptionOrPreview:
23
22
  return "😀 Mensaje de tipo sticker"
24
23
  elif isinstance(message_content, ChattyContentReaction):
25
24
  return "❤️ Mensaje de tipo reacción"
26
- elif isinstance(message_content, ChattyContentButton):
27
- return f"{message_content.payload}"
28
25
  else:
26
+
29
27
  return "Vista previa del mensaje"
@@ -10,6 +10,10 @@ if TYPE_CHECKING:
10
10
 
11
11
  class SourcesAndTopicsValidator:
12
12
  """This class provides methods to validate sources and topics."""
13
+ @staticmethod
14
+ def normalize_source_name(name: str) -> str:
15
+ return name.strip().lower()
16
+
13
17
  @staticmethod
14
18
  def source_validation_check(sources : Dict[str,Source], topics : Dict[str,Topic], source : Source, existing_source_id : str | None = None) -> None:
15
19
  """Checks validation of a source against other sources and topics."""
@@ -28,9 +32,16 @@ class SourcesAndTopicsValidator:
28
32
  """This method checks that no duplicated source is created.
29
33
  For Pure Ads, it checks that the ad_id is unique and doesn't exist already.
30
34
  For Other Sources, it checks that exact trigger doesn't exist already. """
35
+ normalized_name = SourcesAndTopicsValidator.normalize_source_name(source.name)
36
+ existing_source_name = None
37
+ if existing_source_id and existing_source_id in sources:
38
+ existing_source_name = SourcesAndTopicsValidator.normalize_source_name(sources[existing_source_id].name)
39
+ should_check_name = existing_source_name is None or normalized_name != existing_source_name
31
40
  for source_id, source_to_check in sources.items():
32
41
  if source_id == existing_source_id:
33
42
  continue
43
+ if should_check_name and SourcesAndTopicsValidator.normalize_source_name(source_to_check.name) == normalized_name:
44
+ raise ConflictedSource(f":warning: *Conflict while adding source: Name already exists* \n New source '{source.name}' \n- Id {source.id} \n Existing source '{source_to_check.name}' \n- Id {source_to_check.id}", conflicting_source_id=source_to_check.id)
34
45
  if source == source_to_check: #type: ignore
35
46
  if isinstance(source, OtherSource):
36
47
  raise ConflictedSource(f":warning: *Conflict while adding source: Trigger already exists* \n New source '{source.name}' \n- Id {source.id} \n- Trigger {source.trigger[:30]} \n Existing source '{source_to_check.name}' \n- Id {source_to_check.id} \n- Trigger '{source_to_check.trigger[:30]}'", conflicting_source_id=source_to_check.id)