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.

@@ -0,0 +1,65 @@
1
+ from pathlib import Path
2
+ import sys
3
+
4
+ sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
5
+
6
+ from fusesell_local.api import (
7
+ build_config,
8
+ execute_pipeline,
9
+ prepare_data_directory,
10
+ validate_config,
11
+ )
12
+
13
+
14
+ def base_options(**overrides):
15
+ options = {
16
+ "openai_api_key": "sk-test-123456",
17
+ "org_id": "demo",
18
+ "org_name": "Demo Org",
19
+ "full_input": "Seller: Demo Org, Customer: Example Corp, Communication: English",
20
+ "input_description": "Example Corp, contact@example.com",
21
+ "dry_run": True,
22
+ }
23
+ options.update(overrides)
24
+ return options
25
+
26
+
27
+ def test_build_config_generates_defaults():
28
+ config = build_config(base_options())
29
+
30
+ assert config["execution_id"].startswith("fusesell_")
31
+ assert config["output_format"] == "json"
32
+ assert config["skip_stages"] == []
33
+ assert config["send_immediately"] is False
34
+
35
+
36
+ def test_validate_config_detects_missing_sources():
37
+ config = build_config(
38
+ base_options(
39
+ input_description="",
40
+ input_website="",
41
+ input_freetext="",
42
+ )
43
+ )
44
+
45
+ valid, errors = validate_config(config)
46
+ assert not valid
47
+ assert any("At least one data source" in err for err in errors)
48
+
49
+
50
+ def test_prepare_data_directory_sets_default_log(tmp_path: Path):
51
+ config = build_config(base_options(data_dir=str(tmp_path / "session-data")))
52
+
53
+ data_dir = prepare_data_directory(config)
54
+
55
+ assert data_dir.exists()
56
+ assert Path(config["log_file"]).parent == data_dir / "logs"
57
+
58
+
59
+ def test_execute_pipeline_returns_dry_run(tmp_path: Path):
60
+ result = execute_pipeline(
61
+ base_options(data_dir=str(tmp_path / "session"), dry_run=True)
62
+ )
63
+
64
+ assert result["status"] == "dry_run"
65
+ assert "execution_id" in result
@@ -0,0 +1,37 @@
1
+ from pathlib import Path
2
+ import sys
3
+
4
+ import pytest
5
+
6
+ sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
7
+
8
+ from fusesell_local.cli import FuseSellCLI
9
+
10
+
11
+ class TestFuseSellCLI:
12
+ def test_dry_run_pipeline_returns_zero(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
13
+ cli = FuseSellCLI()
14
+
15
+ data_dir = tmp_path / "cli-run"
16
+ args = [
17
+ "--openai-api-key",
18
+ "sk-test-123456",
19
+ "--org-id",
20
+ "demo",
21
+ "--org-name",
22
+ "Demo Org",
23
+ "--full-input",
24
+ "Seller: Demo Org, Customer: Example Corp, Communication: English",
25
+ "--input-description",
26
+ "Example Corp lead from CLI test",
27
+ "--data-dir",
28
+ str(data_dir),
29
+ "--dry-run",
30
+ ]
31
+
32
+ exit_code = cli.run(args)
33
+ captured = capsys.readouterr()
34
+
35
+ assert exit_code == 0
36
+ assert "FuseSell Execution Plan" in captured.out
37
+ assert data_dir.exists()
@@ -0,0 +1,15 @@
1
+ """
2
+ FuseSell Utilities - Common utilities and helper functions
3
+ """
4
+
5
+ from .data_manager import LocalDataManager
6
+ from .llm_client import LLMClient
7
+ from .validators import InputValidator
8
+ from .logger import setup_logging
9
+
10
+ __all__ = [
11
+ 'LocalDataManager',
12
+ 'LLMClient',
13
+ 'InputValidator',
14
+ 'setup_logging'
15
+ ]
@@ -0,0 +1,467 @@
1
+ """
2
+ Birthday Email Management System for FuseSell Local
3
+ Handles birthday email template generation, validation, and processing
4
+ """
5
+
6
+ import json
7
+ import logging
8
+ from typing import Dict, Any, Optional, List
9
+ from datetime import datetime, timedelta
10
+ import uuid
11
+ from .data_manager import LocalDataManager
12
+ from .llm_client import LLMClient
13
+
14
+
15
+ class BirthdayEmailManager:
16
+ """
17
+ Manages birthday email functionality including template generation,
18
+ validation, and processing based on server-side logic from gs_scheduler.py
19
+ """
20
+
21
+ def __init__(self, config: Dict[str, Any]):
22
+ """
23
+ Initialize birthday email manager.
24
+
25
+ Args:
26
+ config: Configuration dictionary with API keys and settings
27
+ """
28
+ self.config = config
29
+ self.data_manager = LocalDataManager(config.get('data_dir', './fusesell_data'))
30
+ self.llm_client = LLMClient(
31
+ api_key=config.get('openai_api_key'),
32
+ model=config.get('llm_model', 'gpt-4o-mini'),
33
+ base_url=config.get('llm_base_url')
34
+ )
35
+ self.logger = logging.getLogger("fusesell.birthday_email")
36
+
37
+ # Initialize database tables
38
+ self._initialize_tables()
39
+
40
+ def validate_birthday_prompt(self, prompt: str) -> Dict[str, Any]:
41
+ """
42
+ Validate birthday email prompt using LLM analysis.
43
+ Based on gs_scheduler.py birthday email check logic.
44
+
45
+ Args:
46
+ prompt: User input prompt to validate
47
+
48
+ Returns:
49
+ Dictionary with validation results
50
+ """
51
+ try:
52
+ validation_prompt = (
53
+ "Analyze the following text and return a JSON object with exactly 2 fields: "
54
+ "'is_complete_prompt' (boolean - true if it's a complete prompt for writing "
55
+ "birthday email content/drafts with detailed instructions on what to write, "
56
+ "tone, style, etc. NOT configuration/settings instructions) and 'is_enabled' "
57
+ "(boolean - true if birthday email functionality is enabled, default to true). "
58
+ "Configuration instructions like 'update settings', 'enable birthday email', "
59
+ "'set timezone', etc. should return false for is_complete_prompt. "
60
+ "Your output should be a valid JSON object only. Here's the text to analyze:\n\n"
61
+ + prompt
62
+ )
63
+
64
+ messages = [{"role": "user", "content": validation_prompt}]
65
+ response = self.llm_client.chat_completion(messages, temperature=0.3)
66
+
67
+ try:
68
+ validation_result = json.loads(response)
69
+ return {
70
+ 'is_complete_prompt': validation_result.get('is_complete_prompt', False),
71
+ 'is_enabled': validation_result.get('is_enabled', True),
72
+ 'validation_successful': True
73
+ }
74
+ except json.JSONDecodeError:
75
+ self.logger.warning("Failed to parse LLM validation response")
76
+ return {
77
+ 'is_complete_prompt': False,
78
+ 'is_enabled': False,
79
+ 'validation_successful': False
80
+ }
81
+
82
+ except Exception as e:
83
+ self.logger.error(f"Birthday prompt validation failed: {str(e)}")
84
+ return {
85
+ 'is_complete_prompt': False,
86
+ 'is_enabled': False,
87
+ 'validation_successful': False,
88
+ 'error': str(e)
89
+ }
90
+
91
+ def generate_birthday_settings_rule(self, prompt: str) -> Dict[str, Any]:
92
+ """
93
+ Generate birthday email settings rule from user prompt.
94
+ Based on gs_scheduler.py birthday email rule generation logic.
95
+
96
+ Args:
97
+ prompt: User input prompt
98
+
99
+ Returns:
100
+ Dictionary with generated rule settings
101
+ """
102
+ try:
103
+ rule_prompt = (
104
+ "You are an AI assistant tasked with converting user input into a structured "
105
+ "JSON format for birthday email composition. Analyze the following input and "
106
+ "extract key parameters for crafting an email. Your output should be a valid "
107
+ "JSON object with fields such as 'fewshots_strict_follow' (boolean, required, "
108
+ "indicating if the drafts must follow the examples or not, false if is not mentioned), "
109
+ "'maximum_words' (integer), 'mail_tone' (string), and 'org_timezone' (string, "
110
+ "UTC timezone format like 'UTC+07' or 'UTC-04', extract from the input if mentioned).\n\n"
111
+ "For example, if the input is \"Độ dài giới hạn 400 từ, xưng hô là 'mình' hoặc "
112
+ "tên người gửi, múi giờ UTC+7.\", your output might be:\n"
113
+ "{\n"
114
+ " \"maximum_words\": 400,\n"
115
+ " \"mail_tone\": \"Friendly\",\n"
116
+ " \"pronoun\": \"mình\",\n"
117
+ " \"fewshots_strict_follow\": true,\n"
118
+ " \"org_timezone\": \"UTC+07\"\n"
119
+ "}\n\n"
120
+ "Ensure your output is a single, valid JSON object. Include only the fields "
121
+ "that are explicitly mentioned or strongly implied in the input. For org_timezone, "
122
+ "look for timezone information in formats like 'UTC+7', 'GMT+7', '+7', 'timezone +7', "
123
+ "etc. and convert to UTC format (UTC+07 or UTC-04). NO REDUNDANT WORDS. "
124
+ "NO NEED TO BE WRAPPED IN ``` CODE BLOCK CHARACTERS. Here's the input to analyze:\n"
125
+ + prompt
126
+ )
127
+
128
+ messages = [{"role": "user", "content": rule_prompt}]
129
+ response = self.llm_client.chat_completion(messages, temperature=0.3)
130
+
131
+ try:
132
+ rule = json.loads(response)
133
+
134
+ # Add default values and validation
135
+ rule.setdefault('fewshots_strict_follow', False)
136
+ rule.setdefault('maximum_words', 200)
137
+ rule.setdefault('mail_tone', 'Friendly')
138
+ rule.setdefault('org_timezone', 'UTC+07')
139
+
140
+ # Add extra guide
141
+ rule['extra_guide'] = prompt
142
+
143
+ # Add birthday email check
144
+ validation_result = self.validate_birthday_prompt(prompt)
145
+ rule['birthday_email_check'] = {
146
+ 'is_complete_prompt': validation_result['is_complete_prompt'],
147
+ 'is_enabled': validation_result['is_enabled']
148
+ }
149
+
150
+ return rule
151
+
152
+ except json.JSONDecodeError:
153
+ self.logger.warning("Failed to parse LLM rule generation response")
154
+ return self._get_default_birthday_rule(prompt)
155
+
156
+ except Exception as e:
157
+ self.logger.error(f"Birthday rule generation failed: {str(e)}")
158
+ return self._get_default_birthday_rule(prompt)
159
+
160
+ def _get_default_birthday_rule(self, prompt: str) -> Dict[str, Any]:
161
+ """Get default birthday email rule when LLM processing fails."""
162
+ return {
163
+ 'fewshots_strict_follow': False,
164
+ 'maximum_words': 200,
165
+ 'mail_tone': 'Friendly',
166
+ 'org_timezone': 'UTC+07',
167
+ 'extra_guide': prompt,
168
+ 'birthday_email_check': {
169
+ 'is_complete_prompt': False,
170
+ 'is_enabled': True
171
+ }
172
+ }
173
+
174
+ def generate_birthday_template(self, team_id: str, org_id: str,
175
+ prompt: str, **kwargs) -> Dict[str, Any]:
176
+ """
177
+ Generate birthday email template.
178
+ Simulates the flowai/auto_interaction_generate_email_template workflow.
179
+
180
+ Args:
181
+ team_id: Team identifier
182
+ org_id: Organization identifier
183
+ prompt: User prompt for template generation
184
+ **kwargs: Additional parameters
185
+
186
+ Returns:
187
+ Dictionary with template generation results
188
+ """
189
+ try:
190
+ template_id = f"uuid:{str(uuid.uuid4())}"
191
+ template_type = "birthday_email"
192
+
193
+ # Generate template content using LLM
194
+ template_prompt = (
195
+ f"Generate a birthday email template based on the following requirements:\n"
196
+ f"Team ID: {team_id}\n"
197
+ f"Organization: {org_id}\n"
198
+ f"Requirements: {prompt}\n\n"
199
+ f"Create a professional birthday email template that can be personalized "
200
+ f"for customers. Include placeholders for customer name, company name, "
201
+ f"and other relevant details. The template should be warm, professional, "
202
+ f"and appropriate for business relationships.\n\n"
203
+ f"Return the template as a JSON object with fields: 'subject', 'content', "
204
+ f"'placeholders' (list of available placeholders), and 'tone'."
205
+ )
206
+
207
+ messages = [{"role": "user", "content": template_prompt}]
208
+ response = self.llm_client.chat_completion(messages, temperature=0.7)
209
+
210
+ try:
211
+ template_data = json.loads(response)
212
+ except json.JSONDecodeError:
213
+ # Fallback template
214
+ template_data = {
215
+ 'subject': 'Happy Birthday from {{company_name}}!',
216
+ 'content': (
217
+ 'Dear {{customer_name}},\n\n'
218
+ 'On behalf of everyone at {{company_name}}, I wanted to take a moment '
219
+ 'to wish you a very happy birthday!\n\n'
220
+ 'We truly appreciate your partnership and look forward to continuing '
221
+ 'our successful relationship in the year ahead.\n\n'
222
+ 'Wishing you all the best on your special day!\n\n'
223
+ 'Best regards,\n'
224
+ '{{sender_name}}\n'
225
+ '{{company_name}}'
226
+ ),
227
+ 'placeholders': ['customer_name', 'company_name', 'sender_name'],
228
+ 'tone': 'professional_warm'
229
+ }
230
+
231
+ # Save template to database
232
+ template_record = {
233
+ 'template_id': template_id,
234
+ 'team_id': team_id,
235
+ 'org_id': org_id,
236
+ 'template_type': template_type,
237
+ 'subject': template_data.get('subject', ''),
238
+ 'content': template_data.get('content', ''),
239
+ 'placeholders': json.dumps(template_data.get('placeholders', [])),
240
+ 'tone': template_data.get('tone', 'professional'),
241
+ 'created_at': datetime.now().isoformat(),
242
+ 'created_by': kwargs.get('username', 'system'),
243
+ 'prompt': prompt
244
+ }
245
+
246
+ success = self._save_birthday_template(template_record)
247
+
248
+ return {
249
+ 'template_id': template_id,
250
+ 'template_data': template_data,
251
+ 'saved': success,
252
+ 'message': 'Birthday email template generated successfully' if success else 'Template generated but save failed'
253
+ }
254
+
255
+ except Exception as e:
256
+ self.logger.error(f"Birthday template generation failed: {str(e)}")
257
+ return {
258
+ 'template_id': None,
259
+ 'template_data': None,
260
+ 'saved': False,
261
+ 'error': str(e)
262
+ }
263
+
264
+ def _save_birthday_template(self, template_record: Dict[str, Any]) -> bool:
265
+ """Save birthday email template to database."""
266
+ try:
267
+ import sqlite3
268
+ # Create birthday_templates table if it doesn't exist
269
+ with sqlite3.connect(self.data_manager.db_path) as conn:
270
+ cursor = conn.cursor()
271
+ cursor.execute("""
272
+ CREATE TABLE IF NOT EXISTS birthday_templates (
273
+ template_id TEXT PRIMARY KEY,
274
+ team_id TEXT NOT NULL,
275
+ org_id TEXT NOT NULL,
276
+ template_type TEXT DEFAULT 'birthday_email',
277
+ subject TEXT,
278
+ content TEXT,
279
+ placeholders TEXT,
280
+ tone TEXT,
281
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
282
+ created_by TEXT,
283
+ prompt TEXT,
284
+ is_active BOOLEAN DEFAULT TRUE
285
+ )
286
+ """)
287
+
288
+ # Insert or update template
289
+ cursor.execute("""
290
+ INSERT OR REPLACE INTO birthday_templates
291
+ (template_id, team_id, org_id, template_type, subject, content,
292
+ placeholders, tone, created_at, created_by, prompt, is_active)
293
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
294
+ """, (
295
+ template_record['template_id'],
296
+ template_record['team_id'],
297
+ template_record['org_id'],
298
+ template_record['template_type'],
299
+ template_record['subject'],
300
+ template_record['content'],
301
+ template_record['placeholders'],
302
+ template_record['tone'],
303
+ template_record['created_at'],
304
+ template_record['created_by'],
305
+ template_record['prompt'],
306
+ True
307
+ ))
308
+
309
+ conn.commit()
310
+ return True
311
+
312
+ except Exception as e:
313
+ self.logger.error(f"Failed to save birthday template: {str(e)}")
314
+ return False
315
+
316
+ def get_birthday_template(self, template_id: str) -> Optional[Dict[str, Any]]:
317
+ """Get birthday email template by ID."""
318
+ try:
319
+ import sqlite3
320
+ with sqlite3.connect(self.data_manager.db_path) as conn:
321
+ conn.row_factory = sqlite3.Row
322
+ cursor = conn.cursor()
323
+ cursor.execute("""
324
+ SELECT * FROM birthday_templates
325
+ WHERE template_id = ? AND is_active = TRUE
326
+ """, (template_id,))
327
+
328
+ row = cursor.fetchone()
329
+ if row:
330
+ # Parse placeholders JSON
331
+ if row['placeholders']:
332
+ try:
333
+ row['placeholders'] = json.loads(row['placeholders'])
334
+ except json.JSONDecodeError:
335
+ row['placeholders'] = []
336
+ return dict(row)
337
+ return None
338
+
339
+ except Exception as e:
340
+ self.logger.error(f"Failed to get birthday template: {str(e)}")
341
+ return None
342
+
343
+ def list_birthday_templates(self, team_id: str = None, org_id: str = None) -> List[Dict[str, Any]]:
344
+ """List birthday email templates."""
345
+ try:
346
+ import sqlite3
347
+ with sqlite3.connect(self.data_manager.db_path) as conn:
348
+ conn.row_factory = sqlite3.Row
349
+ cursor = conn.cursor()
350
+
351
+ query = "SELECT * FROM birthday_templates WHERE is_active = TRUE"
352
+ params = []
353
+
354
+ if team_id:
355
+ query += " AND team_id = ?"
356
+ params.append(team_id)
357
+
358
+ if org_id:
359
+ query += " AND org_id = ?"
360
+ params.append(org_id)
361
+
362
+ query += " ORDER BY created_at DESC"
363
+
364
+ cursor.execute(query, params)
365
+ rows = cursor.fetchall()
366
+
367
+ templates = []
368
+ for row in rows:
369
+ row_dict = dict(row)
370
+ # Parse placeholders JSON
371
+ if row_dict['placeholders']:
372
+ try:
373
+ row_dict['placeholders'] = json.loads(row_dict['placeholders'])
374
+ except json.JSONDecodeError:
375
+ row_dict['placeholders'] = []
376
+ templates.append(row_dict)
377
+
378
+ return templates
379
+
380
+ except Exception as e:
381
+ self.logger.error(f"Failed to list birthday templates: {str(e)}")
382
+ return []
383
+
384
+ def process_birthday_email_settings(self, team_id: str, prompt: str,
385
+ org_id: str, **kwargs) -> Dict[str, Any]:
386
+ """
387
+ Process birthday email settings configuration.
388
+ Main entry point that combines validation, rule generation, and template creation.
389
+
390
+ Args:
391
+ team_id: Team identifier
392
+ prompt: User input prompt
393
+ org_id: Organization identifier
394
+ **kwargs: Additional parameters
395
+
396
+ Returns:
397
+ Dictionary with processing results
398
+ """
399
+ try:
400
+ # Step 1: Validate the prompt
401
+ validation_result = self.validate_birthday_prompt(prompt)
402
+
403
+ # Step 2: Generate settings rule
404
+ rule = self.generate_birthday_settings_rule(prompt)
405
+
406
+ # Step 3: Generate template if it's a complete prompt
407
+ template_result = None
408
+ if validation_result.get('is_complete_prompt', False):
409
+ template_result = self.generate_birthday_template(
410
+ team_id, org_id, prompt, **kwargs
411
+ )
412
+
413
+ # Step 4: Prepare final settings
414
+ birthday_settings = {
415
+ 'mail_tone': rule.get('mail_tone', 'Friendly'),
416
+ 'extra_guide': rule.get('extra_guide', prompt),
417
+ 'org_timezone': rule.get('org_timezone', 'UTC+07'),
418
+ 'maximum_words': rule.get('maximum_words', 200),
419
+ 'birthday_email_check': rule.get('birthday_email_check', {
420
+ 'is_enabled': True,
421
+ 'is_complete_prompt': False
422
+ }),
423
+ 'fewshots_strict_follow': rule.get('fewshots_strict_follow', False)
424
+ }
425
+
426
+ return {
427
+ 'success': True,
428
+ 'settings': birthday_settings,
429
+ 'validation': validation_result,
430
+ 'rule': rule,
431
+ 'template': template_result,
432
+ 'message': 'Birthday email settings processed successfully'
433
+ }
434
+
435
+ except Exception as e:
436
+ self.logger.error(f"Birthday email settings processing failed: {str(e)}")
437
+ return {
438
+ 'success': False,
439
+ 'error': str(e),
440
+ 'message': 'Failed to process birthday email settings'
441
+ }
442
+
443
+ def _initialize_tables(self) -> None:
444
+ """Initialize birthday email database tables."""
445
+ try:
446
+ import sqlite3
447
+ with sqlite3.connect(self.data_manager.db_path) as conn:
448
+ cursor = conn.cursor()
449
+ cursor.execute("""
450
+ CREATE TABLE IF NOT EXISTS birthday_templates (
451
+ template_id TEXT PRIMARY KEY,
452
+ team_id TEXT NOT NULL,
453
+ org_id TEXT NOT NULL,
454
+ template_type TEXT DEFAULT 'birthday_email',
455
+ subject TEXT,
456
+ content TEXT,
457
+ placeholders TEXT,
458
+ tone TEXT,
459
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
460
+ created_by TEXT,
461
+ prompt TEXT,
462
+ is_active BOOLEAN DEFAULT TRUE
463
+ )
464
+ """)
465
+ conn.commit()
466
+ except Exception as e:
467
+ self.logger.error(f"Failed to initialize birthday email tables: {str(e)}")