fusesell 1.3.42__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fusesell-1.3.42.dist-info/METADATA +873 -0
- fusesell-1.3.42.dist-info/RECORD +35 -0
- fusesell-1.3.42.dist-info/WHEEL +5 -0
- fusesell-1.3.42.dist-info/entry_points.txt +2 -0
- fusesell-1.3.42.dist-info/licenses/LICENSE +21 -0
- fusesell-1.3.42.dist-info/top_level.txt +2 -0
- fusesell.py +20 -0
- fusesell_local/__init__.py +37 -0
- fusesell_local/api.py +343 -0
- fusesell_local/cli.py +1480 -0
- fusesell_local/config/__init__.py +11 -0
- fusesell_local/config/default_email_templates.json +34 -0
- fusesell_local/config/default_prompts.json +19 -0
- fusesell_local/config/default_scoring_criteria.json +154 -0
- fusesell_local/config/prompts.py +245 -0
- fusesell_local/config/settings.py +277 -0
- fusesell_local/pipeline.py +978 -0
- fusesell_local/stages/__init__.py +19 -0
- fusesell_local/stages/base_stage.py +603 -0
- fusesell_local/stages/data_acquisition.py +1820 -0
- fusesell_local/stages/data_preparation.py +1238 -0
- fusesell_local/stages/follow_up.py +1728 -0
- fusesell_local/stages/initial_outreach.py +2972 -0
- fusesell_local/stages/lead_scoring.py +1452 -0
- fusesell_local/utils/__init__.py +36 -0
- fusesell_local/utils/agent_context.py +552 -0
- fusesell_local/utils/auto_setup.py +361 -0
- fusesell_local/utils/birthday_email_manager.py +467 -0
- fusesell_local/utils/data_manager.py +4857 -0
- fusesell_local/utils/event_scheduler.py +959 -0
- fusesell_local/utils/llm_client.py +342 -0
- fusesell_local/utils/logger.py +203 -0
- fusesell_local/utils/output_helpers.py +2443 -0
- fusesell_local/utils/timezone_detector.py +914 -0
- fusesell_local/utils/validators.py +436 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FuseSell Utilities - Common utilities and helper functions
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .data_manager import LocalDataManager
|
|
6
|
+
from .llm_client import LLMClient, normalize_llm_base_url
|
|
7
|
+
from .validators import InputValidator
|
|
8
|
+
from .logger import setup_logging
|
|
9
|
+
from .output_helpers import write_full_output_html
|
|
10
|
+
from .auto_setup import (
|
|
11
|
+
check_settings_completion,
|
|
12
|
+
auto_initialize_auto_interaction,
|
|
13
|
+
auto_populate_from_email,
|
|
14
|
+
generate_agent_context,
|
|
15
|
+
should_update_agent_context,
|
|
16
|
+
get_gmail_email_safe,
|
|
17
|
+
)
|
|
18
|
+
from .agent_context import notify_action_completed, write_agent_md, get_agent_md_path
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
'LocalDataManager',
|
|
22
|
+
'LLMClient',
|
|
23
|
+
'normalize_llm_base_url',
|
|
24
|
+
'InputValidator',
|
|
25
|
+
'setup_logging',
|
|
26
|
+
'write_full_output_html',
|
|
27
|
+
'check_settings_completion',
|
|
28
|
+
'auto_initialize_auto_interaction',
|
|
29
|
+
'auto_populate_from_email',
|
|
30
|
+
'generate_agent_context',
|
|
31
|
+
'should_update_agent_context',
|
|
32
|
+
'get_gmail_email_safe',
|
|
33
|
+
'notify_action_completed',
|
|
34
|
+
'write_agent_md',
|
|
35
|
+
'get_agent_md_path',
|
|
36
|
+
]
|
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent context helpers for FuseSell.
|
|
3
|
+
|
|
4
|
+
Provides a lightweight agent.md writer and an action hook that can be used by
|
|
5
|
+
RealtimeX flows (or other consumers) to refresh agent context after critical
|
|
6
|
+
mutations. The helpers are resilient to missing realtimex_toolkit; when the
|
|
7
|
+
toolkit is unavailable, agent.md is written directly to the expected path.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
from datetime import datetime, timedelta
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
from .auto_setup import generate_agent_context, should_update_agent_context
|
|
18
|
+
from .data_manager import LocalDataManager
|
|
19
|
+
|
|
20
|
+
# Default action priorities, aligned with the RealtimeX flows.
|
|
21
|
+
CRITICAL_ACTIONS = [
|
|
22
|
+
'product_create', 'product_update', 'product_delete', 'product_status_change', 'product_bulk_import',
|
|
23
|
+
'process_create',
|
|
24
|
+
'team_create', 'team_update',
|
|
25
|
+
'gs_organization', 'sales_rep', 'product_team', 'auto_interaction',
|
|
26
|
+
'initial_outreach', 'follow_up', 'schedule_time', 'followup_schedule_time',
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
SKIP_ACTIONS = [
|
|
30
|
+
'product_view', 'product_list', 'team_view', 'team_list', 'settings_view',
|
|
31
|
+
'process_query', 'process_status_change', 'draft_list', 'event_list', 'event_update',
|
|
32
|
+
'knowledge_query', 'proposal_generate',
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_agent_md_path(workspace_slug: str, agent_id: str) -> Path:
|
|
37
|
+
"""
|
|
38
|
+
Construct the standardized agent.md path used by RealtimeX.
|
|
39
|
+
"""
|
|
40
|
+
home_dir = Path.home()
|
|
41
|
+
return (
|
|
42
|
+
home_dir
|
|
43
|
+
/ ".realtimex.ai"
|
|
44
|
+
/ "Resources"
|
|
45
|
+
/ "agent-skills"
|
|
46
|
+
/ "workspaces"
|
|
47
|
+
/ workspace_slug
|
|
48
|
+
/ agent_id
|
|
49
|
+
/ "agent.md"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _format_products(products: List[Dict[str, Any]], limit: int = 5) -> str:
|
|
54
|
+
if not products:
|
|
55
|
+
return "None configured"
|
|
56
|
+
|
|
57
|
+
lines: List[str] = []
|
|
58
|
+
for product in products[:limit]:
|
|
59
|
+
name = product.get('product_name') or product.get('name') or 'Unknown product'
|
|
60
|
+
lines.append(f"- {name}")
|
|
61
|
+
remaining = len(products) - limit
|
|
62
|
+
if remaining > 0:
|
|
63
|
+
lines.append(f"... ({remaining} more)")
|
|
64
|
+
return "\n".join(lines)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _format_processes(processes: List[Dict[str, Any]], limit: int = 5, detail_limit: int = 160) -> str:
|
|
68
|
+
if not processes:
|
|
69
|
+
return "None"
|
|
70
|
+
|
|
71
|
+
lines: List[str] = []
|
|
72
|
+
for process in processes[:limit]:
|
|
73
|
+
task_id = process.get('task_id') or process.get('id') or 'unknown'
|
|
74
|
+
customer = process.get('customer_name') or process.get('customer_company') or 'unknown customer'
|
|
75
|
+
status = process.get('status') or 'unknown'
|
|
76
|
+
notes = process.get('notes') or ''
|
|
77
|
+
if notes and len(notes) > detail_limit:
|
|
78
|
+
notes = notes[:detail_limit] + '...'
|
|
79
|
+
lines.append(f"- {task_id}: {customer} [{status}]")
|
|
80
|
+
if notes:
|
|
81
|
+
lines.append(f" - notes: {notes}")
|
|
82
|
+
if len(processes) > limit:
|
|
83
|
+
lines.append(f"... ({len(processes) - limit} more)")
|
|
84
|
+
return "\n".join(lines)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _render_agent_markdown(context: Dict[str, Any]) -> str:
|
|
88
|
+
"""
|
|
89
|
+
Render the structured context returned by generate_agent_context into markdown.
|
|
90
|
+
"""
|
|
91
|
+
stats = context.get('statistics') or {}
|
|
92
|
+
team_settings = context.get('team_settings') or {}
|
|
93
|
+
|
|
94
|
+
settings_completion = stats.get('settings_completion') or {}
|
|
95
|
+
products = context.get('products') or []
|
|
96
|
+
active_processes = context.get('active_processes') or []
|
|
97
|
+
|
|
98
|
+
product_by_id = {p.get('product_id'): p for p in products if p.get('product_id')}
|
|
99
|
+
|
|
100
|
+
# Product counts
|
|
101
|
+
active_products = [p for p in products if (p.get('status') or '').lower() == 'active']
|
|
102
|
+
inactive_products = [p for p in products if (p.get('status') or '').lower() != 'active']
|
|
103
|
+
total_products = stats.get('total_products', len(products))
|
|
104
|
+
|
|
105
|
+
# Process counts (best effort)
|
|
106
|
+
active_processes_count = stats.get('active_processes', len(active_processes))
|
|
107
|
+
total_processes = stats.get('total_processes', active_processes_count)
|
|
108
|
+
|
|
109
|
+
# Quick settings flags (condensed)
|
|
110
|
+
org_profile_configured = bool(team_settings.get('gs_team_organization'))
|
|
111
|
+
reps_configured = bool(team_settings.get('gs_team_rep'))
|
|
112
|
+
linked_products_data = team_settings.get('gs_team_product') if isinstance(team_settings, dict) else None
|
|
113
|
+
products_linked = bool(linked_products_data)
|
|
114
|
+
auto_interaction_configured = bool(team_settings.get('gs_team_auto_interaction'))
|
|
115
|
+
follow_up_configured = bool(team_settings.get('gs_team_follow_up'))
|
|
116
|
+
|
|
117
|
+
completion_lines = [
|
|
118
|
+
f"- auto_interaction: {bool(settings_completion.get('auto_interaction_completed'))}",
|
|
119
|
+
f"- rep: {bool(settings_completion.get('rep_completed'))}",
|
|
120
|
+
f"- product: {bool(settings_completion.get('product_completed'))}",
|
|
121
|
+
f"- organization: {bool(settings_completion.get('organization_completed'))}",
|
|
122
|
+
f"- follow_up: {bool(settings_completion.get('follow_up_completed'))}",
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
workspace_slug = context.get('workspace_slug', 'workspace-default')
|
|
126
|
+
org_id = context.get('org_id', 'unknown@example.com')
|
|
127
|
+
team_id = context.get('team_id', 'N/A')
|
|
128
|
+
team_name = context.get('team_name', 'Workspace Team')
|
|
129
|
+
team_created_at = context.get('team_created_at', 'N/A')
|
|
130
|
+
|
|
131
|
+
# Recent activity (best effort) capped at 5
|
|
132
|
+
recent_activity: List[str] = []
|
|
133
|
+
seven_days_ago = datetime.utcnow() - timedelta(days=7)
|
|
134
|
+
if team_created_at and team_created_at != 'N/A':
|
|
135
|
+
recent_activity.append(f"- Team created: {team_created_at}")
|
|
136
|
+
for product in sorted(products, key=lambda p: p.get('created_at') or '', reverse=True):
|
|
137
|
+
created = product.get('created_at')
|
|
138
|
+
if not created:
|
|
139
|
+
continue
|
|
140
|
+
try:
|
|
141
|
+
created_dt = datetime.fromisoformat(created.replace('Z', '+00:00'))
|
|
142
|
+
if created_dt >= seven_days_ago:
|
|
143
|
+
recent_activity.append(
|
|
144
|
+
f"- Product added: {product.get('product_name', 'Unknown')} ({created_dt.date()})"
|
|
145
|
+
)
|
|
146
|
+
except Exception:
|
|
147
|
+
continue
|
|
148
|
+
if not recent_activity:
|
|
149
|
+
recent_activity.append("None")
|
|
150
|
+
if len(recent_activity) > 5:
|
|
151
|
+
extra = len(recent_activity) - 5
|
|
152
|
+
recent_activity = recent_activity[:5] + [f"... ({extra} more)"]
|
|
153
|
+
|
|
154
|
+
# Sales reps
|
|
155
|
+
reps_data = team_settings.get('gs_team_rep') if isinstance(team_settings, dict) else None
|
|
156
|
+
reps_list: List[str] = []
|
|
157
|
+
sales_poc = "Not configured"
|
|
158
|
+
if isinstance(reps_data, list):
|
|
159
|
+
for rep in reps_data:
|
|
160
|
+
if not isinstance(rep, dict):
|
|
161
|
+
continue
|
|
162
|
+
name = rep.get('name') or 'Unnamed'
|
|
163
|
+
email = rep.get('email') or ''
|
|
164
|
+
title = rep.get('position') or rep.get('title') or ''
|
|
165
|
+
line = f"- {name}"
|
|
166
|
+
if title:
|
|
167
|
+
line += f" ({title})"
|
|
168
|
+
if email:
|
|
169
|
+
line += f" - {email}"
|
|
170
|
+
reps_list.append(line)
|
|
171
|
+
if reps_list:
|
|
172
|
+
first_rep = reps_data[0]
|
|
173
|
+
sales_poc = f"{first_rep.get('name', 'Unknown')} ({first_rep.get('email', '')})".strip()
|
|
174
|
+
if not reps_list:
|
|
175
|
+
reps_list.append("No sales reps configured")
|
|
176
|
+
if len(reps_list) > 5:
|
|
177
|
+
extra = len(reps_list) - 5
|
|
178
|
+
reps_list = reps_list[:5] + [f"... ({extra} more)"]
|
|
179
|
+
|
|
180
|
+
# Linked products
|
|
181
|
+
linked_products_list: List[str] = []
|
|
182
|
+
if isinstance(linked_products_data, list):
|
|
183
|
+
for link in linked_products_data:
|
|
184
|
+
if not isinstance(link, dict):
|
|
185
|
+
continue
|
|
186
|
+
pid = link.get('product_id')
|
|
187
|
+
if not pid:
|
|
188
|
+
continue
|
|
189
|
+
product = product_by_id.get(pid)
|
|
190
|
+
linked_products_list.append(f"- {product.get('product_name', pid) if product else pid}")
|
|
191
|
+
if not linked_products_list:
|
|
192
|
+
linked_products_list.append("No products linked to workspace")
|
|
193
|
+
if len(linked_products_list) > 5:
|
|
194
|
+
extra = len(linked_products_list) - 5
|
|
195
|
+
linked_products_list = linked_products_list[:5] + [f"... ({extra} more)"]
|
|
196
|
+
|
|
197
|
+
# Org profile
|
|
198
|
+
org_profile_data = team_settings.get('gs_team_organization') if isinstance(team_settings, dict) else None
|
|
199
|
+
org_profile_lines: List[str] = []
|
|
200
|
+
if isinstance(org_profile_data, list):
|
|
201
|
+
org_profile_data = org_profile_data[0] if org_profile_data else {}
|
|
202
|
+
if isinstance(org_profile_data, dict):
|
|
203
|
+
if org_profile_data.get('legal_name'):
|
|
204
|
+
org_profile_lines.append(f"- Org Name: {org_profile_data['legal_name']}")
|
|
205
|
+
if org_profile_data.get('primary_email'):
|
|
206
|
+
org_profile_lines.append(f"- Primary Email: {org_profile_data['primary_email']}")
|
|
207
|
+
if org_profile_data.get('address'):
|
|
208
|
+
org_profile_lines.append(f"- Address: {org_profile_data['address']}")
|
|
209
|
+
if not org_profile_lines:
|
|
210
|
+
org_profile_lines.append("Not configured")
|
|
211
|
+
|
|
212
|
+
# Required settings readiness
|
|
213
|
+
required_settings = {
|
|
214
|
+
"Organization Profile": org_profile_configured,
|
|
215
|
+
"Sales Representatives": reps_configured,
|
|
216
|
+
"Product Catalog": bool(linked_products_data),
|
|
217
|
+
"Email Automation": auto_interaction_configured,
|
|
218
|
+
}
|
|
219
|
+
required_settings_count = sum(1 for val in required_settings.values() if val)
|
|
220
|
+
total_required = len(required_settings)
|
|
221
|
+
if required_settings_count >= total_required:
|
|
222
|
+
sales_ready_status = "Fully Ready"
|
|
223
|
+
elif required_settings_count >= total_required - 1:
|
|
224
|
+
sales_ready_status = "Almost Ready (1 setting remaining)"
|
|
225
|
+
elif required_settings_count >= 2:
|
|
226
|
+
sales_ready_status = "Partially Ready"
|
|
227
|
+
else:
|
|
228
|
+
sales_ready_status = "Not Ready"
|
|
229
|
+
configured_settings_text = "\n".join(
|
|
230
|
+
[f"- {name}" for name, done in required_settings.items() if done]
|
|
231
|
+
) or "None configured"
|
|
232
|
+
missing_settings_text = "\n".join(
|
|
233
|
+
[f"- {name}" for name, done in required_settings.items() if not done]
|
|
234
|
+
) or "None"
|
|
235
|
+
ready_to_start = (
|
|
236
|
+
f"**Status**: Yes, ready to go\n\n"
|
|
237
|
+
f"- Product: {(active_products[0].get('product_name') if active_products else 'N/A')}\n"
|
|
238
|
+
f"- Sales Rep: {sales_poc}\n"
|
|
239
|
+
f"- Method: Email outreach"
|
|
240
|
+
if required_settings_count >= total_required
|
|
241
|
+
else f"**Status**: Not yet\n\nComplete these required settings first:\n{missing_settings_text}"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
primary_product = active_products[0].get('product_name', 'No products configured') if active_products else 'No products configured'
|
|
245
|
+
target_market = active_products[0].get('short_description', 'Not specified') if active_products else 'Not specified'
|
|
246
|
+
|
|
247
|
+
# Process details
|
|
248
|
+
recently_completed_processes = "None"
|
|
249
|
+
|
|
250
|
+
# Auto interaction detail
|
|
251
|
+
auto_interaction_details = "Not configured"
|
|
252
|
+
auto_data = team_settings.get('gs_team_auto_interaction') if isinstance(team_settings, dict) else None
|
|
253
|
+
if isinstance(auto_data, list) and auto_data:
|
|
254
|
+
entry = auto_data[0]
|
|
255
|
+
if isinstance(entry, dict):
|
|
256
|
+
from_email = ""
|
|
257
|
+
from_block = entry.get('from') or {}
|
|
258
|
+
if isinstance(from_block, dict):
|
|
259
|
+
from_email = from_block.get('email') or ""
|
|
260
|
+
if entry.get('from_email'):
|
|
261
|
+
from_email = entry.get('from_email')
|
|
262
|
+
tool = entry.get('tool') or entry.get('tool_type') or 'Unknown tool'
|
|
263
|
+
auto_interaction_details = f"- Tool: {tool}"
|
|
264
|
+
if from_email:
|
|
265
|
+
auto_interaction_details += f"\n- From email: {from_email}"
|
|
266
|
+
elif isinstance(auto_data, dict) and auto_data:
|
|
267
|
+
from_email = auto_data.get('from_email') or ''
|
|
268
|
+
tool = auto_data.get('tool') or auto_data.get('tool_type') or 'Unknown tool'
|
|
269
|
+
auto_interaction_details = f"- Tool: {tool}"
|
|
270
|
+
if from_email:
|
|
271
|
+
auto_interaction_details += f"\n- From email: {from_email}"
|
|
272
|
+
|
|
273
|
+
return f"""# Workspace Context: {team_name}
|
|
274
|
+
|
|
275
|
+
> Auto-generated workspace context for FuseSell agent
|
|
276
|
+
> Last Updated: {context.get('last_updated', 'unknown')}
|
|
277
|
+
|
|
278
|
+
## Workspace Identity
|
|
279
|
+
|
|
280
|
+
- **Workspace ID**: `{workspace_slug}`
|
|
281
|
+
- **Primary User (Org ID)**: {org_id}
|
|
282
|
+
|
|
283
|
+
## Hidden Team (Single Team Model)
|
|
284
|
+
|
|
285
|
+
- **Team ID**: `{team_id}`
|
|
286
|
+
- **Team Name**: {team_name}
|
|
287
|
+
- **Created**: {team_created_at}
|
|
288
|
+
|
|
289
|
+
## Product Catalog
|
|
290
|
+
|
|
291
|
+
**Total Products**: {total_products} ({len(active_products)} active, {len(inactive_products)} inactive)
|
|
292
|
+
|
|
293
|
+
### Active Products
|
|
294
|
+
{_format_products(active_products)}
|
|
295
|
+
|
|
296
|
+
### Draft/Inactive Products
|
|
297
|
+
{_format_products(inactive_products)}
|
|
298
|
+
|
|
299
|
+
### Recent Activity (last 7 days)
|
|
300
|
+
{chr(10).join(recent_activity)}
|
|
301
|
+
|
|
302
|
+
## Active Sales Processes
|
|
303
|
+
|
|
304
|
+
**Total Active**: {active_processes_count}
|
|
305
|
+
**Total Processes**: {total_processes}
|
|
306
|
+
|
|
307
|
+
### In Progress
|
|
308
|
+
{_format_processes(active_processes)}
|
|
309
|
+
|
|
310
|
+
### Recently Completed
|
|
311
|
+
{recently_completed_processes}
|
|
312
|
+
|
|
313
|
+
## Sales Settings Configuration
|
|
314
|
+
|
|
315
|
+
### Organization Profile (`gs_team_organization`)
|
|
316
|
+
{chr(10).join(org_profile_lines)}
|
|
317
|
+
|
|
318
|
+
### Sales Representatives (`gs_team_rep`)
|
|
319
|
+
**Configured Reps**: {len(reps_data) if isinstance(reps_data, list) else 0}
|
|
320
|
+
{chr(10).join(reps_list)}
|
|
321
|
+
|
|
322
|
+
### Product-Team Links (`gs_team_product`)
|
|
323
|
+
**Linked Products**: {len(linked_products_data) if isinstance(linked_products_data, list) else 0}
|
|
324
|
+
{chr(10).join(linked_products_list)}
|
|
325
|
+
|
|
326
|
+
### Initial Outreach (`gs_team_initial_outreach`)
|
|
327
|
+
{"Configured" if team_settings.get('gs_team_initial_outreach') else "Not configured"}
|
|
328
|
+
|
|
329
|
+
### Follow-up Configuration (`gs_team_follow_up`)
|
|
330
|
+
{"Configured" if follow_up_configured else "Not configured"}
|
|
331
|
+
|
|
332
|
+
### Schedule Windows
|
|
333
|
+
**Initial Outreach** (`gs_team_schedule_time`):
|
|
334
|
+
{"Configured" if team_settings.get('gs_team_schedule_time') else "Not configured"}
|
|
335
|
+
|
|
336
|
+
**Follow-up** (`gs_team_followup_schedule_time`):
|
|
337
|
+
{"Configured" if team_settings.get('gs_team_followup_schedule_time') else "Not configured"}
|
|
338
|
+
|
|
339
|
+
### Automation Settings (`gs_team_auto_interaction`)
|
|
340
|
+
{auto_interaction_details}
|
|
341
|
+
|
|
342
|
+
### Birthday Email (`gs_team_birthday_email`)
|
|
343
|
+
{"Configured" if team_settings.get('gs_team_birthday_email') else "Not configured"}
|
|
344
|
+
|
|
345
|
+
## Configuration Readiness
|
|
346
|
+
|
|
347
|
+
- **Sales Ready**: {sales_ready_status}
|
|
348
|
+
- **Setup Completion**: {int((required_settings_count/total_required)*100)}% ({required_settings_count} of {total_required} required settings configured)
|
|
349
|
+
|
|
350
|
+
### Required Configuration Status
|
|
351
|
+
|
|
352
|
+
#### Configured Settings
|
|
353
|
+
{configured_settings_text}
|
|
354
|
+
|
|
355
|
+
### Ready to Start Selling?
|
|
356
|
+
|
|
357
|
+
{ready_to_start}
|
|
358
|
+
|
|
359
|
+
### Quick Reference
|
|
360
|
+
|
|
361
|
+
**Primary Product**: {primary_product}
|
|
362
|
+
**Target Market**: {target_market}
|
|
363
|
+
**Sales Point of Contact**: {sales_poc}
|
|
364
|
+
"""
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def write_agent_md(
|
|
368
|
+
workspace_slug: Optional[str] = None,
|
|
369
|
+
force: bool = False,
|
|
370
|
+
*,
|
|
371
|
+
manager: Optional[LocalDataManager] = None,
|
|
372
|
+
org_id: Optional[str] = None,
|
|
373
|
+
data_dir: Optional[str] = None,
|
|
374
|
+
detail_limit: int = 180,
|
|
375
|
+
) -> Optional[Path]:
|
|
376
|
+
"""
|
|
377
|
+
Generate and write agent.md using package helpers, preserving agent-written content.
|
|
378
|
+
|
|
379
|
+
If realtimex_toolkit is available, uses save_agent_memory to write to the
|
|
380
|
+
standardized path. Otherwise writes directly to the expected path on disk.
|
|
381
|
+
"""
|
|
382
|
+
try:
|
|
383
|
+
from realtimex_toolkit import ( # type: ignore
|
|
384
|
+
get_flow_variable,
|
|
385
|
+
get_workspace_slug,
|
|
386
|
+
get_workspace_data_dir,
|
|
387
|
+
get_agent_id,
|
|
388
|
+
save_agent_memory,
|
|
389
|
+
)
|
|
390
|
+
except Exception: # noqa: BLE001
|
|
391
|
+
get_flow_variable = None
|
|
392
|
+
get_workspace_slug = None
|
|
393
|
+
get_workspace_data_dir = None
|
|
394
|
+
get_agent_id = None
|
|
395
|
+
save_agent_memory = None
|
|
396
|
+
|
|
397
|
+
# Resolve workspace slug from provided arg, toolkit, or flow variables
|
|
398
|
+
if workspace_slug is None:
|
|
399
|
+
resolved_slug = None
|
|
400
|
+
if get_flow_variable:
|
|
401
|
+
try:
|
|
402
|
+
resolved_slug = get_flow_variable("workspace_slug", default_value=None)
|
|
403
|
+
if not resolved_slug:
|
|
404
|
+
resolved_slug = get_flow_variable("workspace", default_value=None)
|
|
405
|
+
except Exception:
|
|
406
|
+
resolved_slug = None
|
|
407
|
+
if not resolved_slug and get_workspace_slug:
|
|
408
|
+
resolved_slug = get_workspace_slug(default_value="workspace-default")
|
|
409
|
+
workspace_slug = resolved_slug or "workspace-default"
|
|
410
|
+
|
|
411
|
+
if org_id is None and get_flow_variable:
|
|
412
|
+
user = get_flow_variable("user", default_value={'email': 'unknown@example.com'})
|
|
413
|
+
org_id = user.get('email', 'unknown@example.com') if isinstance(user, dict) else 'unknown@example.com'
|
|
414
|
+
if org_id is None:
|
|
415
|
+
org_id = "unknown@example.com"
|
|
416
|
+
|
|
417
|
+
agent_id = "agent-default"
|
|
418
|
+
if get_agent_id:
|
|
419
|
+
agent_id = get_agent_id(default_value="ef0f035a-84c1-4b88-9050-cd7f6fa40ed6")
|
|
420
|
+
|
|
421
|
+
# Resolve workspace and data directories
|
|
422
|
+
workspace_dir = None
|
|
423
|
+
if get_workspace_data_dir:
|
|
424
|
+
try:
|
|
425
|
+
workspace_dir = Path(get_workspace_data_dir(default_workspace_slug=workspace_slug)).expanduser()
|
|
426
|
+
except Exception:
|
|
427
|
+
workspace_dir = None
|
|
428
|
+
if workspace_dir is None:
|
|
429
|
+
workspace_dir = Path.home() / ".realtimex.ai" / "Workspaces" / workspace_slug
|
|
430
|
+
|
|
431
|
+
data_dir_path = Path(data_dir).expanduser() if data_dir else workspace_dir / "fusesell_data"
|
|
432
|
+
|
|
433
|
+
agent_md_path = get_agent_md_path(workspace_slug, agent_id)
|
|
434
|
+
|
|
435
|
+
# TTL check (5 minutes)
|
|
436
|
+
if not force and agent_md_path.exists():
|
|
437
|
+
age_minutes = (time.time() - agent_md_path.stat().st_mtime) / 60
|
|
438
|
+
if age_minutes < 5:
|
|
439
|
+
return agent_md_path
|
|
440
|
+
|
|
441
|
+
agent_section = ""
|
|
442
|
+
if agent_md_path.exists():
|
|
443
|
+
try:
|
|
444
|
+
existing_content = agent_md_path.read_text(encoding='utf-8')
|
|
445
|
+
if "<!-- END AUTO-GENERATED -->" in existing_content:
|
|
446
|
+
parts = existing_content.split("<!-- END AUTO-GENERATED -->", 1)
|
|
447
|
+
if len(parts) > 1:
|
|
448
|
+
agent_section = parts[1].strip()
|
|
449
|
+
except Exception as exc: # noqa: BLE001
|
|
450
|
+
print(f"[WARNING] Could not read existing agent.md: {exc}", file=sys.stderr)
|
|
451
|
+
|
|
452
|
+
try:
|
|
453
|
+
dm = manager or LocalDataManager(data_dir=str(data_dir_path))
|
|
454
|
+
context_data = generate_agent_context(
|
|
455
|
+
manager=dm,
|
|
456
|
+
org_id=org_id,
|
|
457
|
+
detail_limit=detail_limit,
|
|
458
|
+
)
|
|
459
|
+
# Inject workspace metadata for rendering
|
|
460
|
+
context_data['workspace_slug'] = workspace_slug
|
|
461
|
+
context_data['org_id'] = org_id
|
|
462
|
+
if context_data.get('team_name') is None:
|
|
463
|
+
context_data['team_name'] = workspace_slug
|
|
464
|
+
if context_data.get('team_id') is None:
|
|
465
|
+
context_data['team_id'] = 'N/A'
|
|
466
|
+
if context_data.get('team_created_at') is None:
|
|
467
|
+
context_data['team_created_at'] = 'N/A'
|
|
468
|
+
auto_generated_content = _render_agent_markdown(context_data)
|
|
469
|
+
|
|
470
|
+
timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')
|
|
471
|
+
complete_content = f"""<!-- AUTO-GENERATED SECTION -->
|
|
472
|
+
<!-- Last Updated: {timestamp} -->
|
|
473
|
+
<!-- WARNING: Do not edit content between AUTO-GENERATED and END AUTO-GENERATED markers -->
|
|
474
|
+
<!-- This section is automatically regenerated from database on critical actions -->
|
|
475
|
+
|
|
476
|
+
{auto_generated_content}
|
|
477
|
+
|
|
478
|
+
<!-- END AUTO-GENERATED -->"""
|
|
479
|
+
|
|
480
|
+
if save_agent_memory:
|
|
481
|
+
new_agent_path = save_agent_memory(
|
|
482
|
+
workspace_slug=workspace_slug,
|
|
483
|
+
agent_id=agent_id,
|
|
484
|
+
content=complete_content,
|
|
485
|
+
mode="overwrite",
|
|
486
|
+
)
|
|
487
|
+
return Path(new_agent_path)
|
|
488
|
+
|
|
489
|
+
agent_md_path.parent.mkdir(parents=True, exist_ok=True)
|
|
490
|
+
agent_md_path.write_text(complete_content, encoding='utf-8')
|
|
491
|
+
return agent_md_path
|
|
492
|
+
|
|
493
|
+
except Exception as exc: # noqa: BLE001
|
|
494
|
+
print(f"[ERROR] Failed to generate agent.md: {exc}", file=sys.stderr)
|
|
495
|
+
import traceback
|
|
496
|
+
traceback.print_exc()
|
|
497
|
+
return None
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def notify_action_completed(
|
|
501
|
+
action: str,
|
|
502
|
+
*,
|
|
503
|
+
workspace_slug: Optional[str] = None,
|
|
504
|
+
force: bool = False,
|
|
505
|
+
manager: Optional[LocalDataManager] = None,
|
|
506
|
+
org_id: Optional[str] = None,
|
|
507
|
+
data_dir: Optional[str] = None,
|
|
508
|
+
) -> Dict[str, Any]:
|
|
509
|
+
"""
|
|
510
|
+
Update agent.md if action requires it. Returns a status dictionary.
|
|
511
|
+
"""
|
|
512
|
+
if not should_update_agent_context(
|
|
513
|
+
action=action,
|
|
514
|
+
critical_actions=CRITICAL_ACTIONS,
|
|
515
|
+
skip_actions=SKIP_ACTIONS,
|
|
516
|
+
):
|
|
517
|
+
return {
|
|
518
|
+
'status': 'skipped',
|
|
519
|
+
'message': 'Agent context skipped (read-only action)',
|
|
520
|
+
'updated': False,
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
should_force = force or action in CRITICAL_ACTIONS
|
|
524
|
+
|
|
525
|
+
agent_md_path = write_agent_md(
|
|
526
|
+
workspace_slug=workspace_slug,
|
|
527
|
+
force=should_force,
|
|
528
|
+
manager=manager,
|
|
529
|
+
org_id=org_id,
|
|
530
|
+
data_dir=data_dir,
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
if agent_md_path:
|
|
534
|
+
return {
|
|
535
|
+
'status': 'success',
|
|
536
|
+
'message': 'Agent context updated',
|
|
537
|
+
'updated': True,
|
|
538
|
+
'path': str(agent_md_path),
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return {
|
|
542
|
+
'status': 'error',
|
|
543
|
+
'message': 'Failed to update agent context',
|
|
544
|
+
'updated': False,
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
__all__ = [
|
|
549
|
+
'notify_action_completed',
|
|
550
|
+
'write_agent_md',
|
|
551
|
+
'get_agent_md_path',
|
|
552
|
+
]
|