letschatty 0.4.305__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.
- letschatty/models/ai_microservices/expected_output.py +35 -1
- letschatty/models/ai_microservices/lambda_events.py +66 -5
- letschatty/models/ai_microservices/lambda_invokation_types.py +3 -0
- letschatty/models/analytics/events/__init__.py +1 -1
- letschatty/models/analytics/events/chat_based_events/chat_funnel.py +69 -13
- letschatty/models/analytics/events/company_based_events/asset_events.py +9 -2
- letschatty/models/analytics/events/event_type_to_classes.py +6 -3
- letschatty/models/analytics/events/event_types.py +13 -5
- letschatty/models/chat/chat.py +13 -2
- letschatty/models/chat/chat_with_assets.py +6 -1
- letschatty/models/chat/client.py +0 -2
- letschatty/models/chat/continuous_conversation.py +1 -1
- letschatty/models/company/CRM/funnel.py +365 -33
- letschatty/models/company/__init__.py +3 -1
- letschatty/models/company/assets/ai_agents_v2/ai_agent_message_draft.py +58 -0
- letschatty/models/company/assets/ai_agents_v2/chain_of_thought_in_chat.py +5 -3
- letschatty/models/company/assets/ai_agents_v2/chatty_ai_agent.py +30 -2
- letschatty/models/company/assets/ai_agents_v2/chatty_ai_agent_in_chat.py +91 -1
- letschatty/models/company/assets/ai_agents_v2/pre_qualify_config.py +111 -0
- letschatty/models/company/assets/ai_agents_v2/statuses.py +33 -0
- letschatty/models/company/assets/assignment/__init__.py +14 -0
- letschatty/models/company/assets/assignment/assignment_assets.py +75 -0
- letschatty/models/company/assets/automation.py +10 -1
- letschatty/models/company/assets/chat_assets.py +12 -2
- letschatty/models/company/assets/company_assets.py +3 -0
- letschatty/models/company/assets/launch/__init__.py +12 -0
- letschatty/models/company/assets/launch/launch.py +128 -0
- letschatty/models/company/assets/launch/scheduled_communication.py +44 -0
- letschatty/models/company/assets/launch/subscription.py +63 -0
- letschatty/models/company/assets/sale.py +3 -3
- letschatty/models/company/assets/users/user.py +5 -1
- letschatty/models/company/company_messaging_settgins.py +2 -1
- letschatty/models/company/form_field.py +182 -12
- letschatty/models/messages/chatty_messages/schema/chatty_content/content_document.py +4 -2
- letschatty/models/messages/chatty_messages/schema/chatty_content/content_media.py +4 -3
- letschatty/models/utils/custom_exceptions/custom_exceptions.py +24 -0
- letschatty/services/ai_agents/smart_follow_up_service_v2.py +10 -0
- letschatty/services/chat/chat_service.py +79 -14
- letschatty/services/factories/analytics/events_factory.py +5 -3
- letschatty/services/users/user_factory.py +14 -8
- letschatty/services/validators/analytics_validator.py +11 -0
- {letschatty-0.4.305.dist-info → letschatty-0.4.343.dist-info}/METADATA +2 -1
- {letschatty-0.4.305.dist-info → letschatty-0.4.343.dist-info}/RECORD +45 -36
- {letschatty-0.4.305.dist-info → letschatty-0.4.343.dist-info}/WHEEL +1 -1
- {letschatty-0.4.305.dist-info → letschatty-0.4.343.dist-info}/LICENSE +0 -0
|
@@ -1,69 +1,401 @@
|
|
|
1
|
-
from ...base_models import CompanyAssetModel
|
|
2
|
-
from typing import List
|
|
1
|
+
from ...base_models import CompanyAssetModel, ChattyAssetPreview, ChattyAssetModel
|
|
2
|
+
from typing import List, Optional, ClassVar, Any
|
|
3
3
|
from ...utils.types.identifier import StrObjectId
|
|
4
|
-
from pydantic import BaseModel, Field
|
|
4
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
5
5
|
from enum import StrEnum
|
|
6
6
|
from datetime import datetime
|
|
7
|
-
from typing import Optional
|
|
8
7
|
from zoneinfo import ZoneInfo
|
|
9
8
|
from ...utils.types.executor_types import ExecutorType
|
|
9
|
+
from ..assets.automation import Automation
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ============================================================================
|
|
13
|
+
# Enums
|
|
14
|
+
# ============================================================================
|
|
10
15
|
|
|
11
16
|
class FunnelStatus(StrEnum):
|
|
17
|
+
"""Status of a chat within a funnel"""
|
|
12
18
|
IN_PROGRESS = "in_progress"
|
|
13
19
|
COMPLETED = "completed"
|
|
14
20
|
ABANDONED = "abandoned"
|
|
15
21
|
|
|
22
|
+
|
|
23
|
+
class FunnelMemberRole(StrEnum):
|
|
24
|
+
"""Role of a user within a funnel"""
|
|
25
|
+
ADMIN = "admin" # Full control over funnel settings, stages, and members
|
|
26
|
+
EDITOR = "editor" # Can move chats between stages, view all chats
|
|
27
|
+
VIEWER = "viewer" # Read-only access to funnel and chats
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ============================================================================
|
|
31
|
+
# Embedded Models (BaseModel)
|
|
32
|
+
# ============================================================================
|
|
33
|
+
|
|
16
34
|
class StageTransition(BaseModel):
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
35
|
+
"""
|
|
36
|
+
Record of a chat transitioning between stages within a funnel.
|
|
37
|
+
Tracks the transition details and time spent in the previous stage.
|
|
38
|
+
"""
|
|
39
|
+
from_stage_id: Optional[StrObjectId] = Field(
|
|
40
|
+
default=None,
|
|
41
|
+
description="The stage the chat came from (None if entering funnel)"
|
|
42
|
+
)
|
|
43
|
+
to_stage_id: StrObjectId = Field(description="The stage the chat moved to")
|
|
44
|
+
transitioned_at: datetime = Field(default_factory=lambda: datetime.now(ZoneInfo("UTC")))
|
|
45
|
+
executor_type: ExecutorType = Field(description="Type of executor that triggered the transition")
|
|
46
|
+
executor_id: StrObjectId = Field(description="ID of the executor that triggered the transition")
|
|
47
|
+
time_in_previous_stage_seconds: Optional[int] = Field(
|
|
48
|
+
default=None,
|
|
49
|
+
description="Time spent in the previous stage before this transition"
|
|
50
|
+
)
|
|
51
|
+
from_stage_order: Optional[int] = Field(
|
|
52
|
+
default=None,
|
|
53
|
+
description="Order of the from_stage at transition time (for detecting regressions)"
|
|
54
|
+
)
|
|
55
|
+
to_stage_order: Optional[int] = Field(
|
|
56
|
+
default=None,
|
|
57
|
+
description="Order of the to_stage at transition time"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def is_regression(self) -> bool:
|
|
62
|
+
"""Check if this transition moved backwards in the funnel"""
|
|
63
|
+
if self.from_stage_order is None or self.to_stage_order is None:
|
|
64
|
+
return False
|
|
65
|
+
return self.to_stage_order < self.from_stage_order
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def is_entry(self) -> bool:
|
|
69
|
+
"""Check if this is an entry transition (no previous stage)"""
|
|
70
|
+
return self.from_stage_id is None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ActiveFunnel(BaseModel):
|
|
74
|
+
"""
|
|
75
|
+
Lightweight funnel state embedded in Chat for fast filtering and display.
|
|
76
|
+
|
|
77
|
+
The full history (stage_transitions, time metrics) is stored in the
|
|
78
|
+
separate chat_funnels collection via ChatFunnel.
|
|
79
|
+
"""
|
|
80
|
+
chat_funnel_id: StrObjectId = Field(
|
|
81
|
+
description="Reference to the full ChatFunnel document in chat_funnels collection"
|
|
82
|
+
)
|
|
83
|
+
funnel_id: StrObjectId = Field(description="The funnel the chat is in")
|
|
84
|
+
funnel_name: str = Field(description="Denormalized funnel name for display")
|
|
85
|
+
current_stage_id: StrObjectId = Field(description="Current stage ID")
|
|
86
|
+
current_stage_name: str = Field(description="Denormalized stage name for display")
|
|
87
|
+
entered_current_stage_at: datetime = Field(
|
|
88
|
+
default_factory=lambda: datetime.now(ZoneInfo("UTC")),
|
|
89
|
+
description="When the chat entered the current stage"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def time_in_current_stage_seconds(self) -> int:
|
|
94
|
+
"""Calculate time spent in the current stage"""
|
|
95
|
+
return int((datetime.now(tz=ZoneInfo("UTC")) - self.entered_current_stage_at).total_seconds())
|
|
96
|
+
|
|
97
|
+
def update_stage(self, stage_id: StrObjectId, stage_name: str) -> None:
|
|
98
|
+
"""Update the current stage (called when transitioning)"""
|
|
99
|
+
self.current_stage_id = stage_id
|
|
100
|
+
self.current_stage_name = stage_name
|
|
101
|
+
self.entered_current_stage_at = datetime.now(tz=ZoneInfo("UTC"))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ============================================================================
|
|
105
|
+
# Preview Classes - For efficient listing
|
|
106
|
+
# ============================================================================
|
|
107
|
+
|
|
108
|
+
class FunnelPreview(ChattyAssetPreview):
|
|
109
|
+
"""Preview of a Funnel for listing without full data"""
|
|
110
|
+
is_active: bool = Field(default=True)
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def get_projection(cls) -> dict[str, Any]:
|
|
114
|
+
base = super().get_projection()
|
|
115
|
+
base["is_active"] = 1
|
|
116
|
+
return base
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def from_asset(cls, asset: 'Funnel') -> 'FunnelPreview':
|
|
120
|
+
return cls(
|
|
121
|
+
_id=asset.id,
|
|
122
|
+
name=asset.name,
|
|
123
|
+
company_id=asset.company_id,
|
|
124
|
+
created_at=asset.created_at,
|
|
125
|
+
deleted_at=asset.deleted_at,
|
|
126
|
+
is_active=asset.is_active
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class FunnelStagePreview(ChattyAssetPreview):
|
|
131
|
+
"""Preview of a FunnelStage for listing"""
|
|
25
132
|
funnel_id: StrObjectId
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
133
|
+
color: str
|
|
134
|
+
order: int
|
|
135
|
+
is_exit_stage: bool = Field(default=False)
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def get_projection(cls) -> dict[str, Any]:
|
|
139
|
+
base = super().get_projection()
|
|
140
|
+
base.update({
|
|
141
|
+
"funnel_id": 1,
|
|
142
|
+
"color": 1,
|
|
143
|
+
"order": 1,
|
|
144
|
+
"is_exit_stage": 1
|
|
145
|
+
})
|
|
146
|
+
return base
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def from_asset(cls, asset: 'FunnelStage') -> 'FunnelStagePreview':
|
|
150
|
+
return cls(
|
|
151
|
+
_id=asset.id,
|
|
152
|
+
name=asset.name,
|
|
153
|
+
company_id=asset.company_id,
|
|
154
|
+
created_at=asset.created_at,
|
|
155
|
+
deleted_at=asset.deleted_at,
|
|
156
|
+
funnel_id=asset.funnel_id,
|
|
157
|
+
color=asset.color,
|
|
158
|
+
order=asset.order,
|
|
159
|
+
is_exit_stage=asset.is_exit_stage
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ============================================================================
|
|
164
|
+
# Company Assets (CompanyAssetModel) - Stored in separate collections
|
|
165
|
+
# ============================================================================
|
|
166
|
+
|
|
167
|
+
class Funnel(CompanyAssetModel):
|
|
168
|
+
"""
|
|
169
|
+
A funnel represents a pipeline/process for managing chats (e.g., sales funnel, support pipeline).
|
|
170
|
+
Companies can create multiple funnels to organize their chat workflows.
|
|
171
|
+
"""
|
|
172
|
+
name: str = Field(description="Name of the funnel")
|
|
173
|
+
description: Optional[str] = Field(default=None, description="Description of the funnel's purpose")
|
|
174
|
+
created_by: StrObjectId = Field(description="User ID who created the funnel")
|
|
175
|
+
is_active: bool = Field(default=True, description="Whether the funnel is active or archived")
|
|
176
|
+
|
|
177
|
+
preview_class: ClassVar[type[FunnelPreview]] = FunnelPreview
|
|
178
|
+
|
|
179
|
+
model_config = ConfigDict(
|
|
180
|
+
validate_by_name=True,
|
|
181
|
+
validate_by_alias=True
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class FunnelStage(CompanyAssetModel):
|
|
186
|
+
"""
|
|
187
|
+
A stage within a funnel. Stages are ordered and can have automations
|
|
188
|
+
that execute when a chat enters the stage.
|
|
189
|
+
"""
|
|
190
|
+
funnel_id: StrObjectId = Field(frozen=True, description="The funnel this stage belongs to")
|
|
191
|
+
name: str = Field(description="Name of the stage")
|
|
192
|
+
description: Optional[str] = Field(default=None, description="Description of the stage")
|
|
193
|
+
color: str = Field(default="#808080", description="Hex color for the stage (e.g., '#FFAA00')")
|
|
194
|
+
order: int = Field(ge=0, description="Position of the stage in the funnel (0-indexed)")
|
|
195
|
+
inflexion_conversion_point: bool = Field(
|
|
196
|
+
default=False,
|
|
197
|
+
description="If true, prioritized for chat assignment and sends notifications in automatic mode"
|
|
198
|
+
)
|
|
199
|
+
is_exit_stage: bool = Field(
|
|
200
|
+
default=False,
|
|
201
|
+
description="If true, moving to this stage completes the funnel for the chat"
|
|
202
|
+
)
|
|
203
|
+
automations: Automation = Field(
|
|
204
|
+
default_factory=Automation,
|
|
205
|
+
description="Automations to execute when a chat enters this stage"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
preview_class: ClassVar[type[FunnelStagePreview]] = FunnelStagePreview
|
|
209
|
+
|
|
210
|
+
model_config = ConfigDict(
|
|
211
|
+
validate_by_name=True,
|
|
212
|
+
validate_by_alias=True
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class FunnelMember(CompanyAssetModel):
|
|
217
|
+
"""
|
|
218
|
+
A user's membership in a funnel with their assigned role.
|
|
219
|
+
Determines what actions the user can perform within the funnel.
|
|
220
|
+
"""
|
|
221
|
+
name: str = Field(default="Funnel Member", description="Name for asset compatibility")
|
|
222
|
+
funnel_id: StrObjectId = Field(frozen=True, description="The funnel this membership belongs to")
|
|
223
|
+
user_id: StrObjectId = Field(frozen=True, description="The user who is a member")
|
|
224
|
+
role: FunnelMemberRole = Field(description="The user's role within the funnel")
|
|
225
|
+
|
|
226
|
+
model_config = ConfigDict(
|
|
227
|
+
validate_by_name=True,
|
|
228
|
+
validate_by_alias=True
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def can_edit(self) -> bool:
|
|
233
|
+
"""Check if the member can edit (move chats, etc.)"""
|
|
234
|
+
return self.role in (FunnelMemberRole.ADMIN, FunnelMemberRole.EDITOR)
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def can_admin(self) -> bool:
|
|
238
|
+
"""Check if the member can administer the funnel"""
|
|
239
|
+
return self.role == FunnelMemberRole.ADMIN
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ============================================================================
|
|
243
|
+
# ChatFunnel - Stored in separate chat_funnels collection
|
|
244
|
+
# This is a ChattyAssetModel (not CompanyAssetModel) because it's a chat history
|
|
245
|
+
# record rather than a company-level asset.
|
|
246
|
+
# ============================================================================
|
|
247
|
+
|
|
248
|
+
class ChatFunnel(ChattyAssetModel):
|
|
249
|
+
"""
|
|
250
|
+
Full record of a chat's journey through a funnel.
|
|
251
|
+
Stored in a separate 'chat_funnels' collection, NOT embedded in chat.
|
|
252
|
+
|
|
253
|
+
This extends ChattyAssetModel (not CompanyAssetModel) because it's a
|
|
254
|
+
chat-level record tracking funnel history, not a company-owned asset.
|
|
255
|
+
|
|
256
|
+
A new ChatFunnel is created each time a chat enters a funnel.
|
|
257
|
+
If a chat completes a funnel and enters it again, a NEW ChatFunnel is created.
|
|
258
|
+
|
|
259
|
+
The Chat document stores a lightweight ActiveFunnel for fast
|
|
260
|
+
filtering and display, with a reference to this full record.
|
|
261
|
+
"""
|
|
262
|
+
company_id: StrObjectId = Field(frozen=True, description="Company for multi-tenant isolation")
|
|
263
|
+
chat_id: StrObjectId = Field(frozen=True, description="The chat this funnel record belongs to")
|
|
264
|
+
funnel_id: StrObjectId = Field(frozen=True, description="The funnel the chat entered")
|
|
265
|
+
status: FunnelStatus = Field(default=FunnelStatus.IN_PROGRESS)
|
|
266
|
+
started_at: datetime = Field(default_factory=lambda: datetime.now(ZoneInfo("UTC")))
|
|
267
|
+
completed_at: Optional[datetime] = Field(default=None)
|
|
268
|
+
abandoned_at: Optional[datetime] = Field(default=None)
|
|
269
|
+
current_stage_id: Optional[StrObjectId] = Field(
|
|
270
|
+
default=None,
|
|
271
|
+
description="Current stage ID (None if funnel is completed/abandoned)"
|
|
272
|
+
)
|
|
273
|
+
entered_current_stage_at: Optional[datetime] = Field(
|
|
274
|
+
default=None,
|
|
275
|
+
description="When the chat entered the current stage"
|
|
276
|
+
)
|
|
277
|
+
stage_transitions: List[StageTransition] = Field(
|
|
278
|
+
default_factory=list,
|
|
279
|
+
description="Complete history of all stage transitions"
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
model_config = ConfigDict(
|
|
283
|
+
validate_by_name=True,
|
|
284
|
+
validate_by_alias=True
|
|
285
|
+
)
|
|
33
286
|
|
|
34
287
|
@property
|
|
35
288
|
def is_completed(self) -> bool:
|
|
36
|
-
|
|
289
|
+
"""Check if the chat has completed the funnel"""
|
|
290
|
+
return self.status == FunnelStatus.COMPLETED and self.completed_at is not None
|
|
37
291
|
|
|
38
292
|
@property
|
|
39
293
|
def is_abandoned(self) -> bool:
|
|
40
|
-
|
|
294
|
+
"""Check if the chat has abandoned the funnel"""
|
|
295
|
+
return self.status == FunnelStatus.ABANDONED and self.abandoned_at is not None
|
|
296
|
+
|
|
297
|
+
@property
|
|
298
|
+
def is_active(self) -> bool:
|
|
299
|
+
"""Check if the chat is still active in the funnel"""
|
|
300
|
+
return self.status == FunnelStatus.IN_PROGRESS
|
|
41
301
|
|
|
42
302
|
@property
|
|
43
303
|
def time_in_funnel_seconds(self) -> int:
|
|
304
|
+
"""Calculate total time spent in the funnel"""
|
|
44
305
|
end_time = self.completed_at or self.abandoned_at or datetime.now(tz=ZoneInfo("UTC"))
|
|
45
306
|
return int((end_time - self.started_at).total_seconds())
|
|
46
307
|
|
|
47
308
|
@property
|
|
48
309
|
def time_in_current_stage_seconds(self) -> Optional[int]:
|
|
49
|
-
|
|
310
|
+
"""Calculate time spent in the current stage"""
|
|
311
|
+
if not self.entered_current_stage_at or not self.current_stage_id:
|
|
50
312
|
return None
|
|
51
313
|
return int((datetime.now(tz=ZoneInfo("UTC")) - self.entered_current_stage_at).total_seconds())
|
|
52
314
|
|
|
53
315
|
@property
|
|
54
316
|
def unique_stages_visited(self) -> int:
|
|
317
|
+
"""Count of unique stages the chat has visited"""
|
|
55
318
|
return len(set(t.to_stage_id for t in self.stage_transitions))
|
|
56
319
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
inflexion_conversion_point: bool = Field(default=False, description="If true, it'll be prioritized for chat assignment and will send notifications in automatic mode to all agents.")
|
|
62
|
-
workflow_ids: List[StrObjectId] = Field(default_factory=list)
|
|
320
|
+
@property
|
|
321
|
+
def total_transitions(self) -> int:
|
|
322
|
+
"""Total number of stage transitions"""
|
|
323
|
+
return len(self.stage_transitions)
|
|
63
324
|
|
|
325
|
+
@property
|
|
326
|
+
def regression_count(self) -> int:
|
|
327
|
+
"""Count of times the chat moved backwards in the funnel"""
|
|
328
|
+
return sum(1 for t in self.stage_transitions if t.is_regression)
|
|
64
329
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
330
|
+
@property
|
|
331
|
+
def last_transition(self) -> Optional[StageTransition]:
|
|
332
|
+
"""Get the most recent stage transition"""
|
|
333
|
+
return self.stage_transitions[-1] if self.stage_transitions else None
|
|
334
|
+
|
|
335
|
+
def record_transition(
|
|
336
|
+
self,
|
|
337
|
+
to_stage_id: StrObjectId,
|
|
338
|
+
executor_type: ExecutorType,
|
|
339
|
+
executor_id: StrObjectId,
|
|
340
|
+
from_stage_order: Optional[int] = None,
|
|
341
|
+
to_stage_order: Optional[int] = None
|
|
342
|
+
) -> StageTransition:
|
|
343
|
+
"""
|
|
344
|
+
Record a transition to a new stage.
|
|
345
|
+
Returns the created StageTransition.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
to_stage_id: The stage to transition to
|
|
349
|
+
executor_type: Who/what triggered the transition
|
|
350
|
+
executor_id: ID of the executor
|
|
351
|
+
from_stage_order: Order of current stage (for regression detection)
|
|
352
|
+
to_stage_order: Order of target stage (for regression detection)
|
|
353
|
+
"""
|
|
354
|
+
time_in_previous = self.time_in_current_stage_seconds
|
|
355
|
+
|
|
356
|
+
transition = StageTransition(
|
|
357
|
+
from_stage_id=self.current_stage_id,
|
|
358
|
+
to_stage_id=to_stage_id,
|
|
359
|
+
executor_type=executor_type,
|
|
360
|
+
executor_id=executor_id,
|
|
361
|
+
time_in_previous_stage_seconds=time_in_previous,
|
|
362
|
+
from_stage_order=from_stage_order,
|
|
363
|
+
to_stage_order=to_stage_order
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
self.stage_transitions.append(transition)
|
|
367
|
+
self.current_stage_id = to_stage_id
|
|
368
|
+
self.entered_current_stage_at = transition.transitioned_at
|
|
369
|
+
|
|
370
|
+
return transition
|
|
371
|
+
|
|
372
|
+
def complete(self) -> None:
|
|
373
|
+
"""Mark the funnel as completed"""
|
|
374
|
+
self.status = FunnelStatus.COMPLETED
|
|
375
|
+
self.completed_at = datetime.now(tz=ZoneInfo("UTC"))
|
|
376
|
+
|
|
377
|
+
def abandon(self) -> None:
|
|
378
|
+
"""Mark the funnel as abandoned"""
|
|
379
|
+
self.status = FunnelStatus.ABANDONED
|
|
380
|
+
self.abandoned_at = datetime.now(tz=ZoneInfo("UTC"))
|
|
381
|
+
|
|
382
|
+
def to_active_funnel(self, funnel_name: str, stage_name: str) -> ActiveFunnel:
|
|
383
|
+
"""
|
|
384
|
+
Create an ActiveFunnel for embedding in the Chat document.
|
|
385
|
+
Call this after creating/updating the ChatFunnel.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
funnel_name: Name of the funnel (for denormalization)
|
|
389
|
+
stage_name: Name of the current stage (for denormalization)
|
|
390
|
+
"""
|
|
391
|
+
if not self.current_stage_id or not self.entered_current_stage_at:
|
|
392
|
+
raise ValueError("Cannot create ActiveFunnel without current stage")
|
|
393
|
+
|
|
394
|
+
return ActiveFunnel(
|
|
395
|
+
chat_funnel_id=self.id,
|
|
396
|
+
funnel_id=self.funnel_id,
|
|
397
|
+
funnel_name=funnel_name,
|
|
398
|
+
current_stage_id=self.current_stage_id,
|
|
399
|
+
current_stage_name=stage_name,
|
|
400
|
+
entered_current_stage_at=self.entered_current_stage_at
|
|
401
|
+
)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI Agent Message Draft model.
|
|
3
|
+
|
|
4
|
+
Wrapper for MessageDraft with AI instructions for adaptation.
|
|
5
|
+
Used in launch communications and welcome kits.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
from typing import List
|
|
10
|
+
from letschatty.models.messages.chatty_messages.base.message_draft import MessageDraft
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AIAgentMessageDraft(BaseModel):
|
|
14
|
+
"""
|
|
15
|
+
Wrapper for MessageDraft with instructions for AI adaptation.
|
|
16
|
+
|
|
17
|
+
Allows multimedia messages (text, image, video, audio, document)
|
|
18
|
+
to be sent with context for AI to adapt them to the conversation.
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
```python
|
|
22
|
+
welcome_kit = AIAgentMessageDraft(
|
|
23
|
+
messages=[
|
|
24
|
+
MessageDraft(content=ChattyContentText(body="¡Bienvenido a {{product_name}}!")),
|
|
25
|
+
MessageDraft(content=ChattyContentDocument(url="https://..."))
|
|
26
|
+
],
|
|
27
|
+
instructions="Personaliza el mensaje usando el nombre del usuario si está disponible."
|
|
28
|
+
)
|
|
29
|
+
```
|
|
30
|
+
"""
|
|
31
|
+
messages: List[MessageDraft] = Field(
|
|
32
|
+
description="List of message drafts to be sent (text, image, video, etc.)"
|
|
33
|
+
)
|
|
34
|
+
instructions: str = Field(
|
|
35
|
+
description="Instructions for AI on how to adapt/personalize these messages"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
class Config:
|
|
39
|
+
json_schema_extra = {
|
|
40
|
+
"example": {
|
|
41
|
+
"messages": [
|
|
42
|
+
{
|
|
43
|
+
"content": {
|
|
44
|
+
"type": "text",
|
|
45
|
+
"body": "¡Bienvenido a {{product_name}}! Tu link de acceso: {{access_link}}"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"content": {
|
|
50
|
+
"type": "document",
|
|
51
|
+
"url": "https://example.com/welcome.pdf",
|
|
52
|
+
"filename": "guia_bienvenida.pdf"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"instructions": "Personaliza el mensaje de bienvenida según el contexto de la conversación. Usa el nombre del usuario si está disponible."
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -84,12 +84,13 @@ class ChainOfThoughtInChatPreview(ChattyAssetPreview):
|
|
|
84
84
|
# trigger_id removed - trigger info is in: incoming_message_ids for user_message,
|
|
85
85
|
# triggered_by_user_id for manual_trigger, smart_follow_up_id for follow_up
|
|
86
86
|
chain_of_thought : Optional[str] = Field(default=None, description="The chain of thought")
|
|
87
|
+
confidence: Optional[int] = Field(default=None, ge=0, le=100, description="Confidence 0-100")
|
|
87
88
|
status: Optional[ChainOfThoughtInChatStatus] = Field(default=None, description="The status of the chain of thought")
|
|
88
89
|
name: str = Field(description="A title for the chain of thought")
|
|
89
90
|
|
|
90
91
|
@classmethod
|
|
91
92
|
def get_projection(cls) -> dict[str, Any]:
|
|
92
|
-
return super().get_projection() | {"chat_id": 1, "trigger": 1, "chain_of_thought": 1, "name": 1, "status": 1}
|
|
93
|
+
return super().get_projection() | {"chat_id": 1, "trigger": 1, "chain_of_thought": 1, "confidence": 1, "name": 1, "status": 1}
|
|
93
94
|
|
|
94
95
|
@classmethod
|
|
95
96
|
def from_dict(cls, data: dict) -> 'ChainOfThoughtInChatPreview':
|
|
@@ -103,6 +104,7 @@ class ChainOfThoughtInChatPreview(ChattyAssetPreview):
|
|
|
103
104
|
chat_id=asset.chat_id,
|
|
104
105
|
trigger=asset.trigger,
|
|
105
106
|
chain_of_thought=asset.chain_of_thought,
|
|
107
|
+
confidence=asset.confidence,
|
|
106
108
|
status=asset.status,
|
|
107
109
|
company_id=asset.company_id,
|
|
108
110
|
created_at=asset.created_at,
|
|
@@ -121,6 +123,7 @@ class ChainOfThoughtInChat(CompanyAssetModel):
|
|
|
121
123
|
chatty_ai_agent_id : StrObjectId = Field(description="The chatty ai agent id")
|
|
122
124
|
chatty_ai_agent : Dict[str, Any] = Field(description="The chatty ai agent at the moment of triggering the chain of thought", exclude=True,default_factory=dict)
|
|
123
125
|
chain_of_thought : Optional[str] = Field(default=None, description="The chain of thought")
|
|
126
|
+
confidence: Optional[int] = Field(default=None, ge=0, le=100, description="Confidence 0-100")
|
|
124
127
|
name: str = Field(description="A title for the chain of thought", alias="title")
|
|
125
128
|
preview_class: ClassVar[Type[ChattyAssetPreview]] = ChainOfThoughtInChatPreview
|
|
126
129
|
|
|
@@ -128,5 +131,4 @@ class ChainOfThoughtInChat(CompanyAssetModel):
|
|
|
128
131
|
if self.chain_of_thought is None:
|
|
129
132
|
self.chain_of_thought = cot
|
|
130
133
|
else:
|
|
131
|
-
self.chain_of_thought += f"\n{cot}"
|
|
132
|
-
|
|
134
|
+
self.chain_of_thought += f"\n{cot}"
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
from pydantic import Field, field_validator, BaseModel
|
|
2
|
-
from typing import List, Any, Optional, ClassVar
|
|
2
|
+
from typing import List, Any, Optional, ClassVar, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from .pre_qualify_config import PreQualifyConfig
|
|
3
6
|
|
|
4
7
|
from letschatty.models.utils.definitions import Environment
|
|
5
8
|
from ....base_models import CompanyAssetModel
|
|
@@ -107,6 +110,21 @@ class ChattyAIAgent(CompanyAssetModel):
|
|
|
107
110
|
examples: List[StrObjectId] = Field(default_factory=list, description="Training examples")
|
|
108
111
|
double_checker_enabled: bool = Field(default=False, description="Whether the double checker is enabled")
|
|
109
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
|
+
)
|
|
119
|
+
|
|
120
|
+
# Pre-qualification configuration
|
|
121
|
+
pre_qualify: Optional["PreQualifyConfig"] = Field(
|
|
122
|
+
default=None,
|
|
123
|
+
description="Pre-qualification config: form fields, acceptance criteria, and destination actions"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Launch integration
|
|
127
|
+
active_launch_id: Optional[StrObjectId] = Field(default=None, description="ID of the active launch this agent is managing")
|
|
110
128
|
|
|
111
129
|
"""json example:
|
|
112
130
|
{
|
|
@@ -143,4 +161,14 @@ class ChattyAIAgent(CompanyAssetModel):
|
|
|
143
161
|
@property
|
|
144
162
|
def test_trigger(self) -> str:
|
|
145
163
|
"""Get the test trigger"""
|
|
146
|
-
return f"Hola! Quiero testear al Chatty AI Agent {self.name} {self.id}"
|
|
164
|
+
return f"Hola! Quiero testear al Chatty AI Agent {self.name} {self.id}"
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def has_pre_qualify(self) -> bool:
|
|
168
|
+
"""Check if pre-qualify is configured"""
|
|
169
|
+
return self.pre_qualify is not None and self.pre_qualify.is_configured
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# Import and rebuild for forward reference resolution
|
|
173
|
+
from .pre_qualify_config import PreQualifyConfig
|
|
174
|
+
ChattyAIAgent.model_rebuild()
|