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

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

Potentially problematic release.


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

Files changed (89) hide show
  1. letschatty/models/ai_microservices/__init__.py +4 -4
  2. letschatty/models/ai_microservices/expected_output.py +29 -2
  3. letschatty/models/ai_microservices/lambda_events.py +155 -28
  4. letschatty/models/ai_microservices/lambda_invokation_types.py +4 -1
  5. letschatty/models/ai_microservices/n8n_ai_agents_payload.py +3 -1
  6. letschatty/models/analytics/events/__init__.py +3 -3
  7. letschatty/models/analytics/events/chat_based_events/ai_agent_execution_event.py +71 -0
  8. letschatty/models/analytics/events/chat_based_events/chat_funnel.py +13 -69
  9. letschatty/models/analytics/events/company_based_events/asset_events.py +2 -9
  10. letschatty/models/analytics/events/event_type_to_classes.py +3 -7
  11. letschatty/models/analytics/events/event_types.py +50 -11
  12. letschatty/models/chat/chat.py +2 -13
  13. letschatty/models/chat/chat_with_assets.py +1 -6
  14. letschatty/models/chat/client.py +2 -0
  15. letschatty/models/chat/continuous_conversation.py +1 -1
  16. letschatty/models/company/CRM/funnel.py +33 -365
  17. letschatty/models/company/__init__.py +1 -7
  18. letschatty/models/company/assets/ai_agents_v2/ai_agents_decision_output.py +1 -1
  19. letschatty/models/company/assets/ai_agents_v2/chatty_ai_agent_in_chat.py +4 -0
  20. letschatty/models/company/assets/ai_agents_v2/chatty_ai_mode.py +2 -2
  21. letschatty/models/company/assets/ai_agents_v2/get_chat_with_prompt_response.py +1 -0
  22. letschatty/models/company/assets/ai_agents_v2/pre_qualify_config.py +28 -1
  23. letschatty/models/company/assets/automation.py +19 -10
  24. letschatty/models/company/assets/chat_assets.py +2 -3
  25. letschatty/models/company/assets/company_assets.py +0 -2
  26. letschatty/models/company/assets/sale.py +3 -3
  27. letschatty/models/company/empresa.py +1 -2
  28. letschatty/models/data_base/collection_interface.py +101 -29
  29. letschatty/models/data_base/mongo_connection.py +92 -9
  30. letschatty/models/messages/chatty_messages/schema/chatty_content/content_document.py +2 -4
  31. letschatty/models/messages/chatty_messages/schema/chatty_content/content_media.py +3 -4
  32. letschatty/models/utils/custom_exceptions/custom_exceptions.py +14 -1
  33. letschatty/services/ai_agents/smart_follow_up_context_builder_v2.py +5 -2
  34. letschatty/services/chat/chat_service.py +11 -47
  35. letschatty/services/chatty_assets/__init__.py +12 -0
  36. letschatty/services/chatty_assets/asset_service.py +190 -13
  37. letschatty/services/chatty_assets/assets_collections.py +137 -0
  38. letschatty/services/chatty_assets/base_container.py +3 -2
  39. letschatty/services/chatty_assets/base_container_with_collection.py +35 -26
  40. letschatty/services/chatty_assets/collections/__init__.py +38 -0
  41. letschatty/services/chatty_assets/collections/ai_agent_collection.py +19 -0
  42. letschatty/services/chatty_assets/collections/ai_agent_in_chat_collection.py +32 -0
  43. letschatty/services/chatty_assets/collections/ai_component_collection.py +21 -0
  44. letschatty/services/chatty_assets/collections/chain_of_thought_collection.py +30 -0
  45. letschatty/services/chatty_assets/collections/chat_collection.py +21 -0
  46. letschatty/services/chatty_assets/collections/contact_point_collection.py +21 -0
  47. letschatty/services/chatty_assets/collections/fast_answer_collection.py +21 -0
  48. letschatty/services/chatty_assets/collections/filter_criteria_collection.py +18 -0
  49. letschatty/services/chatty_assets/collections/flow_collection.py +20 -0
  50. letschatty/services/chatty_assets/collections/product_collection.py +20 -0
  51. letschatty/services/chatty_assets/collections/sale_collection.py +20 -0
  52. letschatty/services/chatty_assets/collections/source_collection.py +21 -0
  53. letschatty/services/chatty_assets/collections/tag_collection.py +19 -0
  54. letschatty/services/chatty_assets/collections/topic_collection.py +21 -0
  55. letschatty/services/chatty_assets/collections/user_collection.py +20 -0
  56. letschatty/services/chatty_assets/example_usage.py +44 -0
  57. letschatty/services/chatty_assets/services/__init__.py +37 -0
  58. letschatty/services/chatty_assets/services/ai_agent_in_chat_service.py +73 -0
  59. letschatty/services/chatty_assets/services/ai_agent_service.py +23 -0
  60. letschatty/services/chatty_assets/services/chain_of_thought_service.py +70 -0
  61. letschatty/services/chatty_assets/services/chat_service.py +25 -0
  62. letschatty/services/chatty_assets/services/contact_point_service.py +29 -0
  63. letschatty/services/chatty_assets/services/fast_answer_service.py +32 -0
  64. letschatty/services/chatty_assets/services/filter_criteria_service.py +30 -0
  65. letschatty/services/chatty_assets/services/flow_service.py +25 -0
  66. letschatty/services/chatty_assets/services/product_service.py +30 -0
  67. letschatty/services/chatty_assets/services/sale_service.py +25 -0
  68. letschatty/services/chatty_assets/services/source_service.py +28 -0
  69. letschatty/services/chatty_assets/services/tag_service.py +32 -0
  70. letschatty/services/chatty_assets/services/topic_service.py +31 -0
  71. letschatty/services/chatty_assets/services/user_service.py +32 -0
  72. letschatty/services/continuous_conversation_service/continuous_conversation_helper.py +11 -0
  73. letschatty/services/events/__init__.py +6 -0
  74. letschatty/services/events/events_manager.py +218 -1
  75. letschatty/services/factories/analytics/ai_agent_event_factory.py +161 -0
  76. letschatty/services/factories/analytics/events_factory.py +66 -30
  77. letschatty/services/factories/lambda_ai_orchestrartor/lambda_events_factory.py +46 -8
  78. letschatty/services/messages_helpers/get_caption_or_body_or_preview.py +6 -4
  79. letschatty/services/validators/analytics_validator.py +0 -11
  80. {letschatty-0.4.349.dist-info → letschatty-0.4.351.dist-info}/METADATA +1 -1
  81. {letschatty-0.4.349.dist-info → letschatty-0.4.351.dist-info}/RECORD +83 -53
  82. letschatty/models/analytics/events/chat_based_events/chat_client.py +0 -19
  83. letschatty/models/company/integrations/product_sync_status.py +0 -28
  84. letschatty/models/company/integrations/shopify/company_shopify_integration.py +0 -62
  85. letschatty/models/company/integrations/shopify/shopify_product_sync_status.py +0 -18
  86. letschatty/models/company/integrations/shopify/shopify_webhook_topics.py +0 -40
  87. letschatty/models/company/integrations/sync_status_enum.py +0 -9
  88. {letschatty-0.4.349.dist-info → letschatty-0.4.351.dist-info}/LICENSE +0 -0
  89. {letschatty-0.4.349.dist-info → letschatty-0.4.351.dist-info}/WHEEL +0 -0
@@ -5,8 +5,6 @@ class EventType(StrEnum):
5
5
  CHAT_CREATED = "chat.created"
6
6
  CHAT_STATUS_UPDATED = "chat.status_updated"
7
7
  CHAT_DELETED = "chat.deleted"
8
- ##CHAT CLIENT EVENTS
9
- CHAT_CLIENT_UPDATED = "chat.client.updated"
10
8
  #TAGS
11
9
  TAG_ASSIGNED = "chat.tag.assigned"
12
10
  TAG_REMOVED = "chat.tag.removed"
@@ -14,6 +12,51 @@ class EventType(StrEnum):
14
12
  AI_AGENT_ASSIGNED_TO_CHAT = "chat.chatty_ai_agent.assigned_to_chat"
15
13
  AI_AGENT_REMOVED_FROM_CHAT = "chat.chatty_ai_agent.removed_from_chat"
16
14
  AI_AGENT_UPDATED_ON_CHAT = "chat.chatty_ai_agent.updated_on_chat"
15
+
16
+ #CHATTY AI AGENT EXECUTION EVENTS - 3-level hierarchy for execution tracking
17
+ # Pattern: chatty_ai_agent_in_chat.{operation}.{detail}
18
+ # Note: Execution events are already chat-scoped via CustomerEventData
19
+
20
+ # TRIGGER EVENTS - What initiates AI agent processing
21
+ CHATTY_AI_AGENT_IN_CHAT_TRIGGER_USER_MESSAGE = "chatty_ai_agent_in_chat.trigger.user_message"
22
+ CHATTY_AI_AGENT_IN_CHAT_TRIGGER_FOLLOW_UP = "chatty_ai_agent_in_chat.trigger.follow_up"
23
+ CHATTY_AI_AGENT_IN_CHAT_TRIGGER_MANUAL = "chatty_ai_agent_in_chat.trigger.manual"
24
+ CHATTY_AI_AGENT_IN_CHAT_TRIGGER_RETRY = "chatty_ai_agent_in_chat.trigger.retry"
25
+
26
+ # STATE EVENTS - AI agent state changes
27
+ CHATTY_AI_AGENT_IN_CHAT_STATE_PROCESSING_STARTED = "chatty_ai_agent_in_chat.state.processing_started"
28
+ CHATTY_AI_AGENT_IN_CHAT_STATE_CALL_STARTED = "chatty_ai_agent_in_chat.state.call_started"
29
+ CHATTY_AI_AGENT_IN_CHAT_STATE_ESCALATED = "chatty_ai_agent_in_chat.state.escalated"
30
+ CHATTY_AI_AGENT_IN_CHAT_STATE_UNESCALATED = "chatty_ai_agent_in_chat.state.unescalated"
31
+
32
+ # CALL EVENTS - Outbound calls to services
33
+ CHATTY_AI_AGENT_IN_CHAT_CALL_GET_CHAT_WITH_PROMPT = "chatty_ai_agent_in_chat.call.get_chat_with_prompt"
34
+ CHATTY_AI_AGENT_IN_CHAT_CALL_TAGGER = "chatty_ai_agent_in_chat.call.tagger"
35
+ CHATTY_AI_AGENT_IN_CHAT_CALL_DOUBLE_CHECKER = "chatty_ai_agent_in_chat.call.double_checker"
36
+ CHATTY_AI_AGENT_IN_CHAT_CALL_DEBUGGER = "chatty_ai_agent_in_chat.call.debugger"
37
+
38
+ # CALLBACK EVENTS - Responses received from services
39
+ CHATTY_AI_AGENT_IN_CHAT_CALLBACK_GET_CHAT_WITH_PROMPT = "chatty_ai_agent_in_chat.callback.get_chat_with_prompt"
40
+ CHATTY_AI_AGENT_IN_CHAT_CALLBACK_TAGGER = "chatty_ai_agent_in_chat.callback.tagger"
41
+ CHATTY_AI_AGENT_IN_CHAT_CALLBACK_DOUBLE_CHECKER = "chatty_ai_agent_in_chat.callback.double_checker"
42
+ CHATTY_AI_AGENT_IN_CHAT_CALLBACK_OUTPUT_RECEIVED = "chatty_ai_agent_in_chat.callback.output_received"
43
+
44
+ # DECISION EVENTS - AI agent decisions and actions
45
+ CHATTY_AI_AGENT_IN_CHAT_DECISION_SEND = "chatty_ai_agent_in_chat.decision.send"
46
+ CHATTY_AI_AGENT_IN_CHAT_DECISION_SUGGEST = "chatty_ai_agent_in_chat.decision.suggest"
47
+ CHATTY_AI_AGENT_IN_CHAT_DECISION_ESCALATE = "chatty_ai_agent_in_chat.decision.escalate"
48
+ CHATTY_AI_AGENT_IN_CHAT_DECISION_SKIP = "chatty_ai_agent_in_chat.decision.skip"
49
+ CHATTY_AI_AGENT_IN_CHAT_DECISION_SENT_TO_API = "chatty_ai_agent_in_chat.decision.sent_to_api"
50
+ CHATTY_AI_AGENT_IN_CHAT_DECISION_COMPLETED = "chatty_ai_agent_in_chat.decision.completed"
51
+
52
+ # ERROR EVENTS - Failures and cancellations
53
+ CHATTY_AI_AGENT_IN_CHAT_ERROR_CALL_FAILED = "chatty_ai_agent_in_chat.error.call_failed"
54
+ CHATTY_AI_AGENT_IN_CHAT_ERROR_CALL_CANCELLED = "chatty_ai_agent_in_chat.error.call_cancelled"
55
+ CHATTY_AI_AGENT_IN_CHAT_ERROR_VALIDATION_FAILED = "chatty_ai_agent_in_chat.error.validation_failed"
56
+
57
+ # RATING EVENTS - User feedback
58
+ CHATTY_AI_AGENT_IN_CHAT_RATING_RECEIVED = "chatty_ai_agent_in_chat.rating.received"
59
+
17
60
  #PRODUCTS
18
61
  PRODUCT_ASSIGNED = "chat.product.assigned"
19
62
  PRODUCT_REMOVED = "chat.product.removed"
@@ -44,9 +87,10 @@ class EventType(StrEnum):
44
87
  #CONTINUOUS CONVERSATION
45
88
  CONTINUOUS_CONVERSATION_CREATED = "chat.continuous_conversation.created"
46
89
  CONTINUOUS_CONVERSATION_UPDATED = "chat.continuous_conversation.updated"
47
- #CHAT FUNNEL EVENTS
48
- CHAT_FUNNEL_STARTED = "chat.funnel.started"
49
- CHAT_FUNNEL_STAGE_CHANGED = "chat.funnel.stage_changed"
90
+ #FUNNEL STAGES
91
+ # Funnel-level events
92
+ CHAT_FUNNEL_STARTED = "chat.funnel.started" # New
93
+ CHAT_FUNNEL_UPDATED = "chat.funnel.updated"
50
94
  CHAT_FUNNEL_COMPLETED = "chat.funnel.completed"
51
95
  CHAT_FUNNEL_ABANDONED = "chat.funnel.abandoned"
52
96
 
@@ -85,18 +129,13 @@ class EventType(StrEnum):
85
129
  FAST_ANSWER_CREATED = "company.fast_answer.created"
86
130
  FAST_ANSWER_UPDATED = "company.fast_answer.updated"
87
131
  FAST_ANSWER_DELETED = "company.fast_answer.deleted"
88
- #FUNNELS
132
+ #FUNNEL STAGES
89
133
  FUNNEL_CREATED = "company.funnel.created"
90
134
  FUNNEL_UPDATED = "company.funnel.updated"
91
135
  FUNNEL_DELETED = "company.funnel.deleted"
92
- #FUNNEL STAGES
93
136
  FUNNEL_STAGE_CREATED = "company.funnel_stage.created"
94
137
  FUNNEL_STAGE_UPDATED = "company.funnel_stage.updated"
95
138
  FUNNEL_STAGE_DELETED = "company.funnel_stage.deleted"
96
- #FUNNEL MEMBERS
97
- FUNNEL_MEMBER_ADDED = "company.funnel_member.added"
98
- FUNNEL_MEMBER_UPDATED = "company.funnel_member.updated"
99
- FUNNEL_MEMBER_REMOVED = "company.funnel_member.removed"
100
139
  #BUSINESS AREA
101
140
  BUSINESS_AREA_CREATED = "company.business_area.created"
102
141
  BUSINESS_AREA_UPDATED = "company.business_area.updated"
@@ -19,7 +19,6 @@ from ..utils.custom_exceptions.custom_exceptions import MissingAIAgentForSmartFo
19
19
  from .time_left import TimeLeft
20
20
  from ..company.conversation_topic import TopicTimelineEntry
21
21
  from ..company.form_field import CollectedData
22
- from ..company.CRM.funnel import ActiveFunnel
23
22
  import json
24
23
  import logging
25
24
  logger = logging.getLogger(__name__)
@@ -48,7 +47,6 @@ class Chat(CompanyAssetModel):
48
47
  chatty_ai_agent: Optional[ChattyAIAgentAssignedToChat] = Field(default=None, description="The id of the chatty ai agent that might or might not be assigned to the chat")
49
48
  suggested_messages: List[MessageDraft] = Field(default_factory=list)
50
49
  topics_timeline: List[TopicTimelineEntry] = Field(default_factory=list, description="Timeline of conversation topics throughout the conversation")
51
- active_funnel: Optional[ActiveFunnel] = Field(default=None, description="Current active funnel for this chat")
52
50
  model_config = ConfigDict(extra="ignore")
53
51
 
54
52
  @property
@@ -269,17 +267,7 @@ class Chat(CompanyAssetModel):
269
267
  @property
270
268
  def bought_product_ids(self) -> List[StrObjectId]:
271
269
  """Get all sale ids in the chat"""
272
- product_ids: List[StrObjectId] = []
273
- for sale in self.sales:
274
- sale_product_ids = getattr(sale, "product_ids", None)
275
- if sale_product_ids:
276
- for product_id in sale_product_ids:
277
- if product_id not in product_ids:
278
- product_ids.append(product_id)
279
- elif sale.product_id:
280
- if sale.product_id not in product_ids:
281
- product_ids.append(sale.product_id)
282
- return product_ids
270
+ return [sale.product_id for sale in self.sales]
283
271
 
284
272
  @property
285
273
  def products(self) -> List[AssignedAssetToChat]:
@@ -376,3 +364,4 @@ class Chat(CompanyAssetModel):
376
364
  dump["last_message"] = self.last_message.model_dump_json(serializer=SerializerType.DATABASE) if self.last_message else None
377
365
  dump["flow_states"] = [flow_state.model_dump_json(serializer=SerializerType.DATABASE) for flow_state in self.flow_states]
378
366
  return dump
367
+
@@ -1,6 +1,6 @@
1
+
1
2
  from .chat import Chat
2
3
  from letschatty.models.company.assets import TagPreview, FlowPreview, ContactPoint, Sale, ChattyAIAgentPreview, ProductPreview
3
- from letschatty.models.company.CRM.funnel import ActiveFunnel
4
4
 
5
5
  from letschatty.models.utils.types.serializer_type import SerializerType
6
6
  from pydantic import BaseModel
@@ -15,8 +15,3 @@ class ChatWithAssets(BaseModel):
15
15
  contact_points: List[ContactPoint]
16
16
  flows_links_states: List[FlowPreview]
17
17
  chatty_ai_agent: Optional[ChattyAIAgentPreview]
18
-
19
- @property
20
- def active_funnel(self) -> Optional[ActiveFunnel]:
21
- """Convenience property to access the chat's active funnel"""
22
- return self.chat.active_funnel
@@ -5,6 +5,7 @@ from ..utils.types.identifier import StrObjectId
5
5
  from .quality_scoring import QualityScore
6
6
  from .highlight import Highlight
7
7
  from ..company.assets.chat_assets import AssignedAssetToChat, SaleAssignedToChat, ContactPointAssignedToChat
8
+ from ..company.CRM.funnel import ClientFunnel
8
9
  from ..utils.types.serializer_type import SerializerType
9
10
 
10
11
  class Client(CompanyAssetModel):
@@ -22,6 +23,7 @@ class Client(CompanyAssetModel):
22
23
  contact_points: List[ContactPointAssignedToChat] = Field(default=list())
23
24
  business_area: Optional[StrObjectId] = Field(default=None, description="It's a business related area, that works as a queue for the chats")
24
25
  external_id: Optional[str] = Field(default=None)
26
+ funnels : List[ClientFunnel] = Field(default=list())
25
27
  exclude_fields = {
26
28
  SerializerType.FRONTEND: {"products", "tags", "sales", "contact_points", "highlights"}
27
29
  }
@@ -30,7 +30,7 @@ class ContinuousConversation(ChattyAssetModel):
30
30
  template_message_waid: Optional[str] = None
31
31
  status: Optional[ContinuousConversationStatus] = Field(default=ContinuousConversationStatus.CREATED)
32
32
  active: bool = Field(default=True)
33
- expires_at: datetime = Field(default_factory=lambda: datetime.now(ZoneInfo("UTC")) + timedelta(days=10))
33
+ expires_at: datetime = Field(default=datetime.now(ZoneInfo("UTC")) + timedelta(days=10))
34
34
  messages: List[MessageDraft]
35
35
  creator_id: StrObjectId
36
36
  forced_send: bool = Field(default=False)
@@ -1,401 +1,69 @@
1
- from ...base_models import CompanyAssetModel, ChattyAssetPreview, ChattyAssetModel
2
- from typing import List, Optional, ClassVar, Any
1
+ from ...base_models import CompanyAssetModel
2
+ from typing import List
3
3
  from ...utils.types.identifier import StrObjectId
4
- from pydantic import BaseModel, Field, ConfigDict
4
+ from pydantic import BaseModel, Field
5
5
  from enum import StrEnum
6
6
  from datetime import datetime
7
+ from typing import Optional
7
8
  from zoneinfo import ZoneInfo
8
9
  from ...utils.types.executor_types import ExecutorType
9
- from ..assets.automation import Automation
10
-
11
-
12
- # ============================================================================
13
- # Enums
14
- # ============================================================================
15
10
 
16
11
  class FunnelStatus(StrEnum):
17
- """Status of a chat within a funnel"""
18
12
  IN_PROGRESS = "in_progress"
19
13
  COMPLETED = "completed"
20
14
  ABANDONED = "abandoned"
21
15
 
22
-
23
- class FunnelMemberRole(StrEnum):
24
- """Role of a user within a funnel"""
25
- ADMIN = "admin" # Full control over funnel settings, stages, and members
26
- EDITOR = "editor" # Can move chats between stages, view all chats
27
- VIEWER = "viewer" # Read-only access to funnel and chats
28
-
29
-
30
- # ============================================================================
31
- # Embedded Models (BaseModel)
32
- # ============================================================================
33
-
34
16
  class StageTransition(BaseModel):
35
- """
36
- 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"""
17
+ from_stage_id: Optional[StrObjectId] = None
18
+ to_stage_id: StrObjectId
19
+ transitioned_at: datetime = Field(default=datetime.now(ZoneInfo("UTC")))
20
+ executor_type: ExecutorType
21
+ executor_id: StrObjectId
22
+ time_in_previous_stage_seconds: Optional[int] = None
23
+
24
+ class ClientFunnel(CompanyAssetModel):
132
25
  funnel_id: StrObjectId
133
- 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
- )
26
+ status: FunnelStatus
27
+ started_at: datetime = Field(default=datetime.now(ZoneInfo("UTC")))
28
+ completed_at: Optional[datetime] = None
29
+ abandoned_at: Optional[datetime] = None
30
+ current_stage_id: Optional[StrObjectId] = None
31
+ entered_current_stage_at: datetime
32
+ stage_transitions: List[StageTransition] = Field(default_factory=list)
286
33
 
287
34
  @property
288
35
  def is_completed(self) -> bool:
289
- """Check if the chat has completed the funnel"""
290
- return self.status == FunnelStatus.COMPLETED and self.completed_at is not None
36
+ return self.completed_at is not None
291
37
 
292
38
  @property
293
39
  def is_abandoned(self) -> bool:
294
- """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
40
+ return self.abandoned_at is not None
301
41
 
302
42
  @property
303
43
  def time_in_funnel_seconds(self) -> int:
304
- """Calculate total time spent in the funnel"""
305
44
  end_time = self.completed_at or self.abandoned_at or datetime.now(tz=ZoneInfo("UTC"))
306
45
  return int((end_time - self.started_at).total_seconds())
307
46
 
308
47
  @property
309
48
  def time_in_current_stage_seconds(self) -> Optional[int]:
310
- """Calculate time spent in the current stage"""
311
- if not self.entered_current_stage_at or not self.current_stage_id:
49
+ if not self.entered_current_stage_at:
312
50
  return None
313
51
  return int((datetime.now(tz=ZoneInfo("UTC")) - self.entered_current_stage_at).total_seconds())
314
52
 
315
53
  @property
316
54
  def unique_stages_visited(self) -> int:
317
- """Count of unique stages the chat has visited"""
318
55
  return len(set(t.to_stage_id for t in self.stage_transitions))
319
56
 
320
- @property
321
- def total_transitions(self) -> int:
322
- """Total number of stage transitions"""
323
- return len(self.stage_transitions)
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)
329
-
330
- @property
331
- def last_transition(self) -> Optional[StageTransition]:
332
- """Get the most recent stage transition"""
333
- return self.stage_transitions[-1] if self.stage_transitions else None
334
-
335
- def record_transition(
336
- self,
337
- to_stage_id: StrObjectId,
338
- executor_type: ExecutorType,
339
- executor_id: StrObjectId,
340
- from_stage_order: Optional[int] = None,
341
- to_stage_order: Optional[int] = None
342
- ) -> StageTransition:
343
- """
344
- Record a transition to a new stage.
345
- Returns the created StageTransition.
346
-
347
- Args:
348
- to_stage_id: The stage to transition to
349
- executor_type: Who/what triggered the transition
350
- executor_id: ID of the executor
351
- from_stage_order: Order of current stage (for regression detection)
352
- to_stage_order: Order of target stage (for regression detection)
353
- """
354
- time_in_previous = self.time_in_current_stage_seconds
355
-
356
- transition = StageTransition(
357
- from_stage_id=self.current_stage_id,
358
- to_stage_id=to_stage_id,
359
- executor_type=executor_type,
360
- executor_id=executor_id,
361
- time_in_previous_stage_seconds=time_in_previous,
362
- from_stage_order=from_stage_order,
363
- to_stage_order=to_stage_order
364
- )
365
-
366
- self.stage_transitions.append(transition)
367
- self.current_stage_id = to_stage_id
368
- self.entered_current_stage_at = transition.transitioned_at
369
-
370
- return transition
371
-
372
- def complete(self) -> None:
373
- """Mark the funnel as completed"""
374
- self.status = FunnelStatus.COMPLETED
375
- self.completed_at = datetime.now(tz=ZoneInfo("UTC"))
57
+ class FunnelStage(CompanyAssetModel):
58
+ name: str
59
+ description: str
60
+ index: int
61
+ inflexion_conversion_point: bool = Field(default=False, description="If true, it'll be prioritized for chat assignment and will send notifications in automatic mode to all agents.")
62
+ workflow_ids: List[StrObjectId] = Field(default_factory=list)
376
63
 
377
- def abandon(self) -> None:
378
- """Mark the funnel as abandoned"""
379
- self.status = FunnelStatus.ABANDONED
380
- self.abandoned_at = datetime.now(tz=ZoneInfo("UTC"))
381
64
 
382
- 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
- )
65
+ class Funnel(CompanyAssetModel):
66
+ name: str
67
+ description: str
68
+ stages: List[FunnelStage]
69
+ assignment_priority: int = Field(ge=0, le=10, description="Priority for chat assignment, between funnels")
@@ -1,9 +1,3 @@
1
1
  from .assets import *
2
2
  from .empresa import EmpresaModel
3
- from .form_field import FormField, FormFieldPreview, CollectedData, SystemFormFields
4
- from .CRM.funnel import Funnel, FunnelPreview, ChatFunnel, StageTransition, FunnelStatus
5
- from .integrations.product_sync_status import ProductSyncStatus
6
- from .integrations.sync_status_enum import SyncStatusEnum
7
- from .integrations.shopify.shopify_webhook_topics import ShopifyWebhookTopic
8
- from .integrations.shopify.company_shopify_integration import ShopifyIntegration
9
- from .integrations.shopify.shopify_product_sync_status import ShopifyProductSyncStatus, ShopifyProductSyncStatusEnum
3
+ from .form_field import FormField, FormFieldPreview, CollectedData, SystemFormFields
@@ -55,7 +55,7 @@ class SmartFollowUpDecision(BaseModel):
55
55
  SmartFollowUpDecisionAction.POSTPONE_DELTA_TIME]:
56
56
  # Postpone actions don't require messages
57
57
  if self.messages is not None and len(self.messages) > 0:
58
- raise ValueError("Messages are not allowed when action is postpone/postponed")
58
+ raise ValueError("Messages are not allowed when action is postpone")
59
59
  else:
60
60
  raise ValueError(f"Invalid action: {self.action}")
61
61
 
@@ -92,6 +92,10 @@ class ChattyAIAgentInChat(CompanyAssetModel):
92
92
  default=None,
93
93
  description="If the trigger is a user message, this will be the id of the incoming message"
94
94
  )
95
+ last_reset_message_id: Optional[str] = Field(
96
+ default=None,
97
+ description="Last reset control trigger message id handled for this chat"
98
+ )
95
99
 
96
100
  # Assignment metadata
97
101
  assigned_at: datetime = Field(default_factory=lambda: datetime.now(ZoneInfo("UTC")))