fusesell 1.2.0__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 fusesell might be problematic. Click here for more details.

fusesell_local/cli.py ADDED
@@ -0,0 +1,1450 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ FuseSell Local - Command-line interface for local sales automation pipeline
4
+ """
5
+
6
+ import argparse
7
+ import sys
8
+ import json
9
+ from typing import Dict, Any, Optional
10
+ from datetime import datetime
11
+
12
+ from .api import (
13
+ build_config as build_pipeline_config,
14
+ configure_logging as configure_pipeline_logging,
15
+ prepare_data_directory,
16
+ validate_config as validate_pipeline_config,
17
+ )
18
+ from .pipeline import FuseSellPipeline
19
+ from .utils.validators import InputValidator
20
+
21
+
22
+ class FuseSellCLI:
23
+ """
24
+ Command-line interface for FuseSell local execution.
25
+ Handles argument parsing, validation, and pipeline orchestration.
26
+ """
27
+
28
+ def __init__(self):
29
+ """Initialize CLI with argument parser."""
30
+ self.parser = self._setup_argument_parser()
31
+ self.logger = None # Will be initialized after parsing args
32
+
33
+ def _setup_argument_parser(self) -> argparse.ArgumentParser:
34
+ """
35
+ Set up command-line argument parser with subcommands.
36
+
37
+ Returns:
38
+ Configured ArgumentParser instance
39
+ """
40
+ parser = argparse.ArgumentParser(
41
+ description='FuseSell Local - AI-powered sales automation pipeline',
42
+ formatter_class=argparse.RawDescriptionHelpFormatter,
43
+ epilog="""
44
+ Examples:
45
+ # PIPELINE EXECUTION
46
+ python fusesell.py pipeline --openai-api-key sk-xxx --org-id rta --org-name "RTA Corp" \\
47
+ --full-input "Seller: RTA Corp, Customer: Example Company, Communication: English" \\
48
+ --input-website "https://example.com"
49
+
50
+ # TEAM MANAGEMENT
51
+ python fusesell.py team create --name "Sales Team A" --description "Primary sales team" \\
52
+ --org-id rta --plan-id plan-123
53
+ python fusesell.py team list --org-id rta
54
+ python fusesell.py team describe team-456
55
+
56
+ # PRODUCT MANAGEMENT
57
+ python fusesell.py product create --name "FuseSell Pro" --description "Advanced sales automation" \\
58
+ --org-id rta --product-data '{"category":"Sales Automation"}'
59
+ python fusesell.py product list --org-id rta
60
+
61
+ # SETTINGS MANAGEMENT
62
+ python fusesell.py settings set team-456 --setting-name product_settings \\
63
+ --value-json '[{"product_id": "prod-123"}]'
64
+ python fusesell.py settings view team-456 --setting-name product_settings
65
+
66
+ # BIRTHDAY EMAIL MANAGEMENT
67
+ python fusesell.py settings birthday configure team-456 --org-id rta \\
68
+ --prompt "Send friendly birthday greetings, max 200 words, UTC+07"
69
+ python fusesell.py settings birthday list-templates --team-id team-456
70
+ python fusesell.py settings birthday view-template birthday_email__team-456
71
+ """
72
+ )
73
+
74
+ # Add subcommands (optional for backward compatibility)
75
+ subparsers = parser.add_subparsers(
76
+ dest='command', help='Available commands', required=False)
77
+
78
+ # Team management subcommand
79
+ team_parser = subparsers.add_parser('team', help='Manage teams')
80
+ self._add_team_arguments(team_parser)
81
+
82
+ # Product management subcommand
83
+ product_parser = subparsers.add_parser(
84
+ 'product', help='Manage products')
85
+ self._add_product_arguments(product_parser)
86
+
87
+ # Settings management subcommand
88
+ settings_parser = subparsers.add_parser(
89
+ 'settings', help='Manage team settings')
90
+ self._add_settings_arguments(settings_parser)
91
+
92
+ # Add pipeline arguments directly to main parser for backward compatibility
93
+ self._add_pipeline_arguments(parser)
94
+
95
+ return parser
96
+
97
+ def _add_pipeline_arguments(self, parser: argparse.ArgumentParser) -> None:
98
+ """Add pipeline-specific arguments."""
99
+
100
+ # Required arguments (for pipeline execution)
101
+ required = parser.add_argument_group(
102
+ 'pipeline arguments (required for pipeline execution)')
103
+ required.add_argument(
104
+ '--openai-api-key',
105
+ help='OpenAI API key for LLM processing'
106
+ )
107
+ required.add_argument(
108
+ '--org-id',
109
+ help='Organization ID (seller identifier)'
110
+ )
111
+ required.add_argument(
112
+ '--org-name',
113
+ help='Organization name (seller name)'
114
+ )
115
+
116
+ # Data source fields (at least one required) - matching executor schema
117
+ data_sources = parser.add_argument_group(
118
+ 'data sources (at least one required)')
119
+ data_sources.add_argument(
120
+ '--input-website',
121
+ help='The URL of the website. If no URL is provided, the return value will be empty (maps to input_website)'
122
+ )
123
+ data_sources.add_argument(
124
+ '--input-description',
125
+ help='Full information about the customer. Such as name, phone, email, address, taxcode, another info... (maps to input_description)'
126
+ )
127
+ data_sources.add_argument(
128
+ '--input-business-card',
129
+ help='The image URL of the business card. If no image URL is provided, the return value will be empty (maps to input_business_card)'
130
+ )
131
+ data_sources.add_argument(
132
+ '--input-facebook-url',
133
+ help='Valid URL pointing to a personal profile or business page on Facebook (maps to input_facebook_url)'
134
+ )
135
+ data_sources.add_argument(
136
+ '--input-linkedin-url',
137
+ help='Valid URL pointing to a personal profile or business page on LinkedIn (maps to input_linkedin_url)'
138
+ )
139
+ data_sources.add_argument(
140
+ '--input-freetext',
141
+ help='Free text input with customer information (maps to input_freetext from executor schema)'
142
+ )
143
+
144
+ # Required context fields (matching executor schema)
145
+ context_group = parser.add_argument_group(
146
+ 'context fields (required for pipeline execution)')
147
+ context_group.add_argument(
148
+ '--full-input',
149
+ help='Full information input. Includes: Seller, Customer and Communication information'
150
+ )
151
+
152
+ # Optional context fields
153
+ optional_context = parser.add_argument_group('optional context')
154
+ optional_context.add_argument(
155
+ '--customer-id',
156
+ help='ID of customer, if not provided, return null'
157
+ )
158
+
159
+ # Team and project settings
160
+ team_group = parser.add_argument_group('team settings')
161
+ team_group.add_argument(
162
+ '--team-id',
163
+ help='Team ID for team-specific configurations'
164
+ )
165
+ team_group.add_argument(
166
+ '--team-name',
167
+ help='Team name for team-specific configurations'
168
+ )
169
+ team_group.add_argument(
170
+ '--project-code',
171
+ help='Project code for organization'
172
+ )
173
+ team_group.add_argument(
174
+ '--staff-name',
175
+ default='Sales Team',
176
+ help='Staff member name for email signatures (default: Sales Team)'
177
+ )
178
+
179
+ # Processing options
180
+ processing_group = parser.add_argument_group('processing options')
181
+ processing_group.add_argument(
182
+ '--language',
183
+ default='english',
184
+ choices=['english', 'vietnamese', 'spanish', 'french', 'german'],
185
+ help='Language for communication and processing (default: english)'
186
+ )
187
+ processing_group.add_argument(
188
+ '--skip-stages',
189
+ nargs='*',
190
+ choices=['data_acquisition', 'data_preparation',
191
+ 'lead_scoring', 'initial_outreach', 'follow_up'],
192
+ help='Stages to skip during execution'
193
+ )
194
+ processing_group.add_argument(
195
+ '--stop-after',
196
+ choices=['data_acquisition', 'data_preparation',
197
+ 'lead_scoring', 'initial_outreach', 'follow_up'],
198
+ help='Stop pipeline after specified stage'
199
+ )
200
+
201
+ # Process continuation and action options (matching server executor schema)
202
+ continuation_group = parser.add_argument_group(
203
+ 'process continuation and actions')
204
+ continuation_group.add_argument(
205
+ '--continue-execution',
206
+ help='Continue an existing execution by providing execution ID'
207
+ )
208
+ continuation_group.add_argument(
209
+ '--action',
210
+ choices=['draft_write', 'draft_rewrite', 'send', 'close'],
211
+ default='draft_write',
212
+ help='Action to perform: draft_write (generate new drafts), draft_rewrite (modify existing), send (send approved draft), close (close outreach)'
213
+ )
214
+ continuation_group.add_argument(
215
+ '--selected-draft-id',
216
+ help='ID of existing draft to rewrite or send (required for draft_rewrite and send actions)'
217
+ )
218
+ continuation_group.add_argument(
219
+ '--reason',
220
+ help='Reason for the action (e.g., "customer requested more info", "make tone more casual")'
221
+ )
222
+ continuation_group.add_argument(
223
+ '--recipient-address',
224
+ help='Email address of the recipient (required for send action)'
225
+ )
226
+ continuation_group.add_argument(
227
+ '--recipient-name',
228
+ help='Name of the recipient (optional for send action)'
229
+ )
230
+ continuation_group.add_argument(
231
+ '--interaction-type',
232
+ default='email',
233
+ help='Type of interaction (default: email)'
234
+ )
235
+ continuation_group.add_argument(
236
+ '--send-immediately',
237
+ action='store_true',
238
+ help='Send email immediately instead of scheduling for optimal time'
239
+ )
240
+ continuation_group.add_argument(
241
+ '--customer-timezone',
242
+ help='Customer timezone (e.g., America/New_York, Europe/London). Auto-detected if not provided'
243
+ )
244
+ continuation_group.add_argument(
245
+ '--business-hours-start',
246
+ default='08:00',
247
+ help='Business hours start time in HH:MM format (default: 08:00)'
248
+ )
249
+ continuation_group.add_argument(
250
+ '--business-hours-end',
251
+ default='20:00',
252
+ help='Business hours end time in HH:MM format (default: 20:00)'
253
+ )
254
+ continuation_group.add_argument(
255
+ '--delay-hours',
256
+ type=int,
257
+ default=2,
258
+ help='Default delay in hours before sending (default: 2)'
259
+ )
260
+ continuation_group.add_argument(
261
+ '--human-action-id',
262
+ help='ID of the human action event (for server integration)'
263
+ )
264
+
265
+ # Output and storage options
266
+ output_group = parser.add_argument_group('output options')
267
+ output_group.add_argument(
268
+ '--output-format',
269
+ choices=['json', 'text', 'yaml'],
270
+ default='json',
271
+ help='Output format for results (default: json)'
272
+ )
273
+ output_group.add_argument(
274
+ '--data-dir',
275
+ default='./fusesell_data',
276
+ help='Directory for local data storage (default: ./fusesell_data)'
277
+ )
278
+ output_group.add_argument(
279
+ '--execution-id',
280
+ help='Custom execution ID (auto-generated if not provided)'
281
+ )
282
+ output_group.add_argument(
283
+ '--save-intermediate',
284
+ action='store_true',
285
+ help='Save intermediate results from each stage'
286
+ )
287
+
288
+ # Logging and debugging
289
+ debug_group = parser.add_argument_group('logging and debugging')
290
+ debug_group.add_argument(
291
+ '--log-level',
292
+ choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'],
293
+ default='INFO',
294
+ help='Logging level (default: INFO)'
295
+ )
296
+ debug_group.add_argument(
297
+ '--log-file',
298
+ help='Log file path (logs to console if not specified)'
299
+ )
300
+ debug_group.add_argument(
301
+ '--verbose',
302
+ action='store_true',
303
+ help='Enable verbose output'
304
+ )
305
+ debug_group.add_argument(
306
+ '--dry-run',
307
+ action='store_true',
308
+ help='Validate inputs and show execution plan without running'
309
+ )
310
+
311
+ # Advanced options
312
+ advanced_group = parser.add_argument_group('advanced options')
313
+ advanced_group.add_argument(
314
+ '--llm-model',
315
+ default='gpt-4o-mini',
316
+ help='LLM model to use (default: gpt-4o-mini)'
317
+ )
318
+ advanced_group.add_argument(
319
+ '--llm-base-url',
320
+ help='Custom LLM API base URL'
321
+ )
322
+ advanced_group.add_argument(
323
+ '--temperature',
324
+ type=float,
325
+ default=0.7,
326
+ help='LLM temperature for creativity (0.0-2.0, default: 0.7)'
327
+ )
328
+ advanced_group.add_argument(
329
+ '--max-retries',
330
+ type=int,
331
+ default=3,
332
+ help='Maximum retries for API calls (default: 3)'
333
+ )
334
+ advanced_group.add_argument(
335
+ '--serper-api-key',
336
+ help='Serper API key for enhanced web scraping and company research (optional)'
337
+ )
338
+
339
+ def _add_team_arguments(self, parser: argparse.ArgumentParser) -> None:
340
+ """Add team management arguments."""
341
+ team_subparsers = parser.add_subparsers(
342
+ dest='team_action', help='Team actions')
343
+
344
+ # Team create
345
+ create_parser = team_subparsers.add_parser(
346
+ 'create', help='Create a new team')
347
+ create_parser.add_argument('--name', required=True, help='Team name')
348
+ create_parser.add_argument('--description', help='Team description')
349
+ create_parser.add_argument(
350
+ '--org-id', required=True, help='Organization ID')
351
+ create_parser.add_argument(
352
+ '--org-name', required=True, help='Organization name')
353
+ create_parser.add_argument('--plan-id', required=True, help='Plan ID')
354
+ create_parser.add_argument('--plan-name', help='Plan name')
355
+ create_parser.add_argument('--project-code', help='Project code')
356
+ create_parser.add_argument('--avatar', help='Avatar URL')
357
+
358
+ # Team update
359
+ update_parser = team_subparsers.add_parser(
360
+ 'update', help='Update an existing team')
361
+ update_parser.add_argument('team_id', help='Team ID to update')
362
+ update_parser.add_argument('--name', help='New team name')
363
+ update_parser.add_argument(
364
+ '--description', help='New team description')
365
+ update_parser.add_argument('--plan-name', help='New plan name')
366
+ update_parser.add_argument('--project-code', help='New project code')
367
+ update_parser.add_argument('--avatar', help='New avatar URL')
368
+
369
+ # Team list
370
+ list_parser = team_subparsers.add_parser('list', help='List teams')
371
+ list_parser.add_argument(
372
+ '--org-id', required=True, help='Organization ID')
373
+
374
+ # Team describe
375
+ describe_parser = team_subparsers.add_parser(
376
+ 'describe', help='Show team details')
377
+ describe_parser.add_argument('team_id', help='Team ID to describe')
378
+
379
+ def _add_product_arguments(self, parser: argparse.ArgumentParser) -> None:
380
+ """Add product management arguments."""
381
+ product_subparsers = parser.add_subparsers(
382
+ dest='product_action', help='Product actions')
383
+
384
+ # Product create
385
+ create_parser = product_subparsers.add_parser(
386
+ 'create', help='Create a new product')
387
+ create_parser.add_argument(
388
+ '--name', required=True, help='Product name')
389
+ create_parser.add_argument('--description', help='Product description')
390
+ create_parser.add_argument(
391
+ '--org-id', required=True, help='Organization ID')
392
+ create_parser.add_argument(
393
+ '--org-name', required=True, help='Organization name')
394
+ create_parser.add_argument(
395
+ '--product-data', help='Additional product data as JSON')
396
+ create_parser.add_argument('--category', help='Product category')
397
+ create_parser.add_argument('--subcategory', help='Product subcategory')
398
+ create_parser.add_argument('--project-code', help='Project code')
399
+
400
+ # Product update
401
+ update_parser = product_subparsers.add_parser(
402
+ 'update', help='Update an existing product')
403
+ update_parser.add_argument('product_id', help='Product ID to update')
404
+ update_parser.add_argument('--name', help='New product name')
405
+ update_parser.add_argument(
406
+ '--description', help='New product description')
407
+ update_parser.add_argument(
408
+ '--product-data', help='Updated product data as JSON')
409
+ update_parser.add_argument('--category', help='New product category')
410
+ update_parser.add_argument(
411
+ '--subcategory', help='New product subcategory')
412
+
413
+ # Product list
414
+ list_parser = product_subparsers.add_parser(
415
+ 'list', help='List products')
416
+ list_parser.add_argument(
417
+ '--org-id', required=True, help='Organization ID')
418
+
419
+ def _add_settings_arguments(self, parser: argparse.ArgumentParser) -> None:
420
+ """Add settings management arguments."""
421
+ settings_subparsers = parser.add_subparsers(
422
+ dest='settings_action', help='Settings actions')
423
+
424
+ # Settings set (for simple settings)
425
+ set_parser = settings_subparsers.add_parser(
426
+ 'set', help='Set team setting (for simple settings)')
427
+ set_parser.add_argument('team_id', help='Team ID')
428
+ set_parser.add_argument('--setting-name', required=True,
429
+ choices=['gs_team_organization', 'gs_team_rep', 'gs_team_product',
430
+ 'gs_team_schedule_time', 'gs_team_auto_interaction',
431
+ 'gs_team_followup_schedule_time', 'gs_team_birthday_email'],
432
+ help='Setting name to update')
433
+ set_parser.add_argument(
434
+ '--value-json', required=True, help='Setting value as JSON')
435
+
436
+ # Settings configure (for complex settings like initial_outreach and follow_up)
437
+ configure_parser = settings_subparsers.add_parser(
438
+ 'configure', help='Configure complex team settings')
439
+ configure_parser.add_argument('team_id', help='Team ID')
440
+ configure_parser.add_argument('--setting-type', required=True,
441
+ choices=[
442
+ 'initial_outreach', 'follow_up'],
443
+ help='Type of setting to configure')
444
+ configure_parser.add_argument('--user-input', required=True,
445
+ help='User instructions, prompt, or guidance for the setting')
446
+ configure_parser.add_argument('--examples-files', nargs='*',
447
+ help='Paths to example email files (optional)')
448
+ configure_parser.add_argument('--template-mode', choices=['ai_enhancement', 'strict_template'],
449
+ default='ai_enhancement',
450
+ help='Template processing mode (default: ai_enhancement)')
451
+
452
+ # Settings view
453
+ view_parser = settings_subparsers.add_parser(
454
+ 'view', help='View team setting')
455
+ view_parser.add_argument('team_id', help='Team ID')
456
+ view_parser.add_argument('--setting-name', required=True,
457
+ choices=['gs_team_organization', 'gs_team_rep', 'gs_team_product',
458
+ 'gs_team_schedule_time', 'gs_team_initial_outreach', 'gs_team_follow_up',
459
+ 'gs_team_auto_interaction', 'gs_team_followup_schedule_time', 'gs_team_birthday_email'],
460
+ help='Setting name to view')
461
+
462
+ # Birthday email management
463
+ birthday_parser = settings_subparsers.add_parser(
464
+ 'birthday', help='Manage birthday email settings and templates')
465
+ birthday_subparsers = birthday_parser.add_subparsers(
466
+ dest='birthday_action', help='Birthday email actions')
467
+
468
+ # Birthday configure
469
+ birthday_configure_parser = birthday_subparsers.add_parser(
470
+ 'configure', help='Configure birthday email settings')
471
+ birthday_configure_parser.add_argument('team_id', help='Team ID')
472
+ birthday_configure_parser.add_argument('--prompt', required=True,
473
+ help='Birthday email configuration prompt')
474
+ birthday_configure_parser.add_argument('--org-id', required=True,
475
+ help='Organization ID')
476
+
477
+ # Birthday template list
478
+ birthday_list_parser = birthday_subparsers.add_parser(
479
+ 'list-templates', help='List birthday email templates')
480
+ birthday_list_parser.add_argument('--team-id', help='Filter by team ID')
481
+ birthday_list_parser.add_argument('--org-id', help='Filter by organization ID')
482
+
483
+ # Birthday template view
484
+ birthday_view_parser = birthday_subparsers.add_parser(
485
+ 'view-template', help='View birthday email template')
486
+ birthday_view_parser.add_argument('template_id', help='Template ID to view')
487
+
488
+ def parse_args(self, args: Optional[list] = None) -> argparse.Namespace:
489
+ """
490
+ Parse command-line arguments.
491
+
492
+ Args:
493
+ args: Optional list of arguments (uses sys.argv if None)
494
+
495
+ Returns:
496
+ Parsed arguments namespace
497
+ """
498
+ return self.parser.parse_args(args)
499
+
500
+ def validate_args(self, args: argparse.Namespace) -> bool:
501
+ """
502
+ Validate parsed arguments.
503
+
504
+ Args:
505
+ args: Parsed arguments
506
+
507
+ Returns:
508
+ True if arguments are valid, False otherwise
509
+ """
510
+ validator = InputValidator()
511
+
512
+ # Validate required fields
513
+ if not validator.validate_api_key(args.openai_api_key):
514
+ print("Error: Invalid OpenAI API key format", file=sys.stderr)
515
+ return False
516
+
517
+ # Different validation for new vs continuation processes
518
+ if args.continue_execution:
519
+ # Continuation mode - validate continuation parameters
520
+ if not args.action:
521
+ print(
522
+ "Error: --action is required when continuing an execution", file=sys.stderr)
523
+ print(
524
+ "Available actions: draft_write, draft_rewrite, send, close", file=sys.stderr)
525
+ return False
526
+
527
+ if args.action in ['draft_rewrite', 'send'] and not args.selected_draft_id:
528
+ print(
529
+ f"Error: --selected-draft-id is required for action '{args.action}'", file=sys.stderr)
530
+ return False
531
+
532
+ if args.action == 'send' and not args.recipient_address:
533
+ print(
534
+ f"Error: --recipient-address is required for action '{args.action}'", file=sys.stderr)
535
+ return False
536
+ else:
537
+ # New process mode - validate data sources (matching executor schema)
538
+ data_sources = [
539
+ args.input_website,
540
+ args.input_description,
541
+ args.input_business_card,
542
+ args.input_linkedin_url,
543
+ args.input_facebook_url,
544
+ args.input_freetext
545
+ ]
546
+
547
+ if not any(data_sources):
548
+ print(
549
+ "Error: At least one data source is required for new processes:", file=sys.stderr)
550
+ print(" - Website URL (--input-website)", file=sys.stderr)
551
+ print(" - Customer description (--input-description)",
552
+ file=sys.stderr)
553
+ print(" - Business card URL (--input-business-card)",
554
+ file=sys.stderr)
555
+ print(" - LinkedIn URL (--input-linkedin-url)", file=sys.stderr)
556
+ print(" - Facebook URL (--input-facebook-url)", file=sys.stderr)
557
+ print(" - Free text input (--input-freetext)", file=sys.stderr)
558
+ print("", file=sys.stderr)
559
+ print(
560
+ "To continue an existing process, use --continue-execution with --action", file=sys.stderr)
561
+ return False
562
+
563
+ # Validate URLs if provided
564
+ if args.input_website and not validator.validate_url(args.input_website):
565
+ print(
566
+ f"Error: Invalid website URL: {args.input_website}", file=sys.stderr)
567
+ return False
568
+
569
+ if args.input_business_card and not validator.validate_url(args.input_business_card):
570
+ print(
571
+ f"Error: Invalid business card URL: {args.input_business_card}", file=sys.stderr)
572
+ return False
573
+
574
+ if args.input_linkedin_url and not validator.validate_url(args.input_linkedin_url):
575
+ print(
576
+ f"Error: Invalid LinkedIn URL: {args.input_linkedin_url}", file=sys.stderr)
577
+ return False
578
+
579
+ if args.input_facebook_url and not validator.validate_url(args.input_facebook_url):
580
+ print(
581
+ f"Error: Invalid Facebook URL: {args.input_facebook_url}", file=sys.stderr)
582
+ return False
583
+
584
+ # Validate recipient email if provided
585
+ if hasattr(args, 'recipient_address') and args.recipient_address and not validator.validate_email(args.recipient_address):
586
+ print(
587
+ f"Error: Invalid recipient email: {args.recipient_address}", file=sys.stderr)
588
+ return False
589
+
590
+ # Validate temperature range
591
+ if not (0.0 <= args.temperature <= 2.0):
592
+ print(
593
+ f"Error: Temperature must be between 0.0 and 2.0, got: {args.temperature}", file=sys.stderr)
594
+ return False
595
+
596
+ return True
597
+
598
+ def create_config(self, args: argparse.Namespace) -> Dict[str, Any]:
599
+ """
600
+ Create configuration dictionary from parsed arguments.
601
+
602
+ Args:
603
+ args: Parsed arguments
604
+
605
+ Returns:
606
+ Configuration dictionary
607
+ """
608
+ return build_pipeline_config(args)
609
+
610
+ def setup_logging(self, config: Dict[str, Any]) -> None:
611
+ """
612
+ Set up logging based on configuration.
613
+
614
+ Args:
615
+ config: Configuration dictionary
616
+ """
617
+ self.logger = configure_pipeline_logging(config)
618
+
619
+ def validate_configuration(self, config: Dict[str, Any]) -> tuple[bool, list]:
620
+ """
621
+ Comprehensive configuration validation.
622
+
623
+ Args:
624
+ config: Configuration dictionary to validate
625
+
626
+ Returns:
627
+ Tuple of (is_valid, list_of_errors)
628
+ """
629
+ return validate_pipeline_config(config)
630
+
631
+ def setup_output_directories(self, config: Dict[str, Any]) -> None:
632
+ """
633
+ Set up output directories based on configuration.
634
+
635
+ Args:
636
+ config: Configuration dictionary
637
+ """
638
+ try:
639
+ data_dir = prepare_data_directory(config)
640
+ print(f"Output directories set up in: {data_dir}")
641
+ except Exception as e:
642
+ print(
643
+ f"Failed to set up output directories: {str(e)}", file=sys.stderr)
644
+ raise
645
+
646
+ def print_execution_plan(self, config: Dict[str, Any]) -> None:
647
+ """
648
+ Print execution plan for dry run.
649
+
650
+ Args:
651
+ config: Configuration dictionary
652
+ """
653
+ print("FuseSell Execution Plan")
654
+ print("=" * 50)
655
+ print(f"Execution ID: {config['execution_id']}")
656
+ print(f"Organization: {config['org_name']} ({config['org_id']})")
657
+
658
+ # Display data sources
659
+ data_sources = []
660
+ if config.get('input_website'):
661
+ data_sources.append(f"Website: {config['input_website']}")
662
+ if config.get('input_description'):
663
+ data_sources.append(
664
+ f"Description: {config['input_description'][:50]}...")
665
+ if config.get('input_business_card'):
666
+ data_sources.append(
667
+ f"Business Card: {config['input_business_card']}")
668
+ if config.get('input_linkedin_url'):
669
+ data_sources.append(f"LinkedIn: {config['input_linkedin_url']}")
670
+ if config.get('input_facebook_url'):
671
+ data_sources.append(f"Facebook: {config['input_facebook_url']}")
672
+
673
+ print(
674
+ f"Data Sources: {'; '.join(data_sources) if data_sources else 'None'}")
675
+ print(f"Language: {config['language']}")
676
+ print(f"Data Directory: {config['data_dir']}")
677
+ print(f"Output Format: {config['output_format']}")
678
+
679
+ if config['team_id']:
680
+ print(f"Team: {config['team_name']} ({config['team_id']})")
681
+
682
+ print("\nPipeline Stages:")
683
+ stages = ['data_acquisition', 'data_preparation',
684
+ 'lead_scoring', 'initial_outreach', 'follow_up']
685
+ skip_stages = config.get('skip_stages', [])
686
+ stop_after = config.get('stop_after')
687
+
688
+ for i, stage in enumerate(stages, 1):
689
+ status = "SKIP" if stage in skip_stages else "RUN"
690
+ print(f" {i}. {stage.replace('_', ' ').title()}: {status}")
691
+
692
+ if stop_after == stage:
693
+ print(f" Pipeline will stop after {stage}")
694
+ break
695
+
696
+ print(f"\nConfiguration saved to: {config['data_dir']}/config/")
697
+ print("Run without --dry-run to execute the pipeline.")
698
+
699
+ def format_output(self, results: Dict[str, Any], format_type: str) -> str:
700
+ """
701
+ Format execution results for output.
702
+
703
+ Args:
704
+ results: Execution results
705
+ format_type: Output format (json, text, yaml)
706
+
707
+ Returns:
708
+ Formatted output string
709
+ """
710
+ if format_type == 'json':
711
+ return json.dumps(results, indent=2, default=str)
712
+
713
+ elif format_type == 'yaml':
714
+ try:
715
+ import yaml
716
+ return yaml.dump(results, default_flow_style=False)
717
+ except ImportError:
718
+ self.logger.warning(
719
+ "PyYAML not installed, falling back to JSON")
720
+ return json.dumps(results, indent=2, default=str)
721
+
722
+ elif format_type == 'text':
723
+ return self._format_text_output(results)
724
+
725
+ else:
726
+ return json.dumps(results, indent=2, default=str)
727
+
728
+ def _format_text_output(self, results: Dict[str, Any]) -> str:
729
+ """
730
+ Format results as human-readable text.
731
+
732
+ Args:
733
+ results: Execution results
734
+
735
+ Returns:
736
+ Formatted text output
737
+ """
738
+ output = []
739
+ output.append("FuseSell Execution Results")
740
+ output.append("=" * 50)
741
+
742
+ # Basic info
743
+ output.append(f"Execution ID: {results.get('execution_id', 'N/A')}")
744
+ output.append(f"Status: {results.get('status', 'N/A')}")
745
+ output.append(f"Started: {results.get('started_at', 'N/A')}")
746
+ output.append(f"Completed: {results.get('completed_at', 'N/A')}")
747
+ output.append("")
748
+
749
+ # Stage results
750
+ stage_results = results.get('stage_results', {})
751
+ if stage_results:
752
+ output.append("Stage Results:")
753
+ output.append("-" * 20)
754
+ for stage, result in stage_results.items():
755
+ status = result.get('status', 'unknown')
756
+ output.append(
757
+ f"{stage.replace('_', ' ').title()}: {status.upper()}")
758
+
759
+ if status == 'error' and result.get('error_message'):
760
+ output.append(f" Error: {result['error_message']}")
761
+
762
+ # Customer info
763
+ customer_data = results.get('customer_data', {})
764
+ if customer_data:
765
+ output.append("\nCustomer Information:")
766
+ output.append("-" * 20)
767
+ output.append(
768
+ f"Company: {customer_data.get('company_name', 'N/A')}")
769
+ output.append(f"Industry: {customer_data.get('industry', 'N/A')}")
770
+ output.append(f"Website: {customer_data.get('website', 'N/A')}")
771
+
772
+ # Lead scores
773
+ lead_scores = results.get('lead_scores', [])
774
+ if lead_scores:
775
+ output.append("\nLead Scores:")
776
+ output.append("-" * 20)
777
+ for score in lead_scores:
778
+ output.append(
779
+ f"Product {score.get('product_id', 'N/A')}: {score.get('score', 0)}/100")
780
+
781
+ # Email drafts
782
+ email_drafts = results.get('email_drafts', [])
783
+ if email_drafts:
784
+ output.append("\nEmail Drafts Generated:")
785
+ output.append("-" * 20)
786
+ for draft in email_drafts:
787
+ output.append(f"Subject: {draft.get('subject', 'N/A')}")
788
+ output.append(f"Type: {draft.get('draft_type', 'N/A')}")
789
+
790
+ return "\n".join(output)
791
+
792
+ def run(self, args: Optional[list] = None) -> int:
793
+ """
794
+ Main execution method.
795
+
796
+ Args:
797
+ args: Optional command-line arguments
798
+
799
+ Returns:
800
+ Exit code (0 for success, 1 for error)
801
+ """
802
+ try:
803
+ # Parse arguments
804
+ parsed_args = self.parse_args(args)
805
+
806
+ # Handle different commands
807
+ command = getattr(parsed_args, 'command', None)
808
+
809
+ # If no command specified, default to pipeline for backward compatibility
810
+ if command is None:
811
+ # For backward compatibility, treat no subcommand as pipeline
812
+ return self._run_pipeline(parsed_args)
813
+ elif command == 'pipeline':
814
+ return self._run_pipeline(parsed_args)
815
+ elif command == 'team':
816
+ return self._run_team_command(parsed_args)
817
+ elif command == 'product':
818
+ return self._run_product_command(parsed_args)
819
+ elif command == 'settings':
820
+ return self._run_settings_command(parsed_args)
821
+ else:
822
+ print(f"Unknown command: {command}", file=sys.stderr)
823
+ return 1
824
+
825
+ except KeyboardInterrupt:
826
+ print("\nExecution interrupted by user", file=sys.stderr)
827
+ return 1
828
+ except Exception as e:
829
+ print(f"Error: {str(e)}", file=sys.stderr)
830
+ return 1
831
+
832
+ def _run_pipeline(self, args: argparse.Namespace) -> int:
833
+ """Run the sales automation pipeline."""
834
+ try:
835
+ # Validate required pipeline arguments
836
+ if not args.openai_api_key:
837
+ print(
838
+ "Error: --openai-api-key is required for pipeline execution", file=sys.stderr)
839
+ return 1
840
+ if not args.org_id:
841
+ print("Error: --org-id is required for pipeline execution",
842
+ file=sys.stderr)
843
+ return 1
844
+ if not args.org_name:
845
+ print(
846
+ "Error: --org-name is required for pipeline execution", file=sys.stderr)
847
+ return 1
848
+ if not args.full_input:
849
+ print(
850
+ "Error: --full-input is required for pipeline execution", file=sys.stderr)
851
+ return 1
852
+
853
+ if not self.validate_args(args):
854
+ return 1
855
+
856
+ # Create configuration
857
+ config = self.create_config(args)
858
+
859
+ # Set up output directories first
860
+ self.setup_output_directories(config)
861
+
862
+ # Set up logging (after log file path is configured)
863
+ self.setup_logging(config)
864
+
865
+ # Validate configuration
866
+ config_valid, config_errors = self.validate_configuration(config)
867
+ if not config_valid:
868
+ print("Configuration validation failed:", file=sys.stderr)
869
+ for error in config_errors:
870
+ print(f" - {error}", file=sys.stderr)
871
+ return 1
872
+
873
+ self.logger.info(
874
+ f"Starting FuseSell execution: {config['execution_id']}")
875
+
876
+ # Handle dry run
877
+ if config['dry_run']:
878
+ self.print_execution_plan(config)
879
+ return 0
880
+
881
+ # Initialize and run pipeline
882
+ pipeline = FuseSellPipeline(config)
883
+ results = pipeline.execute()
884
+
885
+ # Format and output results
886
+ formatted_output = self.format_output(
887
+ results, config['output_format'])
888
+ print(formatted_output)
889
+
890
+ # Return appropriate exit code
891
+ if results.get('status') == 'completed':
892
+ self.logger.info("FuseSell execution completed successfully")
893
+ return 0
894
+ else:
895
+ self.logger.error("FuseSell execution failed")
896
+ return 1
897
+
898
+ except Exception as e:
899
+ print(f"Pipeline error: {str(e)}", file=sys.stderr)
900
+ return 1
901
+
902
+ def _run_team_command(self, args: argparse.Namespace) -> int:
903
+ """Handle team management commands."""
904
+ from .utils.data_manager import LocalDataManager
905
+
906
+ try:
907
+ data_manager = LocalDataManager()
908
+ action = getattr(args, 'team_action', None)
909
+
910
+ if action == 'create':
911
+ team_id = f"team_{args.org_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
912
+ data_manager.save_team(
913
+ team_id=team_id,
914
+ org_id=args.org_id,
915
+ org_name=args.org_name,
916
+ plan_id=args.plan_id,
917
+ name=args.name,
918
+ description=args.description,
919
+ plan_name=getattr(args, 'plan_name', None),
920
+ project_code=getattr(args, 'project_code', None),
921
+ avatar=getattr(args, 'avatar', None)
922
+ )
923
+ print(f"Team created successfully: {team_id}")
924
+ return 0
925
+
926
+ elif action == 'update':
927
+ success = data_manager.update_team(
928
+ team_id=args.team_id,
929
+ name=getattr(args, 'name', None),
930
+ description=getattr(args, 'description', None),
931
+ plan_name=getattr(args, 'plan_name', None),
932
+ project_code=getattr(args, 'project_code', None),
933
+ avatar=getattr(args, 'avatar', None)
934
+ )
935
+ if success:
936
+ print(f"Team updated successfully: {args.team_id}")
937
+ return 0
938
+ else:
939
+ print(f"Team not found: {args.team_id}", file=sys.stderr)
940
+ return 1
941
+
942
+ elif action == 'list':
943
+ teams = data_manager.list_teams(args.org_id)
944
+ if teams:
945
+ print(f"Teams for organization {args.org_id}:")
946
+ for team in teams:
947
+ print(
948
+ f" {team['team_id']}: {team['name']} - {team.get('description', 'No description')}")
949
+ else:
950
+ print(f"No teams found for organization {args.org_id}")
951
+ return 0
952
+
953
+ elif action == 'describe':
954
+ team = data_manager.get_team(args.team_id)
955
+ if team:
956
+ print(json.dumps(team, indent=2, default=str))
957
+ else:
958
+ print(f"Team not found: {args.team_id}", file=sys.stderr)
959
+ return 1
960
+ return 0
961
+
962
+ else:
963
+ print(f"Unknown team action: {action}", file=sys.stderr)
964
+ return 1
965
+
966
+ except Exception as e:
967
+ print(f"Team command error: {str(e)}", file=sys.stderr)
968
+ return 1
969
+
970
+ def _run_product_command(self, args: argparse.Namespace) -> int:
971
+ """Handle product management commands."""
972
+ from .utils.data_manager import LocalDataManager
973
+
974
+ try:
975
+ data_manager = LocalDataManager()
976
+ action = getattr(args, 'product_action', None)
977
+
978
+ if action == 'create':
979
+ product_data = {
980
+ 'org_id': args.org_id,
981
+ 'org_name': args.org_name,
982
+ 'productName': args.name,
983
+ 'shortDescription': getattr(args, 'description', None),
984
+ 'category': getattr(args, 'category', None),
985
+ 'subcategory': getattr(args, 'subcategory', None),
986
+ 'project_code': getattr(args, 'project_code', None)
987
+ }
988
+
989
+ # Parse additional product data if provided
990
+ if hasattr(args, 'product_data') and args.product_data:
991
+ try:
992
+ additional_data = json.loads(args.product_data)
993
+ product_data.update(additional_data)
994
+ except json.JSONDecodeError:
995
+ print("Invalid JSON in --product-data", file=sys.stderr)
996
+ return 1
997
+
998
+ product_id = data_manager.save_product(product_data)
999
+ print(f"Product created successfully: {product_id}")
1000
+ return 0
1001
+
1002
+ elif action == 'update':
1003
+ product_data = {}
1004
+ if hasattr(args, 'name') and args.name:
1005
+ product_data['productName'] = args.name
1006
+ if hasattr(args, 'description') and args.description:
1007
+ product_data['shortDescription'] = args.description
1008
+ if hasattr(args, 'category') and args.category:
1009
+ product_data['category'] = args.category
1010
+ if hasattr(args, 'subcategory') and args.subcategory:
1011
+ product_data['subcategory'] = args.subcategory
1012
+
1013
+ # Parse additional product data if provided
1014
+ if hasattr(args, 'product_data') and args.product_data:
1015
+ try:
1016
+ additional_data = json.loads(args.product_data)
1017
+ product_data.update(additional_data)
1018
+ except json.JSONDecodeError:
1019
+ print("Invalid JSON in --product-data", file=sys.stderr)
1020
+ return 1
1021
+
1022
+ success = data_manager.update_product(
1023
+ args.product_id, product_data)
1024
+ if success:
1025
+ print(f"Product updated successfully: {args.product_id}")
1026
+ return 0
1027
+ else:
1028
+ print(
1029
+ f"Product not found: {args.product_id}", file=sys.stderr)
1030
+ return 1
1031
+
1032
+ elif action == 'list':
1033
+ products = data_manager.get_products_by_org(args.org_id)
1034
+ if products:
1035
+ print(f"Products for organization {args.org_id}:")
1036
+ for product in products:
1037
+ print(
1038
+ f" {product['product_id']}: {product['product_name']} - {product.get('short_description', 'No description')}")
1039
+ else:
1040
+ print(f"No products found for organization {args.org_id}")
1041
+ return 0
1042
+
1043
+ else:
1044
+ print(f"Unknown product action: {action}", file=sys.stderr)
1045
+ return 1
1046
+
1047
+ except Exception as e:
1048
+ print(f"Product command error: {str(e)}", file=sys.stderr)
1049
+ return 1
1050
+
1051
+ def _run_settings_command(self, args: argparse.Namespace) -> int:
1052
+ """Handle settings management commands."""
1053
+ from .utils.data_manager import LocalDataManager
1054
+
1055
+ try:
1056
+ data_manager = LocalDataManager()
1057
+ action = getattr(args, 'settings_action', None)
1058
+
1059
+ if action == 'set':
1060
+ # Parse the JSON value
1061
+ try:
1062
+ value = json.loads(args.value_json)
1063
+ except json.JSONDecodeError:
1064
+ print("Invalid JSON in --value-json", file=sys.stderr)
1065
+ return 1
1066
+
1067
+ # Validate auto interaction settings format
1068
+ if args.setting_name == 'gs_team_auto_interaction':
1069
+ validation_error = self._validate_auto_interaction_settings(value)
1070
+ if validation_error:
1071
+ print(f"Invalid auto interaction settings: {validation_error}", file=sys.stderr)
1072
+ return 1
1073
+
1074
+ # Get existing team settings or create new ones
1075
+ team_settings = data_manager.get_team_settings(args.team_id)
1076
+ if not team_settings:
1077
+ # Get team info to create settings
1078
+ team = data_manager.get_team(args.team_id)
1079
+ if not team:
1080
+ print(
1081
+ f"Team not found: {args.team_id}", file=sys.stderr)
1082
+ return 1
1083
+
1084
+ # Create new settings with all fields initialized
1085
+ settings_kwargs = {
1086
+ 'team_id': args.team_id,
1087
+ 'org_id': team['org_id'],
1088
+ 'plan_id': team['plan_id'],
1089
+ 'team_name': team['name'],
1090
+ 'gs_team_organization': None,
1091
+ 'gs_team_rep': None,
1092
+ 'gs_team_product': None,
1093
+ 'gs_team_schedule_time': None,
1094
+ 'gs_team_initial_outreach': None,
1095
+ 'gs_team_follow_up': None,
1096
+ 'gs_team_auto_interaction': None,
1097
+ 'gs_team_followup_schedule_time': None,
1098
+ 'gs_team_birthday_email': None
1099
+ }
1100
+ # Set the specific setting being updated
1101
+ settings_kwargs[args.setting_name] = value
1102
+ else:
1103
+ # Update existing settings - preserve all existing values
1104
+ settings_kwargs = {
1105
+ 'team_id': args.team_id,
1106
+ 'org_id': team_settings['org_id'],
1107
+ 'plan_id': team_settings['plan_id'],
1108
+ 'team_name': team_settings.get('team_name', ''),
1109
+ 'gs_team_organization': team_settings.get('gs_team_organization'),
1110
+ 'gs_team_rep': team_settings.get('gs_team_rep'),
1111
+ 'gs_team_product': team_settings.get('gs_team_product'),
1112
+ 'gs_team_schedule_time': team_settings.get('gs_team_schedule_time'),
1113
+ 'gs_team_initial_outreach': team_settings.get('gs_team_initial_outreach'),
1114
+ 'gs_team_follow_up': team_settings.get('gs_team_follow_up'),
1115
+ 'gs_team_auto_interaction': team_settings.get('gs_team_auto_interaction'),
1116
+ 'gs_team_followup_schedule_time': team_settings.get('gs_team_followup_schedule_time'),
1117
+ 'gs_team_birthday_email': team_settings.get('gs_team_birthday_email')
1118
+ }
1119
+ # Update only the specific setting being changed
1120
+ settings_kwargs[args.setting_name] = value
1121
+
1122
+ data_manager.save_team_settings(**settings_kwargs)
1123
+ print(
1124
+ f"Setting '{args.setting_name}' updated for team {args.team_id}")
1125
+ return 0
1126
+
1127
+ elif action == 'configure':
1128
+ # Handle complex settings configuration
1129
+ return self._configure_complex_setting(args, data_manager)
1130
+
1131
+ elif action == 'view':
1132
+ team_settings = data_manager.get_team_settings(args.team_id)
1133
+ if team_settings and args.setting_name in team_settings:
1134
+ setting_value = team_settings[args.setting_name]
1135
+ print(json.dumps(setting_value, indent=2, default=str))
1136
+ else:
1137
+ print(
1138
+ f"Setting '{args.setting_name}' not found for team {args.team_id}", file=sys.stderr)
1139
+ return 1
1140
+ return 0
1141
+
1142
+ elif action == 'birthday':
1143
+ return self._run_birthday_email_command(args, data_manager)
1144
+
1145
+ else:
1146
+ print(f"Unknown settings action: {action}", file=sys.stderr)
1147
+ return 1
1148
+
1149
+ except Exception as e:
1150
+ print(f"Settings command error: {str(e)}", file=sys.stderr)
1151
+ return 1
1152
+
1153
+ def _configure_complex_setting(self, args: argparse.Namespace, data_manager) -> int:
1154
+ """Configure complex settings like initial_outreach and follow_up."""
1155
+ try:
1156
+ # Determine if user input is a complete prompt or instructions
1157
+ user_input = args.user_input.strip()
1158
+ has_examples = bool(args.examples_files)
1159
+
1160
+ # Process according to the flowchart logic
1161
+ if not has_examples:
1162
+ # Case 1: No Examples
1163
+ setting_value = self._process_no_examples_case(
1164
+ user_input, args.setting_type)
1165
+ else:
1166
+ # Case 2 or 3: With Examples
1167
+ if args.template_mode == 'strict_template':
1168
+ # Case 3: Strict Template Mode
1169
+ setting_value = self._process_strict_template_case(
1170
+ user_input, args.examples_files, args.setting_type)
1171
+ else:
1172
+ # Case 2: AI Enhancement Mode
1173
+ setting_value = self._process_ai_enhancement_case(
1174
+ user_input, args.examples_files, args.setting_type)
1175
+
1176
+ # Get team info for settings update
1177
+ team = data_manager.get_team(args.team_id)
1178
+ if not team:
1179
+ print(f"Team not found: {args.team_id}", file=sys.stderr)
1180
+ return 1
1181
+
1182
+ # Get existing team settings or create new ones
1183
+ team_settings = data_manager.get_team_settings(args.team_id)
1184
+
1185
+ # Determine the setting field name
1186
+ setting_field = f"gs_team_{args.setting_type}"
1187
+
1188
+ if not team_settings:
1189
+ # Create new settings with all fields initialized
1190
+ settings_kwargs = {
1191
+ 'team_id': args.team_id,
1192
+ 'org_id': team['org_id'],
1193
+ 'plan_id': team['plan_id'],
1194
+ 'team_name': team['name'],
1195
+ 'gs_team_organization': None,
1196
+ 'gs_team_rep': None,
1197
+ 'gs_team_product': None,
1198
+ 'gs_team_schedule_time': None,
1199
+ 'gs_team_initial_outreach': None,
1200
+ 'gs_team_follow_up': None,
1201
+ 'gs_team_auto_interaction': None,
1202
+ 'gs_team_followup_schedule_time': None,
1203
+ 'gs_team_birthday_email': None
1204
+ }
1205
+ # Set the specific setting being updated
1206
+ settings_kwargs[setting_field] = setting_value
1207
+ else:
1208
+ # Update existing settings - preserve all existing values
1209
+ settings_kwargs = {
1210
+ 'team_id': args.team_id,
1211
+ 'org_id': team_settings['org_id'],
1212
+ 'plan_id': team_settings['plan_id'],
1213
+ 'team_name': team_settings.get('team_name', ''),
1214
+ 'gs_team_organization': team_settings.get('gs_team_organization'),
1215
+ 'gs_team_rep': team_settings.get('gs_team_rep'),
1216
+ 'gs_team_product': team_settings.get('gs_team_product'),
1217
+ 'gs_team_schedule_time': team_settings.get('gs_team_schedule_time'),
1218
+ 'gs_team_initial_outreach': team_settings.get('gs_team_initial_outreach'),
1219
+ 'gs_team_follow_up': team_settings.get('gs_team_follow_up'),
1220
+ 'gs_team_auto_interaction': team_settings.get('gs_team_auto_interaction'),
1221
+ 'gs_team_followup_schedule_time': team_settings.get('gs_team_followup_schedule_time'),
1222
+ 'gs_team_birthday_email': team_settings.get('gs_team_birthday_email')
1223
+ }
1224
+ # Update only the specific setting being changed
1225
+ settings_kwargs[setting_field] = setting_value
1226
+
1227
+ data_manager.save_team_settings(**settings_kwargs)
1228
+ print(
1229
+ f"Complex setting '{args.setting_type}' configured for team {args.team_id}")
1230
+ print(
1231
+ f"Configuration: fewshots={setting_value.get('fewshots', False)}, strict_follow={setting_value.get('fewshots_strict_follow', False)}")
1232
+ return 0
1233
+
1234
+ except Exception as e:
1235
+ print(f"Configuration error: {str(e)}", file=sys.stderr)
1236
+ return 1
1237
+
1238
+ def _run_birthday_email_command(self, args: argparse.Namespace, data_manager) -> int:
1239
+ """Handle birthday email management commands."""
1240
+ from .utils.birthday_email_manager import BirthdayEmailManager
1241
+
1242
+ try:
1243
+ birthday_manager = BirthdayEmailManager(self.create_config(args))
1244
+ birthday_action = getattr(args, 'birthday_action', None)
1245
+
1246
+ if birthday_action == 'configure':
1247
+ # Configure birthday email settings
1248
+ result = birthday_manager.process_birthday_email_settings(
1249
+ team_id=args.team_id,
1250
+ prompt=args.prompt,
1251
+ org_id=args.org_id
1252
+ )
1253
+
1254
+ if result['success']:
1255
+ # Save the generated settings to team settings
1256
+ team = data_manager.get_team(args.team_id)
1257
+ if not team:
1258
+ print(f"Team not found: {args.team_id}", file=sys.stderr)
1259
+ return 1
1260
+
1261
+ # Get existing team settings
1262
+ team_settings = data_manager.get_team_settings(args.team_id)
1263
+
1264
+ if not team_settings:
1265
+ # Create new settings
1266
+ settings_kwargs = {
1267
+ 'team_id': args.team_id,
1268
+ 'org_id': team['org_id'],
1269
+ 'plan_id': team['plan_id'],
1270
+ 'team_name': team['name'],
1271
+ 'gs_team_organization': None,
1272
+ 'gs_team_rep': None,
1273
+ 'gs_team_product': None,
1274
+ 'gs_team_schedule_time': None,
1275
+ 'gs_team_initial_outreach': None,
1276
+ 'gs_team_follow_up': None,
1277
+ 'gs_team_auto_interaction': None,
1278
+ 'gs_team_followup_schedule_time': None,
1279
+ 'gs_team_birthday_email': result['settings']
1280
+ }
1281
+ else:
1282
+ # Update existing settings
1283
+ settings_kwargs = {
1284
+ 'team_id': args.team_id,
1285
+ 'org_id': team_settings['org_id'],
1286
+ 'plan_id': team_settings['plan_id'],
1287
+ 'team_name': team_settings.get('team_name', ''),
1288
+ 'gs_team_organization': team_settings.get('gs_team_organization'),
1289
+ 'gs_team_rep': team_settings.get('gs_team_rep'),
1290
+ 'gs_team_product': team_settings.get('gs_team_product'),
1291
+ 'gs_team_schedule_time': team_settings.get('gs_team_schedule_time'),
1292
+ 'gs_team_initial_outreach': team_settings.get('gs_team_initial_outreach'),
1293
+ 'gs_team_follow_up': team_settings.get('gs_team_follow_up'),
1294
+ 'gs_team_auto_interaction': team_settings.get('gs_team_auto_interaction'),
1295
+ 'gs_team_followup_schedule_time': team_settings.get('gs_team_followup_schedule_time'),
1296
+ 'gs_team_birthday_email': result['settings']
1297
+ }
1298
+
1299
+ data_manager.save_team_settings(**settings_kwargs)
1300
+
1301
+ print(f"Birthday email settings configured for team {args.team_id}")
1302
+ print(f"Settings: {json.dumps(result['settings'], indent=2)}")
1303
+
1304
+ if result.get('template'):
1305
+ print(f"Template generated: {result['template']['template_id']}")
1306
+
1307
+ return 0
1308
+ else:
1309
+ print(f"Birthday email configuration failed: {result.get('error', 'Unknown error')}", file=sys.stderr)
1310
+ return 1
1311
+
1312
+ elif birthday_action == 'list-templates':
1313
+ templates = birthday_manager.list_birthday_templates(
1314
+ team_id=args.team_id,
1315
+ org_id=args.org_id
1316
+ )
1317
+
1318
+ if templates:
1319
+ print("Birthday Email Templates:")
1320
+ for template in templates:
1321
+ print(f" {template['template_id']}: {template.get('subject', 'No subject')} "
1322
+ f"(Team: {template['team_id']}, Created: {template['created_at']})")
1323
+ else:
1324
+ print("No birthday email templates found.")
1325
+ return 0
1326
+
1327
+ elif birthday_action == 'view-template':
1328
+ template = birthday_manager.get_birthday_template(args.template_id)
1329
+
1330
+ if template:
1331
+ print(json.dumps(template, indent=2, default=str))
1332
+ else:
1333
+ print(f"Template not found: {args.template_id}", file=sys.stderr)
1334
+ return 1
1335
+ return 0
1336
+
1337
+ else:
1338
+ print(f"Unknown birthday email action: {birthday_action}", file=sys.stderr)
1339
+ return 1
1340
+
1341
+ except Exception as e:
1342
+ print(f"Birthday email command error: {str(e)}", file=sys.stderr)
1343
+ return 1
1344
+
1345
+ def _process_no_examples_case(self, user_input: str, setting_type: str = "initial_outreach") -> dict:
1346
+ """Process Case 1: No Examples - determine if complete prompt or instructions."""
1347
+ # Simple heuristic to determine if it's a complete prompt or instructions
1348
+ is_complete_prompt = (
1349
+ len(user_input) > 100 and
1350
+ ('##' in user_input or 'create' in user_input.lower()
1351
+ or 'generate' in user_input.lower())
1352
+ )
1353
+
1354
+ if is_complete_prompt:
1355
+ # Use message directly as prompt
1356
+ prompt = user_input
1357
+ else:
1358
+ # Use message + appropriate default prompt
1359
+ default_prompts = {
1360
+ "initial_outreach": "Create professional initial outreach emails for ##customer_name## on behalf of ##staff_name##.",
1361
+ "follow_up": "Create professional follow-up emails for ##customer_name## on behalf of ##staff_name##. Reference previous interactions and add new value."
1362
+ }
1363
+ default_prompt = default_prompts.get(
1364
+ setting_type, default_prompts["initial_outreach"])
1365
+ prompt = f"{default_prompt} Additional guidance: {user_input}"
1366
+
1367
+ return {
1368
+ "fewshots": False,
1369
+ "fewshots_location": [],
1370
+ "fewshots_strict_follow": False,
1371
+ "prompt": prompt,
1372
+ "prompt_in_template": ""
1373
+ }
1374
+
1375
+ def _process_ai_enhancement_case(self, user_input: str, examples_files: list, setting_type: str = "initial_outreach") -> dict:
1376
+ """Process Case 2: AI Enhancement Mode with examples."""
1377
+ email_type = "initial outreach" if setting_type == "initial_outreach" else "follow-up"
1378
+ return {
1379
+ "fewshots": True,
1380
+ "fewshots_location": examples_files,
1381
+ "fewshots_strict_follow": False,
1382
+ "prompt": f"Create {email_type} emails based on provided examples. Additional guidance: {user_input}",
1383
+ "prompt_in_template": "Use the provided examples as inspiration while incorporating the user guidance for improvements and customization."
1384
+ }
1385
+
1386
+ def _process_strict_template_case(self, user_input: str, examples_files: list, setting_type: str = "initial_outreach") -> dict:
1387
+ """Process Case 3: Strict Template Mode with exact template following."""
1388
+ return {
1389
+ "fewshots": True,
1390
+ "fewshots_location": examples_files,
1391
+ "fewshots_strict_follow": True,
1392
+ "prompt": f"Use exact templates from examples for {setting_type}. Context: {user_input}",
1393
+ "prompt_in_template": "Mirror the EXACT CONTENT of provided examples with ZERO wording changes. Only replace the recipient to ##customer_name## from company ##company_name##.\n\nNO PLACEHOLDERS OR COMPANY NAMES AS GREETINGS:\n- Do not use [Contact Name], [Company], etc.\n- If recipient name is unclear, use \"Hi\" or \"Hello\" without a name\n- Never use company name as a greeting\n- No hyperlinks/attachments\n- No invented information\n\nReturn 1 JSON object which include these required fields: mail_tone, subject, body, priority_order, approach, product_mention, product_name, message_type, tags"
1394
+ }
1395
+
1396
+ def _validate_auto_interaction_settings(self, value) -> str:
1397
+ """Validate auto interaction settings format.
1398
+
1399
+ Expected format: [{"from_email": "value", "from_name": "value", "from_number": "value",
1400
+ "tool_type": "Email|Autocall|Notif|SMS", "email_cc": "comma,separated,emails",
1401
+ "email_bcc": "comma,separated,emails"}]
1402
+
1403
+ Returns: Error message if invalid, None if valid
1404
+ """
1405
+ if not isinstance(value, list):
1406
+ return "Auto interaction settings must be a list"
1407
+
1408
+ if len(value) == 0:
1409
+ return "Auto interaction settings list cannot be empty"
1410
+
1411
+ valid_tool_types = ["Email", "Autocall", "Notif", "SMS"]
1412
+ required_fields = ["from_email", "from_name", "from_number", "tool_type", "email_cc", "email_bcc"]
1413
+
1414
+ for i, item in enumerate(value):
1415
+ if not isinstance(item, dict):
1416
+ return f"Item {i} must be an object/dictionary"
1417
+
1418
+ # Check required fields
1419
+ for field in required_fields:
1420
+ if field not in item:
1421
+ return f"Item {i} missing required field: {field}"
1422
+
1423
+ # Validate tool_type
1424
+ if item["tool_type"] not in valid_tool_types:
1425
+ return f"Item {i} has invalid tool_type '{item['tool_type']}'. Must be one of: {', '.join(valid_tool_types)}"
1426
+
1427
+ # Validate email format (basic check)
1428
+ if item["from_email"] and "@" not in item["from_email"]:
1429
+ return f"Item {i} has invalid from_email format"
1430
+
1431
+ # Validate CC/BCC email lists (basic check)
1432
+ for email_field in ["email_cc", "email_bcc"]:
1433
+ if item[email_field]: # Only validate if not empty
1434
+ emails = [email.strip() for email in item[email_field].split(",")]
1435
+ for email in emails:
1436
+ if email and "@" not in email:
1437
+ return f"Item {i} has invalid email in {email_field}: {email}"
1438
+
1439
+ return None # No errors
1440
+
1441
+
1442
+ def main():
1443
+ """Main entry point for command-line execution."""
1444
+ cli = FuseSellCLI()
1445
+ exit_code = cli.run()
1446
+ sys.exit(exit_code)
1447
+
1448
+
1449
+ if __name__ == "__main__":
1450
+ main()