letschatty 0.4.280__py3-none-any.whl → 0.4.343__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 (92) hide show
  1. letschatty/models/ai_microservices/__init__.py +3 -3
  2. letschatty/models/ai_microservices/expected_output.py +35 -1
  3. letschatty/models/ai_microservices/lambda_events.py +85 -45
  4. letschatty/models/ai_microservices/lambda_invokation_types.py +6 -3
  5. letschatty/models/analytics/events/__init__.py +2 -3
  6. letschatty/models/analytics/events/chat_based_events/chat_funnel.py +69 -13
  7. letschatty/models/analytics/events/company_based_events/asset_events.py +9 -2
  8. letschatty/models/analytics/events/event_type_to_classes.py +6 -3
  9. letschatty/models/analytics/events/event_types.py +13 -50
  10. letschatty/models/chat/chat.py +14 -2
  11. letschatty/models/chat/chat_with_assets.py +6 -1
  12. letschatty/models/chat/client.py +0 -2
  13. letschatty/models/chat/continuous_conversation.py +1 -1
  14. letschatty/models/company/CRM/funnel.py +365 -33
  15. letschatty/models/company/__init__.py +3 -1
  16. letschatty/models/company/assets/ai_agents_v2/ai_agent_message_draft.py +58 -0
  17. letschatty/models/company/assets/ai_agents_v2/ai_agents_decision_output.py +1 -1
  18. letschatty/models/company/assets/ai_agents_v2/chain_of_thought_in_chat.py +5 -3
  19. letschatty/models/company/assets/ai_agents_v2/chatty_ai_agent.py +46 -2
  20. letschatty/models/company/assets/ai_agents_v2/chatty_ai_agent_in_chat.py +93 -1
  21. letschatty/models/company/assets/ai_agents_v2/pre_qualify_config.py +111 -0
  22. letschatty/models/company/assets/ai_agents_v2/statuses.py +33 -0
  23. letschatty/models/company/assets/assignment/__init__.py +14 -0
  24. letschatty/models/company/assets/assignment/assignment_assets.py +75 -0
  25. letschatty/models/company/assets/automation.py +10 -19
  26. letschatty/models/company/assets/chat_assets.py +12 -2
  27. letschatty/models/company/assets/company_assets.py +3 -0
  28. letschatty/models/company/assets/launch/__init__.py +12 -0
  29. letschatty/models/company/assets/launch/launch.py +128 -0
  30. letschatty/models/company/assets/launch/scheduled_communication.py +44 -0
  31. letschatty/models/company/assets/launch/subscription.py +63 -0
  32. letschatty/models/company/assets/sale.py +3 -3
  33. letschatty/models/company/assets/users/user.py +5 -1
  34. letschatty/models/company/company_messaging_settgins.py +2 -1
  35. letschatty/models/company/form_field.py +182 -12
  36. letschatty/models/data_base/collection_interface.py +29 -101
  37. letschatty/models/data_base/mongo_connection.py +9 -92
  38. letschatty/models/messages/chatty_messages/schema/chatty_content/content_document.py +4 -2
  39. letschatty/models/messages/chatty_messages/schema/chatty_content/content_media.py +4 -3
  40. letschatty/models/utils/custom_exceptions/custom_exceptions.py +24 -13
  41. letschatty/services/ai_agents/smart_follow_up_service_v2.py +10 -0
  42. letschatty/services/chat/chat_service.py +79 -14
  43. letschatty/services/chatty_assets/__init__.py +0 -12
  44. letschatty/services/chatty_assets/asset_service.py +13 -190
  45. letschatty/services/chatty_assets/base_container.py +2 -3
  46. letschatty/services/chatty_assets/base_container_with_collection.py +26 -35
  47. letschatty/services/continuous_conversation_service/continuous_conversation_helper.py +0 -11
  48. letschatty/services/events/events_manager.py +1 -218
  49. letschatty/services/factories/analytics/events_factory.py +6 -66
  50. letschatty/services/factories/lambda_ai_orchestrartor/lambda_events_factory.py +8 -23
  51. letschatty/services/users/user_factory.py +14 -8
  52. letschatty/services/validators/analytics_validator.py +11 -0
  53. {letschatty-0.4.280.dist-info → letschatty-0.4.343.dist-info}/METADATA +1 -1
  54. {letschatty-0.4.280.dist-info → letschatty-0.4.343.dist-info}/RECORD +56 -83
  55. {letschatty-0.4.280.dist-info → letschatty-0.4.343.dist-info}/WHEEL +1 -1
  56. letschatty/models/analytics/events/chat_based_events/ai_agent_execution_event.py +0 -71
  57. letschatty/services/chatty_assets/assets_collections.py +0 -137
  58. letschatty/services/chatty_assets/collections/__init__.py +0 -38
  59. letschatty/services/chatty_assets/collections/ai_agent_collection.py +0 -19
  60. letschatty/services/chatty_assets/collections/ai_agent_in_chat_collection.py +0 -32
  61. letschatty/services/chatty_assets/collections/ai_component_collection.py +0 -21
  62. letschatty/services/chatty_assets/collections/chain_of_thought_collection.py +0 -30
  63. letschatty/services/chatty_assets/collections/chat_collection.py +0 -21
  64. letschatty/services/chatty_assets/collections/contact_point_collection.py +0 -21
  65. letschatty/services/chatty_assets/collections/fast_answer_collection.py +0 -21
  66. letschatty/services/chatty_assets/collections/filter_criteria_collection.py +0 -18
  67. letschatty/services/chatty_assets/collections/flow_collection.py +0 -20
  68. letschatty/services/chatty_assets/collections/product_collection.py +0 -20
  69. letschatty/services/chatty_assets/collections/sale_collection.py +0 -20
  70. letschatty/services/chatty_assets/collections/source_collection.py +0 -21
  71. letschatty/services/chatty_assets/collections/tag_collection.py +0 -19
  72. letschatty/services/chatty_assets/collections/topic_collection.py +0 -21
  73. letschatty/services/chatty_assets/collections/user_collection.py +0 -20
  74. letschatty/services/chatty_assets/example_usage.py +0 -44
  75. letschatty/services/chatty_assets/services/__init__.py +0 -37
  76. letschatty/services/chatty_assets/services/ai_agent_in_chat_service.py +0 -73
  77. letschatty/services/chatty_assets/services/ai_agent_service.py +0 -23
  78. letschatty/services/chatty_assets/services/chain_of_thought_service.py +0 -70
  79. letschatty/services/chatty_assets/services/chat_service.py +0 -25
  80. letschatty/services/chatty_assets/services/contact_point_service.py +0 -29
  81. letschatty/services/chatty_assets/services/fast_answer_service.py +0 -32
  82. letschatty/services/chatty_assets/services/filter_criteria_service.py +0 -30
  83. letschatty/services/chatty_assets/services/flow_service.py +0 -25
  84. letschatty/services/chatty_assets/services/product_service.py +0 -30
  85. letschatty/services/chatty_assets/services/sale_service.py +0 -25
  86. letschatty/services/chatty_assets/services/source_service.py +0 -28
  87. letschatty/services/chatty_assets/services/tag_service.py +0 -32
  88. letschatty/services/chatty_assets/services/topic_service.py +0 -31
  89. letschatty/services/chatty_assets/services/user_service.py +0 -32
  90. letschatty/services/events/__init__.py +0 -6
  91. letschatty/services/factories/analytics/ai_agent_event_factory.py +0 -161
  92. {letschatty-0.4.280.dist-info → letschatty-0.4.343.dist-info}/LICENSE +0 -0
@@ -7,15 +7,16 @@ independently without loading entire chat documents.
7
7
  """
8
8
 
9
9
  from letschatty.models.analytics.events.base import EventType
10
+ from enum import StrEnum
10
11
  from pydantic import BaseModel, Field, field_validator, ConfigDict
11
12
  from datetime import datetime
12
13
  from zoneinfo import ZoneInfo
13
14
  from typing import Optional, ClassVar, List, Dict, Any
14
- from enum import StrEnum
15
15
  from letschatty.models.base_models.chatty_asset_model import CompanyAssetModel, ChattyAssetPreview
16
16
  from letschatty.models.utils.types.identifier import StrObjectId
17
17
  from .chain_of_thought_in_chat import ChainOfThoughtInChatTrigger
18
18
  from .chatty_ai_mode import ChattyAIMode
19
+ from .statuses import DataCollectionStatus, PreQualifyStatus
19
20
  import logging
20
21
 
21
22
  logger = logging.getLogger(__name__)
@@ -98,6 +99,20 @@ class ChattyAIAgentInChat(CompanyAssetModel):
98
99
 
99
100
  events: List[SimplifiedExecutionEvent] = Field(default_factory=list, description="Simplified events for UI visibility")
100
101
 
102
+ # Data collection status (for agents with pre_qualify.form_fields)
103
+ data_collection_status: Optional[DataCollectionStatus] = Field(
104
+ default=None,
105
+ description="Status of data collection. None if agent has no form_fields, "
106
+ "otherwise tracks progress of data collection."
107
+ )
108
+
109
+ # Pre-qualification status (for agents with pre_qualify config)
110
+ pre_qualify_status: Optional[PreQualifyStatus] = Field(
111
+ default=None,
112
+ description="Status of pre-qualification. None if agent has no pre_qualify config, "
113
+ "otherwise tracks qualification evaluation."
114
+ )
115
+
101
116
  # Preview class
102
117
  preview_class: ClassVar[type[ChattyAssetPreview]] = ChattyAssetPreview
103
118
 
@@ -106,6 +121,8 @@ class ChattyAIAgentInChat(CompanyAssetModel):
106
121
  validate_by_alias=True
107
122
  )
108
123
 
124
+
125
+
109
126
  @property
110
127
  def requires_human_intervention(self) -> bool:
111
128
  """Check if the AI agent requires human intervention"""
@@ -229,3 +246,78 @@ class ChattyAIAgentInChat(CompanyAssetModel):
229
246
  self.human_intervention = None
230
247
  self.updated_at = datetime.now(ZoneInfo("UTC"))
231
248
 
249
+ # Data collection methods
250
+ @property
251
+ def is_data_collection_complete(self) -> bool:
252
+ """Check if data collection is complete (mandatory or all)"""
253
+ return self.data_collection_status in [
254
+ DataCollectionStatus.MANDATORY_COMPLETED,
255
+ DataCollectionStatus.ALL_COMPLETED
256
+ ]
257
+
258
+ @property
259
+ def is_data_collection_in_progress(self) -> bool:
260
+ """Check if data collection is still in progress"""
261
+ return self.data_collection_status == DataCollectionStatus.COLLECTING
262
+
263
+ def start_data_collection(self) -> None:
264
+ """Start data collection (set status to COLLECTING)"""
265
+ self.data_collection_status = DataCollectionStatus.COLLECTING
266
+ self.pre_qualify_status = PreQualifyStatus.PENDING
267
+ self.updated_at = datetime.now(ZoneInfo("UTC"))
268
+
269
+ def complete_mandatory_data_collection(self) -> None:
270
+ """Mark mandatory fields as completed"""
271
+ self.data_collection_status = DataCollectionStatus.MANDATORY_COMPLETED
272
+ self.pre_qualify_status = PreQualifyStatus.EVALUATING
273
+ self.updated_at = datetime.now(ZoneInfo("UTC"))
274
+
275
+ def complete_all_data_collection(self) -> None:
276
+ """Mark all fields as completed"""
277
+ self.data_collection_status = DataCollectionStatus.ALL_COMPLETED
278
+ self.updated_at = datetime.now(ZoneInfo("UTC"))
279
+
280
+ def cancel_data_collection(self) -> None:
281
+ """Cancel data collection"""
282
+ self.data_collection_status = DataCollectionStatus.CANCELLED
283
+ self.updated_at = datetime.now(ZoneInfo("UTC"))
284
+
285
+ # Pre-qualification methods
286
+ @property
287
+ def is_qualified(self) -> bool:
288
+ """Check if user is qualified"""
289
+ return self.pre_qualify_status == PreQualifyStatus.QUALIFIED
290
+
291
+ @property
292
+ def is_unqualified(self) -> bool:
293
+ """Check if user is unqualified"""
294
+ return self.pre_qualify_status == PreQualifyStatus.UNQUALIFIED
295
+
296
+ @property
297
+ def is_pre_qualify_pending(self) -> bool:
298
+ """Check if pre-qualification is still pending"""
299
+ return self.pre_qualify_status in [PreQualifyStatus.PENDING, PreQualifyStatus.EVALUATING]
300
+
301
+ @property
302
+ def has_terminal_pre_qualify_status(self) -> bool:
303
+ """Check if pre-qualification has reached a terminal status"""
304
+ return self.pre_qualify_status in [
305
+ PreQualifyStatus.QUALIFIED,
306
+ PreQualifyStatus.UNQUALIFIED
307
+ ]
308
+
309
+ def mark_as_qualified(self) -> None:
310
+ """Mark user as qualified (met acceptance criteria)"""
311
+ self.pre_qualify_status = PreQualifyStatus.QUALIFIED
312
+ self.updated_at = datetime.now(ZoneInfo("UTC"))
313
+
314
+ def mark_as_unqualified(self) -> None:
315
+ """Mark user as unqualified (did NOT meet acceptance criteria)"""
316
+ self.pre_qualify_status = PreQualifyStatus.UNQUALIFIED
317
+ self.updated_at = datetime.now(ZoneInfo("UTC"))
318
+ self.updated_at = datetime.now(ZoneInfo("UTC"))
319
+
320
+ def cancel_data_collection(self) -> None:
321
+ """Cancel data collection"""
322
+ self.data_collection_status = DataCollectionStatus.CANCELLED
323
+ self.updated_at = datetime.now(ZoneInfo("UTC"))
@@ -0,0 +1,111 @@
1
+ """
2
+ Pre-qualification configuration for AI agents.
3
+
4
+ Defines the data collection, acceptance criteria, and destination actions
5
+ for qualifying/disqualifying users.
6
+ """
7
+
8
+ from pydantic import BaseModel, Field
9
+ from typing import Optional, List
10
+ from enum import StrEnum
11
+ from letschatty.models.utils.types.identifier import StrObjectId
12
+
13
+
14
+ class PreQualifyDestination(StrEnum):
15
+ """
16
+ Destination/action when pre-qualification reaches a terminal state.
17
+ """
18
+ SUBSCRIBE_TO_LAUNCH = "subscribe_to_launch" # Subscribe to launch + welcome kit
19
+ CALENDAR_SCHEDULER = "calendar_scheduler" # Allow AI agent to schedule meetings
20
+ ESCALATE = "escalate" # Escalate to human
21
+ CUSTOM_MESSAGE = "custom_message" # Send a custom message
22
+ CONTINUE = "continue" # Continue normal AI agent flow
23
+ NONE = "none" # Do nothing
24
+
25
+
26
+ class PreQualifyFormField(BaseModel):
27
+ """
28
+ Form field reference with required flag.
29
+ The required flag is specific to this pre-qualify config, not the form field itself.
30
+ """
31
+ field_key: str = Field(description="The field_key of the FormField")
32
+ required: bool = Field(default=False, description="Whether this field is required for pre-qualification")
33
+
34
+
35
+ class PreQualifyConfig(BaseModel):
36
+ """
37
+ Configuration for pre-qualification process.
38
+ Embedded in ChattyAIAgent.
39
+ """
40
+ # Form fields to collect (with required flag per field)
41
+ form_fields: List[PreQualifyFormField] = Field(
42
+ default_factory=list,
43
+ description="List of form fields to collect with their required status"
44
+ )
45
+
46
+ # Acceptance criteria
47
+ acceptance_criteria: str = Field(
48
+ default="",
49
+ description="Description of criteria for AI to evaluate if user qualifies. Empty = no criteria (auto-qualify on mandatory completion)"
50
+ )
51
+
52
+ # On qualified actions
53
+ on_qualified_destination: PreQualifyDestination = Field(
54
+ default=PreQualifyDestination.CONTINUE,
55
+ description="Action when user qualifies"
56
+ )
57
+ on_qualified_message: Optional[str] = Field(
58
+ default=None,
59
+ description="Custom message to send when user qualifies (if destination is custom_message)"
60
+ )
61
+
62
+ # On unqualified actions
63
+ on_unqualified_destination: PreQualifyDestination = Field(
64
+ default=PreQualifyDestination.NONE,
65
+ description="Action when user does NOT qualify"
66
+ )
67
+ on_unqualified_message: Optional[str] = Field(
68
+ default=None,
69
+ description="Custom message to send when user does NOT qualify (if destination is custom_message or escalate)"
70
+ )
71
+
72
+ @property
73
+ def has_form_fields(self) -> bool:
74
+ """Check if pre-qualify has form fields configured"""
75
+ return len(self.form_fields) > 0
76
+
77
+ @property
78
+ def has_acceptance_criteria(self) -> bool:
79
+ """Check if acceptance criteria is configured"""
80
+ return bool(self.acceptance_criteria.strip())
81
+
82
+ @property
83
+ def is_configured(self) -> bool:
84
+ """Check if pre-qualify is configured (has form fields)"""
85
+ return self.has_form_fields
86
+
87
+ def get_field_keys(self) -> List[str]:
88
+ """Get list of all field_keys"""
89
+ return [f.field_key for f in self.form_fields]
90
+
91
+ def get_required_field_keys(self) -> List[str]:
92
+ """Get list of required field_keys"""
93
+ return [f.field_key for f in self.form_fields if f.required]
94
+
95
+ def get_optional_field_keys(self) -> List[str]:
96
+ """Get list of optional field_keys"""
97
+ return [f.field_key for f in self.form_fields if not f.required]
98
+
99
+ def is_field_required(self, field_key: str) -> bool:
100
+ """Check if a specific field is required"""
101
+ for f in self.form_fields:
102
+ if f.field_key == field_key:
103
+ return f.required
104
+ return False
105
+
106
+ def remove_field(self, field_key: str) -> bool:
107
+ """Remove a field from the config. Returns True if field was found and removed."""
108
+ original_len = len(self.form_fields)
109
+ self.form_fields = [f for f in self.form_fields if f.field_key != field_key]
110
+ return len(self.form_fields) < original_len
111
+
@@ -0,0 +1,33 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class DataCollectionStatus(StrEnum):
5
+ """
6
+ Status of data collection for the AI agent in this chat.
7
+ Only tracks field collection, not qualification status.
8
+
9
+ - COLLECTING: Still collecting data from the user
10
+ - MANDATORY_COMPLETED: All mandatory fields have been collected
11
+ - ALL_COMPLETED: All fields (mandatory + optional) have been collected
12
+ - CANCELLED: Data collection was cancelled
13
+ """
14
+ COLLECTING = "collecting"
15
+ MANDATORY_COMPLETED = "mandatory_completed"
16
+ ALL_COMPLETED = "all_completed"
17
+ CANCELLED = "cancelled"
18
+
19
+
20
+ class PreQualifyStatus(StrEnum):
21
+ """
22
+ Status of pre-qualification for the AI agent in this chat.
23
+ Separate from data collection - tracks qualification evaluation.
24
+
25
+ - PENDING: Waiting for data collection to complete
26
+ - EVALUATING: Data collected, evaluating acceptance criteria
27
+ - QUALIFIED: User met acceptance criteria
28
+ - UNQUALIFIED: User did NOT meet acceptance criteria
29
+ """
30
+ PENDING = "pending"
31
+ EVALUATING = "evaluating"
32
+ QUALIFIED = "qualified"
33
+ UNQUALIFIED = "unqualified"
@@ -0,0 +1,14 @@
1
+ from .assignment_assets import (
2
+ AssignmentStrategy,
3
+ AssignmentRoom,
4
+ AssignmentConfig,
5
+ AssignmentLogEntry,
6
+ )
7
+
8
+ __all__ = [
9
+ "AssignmentStrategy",
10
+ "AssignmentRoom",
11
+ "AssignmentConfig",
12
+ "AssignmentLogEntry",
13
+ ]
14
+
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import StrEnum
4
+ from typing import Any, ClassVar, Dict, Optional
5
+
6
+ from pydantic import ConfigDict, Field
7
+
8
+ from ....base_models.chatty_asset_model import CompanyAssetModel
9
+ from ....utils.types.identifier import StrObjectId
10
+ from ....utils.types.serializer_type import SerializerType
11
+
12
+
13
+ class AssignmentStrategy(StrEnum):
14
+ """Assignment strategy types."""
15
+
16
+ ROUND_ROBIN = "ROUND_ROBIN"
17
+
18
+
19
+ class AssignmentRoom(CompanyAssetModel):
20
+ """Scope identifier for assignment configuration, rooms and logs."""
21
+
22
+ COLLECTION: ClassVar[str] = "assignment_room"
23
+
24
+ funnel_id: Optional[StrObjectId] = Field(default=None, alias="funnelId")
25
+ area_id: Optional[StrObjectId] = Field(default=None, alias="areaId")
26
+ strategy: AssignmentStrategy = Field(default=AssignmentStrategy.ROUND_ROBIN)
27
+ state: Dict[str, Any] = Field(default_factory=dict)
28
+
29
+ exclude_fields = {
30
+ SerializerType.FRONTEND_ASSET_PREVIEW: {"state"},
31
+ }
32
+
33
+ model_config = ConfigDict(
34
+ validate_by_name=True,
35
+ validate_by_alias=True,
36
+ populate_by_name=True,
37
+ )
38
+
39
+
40
+ class AssignmentConfig(CompanyAssetModel):
41
+ """Assignment configuration document stored in MongoDB."""
42
+
43
+ COLLECTION: ClassVar[str] = "assignment_config"
44
+
45
+ funnel_id: Optional[StrObjectId] = Field(default=None, alias="funnelId")
46
+ area_id: Optional[StrObjectId] = Field(default=None, alias="areaId")
47
+ strategy: AssignmentStrategy = Field(default=AssignmentStrategy.ROUND_ROBIN)
48
+ params: Dict[str, Any] = Field(default_factory=dict)
49
+
50
+ model_config = ConfigDict(
51
+ validate_by_name=True,
52
+ validate_by_alias=True,
53
+ populate_by_name=True,
54
+ )
55
+
56
+
57
+ class AssignmentLogEntry(CompanyAssetModel):
58
+ """Immutable record inserted into assignment_log_entry collection."""
59
+
60
+ COLLECTION: ClassVar[str] = "assignment_log_entry"
61
+
62
+ chat_id: StrObjectId = Field(alias="chatId")
63
+ agent_id: StrObjectId = Field(alias="agentId")
64
+ strategy: AssignmentStrategy
65
+ room: AssignmentRoom
66
+
67
+ exclude_fields = {
68
+ SerializerType.FRONTEND_ASSET_PREVIEW: {"room"},
69
+ }
70
+
71
+ model_config = ConfigDict(
72
+ validate_by_name=True,
73
+ validate_by_alias=True,
74
+ populate_by_name=True,
75
+ )
@@ -14,27 +14,18 @@ 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
- @property
21
- def has_automation(self) -> bool:
22
- """
23
- Check if there's an actual automation defined (tags, products, or flow).
24
-
25
- Returns:
26
- bool: True if there's at least one tag, product, or flow defined
27
- """
28
- return (
29
- len(self.tags) > 0 or
30
- len(self.products) > 0 or
31
- len(self.flow) > 0 or
32
- self.quality_score is not None or
33
- self.chatty_ai_agent_config is not None or
34
- self.area is not None or
35
- self.highlight_description is not None
36
- )
37
-
38
29
  @model_validator(mode='after')
39
30
  def check_agent_id(self):
40
31
  if self.area == Area.WITH_AGENT and not self.agent_id:
@@ -4,12 +4,13 @@ from enum import StrEnum
4
4
  from pydantic_core.core_schema import str_schema
5
5
 
6
6
  from letschatty.models.company.assets.ai_agents_v2.chain_of_thought_in_chat import ChainOfThoughtInChatTrigger
7
+ from letschatty.models.company.assets.ai_agents_v2.statuses import DataCollectionStatus, PreQualifyStatus
7
8
  from ...utils.types.identifier import StrObjectId
8
9
  from datetime import datetime
9
10
  from zoneinfo import ZoneInfo
10
11
  from bson import ObjectId
11
12
  import json
12
- from typing import Dict, Any, Optional
13
+ from typing import Dict, Any, Optional, List
13
14
  from letschatty.models.utils.types.serializer_type import SerializerType
14
15
  from letschatty.models.company.assets.ai_agents_v2.chatty_ai_mode import ChattyAIMode
15
16
 
@@ -66,7 +67,8 @@ class AssignedAssetToChat(BaseModel):
66
67
 
67
68
 
68
69
  class SaleAssignedToChat(AssignedAssetToChat):
69
- product_id: StrObjectId = Field(frozen=True)
70
+ product_id: Optional[StrObjectId] = Field(default=None)
71
+ product_ids: List[StrObjectId] = Field(default_factory=list)
70
72
 
71
73
  class ContactPointAssignedToChat(AssignedAssetToChat):
72
74
  source_id: StrObjectId = Field(frozen=True)
@@ -75,6 +77,14 @@ class ChattyAIAgentAssignedToChat(AssignedAssetToChat):
75
77
  mode: ChattyAIMode = Field(default=ChattyAIMode.OFF)
76
78
  requires_human_intervention: bool = Field(default=False)
77
79
  is_processing: bool = Field(default=False)
80
+ data_collection_status: Optional[DataCollectionStatus] = Field(
81
+ default=None,
82
+ description="Status of data collection for pre-qualification"
83
+ )
84
+ pre_qualify_status: Optional[PreQualifyStatus] = Field(
85
+ default=None,
86
+ description="Status of pre-qualification"
87
+ )
78
88
  last_call_started_at: Optional[datetime] = Field(default=None, description="The timestamp of the get chat with prompt (the moment n8n started processing the call)")
79
89
  trigger_timestamp: Optional[datetime] = Field(default=None, description="The timestamp of the trigger that started the call, if it's a manual trigger, it will be the timestamp of the manual trigger, if it's a follow up, it will be the timestamp of the follow up, if it's a user message, it will be the timestamp of the user message")
80
90
  last_call_cot_id: Optional[StrObjectId] = Field(default=None)
@@ -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"
@@ -20,6 +22,7 @@ class CompanyAssetType(StrEnum):
20
22
  WORKFLOWS = "workflows"
21
23
  CHATTY_AI_AGENTS = "chatty_ai_agents"
22
24
  FILTER_CRITERIA = "filter_criteria"
25
+ FORM_FIELDS = "form_fields"
23
26
 
24
27
  @classmethod
25
28
  def get_all(cls) -> List[str]:
@@ -0,0 +1,12 @@
1
+ from .launch import Launch, LaunchStatus
2
+ from .scheduled_communication import LaunchScheduledCommunication
3
+ from .subscription import LaunchSubscription, LaunchSubscriptionStatus
4
+
5
+ __all__ = [
6
+ 'Launch',
7
+ 'LaunchStatus',
8
+ 'LaunchScheduledCommunication',
9
+ 'LaunchSubscription',
10
+ 'LaunchSubscriptionStatus'
11
+ ]
12
+
@@ -0,0 +1,128 @@
1
+ from pydantic import Field
2
+ from typing import List, Optional
3
+ from datetime import datetime
4
+ from enum import StrEnum
5
+ from letschatty.models.base_models.chatty_asset_model import CompanyAssetModel
6
+ from letschatty.models.utils.types.identifier import StrObjectId
7
+ from .scheduled_communication import LaunchScheduledCommunication
8
+ from .subscription import LaunchSubscription, LaunchSubscriptionStatus
9
+ from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
10
+
11
+
12
+ class LaunchStatus(StrEnum):
13
+ """Status of the launch"""
14
+ DRAFT = "draft"
15
+ ACTIVE = "active"
16
+ COMPLETED = "completed"
17
+ CANCELLED = "cancelled"
18
+
19
+
20
+ class Launch(CompanyAssetModel):
21
+ """
22
+ Launch asset model.
23
+ Represents a product launch event with communications and subscriptions.
24
+ """
25
+ # Basic info
26
+ name: str = Field(description="Name of the launch")
27
+ product_name: str = Field(description="Name of the product being launched")
28
+ product_description: str = Field(description="Description of the product being launched")
29
+ launch_datetime: datetime = Field(description="Date and time of the launch")
30
+ access_link: str = Field(description="Base link for the launch event")
31
+ status: LaunchStatus = Field(default=LaunchStatus.DRAFT, description="Current status of the launch")
32
+
33
+ # Data Collection & Pre-qualification
34
+ form_fields: List[StrObjectId] = Field(
35
+ default_factory=list,
36
+ description="FormField IDs required before subscription"
37
+ )
38
+ acceptance_criteria: str = Field(
39
+ default="",
40
+ description="Instructions for AI to determine when to subscribe the user"
41
+ )
42
+ enable_tracking: bool = Field(
43
+ default=False,
44
+ description="If True, generate personal trackable links for each subscriber"
45
+ )
46
+
47
+ # Communications
48
+ welcome_kit: Optional[LaunchScheduledCommunication] = Field(
49
+ default=None,
50
+ description="Communication sent immediately upon subscription (delta_minutes=0)"
51
+ )
52
+ pre_launch_communications: List[LaunchScheduledCommunication] = Field(
53
+ default_factory=list,
54
+ description="Communications scheduled before the launch"
55
+ )
56
+ post_launch_attended_communications: List[LaunchScheduledCommunication] = Field(
57
+ default_factory=list,
58
+ description="Communications for attendees after the launch"
59
+ )
60
+ post_launch_missed_communications: List[LaunchScheduledCommunication] = Field(
61
+ default_factory=list,
62
+ description="Communications for those who missed the launch"
63
+ )
64
+
65
+ # Subscriptions (embedded)
66
+ subscriptions: List[LaunchSubscription] = Field(
67
+ default_factory=list,
68
+ description="List of chat subscriptions to this launch"
69
+ )
70
+
71
+ # AI Configuration
72
+ chat_examples: List[StrObjectId] = Field(
73
+ default_factory=list,
74
+ description="Chat examples for AI message adaptation"
75
+ )
76
+ contexts: List[StrObjectId] = Field(
77
+ default_factory=list,
78
+ description="Specific contexts for the launch"
79
+ )
80
+
81
+ @property
82
+ def all_communications(self) -> List[LaunchScheduledCommunication]:
83
+ """Get all communications including welcome kit"""
84
+ comms = []
85
+ if self.welcome_kit:
86
+ comms.append(self.welcome_kit)
87
+ comms.extend(self.pre_launch_communications)
88
+ comms.extend(self.post_launch_attended_communications)
89
+ comms.extend(self.post_launch_missed_communications)
90
+ return comms
91
+
92
+ def get_subscription_by_chat_id(self, chat_id: StrObjectId) -> Optional[LaunchSubscription]:
93
+ """Find a subscription by chat ID"""
94
+ for sub in self.subscriptions:
95
+ if str(sub.chat_id) == str(chat_id):
96
+ return sub
97
+ return None
98
+
99
+ def is_chat_subscribed(self, chat_id: StrObjectId) -> bool:
100
+ """Check if a chat is subscribed to this launch"""
101
+ sub = self.get_subscription_by_chat_id(chat_id)
102
+ return sub is not None and sub.status in [
103
+ LaunchSubscriptionStatus.SUBSCRIBED,
104
+ LaunchSubscriptionStatus.ATTENDED
105
+ ]
106
+
107
+ def get_access_link_for_subscriber(self, chat_id: StrObjectId) -> str:
108
+ """
109
+ Returns the personal trackable link for a subscriber if tracking is enabled,
110
+ otherwise returns the general access_link.
111
+ """
112
+ if self.enable_tracking:
113
+ subscription = self.get_subscription_by_chat_id(chat_id)
114
+ if subscription and subscription.personal_access_link:
115
+ return subscription.personal_access_link
116
+ return self.access_link
117
+
118
+ def generate_personal_link(self, chat_id: StrObjectId) -> str:
119
+ """
120
+ Generates a unique trackable link for a given chat_id.
121
+ Adds a 'subscriber' query parameter to the base access_link.
122
+ """
123
+ parsed_url = urlparse(self.access_link)
124
+ query_params = parse_qs(parsed_url.query)
125
+ query_params['subscriber'] = [str(chat_id)]
126
+ new_query = urlencode(query_params, doseq=True)
127
+ return urlunparse(parsed_url._replace(query=new_query))
128
+
@@ -0,0 +1,44 @@
1
+ from pydantic import BaseModel, Field
2
+ from typing import List, Optional
3
+ from datetime import datetime
4
+ from letschatty.models.messages.chatty_messages.base.message_draft import MessageDraft
5
+ from letschatty.models.company.assets.ai_agents_v2.ai_agent_message_draft import AIAgentMessageDraft
6
+
7
+
8
+ class LaunchScheduledCommunication(BaseModel):
9
+ """
10
+ Scheduled communication for pre/post launch.
11
+ Can be AI-adapted or literal messages.
12
+ """
13
+ id: str = Field(description="Unique identifier for the communication")
14
+ name: str = Field(description="Name of the communication (e.g., '5 dias antes', '2 horas antes')")
15
+ delta_minutes: int = Field(
16
+ description="Minutes relative to the launch (negative=before, positive=after, 0=welcome kit)"
17
+ )
18
+
19
+ # Content - one or the other
20
+ ai_communication: Optional[AIAgentMessageDraft] = Field(
21
+ default=None,
22
+ description="AI adapted communication content and instructions"
23
+ )
24
+ literal_messages: Optional[List[MessageDraft]] = Field(
25
+ default=None,
26
+ description="Literal messages to be sent without AI adaptation"
27
+ )
28
+
29
+ # State
30
+ sent_at: Optional[datetime] = Field(
31
+ default=None,
32
+ description="Timestamp when this communication was sent globally for the launch"
33
+ )
34
+
35
+ @property
36
+ def requires_ai(self) -> bool:
37
+ """Check if this communication requires AI adaptation"""
38
+ return self.ai_communication is not None
39
+
40
+ @property
41
+ def is_sent(self) -> bool:
42
+ """Check if this communication has been sent"""
43
+ return self.sent_at is not None
44
+