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,11 @@
1
+ """
2
+ FuseSell Configuration - Configuration management and templates
3
+ """
4
+
5
+ from .prompts import PromptManager
6
+ from .settings import ConfigManager
7
+
8
+ __all__ = [
9
+ 'PromptManager',
10
+ 'ConfigManager'
11
+ ]
@@ -0,0 +1,245 @@
1
+ """
2
+ Prompt Manager for FuseSell Local
3
+ Handles LLM prompt templates and variable substitution
4
+ """
5
+
6
+ from typing import Dict, Any, Optional
7
+ import re
8
+ import logging
9
+ from .settings import ConfigManager
10
+
11
+
12
+ class PromptManager:
13
+ """
14
+ Manages LLM prompts with template substitution and customization.
15
+ """
16
+
17
+ def __init__(self, config_manager: ConfigManager):
18
+ """
19
+ Initialize prompt manager.
20
+
21
+ Args:
22
+ config_manager: Configuration manager instance
23
+ """
24
+ self.config_manager = config_manager
25
+ self.logger = logging.getLogger("fusesell.prompts")
26
+
27
+ def get_prompt(
28
+ self,
29
+ stage: str,
30
+ prompt_key: str,
31
+ variables: Optional[Dict[str, Any]] = None,
32
+ team_id: Optional[str] = None,
33
+ language: str = "english"
34
+ ) -> str:
35
+ """
36
+ Get formatted prompt for a specific stage and key.
37
+
38
+ Args:
39
+ stage: Pipeline stage name
40
+ prompt_key: Specific prompt identifier
41
+ variables: Variables for template substitution
42
+ team_id: Optional team ID for customization
43
+ language: Language for prompts
44
+
45
+ Returns:
46
+ Formatted prompt string
47
+ """
48
+ try:
49
+ # Get prompts configuration
50
+ prompts = self.config_manager.get_prompts(team_id, language)
51
+
52
+ # Get stage-specific prompts
53
+ stage_prompts = prompts.get(stage, {})
54
+ if not stage_prompts:
55
+ self.logger.warning(f"No prompts found for stage: {stage}")
56
+ return ""
57
+
58
+ # Get specific prompt template
59
+ prompt_template = stage_prompts.get(prompt_key, "")
60
+ if not prompt_template:
61
+ self.logger.warning(f"No prompt found for {stage}.{prompt_key}")
62
+ return ""
63
+
64
+ # Apply variable substitution
65
+ if variables:
66
+ prompt_template = self._substitute_variables(prompt_template, variables)
67
+
68
+ return prompt_template
69
+
70
+ except Exception as e:
71
+ self.logger.error(f"Failed to get prompt {stage}.{prompt_key}: {str(e)}")
72
+ return ""
73
+
74
+ def get_data_acquisition_prompts(self, variables: Dict[str, Any], **kwargs) -> Dict[str, str]:
75
+ """Get all prompts for data acquisition stage."""
76
+ return {
77
+ 'website_extraction': self.get_prompt('data_acquisition', 'website_extraction', variables, **kwargs),
78
+ 'business_card_ocr': self.get_prompt('data_acquisition', 'business_card_ocr', variables, **kwargs),
79
+ 'social_media_analysis': self.get_prompt('data_acquisition', 'social_media_analysis', variables, **kwargs),
80
+ 'data_consolidation': self.get_prompt('data_acquisition', 'data_consolidation', variables, **kwargs)
81
+ }
82
+
83
+ def get_data_preparation_prompts(self, variables: Dict[str, Any], **kwargs) -> Dict[str, str]:
84
+ """Get all prompts for data preparation stage."""
85
+ return {
86
+ 'extract_company_info': self.get_prompt('data_preparation', 'extract_company_info', variables, **kwargs),
87
+ 'identify_pain_points': self.get_prompt('data_preparation', 'identify_pain_points', variables, **kwargs),
88
+ 'financial_analysis': self.get_prompt('data_preparation', 'financial_analysis', variables, **kwargs),
89
+ 'development_plans': self.get_prompt('data_preparation', 'development_plans', variables, **kwargs),
90
+ 'technology_analysis': self.get_prompt('data_preparation', 'technology_analysis', variables, **kwargs)
91
+ }
92
+
93
+ def get_lead_scoring_prompts(self, variables: Dict[str, Any], **kwargs) -> Dict[str, str]:
94
+ """Get all prompts for lead scoring stage."""
95
+ return {
96
+ 'evaluate_fit': self.get_prompt('lead_scoring', 'evaluate_fit', variables, **kwargs),
97
+ 'score_calculation': self.get_prompt('lead_scoring', 'score_calculation', variables, **kwargs),
98
+ 'competitive_analysis': self.get_prompt('lead_scoring', 'competitive_analysis', variables, **kwargs),
99
+ 'recommendation': self.get_prompt('lead_scoring', 'recommendation', variables, **kwargs)
100
+ }
101
+
102
+ def get_initial_outreach_prompts(self, variables: Dict[str, Any], **kwargs) -> Dict[str, str]:
103
+ """Get all prompts for initial outreach stage."""
104
+ return {
105
+ 'email_generation': self.get_prompt('initial_outreach', 'email_generation', variables, **kwargs),
106
+ 'subject_line': self.get_prompt('initial_outreach', 'subject_line', variables, **kwargs),
107
+ 'tone_adjustment': self.get_prompt('initial_outreach', 'tone_adjustment', variables, **kwargs),
108
+ 'personalization': self.get_prompt('initial_outreach', 'personalization', variables, **kwargs)
109
+ }
110
+
111
+ def get_follow_up_prompts(self, variables: Dict[str, Any], **kwargs) -> Dict[str, str]:
112
+ """Get all prompts for follow-up stage."""
113
+ return {
114
+ 'analyze_interaction': self.get_prompt('follow_up', 'analyze_interaction', variables, **kwargs),
115
+ 'generate_followup': self.get_prompt('follow_up', 'generate_followup', variables, **kwargs),
116
+ 'timing_analysis': self.get_prompt('follow_up', 'timing_analysis', variables, **kwargs),
117
+ 'sequence_planning': self.get_prompt('follow_up', 'sequence_planning', variables, **kwargs)
118
+ }
119
+
120
+ def _substitute_variables(self, template: str, variables: Dict[str, Any]) -> str:
121
+ """
122
+ Substitute variables in prompt template.
123
+
124
+ Args:
125
+ template: Prompt template with placeholders
126
+ variables: Variables to substitute
127
+
128
+ Returns:
129
+ Template with variables substituted
130
+ """
131
+ try:
132
+ # Handle nested dictionary access (e.g., {customer.name})
133
+ def replace_nested(match):
134
+ var_path = match.group(1)
135
+ value = self._get_nested_value(variables, var_path)
136
+ return str(value) if value is not None else f"{{{var_path}}}"
137
+
138
+ # Replace {variable} and {nested.variable} patterns
139
+ result = re.sub(r'\{([^}]+)\}', replace_nested, template)
140
+
141
+ return result
142
+
143
+ except Exception as e:
144
+ self.logger.warning(f"Variable substitution failed: {str(e)}")
145
+ return template
146
+
147
+ def _get_nested_value(self, data: Dict[str, Any], path: str) -> Any:
148
+ """
149
+ Get value from nested dictionary using dot notation.
150
+
151
+ Args:
152
+ data: Dictionary to search
153
+ path: Dot-separated path (e.g., 'customer.name')
154
+
155
+ Returns:
156
+ Value at path or None if not found
157
+ """
158
+ try:
159
+ keys = path.split('.')
160
+ value = data
161
+
162
+ for key in keys:
163
+ if isinstance(value, dict) and key in value:
164
+ value = value[key]
165
+ else:
166
+ return None
167
+
168
+ return value
169
+
170
+ except Exception:
171
+ return None
172
+
173
+ def validate_prompt_variables(self, template: str, variables: Dict[str, Any]) -> Dict[str, Any]:
174
+ """
175
+ Validate that all required variables are provided for a template.
176
+
177
+ Args:
178
+ template: Prompt template
179
+ variables: Available variables
180
+
181
+ Returns:
182
+ Dictionary with validation results
183
+ """
184
+ # Find all variable placeholders
185
+ placeholders = re.findall(r'\{([^}]+)\}', template)
186
+
187
+ missing_vars = []
188
+ available_vars = []
189
+
190
+ for placeholder in placeholders:
191
+ value = self._get_nested_value(variables, placeholder)
192
+ if value is None:
193
+ missing_vars.append(placeholder)
194
+ else:
195
+ available_vars.append(placeholder)
196
+
197
+ return {
198
+ 'valid': len(missing_vars) == 0,
199
+ 'missing_variables': missing_vars,
200
+ 'available_variables': available_vars,
201
+ 'total_placeholders': len(placeholders)
202
+ }
203
+
204
+ def create_variable_context(self,
205
+ customer_data: Optional[Dict[str, Any]] = None,
206
+ lead_scores: Optional[Dict[str, Any]] = None,
207
+ org_info: Optional[Dict[str, Any]] = None,
208
+ team_info: Optional[Dict[str, Any]] = None,
209
+ **additional_vars) -> Dict[str, Any]:
210
+ """
211
+ Create a comprehensive variable context for prompt substitution.
212
+
213
+ Args:
214
+ customer_data: Customer information
215
+ lead_scores: Lead scoring results
216
+ org_info: Organization information
217
+ team_info: Team information
218
+ **additional_vars: Additional variables
219
+
220
+ Returns:
221
+ Complete variable context
222
+ """
223
+ context = {}
224
+
225
+ if customer_data:
226
+ context['customer'] = customer_data
227
+ context['company'] = customer_data.get('companyInfo', {})
228
+ context['contact'] = customer_data.get('primaryContact', {})
229
+ context['pain_points'] = customer_data.get('painPoints', [])
230
+
231
+ if lead_scores:
232
+ context['lead_scores'] = lead_scores
233
+ context['top_score'] = max(lead_scores.get('scores', []), key=lambda x: x.get('score', 0), default={})
234
+
235
+ if org_info:
236
+ context['org'] = org_info
237
+ context['seller'] = org_info
238
+
239
+ if team_info:
240
+ context['team'] = team_info
241
+
242
+ # Add additional variables
243
+ context.update(additional_vars)
244
+
245
+ return context
@@ -0,0 +1,277 @@
1
+ """
2
+ Configuration Manager for FuseSell Local
3
+ Handles team-specific settings, prompts, and business rules
4
+ """
5
+
6
+ import json
7
+ from typing import Dict, Any, Optional
8
+ from pathlib import Path
9
+ import logging
10
+
11
+
12
+ class ConfigManager:
13
+ """
14
+ Manages configuration settings for FuseSell Local.
15
+ Handles team-specific prompts, scoring criteria, and business rules.
16
+ """
17
+
18
+ def __init__(self, data_dir: str = "./fusesell_data"):
19
+ """
20
+ Initialize configuration manager.
21
+
22
+ Args:
23
+ data_dir: Directory containing configuration files
24
+ """
25
+ self.data_dir = Path(data_dir)
26
+ self.config_dir = self.data_dir / "config"
27
+ self.logger = logging.getLogger("fusesell.config")
28
+
29
+ # Ensure config directory exists
30
+ self.config_dir.mkdir(parents=True, exist_ok=True)
31
+
32
+ # Cache for loaded configurations
33
+ self._cache = {}
34
+
35
+ def get_prompts(self, team_id: Optional[str] = None, language: str = "english") -> Dict[str, Any]:
36
+ """
37
+ Get LLM prompts for stages, with team and language customization.
38
+
39
+ Args:
40
+ team_id: Optional team ID for team-specific prompts
41
+ language: Language for prompts
42
+
43
+ Returns:
44
+ Dictionary of prompts organized by stage
45
+ """
46
+ cache_key = f"prompts_{team_id}_{language}"
47
+
48
+ if cache_key in self._cache:
49
+ return self._cache[cache_key]
50
+
51
+ # Load base prompts
52
+ base_prompts = self._load_json_config("prompts.json", {})
53
+
54
+ # Apply team customizations if available
55
+ if team_id:
56
+ team_prompts = self._load_team_prompts(team_id, language)
57
+ base_prompts = self._merge_configs(base_prompts, team_prompts)
58
+
59
+ # Apply language customizations
60
+ if language != "english":
61
+ lang_prompts = self._load_language_prompts(language)
62
+ base_prompts = self._merge_configs(base_prompts, lang_prompts)
63
+
64
+ self._cache[cache_key] = base_prompts
65
+ return base_prompts
66
+
67
+ def get_scoring_criteria(self, team_id: Optional[str] = None) -> Dict[str, Any]:
68
+ """
69
+ Get lead scoring criteria with team customization.
70
+
71
+ Args:
72
+ team_id: Optional team ID for team-specific criteria
73
+
74
+ Returns:
75
+ Dictionary of scoring criteria and weights
76
+ """
77
+ cache_key = f"scoring_{team_id}"
78
+
79
+ if cache_key in self._cache:
80
+ return self._cache[cache_key]
81
+
82
+ # Load base scoring criteria
83
+ base_criteria = self._load_json_config("scoring_criteria.json", {})
84
+
85
+ # Apply team customizations if available
86
+ if team_id:
87
+ team_criteria = self._load_team_scoring(team_id)
88
+ base_criteria = self._merge_configs(base_criteria, team_criteria)
89
+
90
+ self._cache[cache_key] = base_criteria
91
+ return base_criteria
92
+
93
+ def get_email_templates(self, team_id: Optional[str] = None, language: str = "english") -> Dict[str, Any]:
94
+ """
95
+ Get email templates with team and language customization.
96
+
97
+ Args:
98
+ team_id: Optional team ID for team-specific templates
99
+ language: Language for templates
100
+
101
+ Returns:
102
+ Dictionary of email templates
103
+ """
104
+ cache_key = f"templates_{team_id}_{language}"
105
+
106
+ if cache_key in self._cache:
107
+ return self._cache[cache_key]
108
+
109
+ # Load base templates
110
+ base_templates = self._load_json_config("email_templates.json", {})
111
+
112
+ # Apply team customizations if available
113
+ if team_id:
114
+ team_templates = self._load_team_templates(team_id, language)
115
+ base_templates = self._merge_configs(base_templates, team_templates)
116
+
117
+ # Apply language customizations
118
+ if language != "english":
119
+ lang_templates = self._load_language_templates(language)
120
+ base_templates = self._merge_configs(base_templates, lang_templates)
121
+
122
+ self._cache[cache_key] = base_templates
123
+ return base_templates
124
+
125
+ def get_business_rules(self, team_id: Optional[str] = None) -> Dict[str, Any]:
126
+ """
127
+ Get business rules and pipeline behavior settings.
128
+
129
+ Args:
130
+ team_id: Optional team ID for team-specific rules
131
+
132
+ Returns:
133
+ Dictionary of business rules
134
+ """
135
+ cache_key = f"rules_{team_id}"
136
+
137
+ if cache_key in self._cache:
138
+ return self._cache[cache_key]
139
+
140
+ # Default business rules based on original system
141
+ default_rules = {
142
+ "pipeline": {
143
+ "stop_on_website_fail": True,
144
+ "wait_after_draft_generation": True,
145
+ "max_operations": 10,
146
+ "max_simultaneous_operations": 1,
147
+ "sequential_execution_only": True
148
+ },
149
+ "data_acquisition": {
150
+ "min_execution_time_seconds": 100,
151
+ "required_sources": ["website"],
152
+ "optional_sources": ["business_card", "linkedin", "facebook"]
153
+ },
154
+ "initial_outreach": {
155
+ "actions": ["draft_write", "draft_rewrite", "send", "close"],
156
+ "default_action": "draft_write",
157
+ "require_human_approval": True,
158
+ "one_action_per_trigger": True
159
+ },
160
+ "follow_up": {
161
+ "actions": ["draft_write", "draft_rewrite", "send"],
162
+ "require_explicit_trigger": True,
163
+ "analyze_previous_interactions": True
164
+ }
165
+ }
166
+
167
+ # Apply team customizations if available
168
+ if team_id:
169
+ team_rules = self._load_team_rules(team_id)
170
+ default_rules = self._merge_configs(default_rules, team_rules)
171
+
172
+ self._cache[cache_key] = default_rules
173
+ return default_rules
174
+
175
+ def _load_json_config(self, filename: str, default: Dict[str, Any]) -> Dict[str, Any]:
176
+ """Load JSON configuration file with fallback to default."""
177
+ try:
178
+ config_file = self.config_dir / filename
179
+ if config_file.exists():
180
+ with open(config_file, 'r', encoding='utf-8') as f:
181
+ return json.load(f)
182
+ except Exception as e:
183
+ self.logger.warning(f"Failed to load {filename}: {str(e)}")
184
+
185
+ return default.copy()
186
+
187
+ def _load_team_prompts(self, team_id: str, language: str) -> Dict[str, Any]:
188
+ """Load team-specific prompts."""
189
+ team_file = self.config_dir / f"team_{team_id}_prompts_{language}.json"
190
+ return self._load_json_config(team_file.name, {})
191
+
192
+ def _load_team_scoring(self, team_id: str) -> Dict[str, Any]:
193
+ """Load team-specific scoring criteria."""
194
+ team_file = self.config_dir / f"team_{team_id}_scoring.json"
195
+ return self._load_json_config(team_file.name, {})
196
+
197
+ def _load_team_templates(self, team_id: str, language: str) -> Dict[str, Any]:
198
+ """Load team-specific email templates."""
199
+ team_file = self.config_dir / f"team_{team_id}_templates_{language}.json"
200
+ return self._load_json_config(team_file.name, {})
201
+
202
+ def _load_team_rules(self, team_id: str) -> Dict[str, Any]:
203
+ """Load team-specific business rules."""
204
+ team_file = self.config_dir / f"team_{team_id}_rules.json"
205
+ return self._load_json_config(team_file.name, {})
206
+
207
+ def _load_language_prompts(self, language: str) -> Dict[str, Any]:
208
+ """Load language-specific prompts."""
209
+ lang_file = self.config_dir / f"prompts_{language}.json"
210
+ return self._load_json_config(lang_file.name, {})
211
+
212
+ def _load_language_templates(self, language: str) -> Dict[str, Any]:
213
+ """Load language-specific email templates."""
214
+ lang_file = self.config_dir / f"templates_{language}.json"
215
+ return self._load_json_config(lang_file.name, {})
216
+
217
+ def _merge_configs(self, base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
218
+ """
219
+ Recursively merge configuration dictionaries.
220
+
221
+ Args:
222
+ base: Base configuration
223
+ override: Override configuration
224
+
225
+ Returns:
226
+ Merged configuration
227
+ """
228
+ result = base.copy()
229
+
230
+ for key, value in override.items():
231
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
232
+ result[key] = self._merge_configs(result[key], value)
233
+ else:
234
+ result[key] = value
235
+
236
+ return result
237
+
238
+ def save_team_config(self, team_id: str, config_type: str, config_data: Dict[str, Any], language: str = "english") -> None:
239
+ """
240
+ Save team-specific configuration.
241
+
242
+ Args:
243
+ team_id: Team identifier
244
+ config_type: Type of config (prompts, scoring, templates, rules)
245
+ config_data: Configuration data to save
246
+ language: Language for prompts/templates
247
+ """
248
+ try:
249
+ if config_type in ["prompts", "templates"]:
250
+ filename = f"team_{team_id}_{config_type}_{language}.json"
251
+ else:
252
+ filename = f"team_{team_id}_{config_type}.json"
253
+
254
+ config_file = self.config_dir / filename
255
+
256
+ with open(config_file, 'w', encoding='utf-8') as f:
257
+ json.dump(config_data, f, indent=2, ensure_ascii=False)
258
+
259
+ # Clear cache for this team
260
+ self._clear_team_cache(team_id)
261
+
262
+ self.logger.info(f"Saved team configuration: {filename}")
263
+
264
+ except Exception as e:
265
+ self.logger.error(f"Failed to save team config {config_type} for {team_id}: {str(e)}")
266
+ raise
267
+
268
+ def _clear_team_cache(self, team_id: str) -> None:
269
+ """Clear cached configurations for a specific team."""
270
+ keys_to_remove = [key for key in self._cache.keys() if team_id in key]
271
+ for key in keys_to_remove:
272
+ del self._cache[key]
273
+
274
+ def clear_cache(self) -> None:
275
+ """Clear all cached configurations."""
276
+ self._cache.clear()
277
+ self.logger.debug("Configuration cache cleared")