letschatty 0.4.340__py3-none-any.whl → 0.4.342__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.
- letschatty/models/ai_microservices/__init__.py +3 -3
- letschatty/models/ai_microservices/expected_output.py +29 -2
- letschatty/models/ai_microservices/lambda_events.py +137 -28
- letschatty/models/ai_microservices/lambda_invokation_types.py +3 -1
- letschatty/models/ai_microservices/n8n_ai_agents_payload.py +3 -1
- letschatty/models/analytics/events/__init__.py +3 -2
- letschatty/models/analytics/events/chat_based_events/ai_agent_execution_event.py +71 -0
- letschatty/models/analytics/events/chat_based_events/chat_funnel.py +13 -69
- letschatty/models/analytics/events/company_based_events/asset_events.py +2 -9
- letschatty/models/analytics/events/event_type_to_classes.py +3 -6
- letschatty/models/analytics/events/event_types.py +50 -9
- letschatty/models/chat/chat.py +0 -2
- letschatty/models/chat/chat_with_assets.py +1 -6
- letschatty/models/chat/client.py +2 -0
- letschatty/models/chat/continuous_conversation.py +1 -1
- letschatty/models/company/CRM/funnel.py +33 -365
- letschatty/models/company/__init__.py +1 -2
- letschatty/models/company/assets/ai_agents_v2/ai_agents_decision_output.py +1 -1
- letschatty/models/company/assets/ai_agents_v2/chain_of_thought_in_chat.py +5 -3
- letschatty/models/company/assets/ai_agents_v2/chatty_ai_agent_in_chat.py +4 -0
- letschatty/models/company/assets/ai_agents_v2/chatty_ai_mode.py +2 -2
- letschatty/models/company/assets/ai_agents_v2/get_chat_with_prompt_response.py +1 -0
- letschatty/models/company/assets/ai_agents_v2/pre_qualify_config.py +14 -2
- letschatty/models/company/assets/automation.py +19 -10
- letschatty/models/company/assets/company_assets.py +0 -2
- letschatty/models/company/integrations/shopify/shopify_product_sync_status.py +42 -0
- letschatty/models/data_base/collection_interface.py +101 -29
- letschatty/models/data_base/mongo_connection.py +92 -9
- letschatty/models/messages/chatty_messages/schema/chatty_content/content_document.py +2 -4
- letschatty/models/messages/chatty_messages/schema/chatty_content/content_media.py +3 -4
- letschatty/models/utils/custom_exceptions/custom_exceptions.py +14 -1
- letschatty/services/ai_agents/smart_follow_up_context_builder_v2.py +5 -2
- letschatty/services/chat/chat_service.py +1 -27
- letschatty/services/chatty_assets/__init__.py +12 -0
- letschatty/services/chatty_assets/asset_service.py +190 -13
- letschatty/services/chatty_assets/assets_collections.py +137 -0
- letschatty/services/chatty_assets/base_container.py +3 -2
- letschatty/services/chatty_assets/base_container_with_collection.py +35 -26
- letschatty/services/chatty_assets/collections/__init__.py +38 -0
- letschatty/services/chatty_assets/collections/ai_agent_collection.py +19 -0
- letschatty/services/chatty_assets/collections/ai_agent_in_chat_collection.py +32 -0
- letschatty/services/chatty_assets/collections/ai_component_collection.py +21 -0
- letschatty/services/chatty_assets/collections/chain_of_thought_collection.py +30 -0
- letschatty/services/chatty_assets/collections/chat_collection.py +21 -0
- letschatty/services/chatty_assets/collections/contact_point_collection.py +21 -0
- letschatty/services/chatty_assets/collections/fast_answer_collection.py +21 -0
- letschatty/services/chatty_assets/collections/filter_criteria_collection.py +18 -0
- letschatty/services/chatty_assets/collections/flow_collection.py +20 -0
- letschatty/services/chatty_assets/collections/product_collection.py +20 -0
- letschatty/services/chatty_assets/collections/sale_collection.py +20 -0
- letschatty/services/chatty_assets/collections/source_collection.py +21 -0
- letschatty/services/chatty_assets/collections/tag_collection.py +19 -0
- letschatty/services/chatty_assets/collections/topic_collection.py +21 -0
- letschatty/services/chatty_assets/collections/user_collection.py +20 -0
- letschatty/services/chatty_assets/example_usage.py +44 -0
- letschatty/services/chatty_assets/services/__init__.py +37 -0
- letschatty/services/chatty_assets/services/ai_agent_in_chat_service.py +73 -0
- letschatty/services/chatty_assets/services/ai_agent_service.py +23 -0
- letschatty/services/chatty_assets/services/chain_of_thought_service.py +70 -0
- letschatty/services/chatty_assets/services/chat_service.py +25 -0
- letschatty/services/chatty_assets/services/contact_point_service.py +29 -0
- letschatty/services/chatty_assets/services/fast_answer_service.py +32 -0
- letschatty/services/chatty_assets/services/filter_criteria_service.py +30 -0
- letschatty/services/chatty_assets/services/flow_service.py +25 -0
- letschatty/services/chatty_assets/services/product_service.py +30 -0
- letschatty/services/chatty_assets/services/sale_service.py +25 -0
- letschatty/services/chatty_assets/services/source_service.py +28 -0
- letschatty/services/chatty_assets/services/tag_service.py +32 -0
- letschatty/services/chatty_assets/services/topic_service.py +31 -0
- letschatty/services/chatty_assets/services/user_service.py +32 -0
- letschatty/services/continuous_conversation_service/continuous_conversation_helper.py +11 -0
- letschatty/services/events/__init__.py +6 -0
- letschatty/services/events/events_manager.py +218 -1
- letschatty/services/factories/analytics/ai_agent_event_factory.py +161 -0
- letschatty/services/factories/analytics/events_factory.py +66 -6
- letschatty/services/factories/lambda_ai_orchestrartor/lambda_events_factory.py +25 -8
- letschatty/services/messages_helpers/get_caption_or_body_or_preview.py +6 -4
- {letschatty-0.4.340.dist-info → letschatty-0.4.342.dist-info}/METADATA +1 -1
- {letschatty-0.4.340.dist-info → letschatty-0.4.342.dist-info}/RECORD +81 -44
- {letschatty-0.4.340.dist-info → letschatty-0.4.342.dist-info}/LICENSE +0 -0
- {letschatty-0.4.340.dist-info → letschatty-0.4.342.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Product Service - Pre-configured AssetService for Products"""
|
|
2
|
+
|
|
3
|
+
from ..asset_service import AssetService, CacheConfig
|
|
4
|
+
from ..collections import ProductCollection
|
|
5
|
+
from ....models.company.assets.product import Product, ProductPreview
|
|
6
|
+
from ....models.data_base.mongo_connection import MongoConnection
|
|
7
|
+
from ....models.analytics.events import CompanyAssetType, EventType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProductService(AssetService[Product, ProductPreview]):
|
|
11
|
+
"""Pre-configured service for Product assets with sensible defaults"""
|
|
12
|
+
|
|
13
|
+
# Event configuration - enables automatic event handling in API
|
|
14
|
+
asset_type_enum = CompanyAssetType.PRODUCTS
|
|
15
|
+
event_type_created = EventType.PRODUCT_CREATED
|
|
16
|
+
event_type_updated = EventType.PRODUCT_UPDATED
|
|
17
|
+
event_type_deleted = EventType.PRODUCT_DELETED
|
|
18
|
+
|
|
19
|
+
def __init__(self,
|
|
20
|
+
connection: MongoConnection,
|
|
21
|
+
cache_config: CacheConfig = CacheConfig(
|
|
22
|
+
keep_items_always_in_memory=False,
|
|
23
|
+
keep_previews_always_in_memory=True
|
|
24
|
+
)):
|
|
25
|
+
collection = ProductCollection(connection)
|
|
26
|
+
super().__init__(
|
|
27
|
+
collection=collection,
|
|
28
|
+
cache_config=cache_config
|
|
29
|
+
)
|
|
30
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Sale Service - Pre-configured AssetService for Sales"""
|
|
2
|
+
|
|
3
|
+
from ..asset_service import AssetService, CacheConfig
|
|
4
|
+
from ..collections import SaleCollection
|
|
5
|
+
from ....models.company.assets.sale import Sale
|
|
6
|
+
from ....models.base_models import ChattyAssetPreview
|
|
7
|
+
from ....models.data_base.mongo_connection import MongoConnection
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SaleService(AssetService[Sale, ChattyAssetPreview]):
|
|
11
|
+
"""
|
|
12
|
+
Pre-configured service for Sale assets with sensible defaults.
|
|
13
|
+
|
|
14
|
+
Note: No event configuration - Sale events are managed by the sales editor, not AssetService.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self,
|
|
18
|
+
connection: MongoConnection,
|
|
19
|
+
cache_config: CacheConfig = CacheConfig.default()):
|
|
20
|
+
collection = SaleCollection(connection)
|
|
21
|
+
super().__init__(
|
|
22
|
+
collection=collection,
|
|
23
|
+
cache_config=cache_config
|
|
24
|
+
)
|
|
25
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Source Service - Pre-configured AssetService for Sources"""
|
|
2
|
+
|
|
3
|
+
from ..asset_service import AssetService, CacheConfig
|
|
4
|
+
from ..collections import SourceCollection
|
|
5
|
+
from ....models.analytics.sources import SourceBase
|
|
6
|
+
from ....models.base_models import ChattyAssetPreview
|
|
7
|
+
from ....models.data_base.mongo_connection import MongoConnection
|
|
8
|
+
from ....models.analytics.events import CompanyAssetType, EventType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SourceService(AssetService[SourceBase, ChattyAssetPreview]):
|
|
12
|
+
"""Pre-configured service for Source assets with sensible defaults"""
|
|
13
|
+
|
|
14
|
+
# Event configuration - enables automatic event handling in API
|
|
15
|
+
asset_type_enum = CompanyAssetType.SOURCES
|
|
16
|
+
event_type_created = EventType.SOURCE_CREATED
|
|
17
|
+
event_type_updated = EventType.SOURCE_UPDATED
|
|
18
|
+
event_type_deleted = EventType.SOURCE_DELETED
|
|
19
|
+
|
|
20
|
+
def __init__(self,
|
|
21
|
+
connection: MongoConnection,
|
|
22
|
+
cache_config: CacheConfig = CacheConfig(keep_items_always_in_memory=True)):
|
|
23
|
+
collection = SourceCollection(connection)
|
|
24
|
+
super().__init__(
|
|
25
|
+
collection=collection,
|
|
26
|
+
cache_config=cache_config
|
|
27
|
+
)
|
|
28
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Tag Service - Pre-configured AssetService for Tags"""
|
|
2
|
+
|
|
3
|
+
from ..asset_service import AssetService, CacheConfig
|
|
4
|
+
from ..collections import TagCollection
|
|
5
|
+
from ....models.company.assets.tag import Tag, TagPreview
|
|
6
|
+
from ....models.data_base.mongo_connection import MongoConnection
|
|
7
|
+
from ....models.analytics.events import CompanyAssetType, EventType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TagService(AssetService[Tag, TagPreview]):
|
|
11
|
+
"""Pre-configured service for Tag assets with sensible defaults"""
|
|
12
|
+
|
|
13
|
+
# Event configuration - enables automatic event handling in API
|
|
14
|
+
asset_type_enum = CompanyAssetType.TAGS
|
|
15
|
+
event_type_created = EventType.TAG_CREATED
|
|
16
|
+
event_type_updated = EventType.TAG_UPDATED
|
|
17
|
+
event_type_deleted = EventType.TAG_DELETED
|
|
18
|
+
|
|
19
|
+
def __init__(self,
|
|
20
|
+
connection: MongoConnection,
|
|
21
|
+
cache_config: CacheConfig = CacheConfig(
|
|
22
|
+
keep_items_always_in_memory=False,
|
|
23
|
+
keep_previews_always_in_memory=True
|
|
24
|
+
)):
|
|
25
|
+
collection = TagCollection(connection)
|
|
26
|
+
super().__init__(
|
|
27
|
+
collection=collection,
|
|
28
|
+
cache_config=cache_config
|
|
29
|
+
)
|
|
30
|
+
# Load all tags into memory by default
|
|
31
|
+
self.load_from_db(company_id=None)
|
|
32
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Topic Service - Pre-configured AssetService for Topics"""
|
|
2
|
+
|
|
3
|
+
from ..asset_service import AssetService, CacheConfig
|
|
4
|
+
from ..collections.topic_collection import TopicCollection
|
|
5
|
+
from ....models.analytics.smart_messages.topic import Topic
|
|
6
|
+
from ....models.base_models import ChattyAssetPreview
|
|
7
|
+
from ....models.data_base.mongo_connection import MongoConnection
|
|
8
|
+
from ....models.analytics.events import CompanyAssetType, EventType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TopicService(AssetService[Topic, ChattyAssetPreview]):
|
|
12
|
+
"""Pre-configured service for Topic assets with sensible defaults"""
|
|
13
|
+
|
|
14
|
+
# Event configuration - enables automatic event handling in API
|
|
15
|
+
asset_type_enum = CompanyAssetType.TOPICS
|
|
16
|
+
event_type_created = EventType.TOPIC_CREATED
|
|
17
|
+
event_type_updated = EventType.TOPIC_UPDATED
|
|
18
|
+
event_type_deleted = EventType.TOPIC_DELETED
|
|
19
|
+
|
|
20
|
+
def __init__(self,
|
|
21
|
+
connection: MongoConnection,
|
|
22
|
+
cache_config: CacheConfig = CacheConfig(
|
|
23
|
+
keep_items_always_in_memory=True,
|
|
24
|
+
keep_previews_always_in_memory=True
|
|
25
|
+
)):
|
|
26
|
+
collection = TopicCollection(connection)
|
|
27
|
+
super().__init__(
|
|
28
|
+
collection=collection,
|
|
29
|
+
cache_config=cache_config
|
|
30
|
+
)
|
|
31
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""User Service - Pre-configured AssetService for Users"""
|
|
2
|
+
|
|
3
|
+
from ..asset_service import AssetService, CacheConfig
|
|
4
|
+
from ..collections import UserCollection
|
|
5
|
+
from ....models.company.assets.users.user import User, UserPreview
|
|
6
|
+
from ....models.data_base.mongo_connection import MongoConnection
|
|
7
|
+
from ....models.analytics.events import CompanyAssetType, EventType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class UserService(AssetService[User, UserPreview]):
|
|
11
|
+
"""Pre-configured service for User assets with sensible defaults"""
|
|
12
|
+
|
|
13
|
+
# Event configuration - enables automatic event handling in API
|
|
14
|
+
asset_type_enum = CompanyAssetType.USERS
|
|
15
|
+
event_type_created = EventType.USER_CREATED
|
|
16
|
+
event_type_updated = EventType.USER_UPDATED
|
|
17
|
+
event_type_deleted = EventType.USER_DELETED
|
|
18
|
+
|
|
19
|
+
def __init__(self,
|
|
20
|
+
connection: MongoConnection,
|
|
21
|
+
cache_config: CacheConfig = CacheConfig(
|
|
22
|
+
keep_items_always_in_memory=False,
|
|
23
|
+
cache_expiration_time_previews=300,
|
|
24
|
+
keep_previews_always_in_memory=True,
|
|
25
|
+
keep_deleted_previews_in_memory=True
|
|
26
|
+
)):
|
|
27
|
+
collection = UserCollection(connection)
|
|
28
|
+
super().__init__(
|
|
29
|
+
collection=collection,
|
|
30
|
+
cache_config=cache_config
|
|
31
|
+
)
|
|
32
|
+
|
|
@@ -269,4 +269,15 @@ 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)
|
|
272
283
|
return cc
|
|
@@ -1,2 +1,219 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
|
+
Events Manager - Handles queuing and publishing events to EventBridge
|
|
2
3
|
|
|
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()
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI Agent Event Factory - Helper for creating AI agent execution events
|
|
3
|
+
|
|
4
|
+
This factory simplifies event creation by providing a consistent interface
|
|
5
|
+
for generating both full analytics events and simplified UI events.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from letschatty.models.analytics.events.chat_based_events.ai_agent_execution_event import (
|
|
9
|
+
AIAgentExecutionEvent,
|
|
10
|
+
AIAgentExecutionEventData
|
|
11
|
+
)
|
|
12
|
+
from letschatty.models.analytics.events.event_types import EventType
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from zoneinfo import ZoneInfo
|
|
15
|
+
from typing import Optional, Dict, Any
|
|
16
|
+
from letschatty.models.utils.types.identifier import StrObjectId
|
|
17
|
+
import logging
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AIAgentEventFactory:
|
|
23
|
+
"""
|
|
24
|
+
Factory for creating AI agent execution events with proper context.
|
|
25
|
+
|
|
26
|
+
Provides a simplified API for event creation while ensuring all required
|
|
27
|
+
fields are properly populated for analytics and monitoring.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def create_event(
|
|
32
|
+
event_type: EventType,
|
|
33
|
+
chat_id: StrObjectId,
|
|
34
|
+
company_id: StrObjectId,
|
|
35
|
+
frozen_company_name: str,
|
|
36
|
+
ai_agent_id: StrObjectId,
|
|
37
|
+
chain_of_thought_id: StrObjectId,
|
|
38
|
+
trigger: str,
|
|
39
|
+
source: str = "chatty.api",
|
|
40
|
+
decision_type: Optional[str] = None,
|
|
41
|
+
error_message: Optional[str] = None,
|
|
42
|
+
duration_ms: Optional[int] = None,
|
|
43
|
+
user_rating: Optional[int] = None,
|
|
44
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
45
|
+
trace_id: Optional[str] = None
|
|
46
|
+
) -> AIAgentExecutionEvent:
|
|
47
|
+
"""
|
|
48
|
+
Create a full AI agent execution event for EventBridge.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
event_type: The type of event (from EventType enum)
|
|
52
|
+
chat_id: ID of the chat where the event occurred
|
|
53
|
+
company_id: ID of the company
|
|
54
|
+
frozen_company_name: Company name snapshot for analytics
|
|
55
|
+
ai_agent_id: ID of the AI agent asset
|
|
56
|
+
chain_of_thought_id: ID of the chain of thought execution
|
|
57
|
+
trigger: What triggered the execution (USER_MESSAGE, FOLLOW_UP, etc.)
|
|
58
|
+
source: Event source (e.g., 'chatty.api', 'chatty.lambda')
|
|
59
|
+
decision_type: Type of decision if applicable (send, suggest, escalate, skip)
|
|
60
|
+
error_message: Error message if this is an error event
|
|
61
|
+
duration_ms: Duration of the operation in milliseconds
|
|
62
|
+
user_rating: User rating (1-5 stars) if applicable
|
|
63
|
+
metadata: Additional event-specific data
|
|
64
|
+
trace_id: Trace ID for tracking event flows across systems
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
AIAgentExecutionEvent ready to be queued to EventBridge
|
|
68
|
+
"""
|
|
69
|
+
return AIAgentExecutionEvent(
|
|
70
|
+
type=event_type,
|
|
71
|
+
time=datetime.now(ZoneInfo("UTC")),
|
|
72
|
+
source=source,
|
|
73
|
+
company_id=company_id,
|
|
74
|
+
frozen_company_name=frozen_company_name,
|
|
75
|
+
specversion="1.0",
|
|
76
|
+
trace_id=trace_id,
|
|
77
|
+
data=AIAgentExecutionEventData(
|
|
78
|
+
chat_id=chat_id,
|
|
79
|
+
ai_agent_id=ai_agent_id,
|
|
80
|
+
chain_of_thought_id=chain_of_thought_id,
|
|
81
|
+
trigger=trigger,
|
|
82
|
+
decision_type=decision_type,
|
|
83
|
+
error_message=error_message,
|
|
84
|
+
duration_ms=duration_ms,
|
|
85
|
+
user_rating=user_rating,
|
|
86
|
+
metadata=metadata
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def get_simplified_event_type(event_type: EventType) -> str:
|
|
92
|
+
"""
|
|
93
|
+
Convert full EventType to simplified event type string for UI.
|
|
94
|
+
|
|
95
|
+
Transforms 'chatty_ai_agent_in_chat.trigger.user_message' -> 'trigger.user_message'
|
|
96
|
+
This provides a cleaner format for embedded events in COT documents.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
event_type: Full EventType enum value
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Simplified event type string (e.g., 'trigger.user_message')
|
|
103
|
+
"""
|
|
104
|
+
# Extract the last two parts after 'chatty_ai_agent_in_chat.'
|
|
105
|
+
parts = event_type.value.split('.')
|
|
106
|
+
if len(parts) >= 3 and parts[0] == 'chatty_ai_agent_in_chat':
|
|
107
|
+
return '.'.join(parts[1:]) # e.g., 'trigger.user_message'
|
|
108
|
+
return event_type.value # Fallback to full type if format doesn't match
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def get_user_friendly_message(event_type: EventType, **context) -> str:
|
|
112
|
+
"""
|
|
113
|
+
Generate a user-friendly message for an event type.
|
|
114
|
+
|
|
115
|
+
This provides human-readable descriptions for events that will be
|
|
116
|
+
displayed in the UI as part of the chain of thought timeline.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
event_type: The type of event
|
|
120
|
+
**context: Additional context for message formatting (e.g., decision_type, error_message)
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
User-friendly message string
|
|
124
|
+
"""
|
|
125
|
+
messages = {
|
|
126
|
+
EventType.CHATTY_AI_AGENT_IN_CHAT_TRIGGER_USER_MESSAGE: "Triggered by user message",
|
|
127
|
+
EventType.CHATTY_AI_AGENT_IN_CHAT_TRIGGER_FOLLOW_UP: "Triggered by smart follow-up",
|
|
128
|
+
EventType.CHATTY_AI_AGENT_IN_CHAT_TRIGGER_MANUAL: "Manually triggered",
|
|
129
|
+
EventType.CHATTY_AI_AGENT_IN_CHAT_TRIGGER_RETRY: "Retry triggered",
|
|
130
|
+
|
|
131
|
+
EventType.CHATTY_AI_AGENT_IN_CHAT_STATE_PROCESSING_STARTED: "Processing started",
|
|
132
|
+
EventType.CHATTY_AI_AGENT_IN_CHAT_STATE_CALL_STARTED: "AI agent call started",
|
|
133
|
+
EventType.CHATTY_AI_AGENT_IN_CHAT_STATE_ESCALATED: "Escalated to human agent",
|
|
134
|
+
EventType.CHATTY_AI_AGENT_IN_CHAT_STATE_UNESCALATED: "Returned to AI agent",
|
|
135
|
+
|
|
136
|
+
EventType.CHATTY_AI_AGENT_IN_CHAT_CALL_GET_CHAT_WITH_PROMPT: "Requesting chat context",
|
|
137
|
+
EventType.CHATTY_AI_AGENT_IN_CHAT_CALL_TAGGER: "Calling tagger service",
|
|
138
|
+
EventType.CHATTY_AI_AGENT_IN_CHAT_CALL_DOUBLE_CHECKER: "Calling double checker",
|
|
139
|
+
EventType.CHATTY_AI_AGENT_IN_CHAT_CALL_DEBUGGER: "Running debugger",
|
|
140
|
+
|
|
141
|
+
EventType.CHATTY_AI_AGENT_IN_CHAT_CALLBACK_GET_CHAT_WITH_PROMPT: "Chat context received",
|
|
142
|
+
EventType.CHATTY_AI_AGENT_IN_CHAT_CALLBACK_TAGGER: "Tagger response received",
|
|
143
|
+
EventType.CHATTY_AI_AGENT_IN_CHAT_CALLBACK_DOUBLE_CHECKER: "Double checker validation complete",
|
|
144
|
+
EventType.CHATTY_AI_AGENT_IN_CHAT_CALLBACK_OUTPUT_RECEIVED: "AI agent output received",
|
|
145
|
+
|
|
146
|
+
EventType.CHATTY_AI_AGENT_IN_CHAT_DECISION_SEND: "Decision: Send message",
|
|
147
|
+
EventType.CHATTY_AI_AGENT_IN_CHAT_DECISION_SUGGEST: "Decision: Suggest message",
|
|
148
|
+
EventType.CHATTY_AI_AGENT_IN_CHAT_DECISION_ESCALATE: "Decision: Escalate to human",
|
|
149
|
+
EventType.CHATTY_AI_AGENT_IN_CHAT_DECISION_SKIP: "Decision: Skip message",
|
|
150
|
+
EventType.CHATTY_AI_AGENT_IN_CHAT_DECISION_SENT_TO_API: "Decision sent to API",
|
|
151
|
+
EventType.CHATTY_AI_AGENT_IN_CHAT_DECISION_COMPLETED: "Decision completed successfully",
|
|
152
|
+
|
|
153
|
+
EventType.CHATTY_AI_AGENT_IN_CHAT_ERROR_CALL_FAILED: f"Call failed: {context.get('error_message', 'Unknown error')}",
|
|
154
|
+
EventType.CHATTY_AI_AGENT_IN_CHAT_ERROR_CALL_CANCELLED: "Call cancelled",
|
|
155
|
+
EventType.CHATTY_AI_AGENT_IN_CHAT_ERROR_VALIDATION_FAILED: f"Validation failed: {context.get('error_message', 'Invalid data')}",
|
|
156
|
+
|
|
157
|
+
EventType.CHATTY_AI_AGENT_IN_CHAT_RATING_RECEIVED: f"User rated: {context.get('user_rating', '?')}/5 stars",
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return messages.get(event_type, str(event_type))
|
|
161
|
+
|