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.
Files changed (23) hide show
  1. letschatty/models/ai_microservices/expected_output.py +8 -0
  2. letschatty/models/ai_microservices/lambda_events.py +7 -1
  3. letschatty/models/analytics/events/__init__.py +1 -1
  4. letschatty/models/analytics/events/chat_based_events/chat_funnel.py +69 -13
  5. letschatty/models/analytics/events/company_based_events/asset_events.py +6 -2
  6. letschatty/models/analytics/events/event_type_to_classes.py +2 -3
  7. letschatty/models/analytics/events/event_types.py +9 -5
  8. letschatty/models/chat/chat.py +2 -0
  9. letschatty/models/chat/chat_with_assets.py +6 -1
  10. letschatty/models/chat/client.py +0 -2
  11. letschatty/models/company/CRM/funnel.py +365 -33
  12. letschatty/models/company/__init__.py +2 -1
  13. letschatty/models/company/assets/ai_agents_v2/chatty_ai_agent.py +7 -1
  14. letschatty/models/company/assets/automation.py +10 -1
  15. letschatty/models/company/assets/company_assets.py +2 -0
  16. letschatty/models/messages/chatty_messages/schema/chatty_content/content_document.py +4 -2
  17. letschatty/models/messages/chatty_messages/schema/chatty_content/content_media.py +4 -3
  18. letschatty/models/utils/custom_exceptions/custom_exceptions.py +24 -0
  19. letschatty/services/factories/analytics/events_factory.py +5 -3
  20. {letschatty-0.4.332.dist-info → letschatty-0.4.334.dist-info}/METADATA +1 -1
  21. {letschatty-0.4.332.dist-info → letschatty-0.4.334.dist-info}/RECORD +23 -23
  22. {letschatty-0.4.332.dist-info → letschatty-0.4.334.dist-info}/LICENSE +0 -0
  23. {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 ClientFunnel, StageTransition
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 ClientFunnel, StageTransition
7
+ from ....company.CRM.funnel import StageTransition
8
8
  import json
9
9
 
10
+
10
11
  class FunnelEventData(CustomerEventData):
11
- funnel_id: StrObjectId
12
- funnel_stage_transition: StageTransition
13
- time_in_funnel_seconds: Optional[int] = None
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
- """Event for tracking chat funnel stage transitions"""
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
- if info.data.get('type') == EventType.CHAT_FUNNEL_UPDATED and v.time_in_funnel_seconds is None:
32
- raise ValueError("time_in_funnel_seconds must be set for CHAT_FUNNEL_UPDATED events")
33
- if info.data.get('type') == EventType.CHAT_FUNNEL_UPDATED and v.funnel_stage_transition.time_in_previous_stage_seconds is None:
34
- raise ValueError("time_in_previous_stage_seconds must be set for CHAT_FUNNEL_UPDATED events")
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
- if "deleted" not in self.type.value and not self.data.asset:
84
- raise ValueError("asset must be set for all events except DELETED")
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 STAGES
45
- # Funnel-level events
44
+ #CHAT FUNNEL EVENTS
46
45
  EventType.CHAT_FUNNEL_STARTED : ChatFunnelEvent,
47
- EventType.CHAT_FUNNEL_UPDATED : ChatFunnelEvent,
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 STAGES
46
- # Funnel-level events
47
- CHAT_FUNNEL_STARTED = "chat.funnel.started" # New
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
- #FUNNEL STAGES
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"
@@ -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
@@ -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
- 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):
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
- 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)
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
- return self.completed_at is not None
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
- return self.abandoned_at is not None
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
- if not self.entered_current_stage_at:
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
- 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)
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
- 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")
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 : Optional[str] = Field(default=None)
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')
@@ -5,6 +5,8 @@ class CompanyAssetType(StrEnum):
5
5
  USERS = "users"
6
6
  BUSINESS_AREAS = "business_areas"
7
7
  FUNNELS = "funnels"
8
+ FUNNEL_STAGES = "funnel_stages"
9
+ FUNNEL_MEMBERS = "funnel_members"
8
10
  PRODUCTS = "products"
9
11
  SALES = "sales"
10
12
  TAGS = "tags"
@@ -1,5 +1,6 @@
1
1
  from pydantic import BaseModel, Field, model_validator, ValidationInfo
2
2
  from typing import Optional
3
+ from urllib.parse import urlparse, unquote
3
4
  from .content_media import ChattyContentMedia
4
5
 
5
6
  class ChattyContentDocument(ChattyContentMedia):
@@ -8,5 +9,6 @@ class ChattyContentDocument(ChattyContentMedia):
8
9
  @model_validator(mode='before')
9
10
  def validate_filename(cls, data: dict, info: ValidationInfo):
10
11
  if not data.get("filename") and data.get("url"):
11
- data["filename"] = data["url"].split("/")[-1]
12
- return data
12
+ parsed = urlparse(data["url"])
13
+ data["filename"] = unquote(parsed.path.split("/")[-1])
14
+ return data
@@ -1,5 +1,6 @@
1
1
  from pydantic import BaseModel, Field, field_validator, HttpUrl
2
2
  from typing import Optional
3
+ from urllib.parse import quote
3
4
  class ChattyContentMedia(BaseModel):
4
5
  id: Optional[str] = Field(description="Unique identifier for the image. Also known as media_id", default="")
5
6
  url: str = Field(description="URL of the media from S3")
@@ -11,9 +12,9 @@ class ChattyContentMedia(BaseModel):
11
12
  def validate_url(cls, v):
12
13
  if not v:
13
14
  raise ValueError("URL is required")
14
- HttpUrl(v)
15
- return v
15
+ encoded = quote(str(v), safe=":/?&=%#")
16
+ HttpUrl(encoded)
17
+ return encoded
16
18
 
17
19
  def get_body_or_caption(self) -> str:
18
20
  return self.caption
19
-
@@ -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: ClientFunnel, funnel_transition: StageTransition, event_type: EventType, time: datetime, company_snapshot: Optional[CompanyChatsSnapshot] = None, agent_snapshot: Optional[AgentChatsSnapshot] = None) -> List[Event]:
324
+ def funnel_stage_events(chat_with_assets: ChatWithAssets, company_info: EmpresaModel, trace_id: str,executor_type: ExecutorType, executor_id: StrObjectId, chat_funnel: ChatFunnel, funnel_transition: StageTransition, event_type: EventType, time: datetime, company_snapshot: Optional[CompanyChatsSnapshot] = None, agent_snapshot: Optional[AgentChatsSnapshot] = None) -> List[Event]:
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
- funnel_stage_transition=funnel_transition,
338
- time_in_funnel_seconds=chat_funnel.time_in_funnel_seconds
337
+ chat_funnel_id=chat_funnel.id,
338
+ stage_transition=funnel_transition,
339
+ time_in_funnel_seconds=chat_funnel.time_in_funnel_seconds,
340
+ time_in_last_stage_seconds=chat_funnel.time_in_current_stage_seconds
339
341
  )
340
342
  event = ChatFunnelEvent(
341
343
  specversion=EventFactory.package_version(),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: letschatty
3
- Version: 0.4.332
3
+ Version: 0.4.334
4
4
  Summary: Models and custom classes to work across the Chattyverse
5
5
  License: MIT
6
6
  Author: Axel
@@ -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=2JuQvQAmaWPuDiZQyHA4r9yAnVAKgzDA5oxe-xaxKMc,9220
5
- letschatty/models/ai_microservices/lambda_events.py,sha256=meIr4rVb9QEZL0VKziZ46NzGfedOzxuo0CA6NgquYhM,14045
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=-0S6DyvbrIbLb7s0FD2HCg0Uqc9eU0OkLyOmM7KD_tg,2084
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=HmasWF9auAZPvwQRuYyHZvGG8jmqx9TXgiRGD71-m3Y,1699
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=Yc5og6bbkpr9_jH8lWQKxP80TITDsUGip6XPUibG6MI,3265
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=cA7FFZei5-Zbc7HT2ZK7qLheo657wyHhqY1bDr_pBaU,4346
32
- letschatty/models/analytics/events/event_types.py,sha256=CWJCrgCCzANQQr1_AbvqEY3F1EdwVX-0GLd33zVjxnU,5278
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=NJcahnCHmY_RLUP6oKQEuEn1J8ycUsIkc7OruQmn1zs,16137
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=AvpcKz5zyiIxfnLjyVx8JSX9c0UQFhVKoACnyW0bu4Q,574
69
- letschatty/models/chat/client.py,sha256=qMaa-s65lBHMXOpSO2XRtKYeEki7Rr3E79ocKI5kcC4,2299
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=Sn39LUJsrGLAhUFOdhHk5E3yblI1-RDlyCG_VRpOwxo,2510
81
- letschatty/models/company/__init__.py,sha256=5D1Ya_ZRGq2sLf9MXXTLZwGcrZPzo87t1NhRKS7Tw6o,140
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=hRDdyG2dTgmu7KUHHCMviGJ03XiU99gVS5jp3EWyVUs,7992
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=droRF1QeSCMt7jEPxAx8FPReuiVwb3yULGiLGi6nyIs,1296
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=YKRo_uXhQYRQ8HvMf4HhrHTBC2zp7xlE_B0nqSMfMFY,726
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=cxQXOFFZ_kCPs5M7QBAye8_HIPReWgNZPPxayHCCSGY,508
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=-K10IU5Snt9TMU9qiuR9uRrLhF0HUEZ0vZRHP25jKSo,775
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=yzakSiZhyA5boBedGffN8-7W7VRfP6P1kYtRed4in2w,10690
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=SiSrieKtURfG8m3YQsMhzq28h8lsqMSkUm4TkQGAqzE,35268
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.332.dist-info/LICENSE,sha256=EClLu_bO2HBLDcThowIwfaIg5EOwIYhpRsBJjVEk92A,1197
278
- letschatty-0.4.332.dist-info/METADATA,sha256=32bLBqSIgu54CEMcJGo4nXmnoDWPzGSbABK5FuxZLnE,3283
279
- letschatty-0.4.332.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
280
- letschatty-0.4.332.dist-info/RECORD,,
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,,