fusesell 1.3.42__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 (35) hide show
  1. fusesell-1.3.42.dist-info/METADATA +873 -0
  2. fusesell-1.3.42.dist-info/RECORD +35 -0
  3. fusesell-1.3.42.dist-info/WHEEL +5 -0
  4. fusesell-1.3.42.dist-info/entry_points.txt +2 -0
  5. fusesell-1.3.42.dist-info/licenses/LICENSE +21 -0
  6. fusesell-1.3.42.dist-info/top_level.txt +2 -0
  7. fusesell.py +20 -0
  8. fusesell_local/__init__.py +37 -0
  9. fusesell_local/api.py +343 -0
  10. fusesell_local/cli.py +1480 -0
  11. fusesell_local/config/__init__.py +11 -0
  12. fusesell_local/config/default_email_templates.json +34 -0
  13. fusesell_local/config/default_prompts.json +19 -0
  14. fusesell_local/config/default_scoring_criteria.json +154 -0
  15. fusesell_local/config/prompts.py +245 -0
  16. fusesell_local/config/settings.py +277 -0
  17. fusesell_local/pipeline.py +978 -0
  18. fusesell_local/stages/__init__.py +19 -0
  19. fusesell_local/stages/base_stage.py +603 -0
  20. fusesell_local/stages/data_acquisition.py +1820 -0
  21. fusesell_local/stages/data_preparation.py +1238 -0
  22. fusesell_local/stages/follow_up.py +1728 -0
  23. fusesell_local/stages/initial_outreach.py +2972 -0
  24. fusesell_local/stages/lead_scoring.py +1452 -0
  25. fusesell_local/utils/__init__.py +36 -0
  26. fusesell_local/utils/agent_context.py +552 -0
  27. fusesell_local/utils/auto_setup.py +361 -0
  28. fusesell_local/utils/birthday_email_manager.py +467 -0
  29. fusesell_local/utils/data_manager.py +4857 -0
  30. fusesell_local/utils/event_scheduler.py +959 -0
  31. fusesell_local/utils/llm_client.py +342 -0
  32. fusesell_local/utils/logger.py +203 -0
  33. fusesell_local/utils/output_helpers.py +2443 -0
  34. fusesell_local/utils/timezone_detector.py +914 -0
  35. fusesell_local/utils/validators.py +436 -0
@@ -0,0 +1,361 @@
1
+ """
2
+ Auto-setup and intelligent initialization utilities for FuseSell.
3
+
4
+ This module provides functions for:
5
+ - Auto-initialization of settings with smart defaults (e.g., Gmail email)
6
+ - Settings completion checking
7
+ - Agent context generation and updates
8
+ """
9
+
10
+ import json
11
+ import logging
12
+ from datetime import datetime, timedelta
13
+ from typing import Any, Dict, List, Optional, Callable
14
+
15
+ from .data_manager import LocalDataManager
16
+
17
+
18
+ logger = logging.getLogger("fusesell.auto_setup")
19
+
20
+
21
+ def get_gmail_email_safe(get_gmail_email_func: Optional[Callable[[], str]] = None) -> Optional[str]:
22
+ """
23
+ Safely retrieve Gmail email from MCP server.
24
+
25
+ Args:
26
+ get_gmail_email_func: Optional function to retrieve Gmail email.
27
+ If None, returns None.
28
+
29
+ Returns:
30
+ Gmail email address if available, None otherwise
31
+ """
32
+ if get_gmail_email_func is None:
33
+ return None
34
+
35
+ try:
36
+ email = get_gmail_email_func()
37
+ if email and isinstance(email, str) and email.strip():
38
+ return email.strip()
39
+ except Exception as exc:
40
+ logger.warning(f"Could not retrieve Gmail email: {exc}")
41
+
42
+ return None
43
+
44
+
45
+ def check_settings_completion(
46
+ manager: LocalDataManager,
47
+ team_id: str
48
+ ) -> Dict[str, Any]:
49
+ """
50
+ Check which settings sections are completed for a team.
51
+
52
+ Args:
53
+ manager: LocalDataManager instance
54
+ team_id: Team identifier
55
+
56
+ Returns:
57
+ Dictionary with completion status for each settings section:
58
+ {
59
+ "team_id": str,
60
+ "auto_interaction_completed": bool,
61
+ "rep_completed": bool,
62
+ "product_completed": bool,
63
+ "organization_completed": bool,
64
+ "follow_up_completed": bool,
65
+ }
66
+ """
67
+ settings = manager.get_team_settings(team_id)
68
+
69
+ def _is_completed(value: Any) -> bool:
70
+ """Check if a settings value is considered completed."""
71
+ if value is None:
72
+ return False
73
+ if isinstance(value, str):
74
+ return value.strip() != ""
75
+ if isinstance(value, (list, tuple, set, frozenset)):
76
+ return len(value) > 0
77
+ if isinstance(value, dict):
78
+ return len(value) > 0
79
+ return True
80
+
81
+ if not settings:
82
+ return {
83
+ "team_id": team_id,
84
+ "auto_interaction_completed": False,
85
+ "rep_completed": False,
86
+ "product_completed": False,
87
+ "organization_completed": False,
88
+ "follow_up_completed": False,
89
+ }
90
+
91
+ return {
92
+ "team_id": team_id,
93
+ "auto_interaction_completed": _is_completed(settings.get("gs_team_auto_interaction")),
94
+ "rep_completed": _is_completed(settings.get("gs_team_rep")),
95
+ "product_completed": _is_completed(settings.get("gs_team_product")),
96
+ "organization_completed": _is_completed(settings.get("gs_team_organization")),
97
+ "follow_up_completed": _is_completed(settings.get("gs_team_follow_up")),
98
+ }
99
+
100
+
101
+ def auto_initialize_auto_interaction(
102
+ manager: LocalDataManager,
103
+ team_id: str,
104
+ gmail_email: Optional[str] = None,
105
+ get_gmail_email_func: Optional[Callable[[], str]] = None
106
+ ) -> bool:
107
+ """
108
+ Auto-initialize auto_interaction settings with Gmail email if:
109
+ 1. Gmail MCP is connected (or gmail_email is provided)
110
+ 2. auto_interaction is not yet set
111
+
112
+ Args:
113
+ manager: LocalDataManager instance
114
+ team_id: Team identifier
115
+ gmail_email: Optional pre-fetched Gmail email
116
+ get_gmail_email_func: Optional function to retrieve Gmail email
117
+ (will be called if gmail_email is None)
118
+
119
+ Returns:
120
+ True if auto-initialization was performed, False otherwise
121
+ """
122
+ # Get Gmail email if not provided
123
+ if gmail_email is None and get_gmail_email_func is not None:
124
+ gmail_email = get_gmail_email_safe(get_gmail_email_func)
125
+
126
+ if not gmail_email:
127
+ return False
128
+
129
+ # Check if already initialized
130
+ settings = manager.get_team_settings(team_id)
131
+ auto_interaction_value = settings.get("gs_team_auto_interaction") if settings else None
132
+
133
+ # Check if auto_interaction is already completed
134
+ if auto_interaction_value:
135
+ if isinstance(auto_interaction_value, list) and len(auto_interaction_value) > 0:
136
+ return False
137
+ if isinstance(auto_interaction_value, dict) and len(auto_interaction_value) > 0:
138
+ return False
139
+
140
+ try:
141
+ # Create default auto_interaction with Gmail email
142
+ default_auto_interaction = [{
143
+ "from_email": gmail_email,
144
+ "from_name": "",
145
+ "from_number": "",
146
+ "tool_type": "Email",
147
+ "email_cc": "",
148
+ "email_bcc": "",
149
+ }]
150
+
151
+ manager.update_team_settings(
152
+ team_id=team_id,
153
+ gs_team_auto_interaction=default_auto_interaction
154
+ )
155
+ logger.info(f"Auto-initialized auto_interaction with Gmail: {gmail_email}")
156
+ return True
157
+ except Exception as exc:
158
+ logger.error(f"Failed to auto-initialize auto_interaction: {exc}")
159
+ return False
160
+
161
+
162
+ def auto_populate_from_email(
163
+ from_email: str,
164
+ gmail_email: Optional[str] = None,
165
+ get_gmail_email_func: Optional[Callable[[], str]] = None
166
+ ) -> str:
167
+ """
168
+ Auto-populate from_email field if empty using Gmail email.
169
+
170
+ Args:
171
+ from_email: Current from_email value
172
+ gmail_email: Optional pre-fetched Gmail email
173
+ get_gmail_email_func: Optional function to retrieve Gmail email
174
+
175
+ Returns:
176
+ Original from_email if not empty, otherwise Gmail email if available,
177
+ otherwise empty string
178
+ """
179
+ # Return existing value if not empty
180
+ if from_email and from_email.strip():
181
+ return from_email.strip()
182
+
183
+ # Get Gmail email if not provided
184
+ if gmail_email is None and get_gmail_email_func is not None:
185
+ gmail_email = get_gmail_email_safe(get_gmail_email_func)
186
+
187
+ if gmail_email:
188
+ logger.info(f"Auto-populated from_email with Gmail: {gmail_email}")
189
+ return gmail_email
190
+
191
+ return ""
192
+
193
+
194
+ def generate_agent_context(
195
+ manager: LocalDataManager,
196
+ org_id: str,
197
+ detail_limit: Optional[int] = None
198
+ ) -> Dict[str, Any]:
199
+ """
200
+ Generate comprehensive agent context for the workspace.
201
+
202
+ This function collects and structures all relevant workspace data
203
+ for agent memory/context updates.
204
+
205
+ Args:
206
+ manager: LocalDataManager instance
207
+ org_id: Organization identifier
208
+ detail_limit: Optional limit for detail fields in products/processes
209
+
210
+ Returns:
211
+ Dictionary containing:
212
+ - workspace_summary: Overview text
213
+ - products: List of active products
214
+ - active_processes: List of active sales processes
215
+ - team_settings: Team configuration
216
+ - statistics: Usage statistics
217
+ - last_updated: Timestamp
218
+ """
219
+ # Get teams
220
+ teams = manager.list_teams(org_id=org_id, status='active')
221
+ team = teams[0] if teams else None
222
+
223
+ team_id = None
224
+ team_name = org_id or 'Workspace Team'
225
+ team_created_at = 'N/A'
226
+
227
+ if team:
228
+ team_id = team.get('team_id')
229
+ team_name = team.get('name') or team.get('team_name') or team_name
230
+ team_created_at = team.get('created_at', 'N/A')
231
+
232
+ # Get products (include all, track active separately)
233
+ products = manager.search_products(org_id=org_id, status="all") or []
234
+ active_products_count = sum(1 for p in products if (p.get('status') or '').lower() == 'active')
235
+ inactive_products_count = len(products) - active_products_count
236
+
237
+ # Apply detail limit if specified
238
+ if detail_limit is not None and detail_limit > 0:
239
+ for product in products:
240
+ for key in ['short_description', 'long_description', 'category']:
241
+ if key in product and isinstance(product[key], str):
242
+ if len(product[key]) > detail_limit:
243
+ product[key] = product[key][:detail_limit] + '...'
244
+
245
+ # Get settings (fallback to empty when team not yet created)
246
+ settings: Dict[str, Any] = {}
247
+ if team_id:
248
+ settings = manager.get_team_settings(team_id) or {}
249
+
250
+ # Get processes
251
+ all_processes = manager.list_tasks(org_id=org_id, limit=100)
252
+ active_processes = [
253
+ p for p in all_processes
254
+ if p.get('status') in ('running', 'pending', 'in_progress')
255
+ ]
256
+
257
+ # Apply detail limit to processes
258
+ if detail_limit is not None and detail_limit > 0:
259
+ for process in active_processes:
260
+ for key in ['customer_name', 'customer_company', 'notes']:
261
+ if key in process and isinstance(process[key], str):
262
+ if len(process[key]) > detail_limit:
263
+ process[key] = process[key][:detail_limit] + '...'
264
+
265
+ # Calculate completion status
266
+ completion_status = check_settings_completion(manager, team_id) if team_id else {
267
+ 'completed': 0,
268
+ 'total': 0,
269
+ 'completed_keys': [],
270
+ 'missing_keys': [
271
+ 'gs_team_organization',
272
+ 'gs_team_rep',
273
+ 'gs_team_product',
274
+ 'gs_team_auto_interaction',
275
+ 'gs_team_initial_outreach',
276
+ 'gs_team_follow_up',
277
+ 'gs_team_schedule_time',
278
+ 'gs_team_followup_schedule_time',
279
+ ],
280
+ 'auto_interaction_completed': False,
281
+ 'has_sales_rep': False,
282
+ 'has_products': len(products) > 0,
283
+ }
284
+
285
+ # Build workspace summary
286
+ workspace_summary_parts = [
287
+ f"Team: {team_name}",
288
+ f"Products: {len(products)} total ({active_products_count} active)",
289
+ f"Active Processes: {len(active_processes)}",
290
+ ]
291
+
292
+ if completion_status.get('auto_interaction_completed'):
293
+ workspace_summary_parts.append("Auto interaction: configured")
294
+ else:
295
+ workspace_summary_parts.append("Auto interaction: not configured")
296
+
297
+ workspace_summary = " | ".join(workspace_summary_parts)
298
+
299
+ return {
300
+ 'workspace_summary': workspace_summary,
301
+ 'workspace_slug': manager.data_dir, # best-effort; flows can override in writer
302
+ 'team_id': team_id,
303
+ 'team_name': team_name,
304
+ 'team_created_at': team_created_at,
305
+ 'org_id': org_id,
306
+ 'products': products,
307
+ 'active_processes': active_processes,
308
+ 'team_settings': settings,
309
+ 'statistics': {
310
+ 'total_products': len(products),
311
+ 'active_products': active_products_count,
312
+ 'inactive_products': inactive_products_count,
313
+ 'active_processes': len(active_processes),
314
+ 'total_processes': len(all_processes),
315
+ 'settings_completion': completion_status,
316
+ },
317
+ 'last_updated': datetime.utcnow().isoformat() + 'Z',
318
+ }
319
+
320
+
321
+ def should_update_agent_context(
322
+ action: str,
323
+ critical_actions: Optional[List[str]] = None,
324
+ skip_actions: Optional[List[str]] = None
325
+ ) -> bool:
326
+ """
327
+ Determine if agent context should be updated based on the action performed.
328
+
329
+ Args:
330
+ action: The action that was performed
331
+ critical_actions: List of actions that trigger context updates
332
+ skip_actions: List of actions that should NOT trigger updates
333
+
334
+ Returns:
335
+ True if agent context should be updated, False otherwise
336
+ """
337
+ # Default critical actions
338
+ if critical_actions is None:
339
+ critical_actions = [
340
+ 'product_create', 'product_update', 'product_delete',
341
+ 'product_status_change', 'product_bulk_import',
342
+ 'process_create',
343
+ 'team_create', 'team_update',
344
+ 'gs_organization', 'sales_rep', 'product_team', 'auto_interaction',
345
+ 'initial_outreach', 'follow_up', 'schedule_time', 'followup_schedule_time',
346
+ ]
347
+
348
+ # Default skip actions
349
+ if skip_actions is None:
350
+ skip_actions = [
351
+ 'product_view', 'product_list', 'team_view', 'team_list', 'settings_view',
352
+ 'process_query', 'process_status_change', 'draft_list', 'event_list',
353
+ 'event_update', 'knowledge_query', 'proposal_generate'
354
+ ]
355
+
356
+ # Skip actions take precedence
357
+ if action in skip_actions:
358
+ return False
359
+
360
+ # Check if action is critical
361
+ return action in critical_actions