letschatty 0.4.332__py3-none-any.whl → 0.4.334__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/expected_output.py +8 -0
- letschatty/models/ai_microservices/lambda_events.py +7 -1
- letschatty/models/analytics/events/__init__.py +1 -1
- letschatty/models/analytics/events/chat_based_events/chat_funnel.py +69 -13
- letschatty/models/analytics/events/company_based_events/asset_events.py +6 -2
- letschatty/models/analytics/events/event_type_to_classes.py +2 -3
- letschatty/models/analytics/events/event_types.py +9 -5
- letschatty/models/chat/chat.py +2 -0
- letschatty/models/chat/chat_with_assets.py +6 -1
- letschatty/models/chat/client.py +0 -2
- letschatty/models/company/CRM/funnel.py +365 -33
- letschatty/models/company/__init__.py +2 -1
- letschatty/models/company/assets/ai_agents_v2/chatty_ai_agent.py +7 -1
- letschatty/models/company/assets/automation.py +10 -1
- letschatty/models/company/assets/company_assets.py +2 -0
- letschatty/models/messages/chatty_messages/schema/chatty_content/content_document.py +4 -2
- letschatty/models/messages/chatty_messages/schema/chatty_content/content_media.py +4 -3
- letschatty/models/utils/custom_exceptions/custom_exceptions.py +24 -0
- letschatty/services/factories/analytics/events_factory.py +5 -3
- {letschatty-0.4.332.dist-info → letschatty-0.4.334.dist-info}/METADATA +1 -1
- {letschatty-0.4.332.dist-info → letschatty-0.4.334.dist-info}/RECORD +23 -23
- {letschatty-0.4.332.dist-info → letschatty-0.4.334.dist-info}/LICENSE +0 -0
- {letschatty-0.4.332.dist-info → letschatty-0.4.334.dist-info}/WHEEL +0 -0
|
@@ -169,6 +169,12 @@ class ExpectedOutputIncomingMessage(BaseModel):
|
|
|
169
169
|
action: IncomingMessageDecisionAction
|
|
170
170
|
messages: List[str] = Field(description="Array of message strings to send to the customer. Required for send/suggest actions, optional for escalate action, empty array for skip/remove actions.")
|
|
171
171
|
chain_of_thought: ChainOfThoughtInChatRequest = Field(description="REQUIRED: Your reasoning process and response decision explanation")
|
|
172
|
+
confidence: Optional[int] = Field(
|
|
173
|
+
default=None,
|
|
174
|
+
ge=0,
|
|
175
|
+
le=100,
|
|
176
|
+
description="Confidence level 0-100"
|
|
177
|
+
)
|
|
172
178
|
|
|
173
179
|
def to_incoming_message_decision_output(self) -> IncomingMessageAIDecision:
|
|
174
180
|
messages_drafts = [
|
|
@@ -187,3 +193,5 @@ class ExpectedOutputIncomingMessage(BaseModel):
|
|
|
187
193
|
return incoming_decision
|
|
188
194
|
|
|
189
195
|
|
|
196
|
+
class ExpectedOutputSmartFollowUp(BaseModel):
|
|
197
|
+
action: Optional[str] = None
|
|
@@ -5,8 +5,9 @@ from typing import Dict, Any, List, Optional
|
|
|
5
5
|
from letschatty.models.base_models.ai_agent_component import AiAgentComponentType
|
|
6
6
|
from letschatty.models.utils.types.identifier import StrObjectId
|
|
7
7
|
from .lambda_invokation_types import InvokationType, LambdaAiEvent
|
|
8
|
-
from .expected_output import ExpectedOutputIncomingMessage, ExpectedOutputSmartTag, ExpectedOutputQualityTest, IncomingMessageAIDecision
|
|
8
|
+
from .expected_output import ExpectedOutputIncomingMessage, ExpectedOutputSmartTag, ExpectedOutputQualityTest, ExpectedOutputSmartFollowUp, IncomingMessageAIDecision
|
|
9
9
|
from ...models.company.assets.ai_agents_v2.ai_agents_decision_output import IncomingMessageDecisionAction
|
|
10
|
+
from ...models.company.assets.ai_agents_v2.chatty_ai_agent_in_chat import HumanInterventionReason
|
|
10
11
|
|
|
11
12
|
class SmartTaggingCallbackMetadata(BaseModel):
|
|
12
13
|
chat_id: StrObjectId
|
|
@@ -77,6 +78,11 @@ class FollowUpEvent(LambdaAiEvent):
|
|
|
77
78
|
type: InvokationType = InvokationType.FOLLOW_UP
|
|
78
79
|
data: ChatData
|
|
79
80
|
|
|
81
|
+
|
|
82
|
+
class SmartFollowUpDecisionOutputEvent(LambdaAiEvent):
|
|
83
|
+
type: InvokationType = InvokationType.FOLLOW_UP
|
|
84
|
+
data: ExpectedOutputSmartFollowUp
|
|
85
|
+
|
|
80
86
|
class QualityTestEventData(BaseModel):
|
|
81
87
|
chat_example_id: StrObjectId
|
|
82
88
|
company_id: StrObjectId
|
|
@@ -4,7 +4,7 @@ from .chat_based_events.business_area import ChatBusinessAreaEvent, BusinessArea
|
|
|
4
4
|
from .chat_based_events.workflow import WorkflowEvent, WorkflowEventData
|
|
5
5
|
from .chat_based_events.chat_status import ChatStatusEvent, ChatStatusEventData, ChatStatusModification, ChatCreatedFrom
|
|
6
6
|
from .chat_based_events.chat_funnel import ChatFunnelEvent, FunnelEventData
|
|
7
|
-
from ...company.CRM.funnel import
|
|
7
|
+
from ...company.CRM.funnel import ChatFunnel, StageTransition, ActiveFunnel
|
|
8
8
|
from .company_based_events.user_events import UserEvent, UserEventData
|
|
9
9
|
from .chat_based_events.quality_scoring import QualityScoringEvent
|
|
10
10
|
from .chat_based_events.message import MessageEvent, MessageData
|
|
@@ -4,37 +4,93 @@ from ..base import Event
|
|
|
4
4
|
from ..event_types import EventType
|
|
5
5
|
from .chat_based_event import CustomerEventData
|
|
6
6
|
from ....utils.types.identifier import StrObjectId
|
|
7
|
-
from ....company.CRM.funnel import
|
|
7
|
+
from ....company.CRM.funnel import StageTransition
|
|
8
8
|
import json
|
|
9
9
|
|
|
10
|
+
|
|
10
11
|
class FunnelEventData(CustomerEventData):
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
"""
|
|
13
|
+
Event data for chat funnel events.
|
|
14
|
+
|
|
15
|
+
Fields required per event type:
|
|
16
|
+
- STARTED: funnel_id, chat_funnel_id, stage_transition
|
|
17
|
+
- STAGE_CHANGED: funnel_id, chat_funnel_id, stage_transition (with time_in_previous_stage_seconds)
|
|
18
|
+
- COMPLETED: funnel_id, chat_funnel_id, time_in_funnel_seconds, time_in_last_stage_seconds
|
|
19
|
+
- ABANDONED: funnel_id, chat_funnel_id, time_in_funnel_seconds, time_in_last_stage_seconds
|
|
20
|
+
"""
|
|
21
|
+
funnel_id: StrObjectId = Field(description="The funnel the chat is in")
|
|
22
|
+
chat_funnel_id: StrObjectId = Field(description="Reference to the ChatFunnel record")
|
|
23
|
+
|
|
24
|
+
# For STARTED and STAGE_CHANGED events
|
|
25
|
+
stage_transition: Optional[StageTransition] = Field(
|
|
26
|
+
default=None,
|
|
27
|
+
description="The stage transition details (for STARTED and STAGE_CHANGED)"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# For COMPLETED and ABANDONED events
|
|
31
|
+
time_in_funnel_seconds: Optional[int] = Field(
|
|
32
|
+
default=None,
|
|
33
|
+
description="Total time spent in the funnel (for COMPLETED and ABANDONED)"
|
|
34
|
+
)
|
|
35
|
+
time_in_last_stage_seconds: Optional[int] = Field(
|
|
36
|
+
default=None,
|
|
37
|
+
description="Time spent in the last stage before completion/abandonment"
|
|
38
|
+
)
|
|
39
|
+
|
|
14
40
|
|
|
15
41
|
class ChatFunnelEvent(Event):
|
|
16
|
-
"""
|
|
42
|
+
"""
|
|
43
|
+
Event for tracking chat funnel lifecycle.
|
|
44
|
+
|
|
45
|
+
Events:
|
|
46
|
+
- CHAT_FUNNEL_STARTED: Chat entered a funnel
|
|
47
|
+
- CHAT_FUNNEL_STAGE_CHANGED: Chat moved between stages
|
|
48
|
+
- CHAT_FUNNEL_COMPLETED: Chat completed the funnel
|
|
49
|
+
- CHAT_FUNNEL_ABANDONED: Chat abandoned the funnel
|
|
50
|
+
"""
|
|
17
51
|
data: FunnelEventData
|
|
18
52
|
|
|
19
|
-
# Define valid event types for this event class
|
|
20
53
|
VALID_TYPES: ClassVar[set] = {
|
|
21
|
-
EventType.CHAT_FUNNEL_UPDATED,
|
|
22
54
|
EventType.CHAT_FUNNEL_STARTED,
|
|
55
|
+
EventType.CHAT_FUNNEL_STAGE_CHANGED,
|
|
23
56
|
EventType.CHAT_FUNNEL_COMPLETED,
|
|
24
57
|
EventType.CHAT_FUNNEL_ABANDONED
|
|
25
58
|
}
|
|
26
59
|
|
|
27
|
-
|
|
28
60
|
@field_validator('data')
|
|
29
61
|
def validate_data_fields(cls, v: FunnelEventData, info: ValidationInfo):
|
|
30
62
|
"""Validate that appropriate fields are set based on event type"""
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
63
|
+
event_type = info.data.get('type')
|
|
64
|
+
|
|
65
|
+
# STARTED: requires stage_transition
|
|
66
|
+
if event_type == EventType.CHAT_FUNNEL_STARTED:
|
|
67
|
+
if v.stage_transition is None:
|
|
68
|
+
raise ValueError("stage_transition must be set for CHAT_FUNNEL_STARTED events")
|
|
69
|
+
|
|
70
|
+
# STAGE_CHANGED: requires stage_transition with time_in_previous_stage_seconds
|
|
71
|
+
elif event_type == EventType.CHAT_FUNNEL_STAGE_CHANGED:
|
|
72
|
+
if v.stage_transition is None:
|
|
73
|
+
raise ValueError("stage_transition must be set for CHAT_FUNNEL_STAGE_CHANGED events")
|
|
74
|
+
if v.stage_transition.time_in_previous_stage_seconds is None:
|
|
75
|
+
raise ValueError("time_in_previous_stage_seconds must be set in stage_transition for CHAT_FUNNEL_STAGE_CHANGED events")
|
|
76
|
+
|
|
77
|
+
# COMPLETED: requires time metrics
|
|
78
|
+
elif event_type == EventType.CHAT_FUNNEL_COMPLETED:
|
|
79
|
+
if v.time_in_funnel_seconds is None:
|
|
80
|
+
raise ValueError("time_in_funnel_seconds must be set for CHAT_FUNNEL_COMPLETED events")
|
|
81
|
+
if v.time_in_last_stage_seconds is None:
|
|
82
|
+
raise ValueError("time_in_last_stage_seconds must be set for CHAT_FUNNEL_COMPLETED events")
|
|
83
|
+
|
|
84
|
+
# ABANDONED: requires time metrics
|
|
85
|
+
elif event_type == EventType.CHAT_FUNNEL_ABANDONED:
|
|
86
|
+
if v.time_in_funnel_seconds is None:
|
|
87
|
+
raise ValueError("time_in_funnel_seconds must be set for CHAT_FUNNEL_ABANDONED events")
|
|
88
|
+
if v.time_in_last_stage_seconds is None:
|
|
89
|
+
raise ValueError("time_in_last_stage_seconds must be set for CHAT_FUNNEL_ABANDONED events")
|
|
90
|
+
|
|
35
91
|
return v
|
|
36
92
|
|
|
37
93
|
def model_dump_json(self, *args, **kwargs):
|
|
38
94
|
dump = json.loads(super().model_dump_json(*args, **kwargs))
|
|
39
95
|
dump['data'] = self.data.model_dump_json()
|
|
40
|
-
return dump
|
|
96
|
+
return dump
|
|
@@ -58,6 +58,9 @@ class AssetEvent(Event):
|
|
|
58
58
|
EventType.FUNNEL_STAGE_CREATED,
|
|
59
59
|
EventType.FUNNEL_STAGE_UPDATED,
|
|
60
60
|
EventType.FUNNEL_STAGE_DELETED,
|
|
61
|
+
EventType.FUNNEL_MEMBER_ADDED,
|
|
62
|
+
EventType.FUNNEL_MEMBER_UPDATED,
|
|
63
|
+
EventType.FUNNEL_MEMBER_REMOVED,
|
|
61
64
|
EventType.WORKFLOW_CREATED,
|
|
62
65
|
EventType.WORKFLOW_UPDATED,
|
|
63
66
|
EventType.WORKFLOW_DELETED,
|
|
@@ -80,8 +83,9 @@ class AssetEvent(Event):
|
|
|
80
83
|
|
|
81
84
|
@model_validator(mode='after')
|
|
82
85
|
def validate_data_fields(self):
|
|
83
|
-
|
|
84
|
-
|
|
86
|
+
# Asset is not required for deleted/removed events
|
|
87
|
+
if "deleted" not in self.type.value and "removed" not in self.type.value and not self.data.asset:
|
|
88
|
+
raise ValueError("asset must be set for all events except DELETED/REMOVED")
|
|
85
89
|
return self
|
|
86
90
|
|
|
87
91
|
def model_dump_json(self, *args, **kwargs):
|
|
@@ -41,10 +41,9 @@ EVENT_TO_TYPE_CLASSES = {
|
|
|
41
41
|
#CONTINUOUS CONVERSATION
|
|
42
42
|
EventType.CONTINUOUS_CONVERSATION_CREATED : ContinuousConversationEvent,
|
|
43
43
|
EventType.CONTINUOUS_CONVERSATION_UPDATED : ContinuousConversationEvent,
|
|
44
|
-
#FUNNEL
|
|
45
|
-
# Funnel-level events
|
|
44
|
+
#CHAT FUNNEL EVENTS
|
|
46
45
|
EventType.CHAT_FUNNEL_STARTED : ChatFunnelEvent,
|
|
47
|
-
EventType.
|
|
46
|
+
EventType.CHAT_FUNNEL_STAGE_CHANGED : ChatFunnelEvent,
|
|
48
47
|
EventType.CHAT_FUNNEL_COMPLETED : ChatFunnelEvent,
|
|
49
48
|
EventType.CHAT_FUNNEL_ABANDONED : ChatFunnelEvent,
|
|
50
49
|
|
|
@@ -42,10 +42,9 @@ class EventType(StrEnum):
|
|
|
42
42
|
#CONTINUOUS CONVERSATION
|
|
43
43
|
CONTINUOUS_CONVERSATION_CREATED = "chat.continuous_conversation.created"
|
|
44
44
|
CONTINUOUS_CONVERSATION_UPDATED = "chat.continuous_conversation.updated"
|
|
45
|
-
#FUNNEL
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
CHAT_FUNNEL_UPDATED = "chat.funnel.updated"
|
|
45
|
+
#CHAT FUNNEL EVENTS
|
|
46
|
+
CHAT_FUNNEL_STARTED = "chat.funnel.started"
|
|
47
|
+
CHAT_FUNNEL_STAGE_CHANGED = "chat.funnel.stage_changed"
|
|
49
48
|
CHAT_FUNNEL_COMPLETED = "chat.funnel.completed"
|
|
50
49
|
CHAT_FUNNEL_ABANDONED = "chat.funnel.abandoned"
|
|
51
50
|
|
|
@@ -84,13 +83,18 @@ class EventType(StrEnum):
|
|
|
84
83
|
FAST_ANSWER_CREATED = "company.fast_answer.created"
|
|
85
84
|
FAST_ANSWER_UPDATED = "company.fast_answer.updated"
|
|
86
85
|
FAST_ANSWER_DELETED = "company.fast_answer.deleted"
|
|
87
|
-
#
|
|
86
|
+
#FUNNELS
|
|
88
87
|
FUNNEL_CREATED = "company.funnel.created"
|
|
89
88
|
FUNNEL_UPDATED = "company.funnel.updated"
|
|
90
89
|
FUNNEL_DELETED = "company.funnel.deleted"
|
|
90
|
+
#FUNNEL STAGES
|
|
91
91
|
FUNNEL_STAGE_CREATED = "company.funnel_stage.created"
|
|
92
92
|
FUNNEL_STAGE_UPDATED = "company.funnel_stage.updated"
|
|
93
93
|
FUNNEL_STAGE_DELETED = "company.funnel_stage.deleted"
|
|
94
|
+
#FUNNEL MEMBERS
|
|
95
|
+
FUNNEL_MEMBER_ADDED = "company.funnel_member.added"
|
|
96
|
+
FUNNEL_MEMBER_UPDATED = "company.funnel_member.updated"
|
|
97
|
+
FUNNEL_MEMBER_REMOVED = "company.funnel_member.removed"
|
|
94
98
|
#BUSINESS AREA
|
|
95
99
|
BUSINESS_AREA_CREATED = "company.business_area.created"
|
|
96
100
|
BUSINESS_AREA_UPDATED = "company.business_area.updated"
|
letschatty/models/chat/chat.py
CHANGED
|
@@ -19,6 +19,7 @@ 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
|
|
22
23
|
import json
|
|
23
24
|
import logging
|
|
24
25
|
logger = logging.getLogger(__name__)
|
|
@@ -47,6 +48,7 @@ class Chat(CompanyAssetModel):
|
|
|
47
48
|
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")
|
|
48
49
|
suggested_messages: List[MessageDraft] = Field(default_factory=list)
|
|
49
50
|
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")
|
|
50
52
|
model_config = ConfigDict(extra="ignore")
|
|
51
53
|
|
|
52
54
|
@property
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
1
|
from .chat import Chat
|
|
3
2
|
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,3 +15,8 @@ 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,7 +5,6 @@ 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
|
|
9
8
|
from ..utils.types.serializer_type import SerializerType
|
|
10
9
|
|
|
11
10
|
class Client(CompanyAssetModel):
|
|
@@ -23,7 +22,6 @@ class Client(CompanyAssetModel):
|
|
|
23
22
|
contact_points: List[ContactPointAssignedToChat] = Field(default=list())
|
|
24
23
|
business_area: Optional[StrObjectId] = Field(default=None, description="It's a business related area, that works as a queue for the chats")
|
|
25
24
|
external_id: Optional[str] = Field(default=None)
|
|
26
|
-
funnels : List[ClientFunnel] = Field(default=list())
|
|
27
25
|
exclude_fields = {
|
|
28
26
|
SerializerType.FRONTEND: {"products", "tags", "sales", "contact_points", "highlights"}
|
|
29
27
|
}
|
|
@@ -1,69 +1,401 @@
|
|
|
1
|
-
from ...base_models import CompanyAssetModel
|
|
2
|
-
from typing import List
|
|
1
|
+
from ...base_models import CompanyAssetModel, ChattyAssetPreview, ChattyAssetModel
|
|
2
|
+
from typing import List, Optional, ClassVar, Any
|
|
3
3
|
from ...utils.types.identifier import StrObjectId
|
|
4
|
-
from pydantic import BaseModel, Field
|
|
4
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
5
5
|
from enum import StrEnum
|
|
6
6
|
from datetime import datetime
|
|
7
|
-
from typing import Optional
|
|
8
7
|
from zoneinfo import ZoneInfo
|
|
9
8
|
from ...utils.types.executor_types import ExecutorType
|
|
9
|
+
from ..assets.automation import Automation
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ============================================================================
|
|
13
|
+
# Enums
|
|
14
|
+
# ============================================================================
|
|
10
15
|
|
|
11
16
|
class FunnelStatus(StrEnum):
|
|
17
|
+
"""Status of a chat within a funnel"""
|
|
12
18
|
IN_PROGRESS = "in_progress"
|
|
13
19
|
COMPLETED = "completed"
|
|
14
20
|
ABANDONED = "abandoned"
|
|
15
21
|
|
|
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
|
+
|
|
16
34
|
class StageTransition(BaseModel):
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
35
|
+
"""
|
|
36
|
+
Record of a chat transitioning between stages within a funnel.
|
|
37
|
+
Tracks the transition details and time spent in the previous stage.
|
|
38
|
+
"""
|
|
39
|
+
from_stage_id: Optional[StrObjectId] = Field(
|
|
40
|
+
default=None,
|
|
41
|
+
description="The stage the chat came from (None if entering funnel)"
|
|
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"""
|
|
25
132
|
funnel_id: StrObjectId
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
133
|
+
color: str
|
|
134
|
+
order: int
|
|
135
|
+
is_exit_stage: bool = Field(default=False)
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def get_projection(cls) -> dict[str, Any]:
|
|
139
|
+
base = super().get_projection()
|
|
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
|
+
)
|
|
33
286
|
|
|
34
287
|
@property
|
|
35
288
|
def is_completed(self) -> bool:
|
|
36
|
-
|
|
289
|
+
"""Check if the chat has completed the funnel"""
|
|
290
|
+
return self.status == FunnelStatus.COMPLETED and self.completed_at is not None
|
|
37
291
|
|
|
38
292
|
@property
|
|
39
293
|
def is_abandoned(self) -> bool:
|
|
40
|
-
|
|
294
|
+
"""Check if the chat has abandoned the funnel"""
|
|
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
|
|
41
301
|
|
|
42
302
|
@property
|
|
43
303
|
def time_in_funnel_seconds(self) -> int:
|
|
304
|
+
"""Calculate total time spent in the funnel"""
|
|
44
305
|
end_time = self.completed_at or self.abandoned_at or datetime.now(tz=ZoneInfo("UTC"))
|
|
45
306
|
return int((end_time - self.started_at).total_seconds())
|
|
46
307
|
|
|
47
308
|
@property
|
|
48
309
|
def time_in_current_stage_seconds(self) -> Optional[int]:
|
|
49
|
-
|
|
310
|
+
"""Calculate time spent in the current stage"""
|
|
311
|
+
if not self.entered_current_stage_at or not self.current_stage_id:
|
|
50
312
|
return None
|
|
51
313
|
return int((datetime.now(tz=ZoneInfo("UTC")) - self.entered_current_stage_at).total_seconds())
|
|
52
314
|
|
|
53
315
|
@property
|
|
54
316
|
def unique_stages_visited(self) -> int:
|
|
317
|
+
"""Count of unique stages the chat has visited"""
|
|
55
318
|
return len(set(t.to_stage_id for t in self.stage_transitions))
|
|
56
319
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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)
|
|
320
|
+
@property
|
|
321
|
+
def total_transitions(self) -> int:
|
|
322
|
+
"""Total number of stage transitions"""
|
|
323
|
+
return len(self.stage_transitions)
|
|
63
324
|
|
|
325
|
+
@property
|
|
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)
|
|
64
329
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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"))
|
|
376
|
+
|
|
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
|
+
|
|
382
|
+
def to_active_funnel(self, funnel_name: str, stage_name: str) -> ActiveFunnel:
|
|
383
|
+
"""
|
|
384
|
+
Create an ActiveFunnel for embedding in the Chat document.
|
|
385
|
+
Call this after creating/updating the ChatFunnel.
|
|
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
|
+
)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
from .assets import *
|
|
2
2
|
from .empresa import EmpresaModel
|
|
3
|
-
from .form_field import FormField, FormFieldPreview, CollectedData, SystemFormFields
|
|
3
|
+
from .form_field import FormField, FormFieldPreview, CollectedData, SystemFormFields
|
|
4
|
+
from .CRM.funnel import Funnel, FunnelPreview, ChatFunnel, StageTransition, FunnelStatus
|
|
@@ -110,6 +110,12 @@ class ChattyAIAgent(CompanyAssetModel):
|
|
|
110
110
|
examples: List[StrObjectId] = Field(default_factory=list, description="Training examples")
|
|
111
111
|
double_checker_enabled: bool = Field(default=False, description="Whether the double checker is enabled")
|
|
112
112
|
double_checker_instructions: Optional[str] = Field(default=None, description="Instructions for the double checker")
|
|
113
|
+
copilot_confidence_threshold: Optional[int] = Field(
|
|
114
|
+
default=None,
|
|
115
|
+
ge=0,
|
|
116
|
+
le=100,
|
|
117
|
+
description="Confidence threshold 0-100 para modo COPILOT (fallback a env si no está)"
|
|
118
|
+
)
|
|
113
119
|
|
|
114
120
|
# Pre-qualification configuration
|
|
115
121
|
pre_qualify: Optional["PreQualifyConfig"] = Field(
|
|
@@ -165,4 +171,4 @@ class ChattyAIAgent(CompanyAssetModel):
|
|
|
165
171
|
|
|
166
172
|
# Import and rebuild for forward reference resolution
|
|
167
173
|
from .pre_qualify_config import PreQualifyConfig
|
|
168
|
-
ChattyAIAgent.model_rebuild()
|
|
174
|
+
ChattyAIAgent.model_rebuild()
|
|
@@ -14,7 +14,16 @@ class Automation(BaseModel):
|
|
|
14
14
|
chatty_ai_agent_config: Optional[ChattyAIConfigForAutomation] = Field(default=None)
|
|
15
15
|
area: Optional[Area] = Field(default=None)
|
|
16
16
|
agent_id: Optional[StrObjectId] = Field(default=None)
|
|
17
|
-
chain_of_thought
|
|
17
|
+
chain_of_thought: Optional[str] = Field(default=None)
|
|
18
|
+
# Funnel transition automations
|
|
19
|
+
target_funnel_id: Optional[StrObjectId] = Field(
|
|
20
|
+
default=None,
|
|
21
|
+
description="Target funnel to move the chat to (for cross-funnel transitions)"
|
|
22
|
+
)
|
|
23
|
+
target_stage_id: Optional[StrObjectId] = Field(
|
|
24
|
+
default=None,
|
|
25
|
+
description="Target stage within the target funnel"
|
|
26
|
+
)
|
|
18
27
|
# client_info: Optional[ClientInfo] = Field(default=None) me gustaría que levante el mail y/o otros atributos
|
|
19
28
|
|
|
20
29
|
@model_validator(mode='after')
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from pydantic import BaseModel, Field, model_validator, ValidationInfo
|
|
2
2
|
from typing import Optional
|
|
3
|
+
from urllib.parse import urlparse, unquote
|
|
3
4
|
from .content_media import ChattyContentMedia
|
|
4
5
|
|
|
5
6
|
class ChattyContentDocument(ChattyContentMedia):
|
|
@@ -8,5 +9,6 @@ class ChattyContentDocument(ChattyContentMedia):
|
|
|
8
9
|
@model_validator(mode='before')
|
|
9
10
|
def validate_filename(cls, data: dict, info: ValidationInfo):
|
|
10
11
|
if not data.get("filename") and data.get("url"):
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
parsed = urlparse(data["url"])
|
|
13
|
+
data["filename"] = unquote(parsed.path.split("/")[-1])
|
|
14
|
+
return data
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from pydantic import BaseModel, Field, field_validator, HttpUrl
|
|
2
2
|
from typing import Optional
|
|
3
|
+
from urllib.parse import quote
|
|
3
4
|
class ChattyContentMedia(BaseModel):
|
|
4
5
|
id: Optional[str] = Field(description="Unique identifier for the image. Also known as media_id", default="")
|
|
5
6
|
url: str = Field(description="URL of the media from S3")
|
|
@@ -11,9 +12,9 @@ class ChattyContentMedia(BaseModel):
|
|
|
11
12
|
def validate_url(cls, v):
|
|
12
13
|
if not v:
|
|
13
14
|
raise ValueError("URL is required")
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
encoded = quote(str(v), safe=":/?&=%#")
|
|
16
|
+
HttpUrl(encoded)
|
|
17
|
+
return encoded
|
|
16
18
|
|
|
17
19
|
def get_body_or_caption(self) -> str:
|
|
18
20
|
return self.caption
|
|
19
|
-
|
|
@@ -59,6 +59,30 @@ class WhatsAppPayloadValidationError(Exception):
|
|
|
59
59
|
def __init__(self, message="WhatsApp payload validation error", status_code=400, **context_data):
|
|
60
60
|
super().__init__(message, status_code=status_code, **context_data)
|
|
61
61
|
|
|
62
|
+
|
|
63
|
+
class ChatWithActiveContinuousConversation(CustomException):
|
|
64
|
+
"""
|
|
65
|
+
Raised when an AI agent is triggered on a chat that has an active Continuous Conversation.
|
|
66
|
+
"""
|
|
67
|
+
def __init__(self, message="Chat has an active continuous conversation", status_code=400, **context_data):
|
|
68
|
+
super().__init__(message, status_code=status_code, **context_data)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ChattyAIModeOff(CustomException):
|
|
72
|
+
"""
|
|
73
|
+
Raised when an AI agent is in OFF mode and cannot be triggered.
|
|
74
|
+
"""
|
|
75
|
+
def __init__(self, message="Chatty AI agent is OFF", status_code=400, **context_data):
|
|
76
|
+
super().__init__(message, status_code=status_code, **context_data)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class MissingAIAgentInChat(CustomException):
|
|
80
|
+
"""
|
|
81
|
+
Raised when a chat has no AI agent assigned but one is required for the operation.
|
|
82
|
+
"""
|
|
83
|
+
def __init__(self, message="AI agent not assigned to chat", status_code=404, **context_data):
|
|
84
|
+
super().__init__(message, status_code=status_code, **context_data)
|
|
85
|
+
|
|
62
86
|
class UnsuportedChannel(CustomException):
|
|
63
87
|
def __init__(self, message="Channel not supported", status_code=400, **context_data):
|
|
64
88
|
super().__init__(message, status_code=status_code, **context_data)
|
|
@@ -321,7 +321,7 @@ class EventFactory:
|
|
|
321
321
|
return events
|
|
322
322
|
|
|
323
323
|
@staticmethod
|
|
324
|
-
def funnel_stage_events(chat_with_assets: ChatWithAssets, company_info: EmpresaModel, trace_id: str,executor_type: ExecutorType, executor_id: StrObjectId, chat_funnel:
|
|
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]:
|
|
325
325
|
events = []
|
|
326
326
|
base_data = EventFactory._create_base_customer_event_data(
|
|
327
327
|
chat_with_assets=chat_with_assets,
|
|
@@ -334,8 +334,10 @@ class EventFactory:
|
|
|
334
334
|
funnel_data = FunnelEventData(
|
|
335
335
|
**base_data.model_dump(),
|
|
336
336
|
funnel_id=chat_funnel.funnel_id,
|
|
337
|
-
|
|
338
|
-
|
|
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
|
|
339
341
|
)
|
|
340
342
|
event = ChatFunnelEvent(
|
|
341
343
|
specversion=EventFactory.package_version(),
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
letschatty/__init__.py,sha256=6dGYdy5edB1dHdgvpqUpENZ347CpIyWgR1CsGYwPSk8,45
|
|
2
2
|
letschatty/models/__init__.py,sha256=obWHa796C-O1mqo_lk22KhUC2ODVvG7cICY3Lo-AqJg,126
|
|
3
3
|
letschatty/models/ai_microservices/__init__.py,sha256=h0xLiuU1wfHpDl1oKCzVKUef3ROYU7qIyVxBsdcdZe0,641
|
|
4
|
-
letschatty/models/ai_microservices/expected_output.py,sha256=
|
|
5
|
-
letschatty/models/ai_microservices/lambda_events.py,sha256=
|
|
4
|
+
letschatty/models/ai_microservices/expected_output.py,sha256=PWXvmGiOhqAuOVCZHhfvUMzb8BwSnAoys4CReJ9dIK4,9441
|
|
5
|
+
letschatty/models/ai_microservices/lambda_events.py,sha256=aazW8g6M7b7Lk-KZbx0R4AoV5oMjgXEpAcnsIwQ7Jf4,14319
|
|
6
6
|
letschatty/models/ai_microservices/lambda_invokation_types.py,sha256=KWvXFvHhwYMaAIHbGpdW8AYE0eqrfCgZ_28jIyGXoME,1918
|
|
7
7
|
letschatty/models/ai_microservices/n8n_ai_agents_payload.py,sha256=E5tu2UcSJCybMSvxfZG3JIxSxHbTBbjdRE74cnRtlGY,696
|
|
8
8
|
letschatty/models/ai_microservices/openai_payloads.py,sha256=HauUGf4c37khJJsKyH4ikxzHKboGWnerjDQgDOnMFrY,271
|
|
9
9
|
letschatty/models/analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
letschatty/models/analytics/events/__init__.py,sha256
|
|
10
|
+
letschatty/models/analytics/events/__init__.py,sha256=LtbKa54_Um7LFKyn-nQ30efHuR4et-aOVFTg3K0Ova4,2096
|
|
11
11
|
letschatty/models/analytics/events/base.py,sha256=vmRnowUYot4OjYyqVjevGY9kF3Kb47IDkMJgJRwvcfU,1793
|
|
12
12
|
letschatty/models/analytics/events/chat_based_events/ai_agent_chat.py,sha256=Ac6Llg2IFM0lftCkAErThjd283f_AEYqg76OkNnihuM,1681
|
|
13
13
|
letschatty/models/analytics/events/chat_based_events/business_area.py,sha256=YZgIejCzbryySxolMSgvCFMuRggMcwBJ-jeQKdEshvM,904
|
|
14
14
|
letschatty/models/analytics/events/chat_based_events/chat_based_event.py,sha256=YDXzX78jZgxuPFkTqipXFAo6G0-ajl_RkJISsjM9YJU,1469
|
|
15
15
|
letschatty/models/analytics/events/chat_based_events/chat_context.py,sha256=Y6VfYorrKurbUgWtIPOsfNMbGQbuTPR5wQQLrbBxD9Q,2226
|
|
16
|
-
letschatty/models/analytics/events/chat_based_events/chat_funnel.py,sha256=
|
|
16
|
+
letschatty/models/analytics/events/chat_based_events/chat_funnel.py,sha256=LX_lcOn1R2VhdffdZeuc7S87PcoHBhEuppWaBXFP6tY,4184
|
|
17
17
|
letschatty/models/analytics/events/chat_based_events/chat_status.py,sha256=9vt8GP6QnMrUuJF-AbUQm68Nkc0Zgexmnb-T1k8tsD4,2222
|
|
18
18
|
letschatty/models/analytics/events/chat_based_events/contact_point.py,sha256=IIzf0EkgxUJKr7vqnv95LHEy0wQA9sVchHZA7Yi0H88,1944
|
|
19
19
|
letschatty/models/analytics/events/chat_based_events/continuous_conversation.py,sha256=4Tbv83Zk5RIoUu6pKYpQdWMtxz9XZi26V-j5OSBP91U,2305
|
|
@@ -25,11 +25,11 @@ letschatty/models/analytics/events/chat_based_events/quality_scoring.py,sha256=f
|
|
|
25
25
|
letschatty/models/analytics/events/chat_based_events/sale.py,sha256=WqpFp10h8yZwa5S5T4NCWCRLcBgkAAAME_CKmCv6K7M,1738
|
|
26
26
|
letschatty/models/analytics/events/chat_based_events/tag_chat.py,sha256=7f1MaNdst-KN8-aIzgzyc6oBES-y8kkH5jVrsOOcxBs,1418
|
|
27
27
|
letschatty/models/analytics/events/chat_based_events/workflow.py,sha256=BZQN2JDIgCVr2VbdcLg3O0J69SL_yPqGgMXzzNCId4E,1399
|
|
28
|
-
letschatty/models/analytics/events/company_based_events/asset_events.py,sha256=
|
|
28
|
+
letschatty/models/analytics/events/company_based_events/asset_events.py,sha256=EtVhl_W3wxkJhflbyk4NQraxAdpLeuesc9IfyOC5QV8,3490
|
|
29
29
|
letschatty/models/analytics/events/company_based_events/company_events.py,sha256=PZAiw2imo706iM0EaCxnIaiFchMKivbwjKd7gsozA8k,1673
|
|
30
30
|
letschatty/models/analytics/events/company_based_events/user_events.py,sha256=ZUoBaS60s63SoTUOUunLpojP1BQ9Cdl4sl_KdgY6NGI,2466
|
|
31
|
-
letschatty/models/analytics/events/event_type_to_classes.py,sha256=
|
|
32
|
-
letschatty/models/analytics/events/event_types.py,sha256=
|
|
31
|
+
letschatty/models/analytics/events/event_type_to_classes.py,sha256=mjE45PLLNAuXIoCuIQaOMSxu-Z3NkIhQYk5y5NOOdOw,4331
|
|
32
|
+
letschatty/models/analytics/events/event_types.py,sha256=kubeWq3ICqFnrM6rm1UNJJIveQy8lfb0KpM2o1aY6Wk,5471
|
|
33
33
|
letschatty/models/analytics/metrics/__init_.py,sha256=DnGDYvEn9S1sSMe56e7mwE5r013nO-Fsf0pgBnf9RjI,276
|
|
34
34
|
letschatty/models/analytics/metrics/daily_contact_points.py,sha256=eWwUmhFpL-Xn5sWQWCcACrulb4xwjMY80jDSAWsTMb4,1524
|
|
35
35
|
letschatty/models/analytics/metrics/daily_new_chats.py,sha256=kTsQFDFwavTq3u7HcHx3RqZ_O517HqTrOD-k8q4JQz0,322
|
|
@@ -63,10 +63,10 @@ letschatty/models/base_models/update_model_interface.py,sha256=80YuvkEM5Rd3Ycysq
|
|
|
63
63
|
letschatty/models/base_models/validate_timestamps_and_id.py,sha256=WRtaoW2EYtItgZjnNOYAffxy9K7M_NMNJ5imRN8Iu80,1971
|
|
64
64
|
letschatty/models/channels/channel.py,sha256=Mgqigm-3uRDozxBIZZYj62PQwtnB0rAKoY2JS4477n8,2029
|
|
65
65
|
letschatty/models/chat/assets_assigned_to_chat.py,sha256=nAdSzRvUSWa8-Plw6KZKnpUxxjCdWs_grml0bhskSis,192
|
|
66
|
-
letschatty/models/chat/chat.py,sha256=
|
|
66
|
+
letschatty/models/chat/chat.py,sha256=8kAQvtH_ccrhk6lnPDdmRvBwvV_iBYsA2h7Q9NcdNOA,16298
|
|
67
67
|
letschatty/models/chat/chat_status_modifications.py,sha256=LcwXNl4WRllPI_ZYKcg5ANRjmSk4Cykkra0naayMAt4,317
|
|
68
|
-
letschatty/models/chat/chat_with_assets.py,sha256=
|
|
69
|
-
letschatty/models/chat/client.py,sha256=
|
|
68
|
+
letschatty/models/chat/chat_with_assets.py,sha256=K4UUwdfLw_Q0ZNHdeoqavqyHkZwisAQOi1OPMBYszOU,814
|
|
69
|
+
letschatty/models/chat/client.py,sha256=oa7PM76V-OiBJL4T-IMytPW0WZdtvh9vFZhNtRQvt6Q,2196
|
|
70
70
|
letschatty/models/chat/continuous_conversation.py,sha256=hoRYOMd_vfYglTGVw30yKS5Ogt5mLcsgDySrfHEli6o,4529
|
|
71
71
|
letschatty/models/chat/filter_parameters.py,sha256=X1IHBYCJr_1y4R8syQYEwKhintdUZc-ZiB_8kSnjiYw,4489
|
|
72
72
|
letschatty/models/chat/flow_link_state.py,sha256=RblDBp6uD-P0ZtaQn1-HzL1EpEgtVhN1rT3iBhO0gMw,5458
|
|
@@ -77,15 +77,15 @@ letschatty/models/chat/scheduled_messages.py,sha256=6z9XXgXUV6lPW8BalAZ4-MsmT8Zn
|
|
|
77
77
|
letschatty/models/chat/temporary_chat.py,sha256=MR68vYrNvoyf40cLs2p5NnI8BNeN1VGTMAcuQw8j9RA,2303
|
|
78
78
|
letschatty/models/chat/time_left.py,sha256=2uA9dS_0QAVnWn7Q2wIwGe4tMrlYBP9qySC7M8I0jb8,3307
|
|
79
79
|
letschatty/models/company/CRM/business_area.py,sha256=U0F_7Rbq-e5tMSj_MugPqMHm2-IHxbAKbN0PWZ_93-o,296
|
|
80
|
-
letschatty/models/company/CRM/funnel.py,sha256=
|
|
81
|
-
letschatty/models/company/__init__.py,sha256=
|
|
80
|
+
letschatty/models/company/CRM/funnel.py,sha256=gBGZhud060sK5QFI5y9NgmFuhLCOAtzK81TKBhK2gxE,15820
|
|
81
|
+
letschatty/models/company/__init__.py,sha256=N_I-kRMHrtnp00c_bbFSiYYM47HJRS-d5slgtbexsl4,229
|
|
82
82
|
letschatty/models/company/assets/__init__.py,sha256=z0xN_lt1D76bXt8DXVdbH3GkofUPQPZU9NioRSLt_lI,244
|
|
83
83
|
letschatty/models/company/assets/ai_agents_v2/ai_agent_message_draft.py,sha256=xbshA34RSjHm2g8J7hW2FkWo-Qm8MH2HTbcRcoYmyvc,2076
|
|
84
84
|
letschatty/models/company/assets/ai_agents_v2/ai_agents_decision_output.py,sha256=aEKZCOsGiFFPSx23fkS5Khfsxo-r8JGk3O0sxiGs8T0,5876
|
|
85
85
|
letschatty/models/company/assets/ai_agents_v2/chain_of_thought_in_chat.py,sha256=LJqtSl1A1V0frPLYwW-5cMZxaGx7-0x1HvujRXsZsNU,5877
|
|
86
86
|
letschatty/models/company/assets/ai_agents_v2/chat_example.py,sha256=yCKt6ifNYch3C78zAvj8To0_Sb9CPAZ8sC-hyoBPa4s,1816
|
|
87
87
|
letschatty/models/company/assets/ai_agents_v2/chat_example_test.py,sha256=vChD-TkX1ORRD9LMbd2HlpbK4QyrsfhDiJd-LDjIqlk,4618
|
|
88
|
-
letschatty/models/company/assets/ai_agents_v2/chatty_ai_agent.py,sha256=
|
|
88
|
+
letschatty/models/company/assets/ai_agents_v2/chatty_ai_agent.py,sha256=R2rCANri8ZiqnmPP10PWoanGddMImCMxrl78Y6d-194,8204
|
|
89
89
|
letschatty/models/company/assets/ai_agents_v2/chatty_ai_agent_config_for_automation.py,sha256=W8orpgp-yG8OHNWGQ16jMv-VgM_Z-Hk0q8PtC0KT2KU,388
|
|
90
90
|
letschatty/models/company/assets/ai_agents_v2/chatty_ai_agent_in_chat.py,sha256=uVmD4FqGP5Tc8noxMBuRYGbkOXrtYlH2w6SAYwQZlLo,15494
|
|
91
91
|
letschatty/models/company/assets/ai_agents_v2/chatty_ai_mode.py,sha256=CKlKI3t_D5eWJPn6hRvN4_V8Wu6W8c0x_EFQlpA50F4,2521
|
|
@@ -100,11 +100,11 @@ letschatty/models/company/assets/ai_agents_v2/n8n_schema_smart_follow_up_output.
|
|
|
100
100
|
letschatty/models/company/assets/ai_agents_v2/pre_qualify_config.py,sha256=o9kKywbnm1lEliTTV0G7oSCFroyqp_w0GkypL--sSk4,4181
|
|
101
101
|
letschatty/models/company/assets/assignment/__init__.py,sha256=eWYZfaDQde5OJNIDed8D-LNRXOa5O_O4mGMToNFtaW8,239
|
|
102
102
|
letschatty/models/company/assets/assignment/assignment_assets.py,sha256=phIJqNh4UGTU-Hux_kaOYhJm2GCqv37AnCGePSDVKmM,2245
|
|
103
|
-
letschatty/models/company/assets/automation.py,sha256=
|
|
103
|
+
letschatty/models/company/assets/automation.py,sha256=RQFOvM-lipBdalfsRSZ8K5opytVgq_GS6YSR0Mz5Qs8,1639
|
|
104
104
|
letschatty/models/company/assets/chat_assets.py,sha256=OV33LPOBOaK02Va0yH7rCH5ufmEyOploQPTEefjlsdk,7524
|
|
105
105
|
letschatty/models/company/assets/chatty_fast_answers/__init__.py,sha256=tbgWS0n1i0nVi9f79U44GPRnrW25NKcjl6Port92alk,48
|
|
106
106
|
letschatty/models/company/assets/chatty_fast_answers/chatty_fast_answer.py,sha256=PxW3eStHXo0BY7Z9hMDqBTHmgO7XcwtOvEpwXi-rA5g,1076
|
|
107
|
-
letschatty/models/company/assets/company_assets.py,sha256=
|
|
107
|
+
letschatty/models/company/assets/company_assets.py,sha256=9Or3hdUsWO0YCLqlwUb0Zcm_c5OerJqDx7rrrUK4_-E,800
|
|
108
108
|
letschatty/models/company/assets/contact_point.py,sha256=2-C00CwQTAQkofQ4ufr5_nJPoxauOXZSDtyL-nj-hF8,2916
|
|
109
109
|
letschatty/models/company/assets/filter_criteria.py,sha256=5hYhHWz5hoMyIURHML2XPZo_Bl66Cyvbh8caVPgdAvE,2283
|
|
110
110
|
letschatty/models/company/assets/flow.py,sha256=mYKB6DG_DHSJymoTpeXm15OWsWI1OP4rkzvMg8AeYzY,1198
|
|
@@ -157,11 +157,11 @@ letschatty/models/messages/chatty_messages/schema/chatty_content/content_audio.p
|
|
|
157
157
|
letschatty/models/messages/chatty_messages/schema/chatty_content/content_button.py,sha256=jdk9QFBN-SaAV6_fnBqkXwl0AFXJLbBNatikhUXLobo,181
|
|
158
158
|
letschatty/models/messages/chatty_messages/schema/chatty_content/content_central.py,sha256=k7J1G4GlfRX5J4ZJIoviOHiekioaLbAc-76In_ZfiIo,712
|
|
159
159
|
letschatty/models/messages/chatty_messages/schema/chatty_content/content_contacts.py,sha256=I1f_NNaT7QYriruMJdWT6K_8JjaU7XVaTSXctNB-_9o,985
|
|
160
|
-
letschatty/models/messages/chatty_messages/schema/chatty_content/content_document.py,sha256=
|
|
160
|
+
letschatty/models/messages/chatty_messages/schema/chatty_content/content_document.py,sha256=sy25r2a2eVxHoJoy_4ZiGSBXz7gYxErXvsq-JWSMLL0,604
|
|
161
161
|
letschatty/models/messages/chatty_messages/schema/chatty_content/content_image.py,sha256=AmILz3zm1HFUh5hjSxjBxLg4AR2DoY9NrBU9NIjjjvk,102
|
|
162
162
|
letschatty/models/messages/chatty_messages/schema/chatty_content/content_interactive.py,sha256=zIdk_-RlTQIAGh9AifnY7Yqs3C7ukyX2RPb-VyhIJ0w,164
|
|
163
163
|
letschatty/models/messages/chatty_messages/schema/chatty_content/content_location.py,sha256=GLk7-QtkksY7G53swRKRiZo481GuhBXA-bsCEBf3B3A,217
|
|
164
|
-
letschatty/models/messages/chatty_messages/schema/chatty_content/content_media.py,sha256
|
|
164
|
+
letschatty/models/messages/chatty_messages/schema/chatty_content/content_media.py,sha256=Y_jm1HkGFORZAr3uxz-cNo4nGGuwdHS6cC0KHu5-9e0,865
|
|
165
165
|
letschatty/models/messages/chatty_messages/schema/chatty_content/content_reaction.py,sha256=ZRCrWXJxUYCnpMikPO6JGj1eeZvQGcj8nKMQkVXw0KA,155
|
|
166
166
|
letschatty/models/messages/chatty_messages/schema/chatty_content/content_sticker.py,sha256=hmV6LQZXorn3iM-tzVk_kxA9BxW4jEiopRjNFY9xcFA,254
|
|
167
167
|
letschatty/models/messages/chatty_messages/schema/chatty_content/content_text.py,sha256=A_gw5WNCcStpSzHC_jzszDvxtv-ChVOJA2po5EHnccs,416
|
|
@@ -202,7 +202,7 @@ letschatty/models/messages/meta_message_model/schema/values/value_messages.py,sh
|
|
|
202
202
|
letschatty/models/messages/meta_message_model/schema/values/value_statuses.py,sha256=SGibpHxdWy6DbzGfJjlrHIxrviqxXbRj54k4rz-fDQM,1966
|
|
203
203
|
letschatty/models/utils/__init__.py,sha256=ziz1DfISJPfq5dE9xjNiwBlwsscHfOks1s0BWD7c1cI,20
|
|
204
204
|
letschatty/models/utils/custom_exceptions/__init__.py,sha256=FrZBwS0o2TuTJcJFRnreRT5v_tv8EY2_EhKL2cAr7PI,32
|
|
205
|
-
letschatty/models/utils/custom_exceptions/custom_exceptions.py,sha256=
|
|
205
|
+
letschatty/models/utils/custom_exceptions/custom_exceptions.py,sha256=mX7AGWsl3qj76lub3wpe8HCUgZEOwnKDYPOw0_3YYOY,11665
|
|
206
206
|
letschatty/models/utils/definitions.py,sha256=ylyTOyDVYpE6UcViTSpxnzoxic4VzPVHE45k2Vc1U6A,826
|
|
207
207
|
letschatty/models/utils/types/CRUD_operations.py,sha256=bRagXYtzeIzM4JSY8mD-4tTaqviUmq_0tY9H74nSS5I,139
|
|
208
208
|
letschatty/models/utils/types/__init__.py,sha256=2z69a0c4s5fWiA9I3thv4P_SbehlvF3e-t757L0vcK4,419
|
|
@@ -244,7 +244,7 @@ letschatty/services/continuous_conversation_service/continuous_conversation_help
|
|
|
244
244
|
letschatty/services/events/events_manager.py,sha256=z2CAc-TqpXkyX9pPHImFXMIDRipLp7e-mN1gRvaM04E,67
|
|
245
245
|
letschatty/services/factories/__init__.py,sha256=cDAQ_0M5xKqZAui5ijHvbtHxn3jFFM4kBcXIXc_Bv38,161
|
|
246
246
|
letschatty/services/factories/analytics/contact_point_factory.py,sha256=5YXkoIwd43gtoYpFUACIXJGCJYyUEqS97XxpR_dBrt4,441
|
|
247
|
-
letschatty/services/factories/analytics/events_factory.py,sha256=
|
|
247
|
+
letschatty/services/factories/analytics/events_factory.py,sha256=o_ojRvzM5X8U3zSzbWqqA2bAWk6TP0YH3CZzPzxC7lY,35384
|
|
248
248
|
letschatty/services/factories/analytics/smart_messages/topics_factory.py,sha256=nJRlOtQFiosafXakZoJj8UCVoiNovjDiOoO52vIh4ok,682
|
|
249
249
|
letschatty/services/factories/analytics/sources/helpers.py,sha256=_2lsaTEhNFqr2ZyO8clMCD-t3MCPTQMyVoSUo5Fio_Y,868
|
|
250
250
|
letschatty/services/factories/analytics/sources/source_factory.py,sha256=kvpS9dLoCsIVnV_wMQzBpFXoN3AyjyphX-IFRg5sO5c,7877
|
|
@@ -274,7 +274,7 @@ letschatty/services/template_campaigns/template_campaign_service.py,sha256=jORgD
|
|
|
274
274
|
letschatty/services/users/agent_service.py,sha256=hIkUUJ1SpkKbh5_uo4i2CeqGtuMTjU7tSV8k5J7WPG4,279
|
|
275
275
|
letschatty/services/users/user_factory.py,sha256=FCB9uiAfjMeYfh4kMdx5h8VDHJ8MCsD-uaxW3X3KaWM,6681
|
|
276
276
|
letschatty/services/validators/analytics_validator.py,sha256=-QBR6XIqEv2qw3stcBQehkwui1EcfWUM6M9DRQODykY,6335
|
|
277
|
-
letschatty-0.4.
|
|
278
|
-
letschatty-0.4.
|
|
279
|
-
letschatty-0.4.
|
|
280
|
-
letschatty-0.4.
|
|
277
|
+
letschatty-0.4.334.dist-info/LICENSE,sha256=EClLu_bO2HBLDcThowIwfaIg5EOwIYhpRsBJjVEk92A,1197
|
|
278
|
+
letschatty-0.4.334.dist-info/METADATA,sha256=1ufTW9UjstNL0GU3fIc9h6Z6e71jgE93skR5bFoxqjc,3283
|
|
279
|
+
letschatty-0.4.334.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
|
280
|
+
letschatty-0.4.334.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|