letschatty 0.4.349__py3-none-any.whl → 0.4.351__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.
- letschatty/models/ai_microservices/__init__.py +4 -4
- letschatty/models/ai_microservices/expected_output.py +29 -2
- letschatty/models/ai_microservices/lambda_events.py +155 -28
- letschatty/models/ai_microservices/lambda_invokation_types.py +4 -1
- letschatty/models/ai_microservices/n8n_ai_agents_payload.py +3 -1
- letschatty/models/analytics/events/__init__.py +3 -3
- 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 -7
- letschatty/models/analytics/events/event_types.py +50 -11
- letschatty/models/chat/chat.py +2 -13
- 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 -7
- letschatty/models/company/assets/ai_agents_v2/ai_agents_decision_output.py +1 -1
- 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 +28 -1
- letschatty/models/company/assets/automation.py +19 -10
- letschatty/models/company/assets/chat_assets.py +2 -3
- letschatty/models/company/assets/company_assets.py +0 -2
- letschatty/models/company/assets/sale.py +3 -3
- letschatty/models/company/empresa.py +1 -2
- 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 +11 -47
- 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 -30
- letschatty/services/factories/lambda_ai_orchestrartor/lambda_events_factory.py +46 -8
- letschatty/services/messages_helpers/get_caption_or_body_or_preview.py +6 -4
- letschatty/services/validators/analytics_validator.py +0 -11
- {letschatty-0.4.349.dist-info → letschatty-0.4.351.dist-info}/METADATA +1 -1
- {letschatty-0.4.349.dist-info → letschatty-0.4.351.dist-info}/RECORD +83 -53
- letschatty/models/analytics/events/chat_based_events/chat_client.py +0 -19
- letschatty/models/company/integrations/product_sync_status.py +0 -28
- letschatty/models/company/integrations/shopify/company_shopify_integration.py +0 -62
- letschatty/models/company/integrations/shopify/shopify_product_sync_status.py +0 -18
- letschatty/models/company/integrations/shopify/shopify_webhook_topics.py +0 -40
- letschatty/models/company/integrations/sync_status_enum.py +0 -9
- {letschatty-0.4.349.dist-info → letschatty-0.4.351.dist-info}/LICENSE +0 -0
- {letschatty-0.4.349.dist-info → letschatty-0.4.351.dist-info}/WHEEL +0 -0
|
@@ -5,8 +5,6 @@ class EventType(StrEnum):
|
|
|
5
5
|
CHAT_CREATED = "chat.created"
|
|
6
6
|
CHAT_STATUS_UPDATED = "chat.status_updated"
|
|
7
7
|
CHAT_DELETED = "chat.deleted"
|
|
8
|
-
##CHAT CLIENT EVENTS
|
|
9
|
-
CHAT_CLIENT_UPDATED = "chat.client.updated"
|
|
10
8
|
#TAGS
|
|
11
9
|
TAG_ASSIGNED = "chat.tag.assigned"
|
|
12
10
|
TAG_REMOVED = "chat.tag.removed"
|
|
@@ -14,6 +12,51 @@ class EventType(StrEnum):
|
|
|
14
12
|
AI_AGENT_ASSIGNED_TO_CHAT = "chat.chatty_ai_agent.assigned_to_chat"
|
|
15
13
|
AI_AGENT_REMOVED_FROM_CHAT = "chat.chatty_ai_agent.removed_from_chat"
|
|
16
14
|
AI_AGENT_UPDATED_ON_CHAT = "chat.chatty_ai_agent.updated_on_chat"
|
|
15
|
+
|
|
16
|
+
#CHATTY AI AGENT EXECUTION EVENTS - 3-level hierarchy for execution tracking
|
|
17
|
+
# Pattern: chatty_ai_agent_in_chat.{operation}.{detail}
|
|
18
|
+
# Note: Execution events are already chat-scoped via CustomerEventData
|
|
19
|
+
|
|
20
|
+
# TRIGGER EVENTS - What initiates AI agent processing
|
|
21
|
+
CHATTY_AI_AGENT_IN_CHAT_TRIGGER_USER_MESSAGE = "chatty_ai_agent_in_chat.trigger.user_message"
|
|
22
|
+
CHATTY_AI_AGENT_IN_CHAT_TRIGGER_FOLLOW_UP = "chatty_ai_agent_in_chat.trigger.follow_up"
|
|
23
|
+
CHATTY_AI_AGENT_IN_CHAT_TRIGGER_MANUAL = "chatty_ai_agent_in_chat.trigger.manual"
|
|
24
|
+
CHATTY_AI_AGENT_IN_CHAT_TRIGGER_RETRY = "chatty_ai_agent_in_chat.trigger.retry"
|
|
25
|
+
|
|
26
|
+
# STATE EVENTS - AI agent state changes
|
|
27
|
+
CHATTY_AI_AGENT_IN_CHAT_STATE_PROCESSING_STARTED = "chatty_ai_agent_in_chat.state.processing_started"
|
|
28
|
+
CHATTY_AI_AGENT_IN_CHAT_STATE_CALL_STARTED = "chatty_ai_agent_in_chat.state.call_started"
|
|
29
|
+
CHATTY_AI_AGENT_IN_CHAT_STATE_ESCALATED = "chatty_ai_agent_in_chat.state.escalated"
|
|
30
|
+
CHATTY_AI_AGENT_IN_CHAT_STATE_UNESCALATED = "chatty_ai_agent_in_chat.state.unescalated"
|
|
31
|
+
|
|
32
|
+
# CALL EVENTS - Outbound calls to services
|
|
33
|
+
CHATTY_AI_AGENT_IN_CHAT_CALL_GET_CHAT_WITH_PROMPT = "chatty_ai_agent_in_chat.call.get_chat_with_prompt"
|
|
34
|
+
CHATTY_AI_AGENT_IN_CHAT_CALL_TAGGER = "chatty_ai_agent_in_chat.call.tagger"
|
|
35
|
+
CHATTY_AI_AGENT_IN_CHAT_CALL_DOUBLE_CHECKER = "chatty_ai_agent_in_chat.call.double_checker"
|
|
36
|
+
CHATTY_AI_AGENT_IN_CHAT_CALL_DEBUGGER = "chatty_ai_agent_in_chat.call.debugger"
|
|
37
|
+
|
|
38
|
+
# CALLBACK EVENTS - Responses received from services
|
|
39
|
+
CHATTY_AI_AGENT_IN_CHAT_CALLBACK_GET_CHAT_WITH_PROMPT = "chatty_ai_agent_in_chat.callback.get_chat_with_prompt"
|
|
40
|
+
CHATTY_AI_AGENT_IN_CHAT_CALLBACK_TAGGER = "chatty_ai_agent_in_chat.callback.tagger"
|
|
41
|
+
CHATTY_AI_AGENT_IN_CHAT_CALLBACK_DOUBLE_CHECKER = "chatty_ai_agent_in_chat.callback.double_checker"
|
|
42
|
+
CHATTY_AI_AGENT_IN_CHAT_CALLBACK_OUTPUT_RECEIVED = "chatty_ai_agent_in_chat.callback.output_received"
|
|
43
|
+
|
|
44
|
+
# DECISION EVENTS - AI agent decisions and actions
|
|
45
|
+
CHATTY_AI_AGENT_IN_CHAT_DECISION_SEND = "chatty_ai_agent_in_chat.decision.send"
|
|
46
|
+
CHATTY_AI_AGENT_IN_CHAT_DECISION_SUGGEST = "chatty_ai_agent_in_chat.decision.suggest"
|
|
47
|
+
CHATTY_AI_AGENT_IN_CHAT_DECISION_ESCALATE = "chatty_ai_agent_in_chat.decision.escalate"
|
|
48
|
+
CHATTY_AI_AGENT_IN_CHAT_DECISION_SKIP = "chatty_ai_agent_in_chat.decision.skip"
|
|
49
|
+
CHATTY_AI_AGENT_IN_CHAT_DECISION_SENT_TO_API = "chatty_ai_agent_in_chat.decision.sent_to_api"
|
|
50
|
+
CHATTY_AI_AGENT_IN_CHAT_DECISION_COMPLETED = "chatty_ai_agent_in_chat.decision.completed"
|
|
51
|
+
|
|
52
|
+
# ERROR EVENTS - Failures and cancellations
|
|
53
|
+
CHATTY_AI_AGENT_IN_CHAT_ERROR_CALL_FAILED = "chatty_ai_agent_in_chat.error.call_failed"
|
|
54
|
+
CHATTY_AI_AGENT_IN_CHAT_ERROR_CALL_CANCELLED = "chatty_ai_agent_in_chat.error.call_cancelled"
|
|
55
|
+
CHATTY_AI_AGENT_IN_CHAT_ERROR_VALIDATION_FAILED = "chatty_ai_agent_in_chat.error.validation_failed"
|
|
56
|
+
|
|
57
|
+
# RATING EVENTS - User feedback
|
|
58
|
+
CHATTY_AI_AGENT_IN_CHAT_RATING_RECEIVED = "chatty_ai_agent_in_chat.rating.received"
|
|
59
|
+
|
|
17
60
|
#PRODUCTS
|
|
18
61
|
PRODUCT_ASSIGNED = "chat.product.assigned"
|
|
19
62
|
PRODUCT_REMOVED = "chat.product.removed"
|
|
@@ -44,9 +87,10 @@ class EventType(StrEnum):
|
|
|
44
87
|
#CONTINUOUS CONVERSATION
|
|
45
88
|
CONTINUOUS_CONVERSATION_CREATED = "chat.continuous_conversation.created"
|
|
46
89
|
CONTINUOUS_CONVERSATION_UPDATED = "chat.continuous_conversation.updated"
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
90
|
+
#FUNNEL STAGES
|
|
91
|
+
# Funnel-level events
|
|
92
|
+
CHAT_FUNNEL_STARTED = "chat.funnel.started" # New
|
|
93
|
+
CHAT_FUNNEL_UPDATED = "chat.funnel.updated"
|
|
50
94
|
CHAT_FUNNEL_COMPLETED = "chat.funnel.completed"
|
|
51
95
|
CHAT_FUNNEL_ABANDONED = "chat.funnel.abandoned"
|
|
52
96
|
|
|
@@ -85,18 +129,13 @@ class EventType(StrEnum):
|
|
|
85
129
|
FAST_ANSWER_CREATED = "company.fast_answer.created"
|
|
86
130
|
FAST_ANSWER_UPDATED = "company.fast_answer.updated"
|
|
87
131
|
FAST_ANSWER_DELETED = "company.fast_answer.deleted"
|
|
88
|
-
#
|
|
132
|
+
#FUNNEL STAGES
|
|
89
133
|
FUNNEL_CREATED = "company.funnel.created"
|
|
90
134
|
FUNNEL_UPDATED = "company.funnel.updated"
|
|
91
135
|
FUNNEL_DELETED = "company.funnel.deleted"
|
|
92
|
-
#FUNNEL STAGES
|
|
93
136
|
FUNNEL_STAGE_CREATED = "company.funnel_stage.created"
|
|
94
137
|
FUNNEL_STAGE_UPDATED = "company.funnel_stage.updated"
|
|
95
138
|
FUNNEL_STAGE_DELETED = "company.funnel_stage.deleted"
|
|
96
|
-
#FUNNEL MEMBERS
|
|
97
|
-
FUNNEL_MEMBER_ADDED = "company.funnel_member.added"
|
|
98
|
-
FUNNEL_MEMBER_UPDATED = "company.funnel_member.updated"
|
|
99
|
-
FUNNEL_MEMBER_REMOVED = "company.funnel_member.removed"
|
|
100
139
|
#BUSINESS AREA
|
|
101
140
|
BUSINESS_AREA_CREATED = "company.business_area.created"
|
|
102
141
|
BUSINESS_AREA_UPDATED = "company.business_area.updated"
|
letschatty/models/chat/chat.py
CHANGED
|
@@ -19,7 +19,6 @@ from ..utils.custom_exceptions.custom_exceptions import MissingAIAgentForSmartFo
|
|
|
19
19
|
from .time_left import TimeLeft
|
|
20
20
|
from ..company.conversation_topic import TopicTimelineEntry
|
|
21
21
|
from ..company.form_field import CollectedData
|
|
22
|
-
from ..company.CRM.funnel import ActiveFunnel
|
|
23
22
|
import json
|
|
24
23
|
import logging
|
|
25
24
|
logger = logging.getLogger(__name__)
|
|
@@ -48,7 +47,6 @@ class Chat(CompanyAssetModel):
|
|
|
48
47
|
chatty_ai_agent: Optional[ChattyAIAgentAssignedToChat] = Field(default=None, description="The id of the chatty ai agent that might or might not be assigned to the chat")
|
|
49
48
|
suggested_messages: List[MessageDraft] = Field(default_factory=list)
|
|
50
49
|
topics_timeline: List[TopicTimelineEntry] = Field(default_factory=list, description="Timeline of conversation topics throughout the conversation")
|
|
51
|
-
active_funnel: Optional[ActiveFunnel] = Field(default=None, description="Current active funnel for this chat")
|
|
52
50
|
model_config = ConfigDict(extra="ignore")
|
|
53
51
|
|
|
54
52
|
@property
|
|
@@ -269,17 +267,7 @@ class Chat(CompanyAssetModel):
|
|
|
269
267
|
@property
|
|
270
268
|
def bought_product_ids(self) -> List[StrObjectId]:
|
|
271
269
|
"""Get all sale ids in the chat"""
|
|
272
|
-
|
|
273
|
-
for sale in self.sales:
|
|
274
|
-
sale_product_ids = getattr(sale, "product_ids", None)
|
|
275
|
-
if sale_product_ids:
|
|
276
|
-
for product_id in sale_product_ids:
|
|
277
|
-
if product_id not in product_ids:
|
|
278
|
-
product_ids.append(product_id)
|
|
279
|
-
elif sale.product_id:
|
|
280
|
-
if sale.product_id not in product_ids:
|
|
281
|
-
product_ids.append(sale.product_id)
|
|
282
|
-
return product_ids
|
|
270
|
+
return [sale.product_id for sale in self.sales]
|
|
283
271
|
|
|
284
272
|
@property
|
|
285
273
|
def products(self) -> List[AssignedAssetToChat]:
|
|
@@ -376,3 +364,4 @@ class Chat(CompanyAssetModel):
|
|
|
376
364
|
dump["last_message"] = self.last_message.model_dump_json(serializer=SerializerType.DATABASE) if self.last_message else None
|
|
377
365
|
dump["flow_states"] = [flow_state.model_dump_json(serializer=SerializerType.DATABASE) for flow_state in self.flow_states]
|
|
378
366
|
return dump
|
|
367
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
|
|
1
2
|
from .chat import Chat
|
|
2
3
|
from letschatty.models.company.assets import TagPreview, FlowPreview, ContactPoint, Sale, ChattyAIAgentPreview, ProductPreview
|
|
3
|
-
from letschatty.models.company.CRM.funnel import ActiveFunnel
|
|
4
4
|
|
|
5
5
|
from letschatty.models.utils.types.serializer_type import SerializerType
|
|
6
6
|
from pydantic import BaseModel
|
|
@@ -15,8 +15,3 @@ class ChatWithAssets(BaseModel):
|
|
|
15
15
|
contact_points: List[ContactPoint]
|
|
16
16
|
flows_links_states: List[FlowPreview]
|
|
17
17
|
chatty_ai_agent: Optional[ChattyAIAgentPreview]
|
|
18
|
-
|
|
19
|
-
@property
|
|
20
|
-
def active_funnel(self) -> Optional[ActiveFunnel]:
|
|
21
|
-
"""Convenience property to access the chat's active funnel"""
|
|
22
|
-
return self.chat.active_funnel
|
letschatty/models/chat/client.py
CHANGED
|
@@ -5,6 +5,7 @@ from ..utils.types.identifier import StrObjectId
|
|
|
5
5
|
from .quality_scoring import QualityScore
|
|
6
6
|
from .highlight import Highlight
|
|
7
7
|
from ..company.assets.chat_assets import AssignedAssetToChat, SaleAssignedToChat, ContactPointAssignedToChat
|
|
8
|
+
from ..company.CRM.funnel import ClientFunnel
|
|
8
9
|
from ..utils.types.serializer_type import SerializerType
|
|
9
10
|
|
|
10
11
|
class Client(CompanyAssetModel):
|
|
@@ -22,6 +23,7 @@ class Client(CompanyAssetModel):
|
|
|
22
23
|
contact_points: List[ContactPointAssignedToChat] = Field(default=list())
|
|
23
24
|
business_area: Optional[StrObjectId] = Field(default=None, description="It's a business related area, that works as a queue for the chats")
|
|
24
25
|
external_id: Optional[str] = Field(default=None)
|
|
26
|
+
funnels : List[ClientFunnel] = Field(default=list())
|
|
25
27
|
exclude_fields = {
|
|
26
28
|
SerializerType.FRONTEND: {"products", "tags", "sales", "contact_points", "highlights"}
|
|
27
29
|
}
|
|
@@ -30,7 +30,7 @@ class ContinuousConversation(ChattyAssetModel):
|
|
|
30
30
|
template_message_waid: Optional[str] = None
|
|
31
31
|
status: Optional[ContinuousConversationStatus] = Field(default=ContinuousConversationStatus.CREATED)
|
|
32
32
|
active: bool = Field(default=True)
|
|
33
|
-
expires_at: datetime = Field(
|
|
33
|
+
expires_at: datetime = Field(default=datetime.now(ZoneInfo("UTC")) + timedelta(days=10))
|
|
34
34
|
messages: List[MessageDraft]
|
|
35
35
|
creator_id: StrObjectId
|
|
36
36
|
forced_send: bool = Field(default=False)
|
|
@@ -1,401 +1,69 @@
|
|
|
1
|
-
from ...base_models import CompanyAssetModel
|
|
2
|
-
from typing import List
|
|
1
|
+
from ...base_models import CompanyAssetModel
|
|
2
|
+
from typing import List
|
|
3
3
|
from ...utils.types.identifier import StrObjectId
|
|
4
|
-
from pydantic import BaseModel, Field
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
5
|
from enum import StrEnum
|
|
6
6
|
from datetime import datetime
|
|
7
|
+
from typing import Optional
|
|
7
8
|
from zoneinfo import ZoneInfo
|
|
8
9
|
from ...utils.types.executor_types import ExecutorType
|
|
9
|
-
from ..assets.automation import Automation
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
# ============================================================================
|
|
13
|
-
# Enums
|
|
14
|
-
# ============================================================================
|
|
15
10
|
|
|
16
11
|
class FunnelStatus(StrEnum):
|
|
17
|
-
"""Status of a chat within a funnel"""
|
|
18
12
|
IN_PROGRESS = "in_progress"
|
|
19
13
|
COMPLETED = "completed"
|
|
20
14
|
ABANDONED = "abandoned"
|
|
21
15
|
|
|
22
|
-
|
|
23
|
-
class FunnelMemberRole(StrEnum):
|
|
24
|
-
"""Role of a user within a funnel"""
|
|
25
|
-
ADMIN = "admin" # Full control over funnel settings, stages, and members
|
|
26
|
-
EDITOR = "editor" # Can move chats between stages, view all chats
|
|
27
|
-
VIEWER = "viewer" # Read-only access to funnel and chats
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
# ============================================================================
|
|
31
|
-
# Embedded Models (BaseModel)
|
|
32
|
-
# ============================================================================
|
|
33
|
-
|
|
34
16
|
class StageTransition(BaseModel):
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
to_stage_id: StrObjectId = Field(description="The stage the chat moved to")
|
|
44
|
-
transitioned_at: datetime = Field(default_factory=lambda: datetime.now(ZoneInfo("UTC")))
|
|
45
|
-
executor_type: ExecutorType = Field(description="Type of executor that triggered the transition")
|
|
46
|
-
executor_id: StrObjectId = Field(description="ID of the executor that triggered the transition")
|
|
47
|
-
time_in_previous_stage_seconds: Optional[int] = Field(
|
|
48
|
-
default=None,
|
|
49
|
-
description="Time spent in the previous stage before this transition"
|
|
50
|
-
)
|
|
51
|
-
from_stage_order: Optional[int] = Field(
|
|
52
|
-
default=None,
|
|
53
|
-
description="Order of the from_stage at transition time (for detecting regressions)"
|
|
54
|
-
)
|
|
55
|
-
to_stage_order: Optional[int] = Field(
|
|
56
|
-
default=None,
|
|
57
|
-
description="Order of the to_stage at transition time"
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
@property
|
|
61
|
-
def is_regression(self) -> bool:
|
|
62
|
-
"""Check if this transition moved backwards in the funnel"""
|
|
63
|
-
if self.from_stage_order is None or self.to_stage_order is None:
|
|
64
|
-
return False
|
|
65
|
-
return self.to_stage_order < self.from_stage_order
|
|
66
|
-
|
|
67
|
-
@property
|
|
68
|
-
def is_entry(self) -> bool:
|
|
69
|
-
"""Check if this is an entry transition (no previous stage)"""
|
|
70
|
-
return self.from_stage_id is None
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
class ActiveFunnel(BaseModel):
|
|
74
|
-
"""
|
|
75
|
-
Lightweight funnel state embedded in Chat for fast filtering and display.
|
|
76
|
-
|
|
77
|
-
The full history (stage_transitions, time metrics) is stored in the
|
|
78
|
-
separate chat_funnels collection via ChatFunnel.
|
|
79
|
-
"""
|
|
80
|
-
chat_funnel_id: StrObjectId = Field(
|
|
81
|
-
description="Reference to the full ChatFunnel document in chat_funnels collection"
|
|
82
|
-
)
|
|
83
|
-
funnel_id: StrObjectId = Field(description="The funnel the chat is in")
|
|
84
|
-
funnel_name: str = Field(description="Denormalized funnel name for display")
|
|
85
|
-
current_stage_id: StrObjectId = Field(description="Current stage ID")
|
|
86
|
-
current_stage_name: str = Field(description="Denormalized stage name for display")
|
|
87
|
-
entered_current_stage_at: datetime = Field(
|
|
88
|
-
default_factory=lambda: datetime.now(ZoneInfo("UTC")),
|
|
89
|
-
description="When the chat entered the current stage"
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
@property
|
|
93
|
-
def time_in_current_stage_seconds(self) -> int:
|
|
94
|
-
"""Calculate time spent in the current stage"""
|
|
95
|
-
return int((datetime.now(tz=ZoneInfo("UTC")) - self.entered_current_stage_at).total_seconds())
|
|
96
|
-
|
|
97
|
-
def update_stage(self, stage_id: StrObjectId, stage_name: str) -> None:
|
|
98
|
-
"""Update the current stage (called when transitioning)"""
|
|
99
|
-
self.current_stage_id = stage_id
|
|
100
|
-
self.current_stage_name = stage_name
|
|
101
|
-
self.entered_current_stage_at = datetime.now(tz=ZoneInfo("UTC"))
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
# ============================================================================
|
|
105
|
-
# Preview Classes - For efficient listing
|
|
106
|
-
# ============================================================================
|
|
107
|
-
|
|
108
|
-
class FunnelPreview(ChattyAssetPreview):
|
|
109
|
-
"""Preview of a Funnel for listing without full data"""
|
|
110
|
-
is_active: bool = Field(default=True)
|
|
111
|
-
|
|
112
|
-
@classmethod
|
|
113
|
-
def get_projection(cls) -> dict[str, Any]:
|
|
114
|
-
base = super().get_projection()
|
|
115
|
-
base["is_active"] = 1
|
|
116
|
-
return base
|
|
117
|
-
|
|
118
|
-
@classmethod
|
|
119
|
-
def from_asset(cls, asset: 'Funnel') -> 'FunnelPreview':
|
|
120
|
-
return cls(
|
|
121
|
-
_id=asset.id,
|
|
122
|
-
name=asset.name,
|
|
123
|
-
company_id=asset.company_id,
|
|
124
|
-
created_at=asset.created_at,
|
|
125
|
-
deleted_at=asset.deleted_at,
|
|
126
|
-
is_active=asset.is_active
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
class FunnelStagePreview(ChattyAssetPreview):
|
|
131
|
-
"""Preview of a FunnelStage for listing"""
|
|
17
|
+
from_stage_id: Optional[StrObjectId] = None
|
|
18
|
+
to_stage_id: StrObjectId
|
|
19
|
+
transitioned_at: datetime = Field(default=datetime.now(ZoneInfo("UTC")))
|
|
20
|
+
executor_type: ExecutorType
|
|
21
|
+
executor_id: StrObjectId
|
|
22
|
+
time_in_previous_stage_seconds: Optional[int] = None
|
|
23
|
+
|
|
24
|
+
class ClientFunnel(CompanyAssetModel):
|
|
132
25
|
funnel_id: StrObjectId
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
base.update({
|
|
141
|
-
"funnel_id": 1,
|
|
142
|
-
"color": 1,
|
|
143
|
-
"order": 1,
|
|
144
|
-
"is_exit_stage": 1
|
|
145
|
-
})
|
|
146
|
-
return base
|
|
147
|
-
|
|
148
|
-
@classmethod
|
|
149
|
-
def from_asset(cls, asset: 'FunnelStage') -> 'FunnelStagePreview':
|
|
150
|
-
return cls(
|
|
151
|
-
_id=asset.id,
|
|
152
|
-
name=asset.name,
|
|
153
|
-
company_id=asset.company_id,
|
|
154
|
-
created_at=asset.created_at,
|
|
155
|
-
deleted_at=asset.deleted_at,
|
|
156
|
-
funnel_id=asset.funnel_id,
|
|
157
|
-
color=asset.color,
|
|
158
|
-
order=asset.order,
|
|
159
|
-
is_exit_stage=asset.is_exit_stage
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
# ============================================================================
|
|
164
|
-
# Company Assets (CompanyAssetModel) - Stored in separate collections
|
|
165
|
-
# ============================================================================
|
|
166
|
-
|
|
167
|
-
class Funnel(CompanyAssetModel):
|
|
168
|
-
"""
|
|
169
|
-
A funnel represents a pipeline/process for managing chats (e.g., sales funnel, support pipeline).
|
|
170
|
-
Companies can create multiple funnels to organize their chat workflows.
|
|
171
|
-
"""
|
|
172
|
-
name: str = Field(description="Name of the funnel")
|
|
173
|
-
description: Optional[str] = Field(default=None, description="Description of the funnel's purpose")
|
|
174
|
-
created_by: StrObjectId = Field(description="User ID who created the funnel")
|
|
175
|
-
is_active: bool = Field(default=True, description="Whether the funnel is active or archived")
|
|
176
|
-
|
|
177
|
-
preview_class: ClassVar[type[FunnelPreview]] = FunnelPreview
|
|
178
|
-
|
|
179
|
-
model_config = ConfigDict(
|
|
180
|
-
validate_by_name=True,
|
|
181
|
-
validate_by_alias=True
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
class FunnelStage(CompanyAssetModel):
|
|
186
|
-
"""
|
|
187
|
-
A stage within a funnel. Stages are ordered and can have automations
|
|
188
|
-
that execute when a chat enters the stage.
|
|
189
|
-
"""
|
|
190
|
-
funnel_id: StrObjectId = Field(frozen=True, description="The funnel this stage belongs to")
|
|
191
|
-
name: str = Field(description="Name of the stage")
|
|
192
|
-
description: Optional[str] = Field(default=None, description="Description of the stage")
|
|
193
|
-
color: str = Field(default="#808080", description="Hex color for the stage (e.g., '#FFAA00')")
|
|
194
|
-
order: int = Field(ge=0, description="Position of the stage in the funnel (0-indexed)")
|
|
195
|
-
inflexion_conversion_point: bool = Field(
|
|
196
|
-
default=False,
|
|
197
|
-
description="If true, prioritized for chat assignment and sends notifications in automatic mode"
|
|
198
|
-
)
|
|
199
|
-
is_exit_stage: bool = Field(
|
|
200
|
-
default=False,
|
|
201
|
-
description="If true, moving to this stage completes the funnel for the chat"
|
|
202
|
-
)
|
|
203
|
-
automations: Automation = Field(
|
|
204
|
-
default_factory=Automation,
|
|
205
|
-
description="Automations to execute when a chat enters this stage"
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
preview_class: ClassVar[type[FunnelStagePreview]] = FunnelStagePreview
|
|
209
|
-
|
|
210
|
-
model_config = ConfigDict(
|
|
211
|
-
validate_by_name=True,
|
|
212
|
-
validate_by_alias=True
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
class FunnelMember(CompanyAssetModel):
|
|
217
|
-
"""
|
|
218
|
-
A user's membership in a funnel with their assigned role.
|
|
219
|
-
Determines what actions the user can perform within the funnel.
|
|
220
|
-
"""
|
|
221
|
-
name: str = Field(default="Funnel Member", description="Name for asset compatibility")
|
|
222
|
-
funnel_id: StrObjectId = Field(frozen=True, description="The funnel this membership belongs to")
|
|
223
|
-
user_id: StrObjectId = Field(frozen=True, description="The user who is a member")
|
|
224
|
-
role: FunnelMemberRole = Field(description="The user's role within the funnel")
|
|
225
|
-
|
|
226
|
-
model_config = ConfigDict(
|
|
227
|
-
validate_by_name=True,
|
|
228
|
-
validate_by_alias=True
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
@property
|
|
232
|
-
def can_edit(self) -> bool:
|
|
233
|
-
"""Check if the member can edit (move chats, etc.)"""
|
|
234
|
-
return self.role in (FunnelMemberRole.ADMIN, FunnelMemberRole.EDITOR)
|
|
235
|
-
|
|
236
|
-
@property
|
|
237
|
-
def can_admin(self) -> bool:
|
|
238
|
-
"""Check if the member can administer the funnel"""
|
|
239
|
-
return self.role == FunnelMemberRole.ADMIN
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
# ============================================================================
|
|
243
|
-
# ChatFunnel - Stored in separate chat_funnels collection
|
|
244
|
-
# This is a ChattyAssetModel (not CompanyAssetModel) because it's a chat history
|
|
245
|
-
# record rather than a company-level asset.
|
|
246
|
-
# ============================================================================
|
|
247
|
-
|
|
248
|
-
class ChatFunnel(ChattyAssetModel):
|
|
249
|
-
"""
|
|
250
|
-
Full record of a chat's journey through a funnel.
|
|
251
|
-
Stored in a separate 'chat_funnels' collection, NOT embedded in chat.
|
|
252
|
-
|
|
253
|
-
This extends ChattyAssetModel (not CompanyAssetModel) because it's a
|
|
254
|
-
chat-level record tracking funnel history, not a company-owned asset.
|
|
255
|
-
|
|
256
|
-
A new ChatFunnel is created each time a chat enters a funnel.
|
|
257
|
-
If a chat completes a funnel and enters it again, a NEW ChatFunnel is created.
|
|
258
|
-
|
|
259
|
-
The Chat document stores a lightweight ActiveFunnel for fast
|
|
260
|
-
filtering and display, with a reference to this full record.
|
|
261
|
-
"""
|
|
262
|
-
company_id: StrObjectId = Field(frozen=True, description="Company for multi-tenant isolation")
|
|
263
|
-
chat_id: StrObjectId = Field(frozen=True, description="The chat this funnel record belongs to")
|
|
264
|
-
funnel_id: StrObjectId = Field(frozen=True, description="The funnel the chat entered")
|
|
265
|
-
status: FunnelStatus = Field(default=FunnelStatus.IN_PROGRESS)
|
|
266
|
-
started_at: datetime = Field(default_factory=lambda: datetime.now(ZoneInfo("UTC")))
|
|
267
|
-
completed_at: Optional[datetime] = Field(default=None)
|
|
268
|
-
abandoned_at: Optional[datetime] = Field(default=None)
|
|
269
|
-
current_stage_id: Optional[StrObjectId] = Field(
|
|
270
|
-
default=None,
|
|
271
|
-
description="Current stage ID (None if funnel is completed/abandoned)"
|
|
272
|
-
)
|
|
273
|
-
entered_current_stage_at: Optional[datetime] = Field(
|
|
274
|
-
default=None,
|
|
275
|
-
description="When the chat entered the current stage"
|
|
276
|
-
)
|
|
277
|
-
stage_transitions: List[StageTransition] = Field(
|
|
278
|
-
default_factory=list,
|
|
279
|
-
description="Complete history of all stage transitions"
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
model_config = ConfigDict(
|
|
283
|
-
validate_by_name=True,
|
|
284
|
-
validate_by_alias=True
|
|
285
|
-
)
|
|
26
|
+
status: FunnelStatus
|
|
27
|
+
started_at: datetime = Field(default=datetime.now(ZoneInfo("UTC")))
|
|
28
|
+
completed_at: Optional[datetime] = None
|
|
29
|
+
abandoned_at: Optional[datetime] = None
|
|
30
|
+
current_stage_id: Optional[StrObjectId] = None
|
|
31
|
+
entered_current_stage_at: datetime
|
|
32
|
+
stage_transitions: List[StageTransition] = Field(default_factory=list)
|
|
286
33
|
|
|
287
34
|
@property
|
|
288
35
|
def is_completed(self) -> bool:
|
|
289
|
-
|
|
290
|
-
return self.status == FunnelStatus.COMPLETED and self.completed_at is not None
|
|
36
|
+
return self.completed_at is not None
|
|
291
37
|
|
|
292
38
|
@property
|
|
293
39
|
def is_abandoned(self) -> bool:
|
|
294
|
-
|
|
295
|
-
return self.status == FunnelStatus.ABANDONED and self.abandoned_at is not None
|
|
296
|
-
|
|
297
|
-
@property
|
|
298
|
-
def is_active(self) -> bool:
|
|
299
|
-
"""Check if the chat is still active in the funnel"""
|
|
300
|
-
return self.status == FunnelStatus.IN_PROGRESS
|
|
40
|
+
return self.abandoned_at is not None
|
|
301
41
|
|
|
302
42
|
@property
|
|
303
43
|
def time_in_funnel_seconds(self) -> int:
|
|
304
|
-
"""Calculate total time spent in the funnel"""
|
|
305
44
|
end_time = self.completed_at or self.abandoned_at or datetime.now(tz=ZoneInfo("UTC"))
|
|
306
45
|
return int((end_time - self.started_at).total_seconds())
|
|
307
46
|
|
|
308
47
|
@property
|
|
309
48
|
def time_in_current_stage_seconds(self) -> Optional[int]:
|
|
310
|
-
|
|
311
|
-
if not self.entered_current_stage_at or not self.current_stage_id:
|
|
49
|
+
if not self.entered_current_stage_at:
|
|
312
50
|
return None
|
|
313
51
|
return int((datetime.now(tz=ZoneInfo("UTC")) - self.entered_current_stage_at).total_seconds())
|
|
314
52
|
|
|
315
53
|
@property
|
|
316
54
|
def unique_stages_visited(self) -> int:
|
|
317
|
-
"""Count of unique stages the chat has visited"""
|
|
318
55
|
return len(set(t.to_stage_id for t in self.stage_transitions))
|
|
319
56
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
def regression_count(self) -> int:
|
|
327
|
-
"""Count of times the chat moved backwards in the funnel"""
|
|
328
|
-
return sum(1 for t in self.stage_transitions if t.is_regression)
|
|
329
|
-
|
|
330
|
-
@property
|
|
331
|
-
def last_transition(self) -> Optional[StageTransition]:
|
|
332
|
-
"""Get the most recent stage transition"""
|
|
333
|
-
return self.stage_transitions[-1] if self.stage_transitions else None
|
|
334
|
-
|
|
335
|
-
def record_transition(
|
|
336
|
-
self,
|
|
337
|
-
to_stage_id: StrObjectId,
|
|
338
|
-
executor_type: ExecutorType,
|
|
339
|
-
executor_id: StrObjectId,
|
|
340
|
-
from_stage_order: Optional[int] = None,
|
|
341
|
-
to_stage_order: Optional[int] = None
|
|
342
|
-
) -> StageTransition:
|
|
343
|
-
"""
|
|
344
|
-
Record a transition to a new stage.
|
|
345
|
-
Returns the created StageTransition.
|
|
346
|
-
|
|
347
|
-
Args:
|
|
348
|
-
to_stage_id: The stage to transition to
|
|
349
|
-
executor_type: Who/what triggered the transition
|
|
350
|
-
executor_id: ID of the executor
|
|
351
|
-
from_stage_order: Order of current stage (for regression detection)
|
|
352
|
-
to_stage_order: Order of target stage (for regression detection)
|
|
353
|
-
"""
|
|
354
|
-
time_in_previous = self.time_in_current_stage_seconds
|
|
355
|
-
|
|
356
|
-
transition = StageTransition(
|
|
357
|
-
from_stage_id=self.current_stage_id,
|
|
358
|
-
to_stage_id=to_stage_id,
|
|
359
|
-
executor_type=executor_type,
|
|
360
|
-
executor_id=executor_id,
|
|
361
|
-
time_in_previous_stage_seconds=time_in_previous,
|
|
362
|
-
from_stage_order=from_stage_order,
|
|
363
|
-
to_stage_order=to_stage_order
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
self.stage_transitions.append(transition)
|
|
367
|
-
self.current_stage_id = to_stage_id
|
|
368
|
-
self.entered_current_stage_at = transition.transitioned_at
|
|
369
|
-
|
|
370
|
-
return transition
|
|
371
|
-
|
|
372
|
-
def complete(self) -> None:
|
|
373
|
-
"""Mark the funnel as completed"""
|
|
374
|
-
self.status = FunnelStatus.COMPLETED
|
|
375
|
-
self.completed_at = datetime.now(tz=ZoneInfo("UTC"))
|
|
57
|
+
class FunnelStage(CompanyAssetModel):
|
|
58
|
+
name: str
|
|
59
|
+
description: str
|
|
60
|
+
index: int
|
|
61
|
+
inflexion_conversion_point: bool = Field(default=False, description="If true, it'll be prioritized for chat assignment and will send notifications in automatic mode to all agents.")
|
|
62
|
+
workflow_ids: List[StrObjectId] = Field(default_factory=list)
|
|
376
63
|
|
|
377
|
-
def abandon(self) -> None:
|
|
378
|
-
"""Mark the funnel as abandoned"""
|
|
379
|
-
self.status = FunnelStatus.ABANDONED
|
|
380
|
-
self.abandoned_at = datetime.now(tz=ZoneInfo("UTC"))
|
|
381
64
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
Args:
|
|
388
|
-
funnel_name: Name of the funnel (for denormalization)
|
|
389
|
-
stage_name: Name of the current stage (for denormalization)
|
|
390
|
-
"""
|
|
391
|
-
if not self.current_stage_id or not self.entered_current_stage_at:
|
|
392
|
-
raise ValueError("Cannot create ActiveFunnel without current stage")
|
|
393
|
-
|
|
394
|
-
return ActiveFunnel(
|
|
395
|
-
chat_funnel_id=self.id,
|
|
396
|
-
funnel_id=self.funnel_id,
|
|
397
|
-
funnel_name=funnel_name,
|
|
398
|
-
current_stage_id=self.current_stage_id,
|
|
399
|
-
current_stage_name=stage_name,
|
|
400
|
-
entered_current_stage_at=self.entered_current_stage_at
|
|
401
|
-
)
|
|
65
|
+
class Funnel(CompanyAssetModel):
|
|
66
|
+
name: str
|
|
67
|
+
description: str
|
|
68
|
+
stages: List[FunnelStage]
|
|
69
|
+
assignment_priority: int = Field(ge=0, le=10, description="Priority for chat assignment, between funnels")
|
|
@@ -1,9 +1,3 @@
|
|
|
1
1
|
from .assets import *
|
|
2
2
|
from .empresa import EmpresaModel
|
|
3
|
-
from .form_field import FormField, FormFieldPreview, CollectedData, SystemFormFields
|
|
4
|
-
from .CRM.funnel import Funnel, FunnelPreview, ChatFunnel, StageTransition, FunnelStatus
|
|
5
|
-
from .integrations.product_sync_status import ProductSyncStatus
|
|
6
|
-
from .integrations.sync_status_enum import SyncStatusEnum
|
|
7
|
-
from .integrations.shopify.shopify_webhook_topics import ShopifyWebhookTopic
|
|
8
|
-
from .integrations.shopify.company_shopify_integration import ShopifyIntegration
|
|
9
|
-
from .integrations.shopify.shopify_product_sync_status import ShopifyProductSyncStatus, ShopifyProductSyncStatusEnum
|
|
3
|
+
from .form_field import FormField, FormFieldPreview, CollectedData, SystemFormFields
|
|
@@ -55,7 +55,7 @@ class SmartFollowUpDecision(BaseModel):
|
|
|
55
55
|
SmartFollowUpDecisionAction.POSTPONE_DELTA_TIME]:
|
|
56
56
|
# Postpone actions don't require messages
|
|
57
57
|
if self.messages is not None and len(self.messages) > 0:
|
|
58
|
-
raise ValueError("Messages are not allowed when action is postpone
|
|
58
|
+
raise ValueError("Messages are not allowed when action is postpone")
|
|
59
59
|
else:
|
|
60
60
|
raise ValueError(f"Invalid action: {self.action}")
|
|
61
61
|
|
|
@@ -92,6 +92,10 @@ class ChattyAIAgentInChat(CompanyAssetModel):
|
|
|
92
92
|
default=None,
|
|
93
93
|
description="If the trigger is a user message, this will be the id of the incoming message"
|
|
94
94
|
)
|
|
95
|
+
last_reset_message_id: Optional[str] = Field(
|
|
96
|
+
default=None,
|
|
97
|
+
description="Last reset control trigger message id handled for this chat"
|
|
98
|
+
)
|
|
95
99
|
|
|
96
100
|
# Assignment metadata
|
|
97
101
|
assigned_at: datetime = Field(default_factory=lambda: datetime.now(ZoneInfo("UTC")))
|