tallyfy 1.0.4__py3-none-any.whl → 1.0.5__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 tallyfy might be problematic. Click here for more details.
- tallyfy/__init__.py +8 -4
- tallyfy/core.py +8 -8
- tallyfy/form_fields_management/__init__.py +70 -0
- tallyfy/form_fields_management/base.py +109 -0
- tallyfy/form_fields_management/crud_operations.py +234 -0
- tallyfy/form_fields_management/options_management.py +222 -0
- tallyfy/form_fields_management/suggestions.py +411 -0
- tallyfy/task_management/__init__.py +81 -0
- tallyfy/task_management/base.py +125 -0
- tallyfy/task_management/creation.py +221 -0
- tallyfy/task_management/retrieval.py +211 -0
- tallyfy/task_management/search.py +196 -0
- tallyfy/template_management/__init__.py +85 -0
- tallyfy/template_management/analysis.py +1093 -0
- tallyfy/template_management/automation.py +469 -0
- tallyfy/template_management/base.py +56 -0
- tallyfy/template_management/basic_operations.py +477 -0
- tallyfy/template_management/health_assessment.py +763 -0
- tallyfy/user_management/__init__.py +69 -0
- tallyfy/user_management/base.py +146 -0
- tallyfy/user_management/invitation.py +286 -0
- tallyfy/user_management/retrieval.py +339 -0
- {tallyfy-1.0.4.dist-info → tallyfy-1.0.5.dist-info}/METADATA +120 -56
- tallyfy-1.0.5.dist-info/RECORD +28 -0
- tallyfy/BUILD.md +0 -5
- tallyfy/form_fields_management.py +0 -582
- tallyfy/task_management.py +0 -356
- tallyfy/template_management.py +0 -2607
- tallyfy/user_management.py +0 -235
- tallyfy-1.0.4.dist-info/RECORD +0 -13
- {tallyfy-1.0.4.dist-info → tallyfy-1.0.5.dist-info}/WHEEL +0 -0
- {tallyfy-1.0.4.dist-info → tallyfy-1.0.5.dist-info}/licenses/LICENSE +0 -0
- {tallyfy-1.0.4.dist-info → tallyfy-1.0.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1093 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Template analysis functionality for providing insights and recommendations
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Optional, Dict, Any
|
|
6
|
+
from .base import TemplateManagerBase
|
|
7
|
+
from ..models import Template, TallyfyError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TemplateAnalysis(TemplateManagerBase):
|
|
11
|
+
"""Handles template analysis and provides optimization recommendations"""
|
|
12
|
+
|
|
13
|
+
def get_step_dependencies(self, org_id: str, template_id: str, step_id: str) -> Dict[str, Any]:
|
|
14
|
+
"""
|
|
15
|
+
Analyze step dependencies and automation effects.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
org_id: Organization ID
|
|
19
|
+
template_id: Template ID
|
|
20
|
+
step_id: Step ID to analyze dependencies for
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Dictionary containing dependency analysis with step info, automation rules, dependencies, and affected elements
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
TallyfyError: If the request fails
|
|
27
|
+
"""
|
|
28
|
+
self._validate_org_id(org_id)
|
|
29
|
+
self._validate_template_id(template_id)
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
# Get template with full data including steps and automations
|
|
33
|
+
template_endpoint = f"organizations/{org_id}/checklists/{template_id}"
|
|
34
|
+
template_params = {'with': 'steps,automated_actions,prerun'}
|
|
35
|
+
template_response = self.sdk._make_request('GET', template_endpoint, params=template_params)
|
|
36
|
+
|
|
37
|
+
template_data = self._extract_data(template_response)
|
|
38
|
+
if not template_data:
|
|
39
|
+
raise TallyfyError("Unable to retrieve template data for dependency analysis")
|
|
40
|
+
|
|
41
|
+
# Find the target step
|
|
42
|
+
steps = template_data.get('steps', [])
|
|
43
|
+
target_step = None
|
|
44
|
+
for step in steps:
|
|
45
|
+
if step.get('id') == step_id:
|
|
46
|
+
target_step = step
|
|
47
|
+
break
|
|
48
|
+
|
|
49
|
+
if not target_step:
|
|
50
|
+
raise TallyfyError(f"Step {step_id} not found in template {template_id}")
|
|
51
|
+
|
|
52
|
+
# Analyze automations for dependencies
|
|
53
|
+
automations = template_data.get('automated_actions', [])
|
|
54
|
+
dependencies = {
|
|
55
|
+
'incoming': [], # Steps that affect this step
|
|
56
|
+
'outgoing': [], # Steps this step affects
|
|
57
|
+
'field_dependencies': [], # Form fields this step depends on
|
|
58
|
+
'conditional_visibility': [] # Visibility conditions
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
step_position = target_step.get('position', 0)
|
|
62
|
+
|
|
63
|
+
# Analyze each automation rule
|
|
64
|
+
for automation in automations:
|
|
65
|
+
conditions = automation.get('conditions', [])
|
|
66
|
+
actions = automation.get('actions', [])
|
|
67
|
+
|
|
68
|
+
# Check if this step is affected by the automation
|
|
69
|
+
step_affected_by_conditions = False
|
|
70
|
+
step_affects_others = False
|
|
71
|
+
|
|
72
|
+
for condition in conditions:
|
|
73
|
+
condition_type = condition.get('type', '')
|
|
74
|
+
condition_step = condition.get('step_id')
|
|
75
|
+
|
|
76
|
+
# Check if condition affects our target step
|
|
77
|
+
if condition_step == step_id:
|
|
78
|
+
step_affected_by_conditions = True
|
|
79
|
+
elif condition_type in ['step_completed', 'step_started']:
|
|
80
|
+
if condition_step:
|
|
81
|
+
for step in steps:
|
|
82
|
+
if step.get('id') == condition_step:
|
|
83
|
+
dependencies['incoming'].append({
|
|
84
|
+
'step_id': condition_step,
|
|
85
|
+
'step_title': step.get('title', 'Unknown'),
|
|
86
|
+
'condition_type': condition_type,
|
|
87
|
+
'automation_id': automation.get('id'),
|
|
88
|
+
'description': f"Step depends on '{step.get('title')}' being {condition_type.replace('_', ' ')}"
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
# Check if this step's actions affect other steps
|
|
92
|
+
for action in actions:
|
|
93
|
+
action_type = action.get('type', '')
|
|
94
|
+
target_step_id = action.get('step_id')
|
|
95
|
+
|
|
96
|
+
if target_step_id and target_step_id != step_id:
|
|
97
|
+
# This step affects another step
|
|
98
|
+
affected_step = None
|
|
99
|
+
for step in steps:
|
|
100
|
+
if step.get('id') == target_step_id:
|
|
101
|
+
affected_step = step
|
|
102
|
+
break
|
|
103
|
+
|
|
104
|
+
if affected_step:
|
|
105
|
+
dependencies['outgoing'].append({
|
|
106
|
+
'step_id': target_step_id,
|
|
107
|
+
'step_title': affected_step.get('title', 'Unknown'),
|
|
108
|
+
'action_type': action_type,
|
|
109
|
+
'automation_id': automation.get('id'),
|
|
110
|
+
'description': f"Step triggers '{action_type}' on '{affected_step.get('title')}'"
|
|
111
|
+
})
|
|
112
|
+
step_affects_others = True
|
|
113
|
+
|
|
114
|
+
# Check for field dependencies
|
|
115
|
+
for condition in conditions:
|
|
116
|
+
if condition.get('type') == 'field_value':
|
|
117
|
+
field_label = condition.get('field_label', 'Unknown Field')
|
|
118
|
+
field_value = condition.get('value', '')
|
|
119
|
+
dependencies['field_dependencies'].append({
|
|
120
|
+
'field_label': field_label,
|
|
121
|
+
'expected_value': field_value,
|
|
122
|
+
'condition_type': 'field_value',
|
|
123
|
+
'automation_id': automation.get('id'),
|
|
124
|
+
'description': f"Depends on field '{field_label}' having value '{field_value}'"
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
# Check for visibility conditions affecting this step
|
|
128
|
+
if step_affected_by_conditions:
|
|
129
|
+
for action in actions:
|
|
130
|
+
if action.get('type') in ['show_step', 'hide_step'] and action.get('step_id') == step_id:
|
|
131
|
+
dependencies['conditional_visibility'].append({
|
|
132
|
+
'action_type': action.get('type'),
|
|
133
|
+
'automation_id': automation.get('id'),
|
|
134
|
+
'description': f"Step visibility controlled by automation conditions"
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
# Calculate dependency complexity score
|
|
138
|
+
total_deps = (len(dependencies['incoming']) + len(dependencies['outgoing']) +
|
|
139
|
+
len(dependencies['field_dependencies']) + len(dependencies['conditional_visibility']))
|
|
140
|
+
|
|
141
|
+
complexity_score = min(100, total_deps * 10) # Cap at 100
|
|
142
|
+
|
|
143
|
+
complexity_level = "Low"
|
|
144
|
+
if complexity_score > 70:
|
|
145
|
+
complexity_level = "High"
|
|
146
|
+
elif complexity_score > 30:
|
|
147
|
+
complexity_level = "Medium"
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
'step_info': {
|
|
151
|
+
'id': step_id,
|
|
152
|
+
'title': target_step.get('title', 'Unknown'),
|
|
153
|
+
'position': step_position,
|
|
154
|
+
'summary': target_step.get('summary', '')
|
|
155
|
+
},
|
|
156
|
+
'dependencies': dependencies,
|
|
157
|
+
'complexity_analysis': {
|
|
158
|
+
'score': complexity_score,
|
|
159
|
+
'level': complexity_level,
|
|
160
|
+
'total_dependencies': total_deps,
|
|
161
|
+
'incoming_count': len(dependencies['incoming']),
|
|
162
|
+
'outgoing_count': len(dependencies['outgoing']),
|
|
163
|
+
'field_dependencies_count': len(dependencies['field_dependencies']),
|
|
164
|
+
'visibility_conditions_count': len(dependencies['conditional_visibility'])
|
|
165
|
+
},
|
|
166
|
+
'recommendations': self._generate_dependency_recommendations(dependencies, complexity_level),
|
|
167
|
+
'template_id': template_id,
|
|
168
|
+
'analysis_timestamp': self.sdk._get_current_timestamp() if hasattr(self.sdk, '_get_current_timestamp') else None
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
except TallyfyError:
|
|
172
|
+
raise
|
|
173
|
+
except Exception as e:
|
|
174
|
+
self._handle_api_error(e, "analyze step dependencies", org_id=org_id, template_id=template_id, step_id=step_id)
|
|
175
|
+
|
|
176
|
+
def suggest_step_deadline(self, org_id: str, template_id: str, step_id: str) -> Dict[str, Any]:
|
|
177
|
+
"""
|
|
178
|
+
Suggest reasonable deadlines based on step analysis.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
org_id: Organization ID
|
|
182
|
+
template_id: Template ID
|
|
183
|
+
step_id: Step ID to suggest deadline for
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Dictionary with suggested deadline configuration, reasoning, and alternatives
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
TallyfyError: If the request fails
|
|
190
|
+
"""
|
|
191
|
+
self._validate_org_id(org_id)
|
|
192
|
+
self._validate_template_id(template_id)
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
# Get template with steps
|
|
196
|
+
template_endpoint = f"organizations/{org_id}/checklists/{template_id}"
|
|
197
|
+
template_params = {'with': 'steps'}
|
|
198
|
+
template_response = self.sdk._make_request('GET', template_endpoint, params=template_params)
|
|
199
|
+
|
|
200
|
+
template_data = self._extract_data(template_response)
|
|
201
|
+
if not template_data:
|
|
202
|
+
raise TallyfyError("Unable to retrieve template data for deadline analysis")
|
|
203
|
+
|
|
204
|
+
# Find the target step
|
|
205
|
+
steps = template_data.get('steps', [])
|
|
206
|
+
target_step = None
|
|
207
|
+
for step in steps:
|
|
208
|
+
if step.get('id') == step_id:
|
|
209
|
+
target_step = step
|
|
210
|
+
break
|
|
211
|
+
|
|
212
|
+
if not target_step:
|
|
213
|
+
raise TallyfyError(f"Step {step_id} not found in template {template_id}")
|
|
214
|
+
|
|
215
|
+
step_title = target_step.get('title', '').lower()
|
|
216
|
+
step_summary = target_step.get('summary', '').lower()
|
|
217
|
+
step_content = f"{step_title} {step_summary}"
|
|
218
|
+
|
|
219
|
+
# Analyze step content for deadline suggestions
|
|
220
|
+
suggestions = []
|
|
221
|
+
|
|
222
|
+
# Quick tasks (same day)
|
|
223
|
+
quick_keywords = ['review', 'approve', 'check', 'confirm', 'verify', 'acknowledge', 'sign off']
|
|
224
|
+
if any(keyword in step_content for keyword in quick_keywords):
|
|
225
|
+
suggestions.append({
|
|
226
|
+
'value': 4,
|
|
227
|
+
'unit': 'hours',
|
|
228
|
+
'option': 'from',
|
|
229
|
+
'confidence': 85,
|
|
230
|
+
'reasoning': 'Quick review/approval tasks typically require immediate attention',
|
|
231
|
+
'category': 'quick_action'
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
# Communication tasks
|
|
235
|
+
comm_keywords = ['email', 'call', 'contact', 'notify', 'inform', 'communicate', 'reach out']
|
|
236
|
+
if any(keyword in step_content for keyword in comm_keywords):
|
|
237
|
+
suggestions.append({
|
|
238
|
+
'value': 1,
|
|
239
|
+
'unit': 'days',
|
|
240
|
+
'option': 'from',
|
|
241
|
+
'confidence': 80,
|
|
242
|
+
'reasoning': 'Communication tasks should be handled promptly for good workflow',
|
|
243
|
+
'category': 'communication'
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
# Analysis/Research tasks
|
|
247
|
+
analysis_keywords = ['analyze', 'research', 'investigate', 'study', 'evaluate', 'assess', 'examine']
|
|
248
|
+
if any(keyword in step_content for keyword in analysis_keywords):
|
|
249
|
+
suggestions.append({
|
|
250
|
+
'value': 3,
|
|
251
|
+
'unit': 'days',
|
|
252
|
+
'option': 'from',
|
|
253
|
+
'confidence': 75,
|
|
254
|
+
'reasoning': 'Analysis tasks require time for thorough investigation',
|
|
255
|
+
'category': 'analysis'
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
# Document/Report creation
|
|
259
|
+
doc_keywords = ['document', 'report', 'write', 'create', 'draft', 'prepare', 'compile']
|
|
260
|
+
if any(keyword in step_content for keyword in doc_keywords):
|
|
261
|
+
suggestions.append({
|
|
262
|
+
'value': 5,
|
|
263
|
+
'unit': 'days',
|
|
264
|
+
'option': 'from',
|
|
265
|
+
'confidence': 70,
|
|
266
|
+
'reasoning': 'Document creation requires adequate time for quality output',
|
|
267
|
+
'category': 'documentation'
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
# Meeting/Presentation tasks
|
|
271
|
+
meeting_keywords = ['meeting', 'present', 'presentation', 'demo', 'workshop', 'training']
|
|
272
|
+
if any(keyword in step_content for keyword in meeting_keywords):
|
|
273
|
+
suggestions.append({
|
|
274
|
+
'value': 1,
|
|
275
|
+
'unit': 'weeks',
|
|
276
|
+
'option': 'from',
|
|
277
|
+
'confidence': 65,
|
|
278
|
+
'reasoning': 'Meetings and presentations need time for scheduling and preparation',
|
|
279
|
+
'category': 'meeting'
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
# Testing/QA tasks
|
|
283
|
+
test_keywords = ['test', 'testing', 'qa', 'quality', 'validate', 'verify functionality']
|
|
284
|
+
if any(keyword in step_content for keyword in test_keywords):
|
|
285
|
+
suggestions.append({
|
|
286
|
+
'value': 2,
|
|
287
|
+
'unit': 'days',
|
|
288
|
+
'option': 'from',
|
|
289
|
+
'confidence': 80,
|
|
290
|
+
'reasoning': 'Testing requires time for thorough validation',
|
|
291
|
+
'category': 'testing'
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
# Default fallback suggestion
|
|
295
|
+
if not suggestions:
|
|
296
|
+
suggestions.append({
|
|
297
|
+
'value': 2,
|
|
298
|
+
'unit': 'days',
|
|
299
|
+
'option': 'from',
|
|
300
|
+
'confidence': 50,
|
|
301
|
+
'reasoning': 'General task deadline based on common workflow patterns',
|
|
302
|
+
'category': 'general'
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
# Sort suggestions by confidence
|
|
306
|
+
suggestions.sort(key=lambda x: x['confidence'], reverse=True)
|
|
307
|
+
|
|
308
|
+
# Get the best suggestion
|
|
309
|
+
best_suggestion = suggestions[0]
|
|
310
|
+
|
|
311
|
+
# Generate alternative suggestions
|
|
312
|
+
alternatives = []
|
|
313
|
+
base_value = best_suggestion['value']
|
|
314
|
+
base_unit = best_suggestion['unit']
|
|
315
|
+
|
|
316
|
+
if base_unit == 'hours':
|
|
317
|
+
alternatives = [
|
|
318
|
+
{'value': base_value * 2, 'unit': 'hours', 'description': 'Extended timeline'},
|
|
319
|
+
{'value': 1, 'unit': 'days', 'description': 'Next business day'}
|
|
320
|
+
]
|
|
321
|
+
elif base_unit == 'days':
|
|
322
|
+
alternatives = [
|
|
323
|
+
{'value': max(1, base_value // 2), 'unit': 'days', 'description': 'Rushed timeline'},
|
|
324
|
+
{'value': base_value * 2, 'unit': 'days', 'description': 'Extended timeline'},
|
|
325
|
+
{'value': 1, 'unit': 'weeks', 'description': 'Weekly milestone'}
|
|
326
|
+
]
|
|
327
|
+
elif base_unit == 'weeks':
|
|
328
|
+
alternatives = [
|
|
329
|
+
{'value': base_value * 7, 'unit': 'days', 'description': 'Daily breakdown'},
|
|
330
|
+
{'value': base_value * 2, 'unit': 'weeks', 'description': 'Extended timeline'}
|
|
331
|
+
]
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
'step_info': {
|
|
335
|
+
'id': step_id,
|
|
336
|
+
'title': target_step.get('title', 'Unknown'),
|
|
337
|
+
'summary': target_step.get('summary', '')
|
|
338
|
+
},
|
|
339
|
+
'suggested_deadline': best_suggestion,
|
|
340
|
+
'alternative_suggestions': alternatives,
|
|
341
|
+
'all_matches': suggestions,
|
|
342
|
+
'analysis': {
|
|
343
|
+
'content_analyzed': step_content[:100] + '...' if len(step_content) > 100 else step_content,
|
|
344
|
+
'keywords_found': [keyword for keyword in quick_keywords + comm_keywords + analysis_keywords + doc_keywords + meeting_keywords + test_keywords if keyword in step_content],
|
|
345
|
+
'suggestion_count': len(suggestions)
|
|
346
|
+
},
|
|
347
|
+
'template_id': template_id
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
except TallyfyError:
|
|
351
|
+
raise
|
|
352
|
+
except Exception as e:
|
|
353
|
+
self._handle_api_error(e, "suggest step deadline", org_id=org_id, template_id=template_id, step_id=step_id)
|
|
354
|
+
|
|
355
|
+
def get_step_visibility_conditions(self, org_id: str, template_id: str, step_id: str) -> Dict[str, Any]:
|
|
356
|
+
"""
|
|
357
|
+
Analyze when/how steps become visible based on automations.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
org_id: Organization ID
|
|
361
|
+
template_id: Template ID
|
|
362
|
+
step_id: Step ID to analyze visibility for
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
Dictionary containing step visibility analysis, rules, behavior patterns, and recommendations
|
|
366
|
+
|
|
367
|
+
Raises:
|
|
368
|
+
TallyfyError: If the request fails
|
|
369
|
+
"""
|
|
370
|
+
self._validate_org_id(org_id)
|
|
371
|
+
self._validate_template_id(template_id)
|
|
372
|
+
|
|
373
|
+
try:
|
|
374
|
+
# Get template with automations and steps
|
|
375
|
+
template_endpoint = f"organizations/{org_id}/checklists/{template_id}"
|
|
376
|
+
template_params = {'with': 'steps,automated_actions,prerun'}
|
|
377
|
+
template_response = self.sdk._make_request('GET', template_endpoint, params=template_params)
|
|
378
|
+
|
|
379
|
+
template_data = self._extract_data(template_response)
|
|
380
|
+
if not template_data:
|
|
381
|
+
raise TallyfyError("Unable to retrieve template data for visibility analysis")
|
|
382
|
+
|
|
383
|
+
# Find the target step
|
|
384
|
+
steps = template_data.get('steps', [])
|
|
385
|
+
target_step = None
|
|
386
|
+
for step in steps:
|
|
387
|
+
if step.get('id') == step_id:
|
|
388
|
+
target_step = step
|
|
389
|
+
break
|
|
390
|
+
|
|
391
|
+
if not target_step:
|
|
392
|
+
raise TallyfyError(f"Step {step_id} not found in template {template_id}")
|
|
393
|
+
|
|
394
|
+
automations = template_data.get('automated_actions', [])
|
|
395
|
+
|
|
396
|
+
visibility_rules = {
|
|
397
|
+
'show_rules': [],
|
|
398
|
+
'hide_rules': [],
|
|
399
|
+
'conditional_rules': []
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
step_name_map = {step.get('id'): step.get('title', 'Unknown') for step in steps}
|
|
403
|
+
|
|
404
|
+
# Analyze automation rules affecting this step's visibility
|
|
405
|
+
for automation in automations:
|
|
406
|
+
automation_id = automation.get('id')
|
|
407
|
+
conditions = automation.get('conditions', [])
|
|
408
|
+
actions = automation.get('actions', [])
|
|
409
|
+
|
|
410
|
+
# Check if this automation affects our target step's visibility
|
|
411
|
+
for action in actions:
|
|
412
|
+
if action.get('step_id') == step_id:
|
|
413
|
+
action_type = action.get('type', '')
|
|
414
|
+
|
|
415
|
+
if action_type in ['show_step', 'hide_step']:
|
|
416
|
+
# Analyze conditions that trigger this visibility change
|
|
417
|
+
rule_conditions = []
|
|
418
|
+
for condition in conditions:
|
|
419
|
+
condition_summary = self._summarize_condition(condition, step_name_map)
|
|
420
|
+
rule_conditions.append(condition_summary)
|
|
421
|
+
|
|
422
|
+
rule_info = {
|
|
423
|
+
'automation_id': automation_id,
|
|
424
|
+
'action_type': action_type,
|
|
425
|
+
'conditions': rule_conditions,
|
|
426
|
+
'conditions_count': len(conditions),
|
|
427
|
+
'rule_logic': automation.get('condition_logic', 'AND'),
|
|
428
|
+
'description': f"Step will be {action_type.replace('_', ' ')} when: {' AND '.join(rule_conditions) if automation.get('condition_logic', 'AND') == 'AND' else ' OR '.join(rule_conditions)}"
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if action_type == 'show_step':
|
|
432
|
+
visibility_rules['show_rules'].append(rule_info)
|
|
433
|
+
elif action_type == 'hide_step':
|
|
434
|
+
visibility_rules['hide_rules'].append(rule_info)
|
|
435
|
+
|
|
436
|
+
# Determine step's default visibility
|
|
437
|
+
default_visibility = "visible" # Most steps are visible by default
|
|
438
|
+
|
|
439
|
+
# Check if step has any show rules (implies it might be hidden by default)
|
|
440
|
+
if visibility_rules['show_rules']:
|
|
441
|
+
default_visibility = "conditionally_visible"
|
|
442
|
+
|
|
443
|
+
# Analyze visibility complexity
|
|
444
|
+
total_rules = len(visibility_rules['show_rules']) + len(visibility_rules['hide_rules'])
|
|
445
|
+
|
|
446
|
+
complexity_level = "Simple"
|
|
447
|
+
if total_rules > 3:
|
|
448
|
+
complexity_level = "Complex"
|
|
449
|
+
elif total_rules > 1:
|
|
450
|
+
complexity_level = "Moderate"
|
|
451
|
+
|
|
452
|
+
# Generate visibility behavior analysis
|
|
453
|
+
behavior_patterns = []
|
|
454
|
+
|
|
455
|
+
if not visibility_rules['show_rules'] and not visibility_rules['hide_rules']:
|
|
456
|
+
behavior_patterns.append("Step is always visible (no conditional visibility rules)")
|
|
457
|
+
|
|
458
|
+
if visibility_rules['show_rules']:
|
|
459
|
+
behavior_patterns.append(f"Step becomes visible based on {len(visibility_rules['show_rules'])} condition(s)")
|
|
460
|
+
|
|
461
|
+
if visibility_rules['hide_rules']:
|
|
462
|
+
behavior_patterns.append(f"Step can be hidden based on {len(visibility_rules['hide_rules'])} condition(s)")
|
|
463
|
+
|
|
464
|
+
if visibility_rules['show_rules'] and visibility_rules['hide_rules']:
|
|
465
|
+
behavior_patterns.append("Step has both show and hide rules - visibility depends on rule evaluation order")
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
'step_info': {
|
|
469
|
+
'id': step_id,
|
|
470
|
+
'title': target_step.get('title', 'Unknown'),
|
|
471
|
+
'default_visibility': default_visibility
|
|
472
|
+
},
|
|
473
|
+
'visibility_rules': visibility_rules,
|
|
474
|
+
'behavior_analysis': {
|
|
475
|
+
'patterns': behavior_patterns,
|
|
476
|
+
'complexity_level': complexity_level,
|
|
477
|
+
'total_rules': total_rules,
|
|
478
|
+
'has_conditional_visibility': total_rules > 0
|
|
479
|
+
},
|
|
480
|
+
'recommendations': self._generate_visibility_recommendations(visibility_rules, complexity_level),
|
|
481
|
+
'template_id': template_id
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
except TallyfyError:
|
|
485
|
+
raise
|
|
486
|
+
except Exception as e:
|
|
487
|
+
self._handle_api_error(e, "analyze step visibility conditions", org_id=org_id, template_id=template_id, step_id=step_id)
|
|
488
|
+
|
|
489
|
+
def suggest_kickoff_fields(self, org_id: str, template_id: str) -> List[Dict[str, Any]]:
|
|
490
|
+
"""
|
|
491
|
+
Suggest relevant kickoff fields based on template analysis.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
org_id: Organization ID
|
|
495
|
+
template_id: Template ID to analyze
|
|
496
|
+
|
|
497
|
+
Returns:
|
|
498
|
+
List of suggested kickoff field configurations with reasoning
|
|
499
|
+
|
|
500
|
+
Raises:
|
|
501
|
+
TallyfyError: If the request fails
|
|
502
|
+
"""
|
|
503
|
+
self._validate_org_id(org_id)
|
|
504
|
+
self._validate_template_id(template_id)
|
|
505
|
+
|
|
506
|
+
try:
|
|
507
|
+
# Get template with full data
|
|
508
|
+
template_endpoint = f"organizations/{org_id}/checklists/{template_id}"
|
|
509
|
+
template_params = {'with': 'steps,automated_actions,prerun'}
|
|
510
|
+
template_response = self.sdk._make_request('GET', template_endpoint, params=template_params)
|
|
511
|
+
|
|
512
|
+
template_data = self._extract_data(template_response)
|
|
513
|
+
if not template_data:
|
|
514
|
+
raise TallyfyError("Unable to retrieve template data for kickoff field analysis")
|
|
515
|
+
|
|
516
|
+
template_title = template_data.get('title', '').lower()
|
|
517
|
+
template_summary = template_data.get('summary', '').lower()
|
|
518
|
+
steps = template_data.get('steps', [])
|
|
519
|
+
existing_prerun = template_data.get('prerun', [])
|
|
520
|
+
|
|
521
|
+
# Collect all step content for analysis
|
|
522
|
+
all_step_content = ""
|
|
523
|
+
for step in steps:
|
|
524
|
+
step_title = step.get('title', '').lower()
|
|
525
|
+
step_summary = step.get('summary', '').lower()
|
|
526
|
+
all_step_content += f" {step_title} {step_summary}"
|
|
527
|
+
|
|
528
|
+
combined_content = f"{template_title} {template_summary} {all_step_content}"
|
|
529
|
+
|
|
530
|
+
suggestions = []
|
|
531
|
+
|
|
532
|
+
# Get existing field labels to avoid duplicates
|
|
533
|
+
existing_labels = [field.get('label', '').lower() for field in existing_prerun]
|
|
534
|
+
|
|
535
|
+
# Client/Customer information fields
|
|
536
|
+
client_keywords = ['client', 'customer', 'company', 'organization', 'business', 'vendor', 'supplier']
|
|
537
|
+
if any(keyword in combined_content for keyword in client_keywords):
|
|
538
|
+
if 'client name' not in existing_labels and 'customer name' not in existing_labels:
|
|
539
|
+
suggestions.append({
|
|
540
|
+
'label': 'Client Name',
|
|
541
|
+
'type': 'text',
|
|
542
|
+
'required': True,
|
|
543
|
+
'description': 'Name of the client or customer for this process',
|
|
544
|
+
'reasoning': 'Template content suggests client/customer involvement',
|
|
545
|
+
'confidence': 85,
|
|
546
|
+
'category': 'client_info'
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
if 'contact email' not in existing_labels and 'email' not in existing_labels:
|
|
550
|
+
suggestions.append({
|
|
551
|
+
'label': 'Contact Email',
|
|
552
|
+
'type': 'email',
|
|
553
|
+
'required': True,
|
|
554
|
+
'description': 'Primary email contact for this process',
|
|
555
|
+
'reasoning': 'Client/customer processes typically require contact information',
|
|
556
|
+
'confidence': 80,
|
|
557
|
+
'category': 'client_info'
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
# Project information fields
|
|
561
|
+
project_keywords = ['project', 'initiative', 'campaign', 'launch', 'implementation', 'rollout']
|
|
562
|
+
if any(keyword in combined_content for keyword in project_keywords):
|
|
563
|
+
if 'project name' not in existing_labels:
|
|
564
|
+
suggestions.append({
|
|
565
|
+
'label': 'Project Name',
|
|
566
|
+
'type': 'text',
|
|
567
|
+
'required': True,
|
|
568
|
+
'description': 'Name or title of the project',
|
|
569
|
+
'reasoning': 'Template appears to be project-related',
|
|
570
|
+
'confidence': 90,
|
|
571
|
+
'category': 'project_info'
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
if 'project budget' not in existing_labels and 'budget' not in existing_labels:
|
|
575
|
+
suggestions.append({
|
|
576
|
+
'label': 'Project Budget',
|
|
577
|
+
'type': 'number',
|
|
578
|
+
'required': False,
|
|
579
|
+
'description': 'Budget allocated for this project',
|
|
580
|
+
'reasoning': 'Project templates often need budget tracking',
|
|
581
|
+
'confidence': 70,
|
|
582
|
+
'category': 'project_info'
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
# Date-related fields
|
|
586
|
+
date_keywords = ['deadline', 'due date', 'launch date', 'start date', 'completion', 'delivery']
|
|
587
|
+
if any(keyword in combined_content for keyword in date_keywords):
|
|
588
|
+
if 'target completion date' not in existing_labels and 'due date' not in existing_labels:
|
|
589
|
+
suggestions.append({
|
|
590
|
+
'label': 'Target Completion Date',
|
|
591
|
+
'type': 'date',
|
|
592
|
+
'required': True,
|
|
593
|
+
'description': 'When this process should be completed',
|
|
594
|
+
'reasoning': 'Template content mentions dates and deadlines',
|
|
595
|
+
'confidence': 85,
|
|
596
|
+
'category': 'scheduling'
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
# Priority/Urgency fields
|
|
600
|
+
priority_keywords = ['priority', 'urgent', 'critical', 'important', 'high priority', 'rush']
|
|
601
|
+
if any(keyword in combined_content for keyword in priority_keywords):
|
|
602
|
+
if 'priority level' not in existing_labels and 'priority' not in existing_labels:
|
|
603
|
+
suggestions.append({
|
|
604
|
+
'label': 'Priority Level',
|
|
605
|
+
'type': 'select',
|
|
606
|
+
'required': True,
|
|
607
|
+
'options': ['Low', 'Medium', 'High', 'Critical'],
|
|
608
|
+
'description': 'Priority level for this process',
|
|
609
|
+
'reasoning': 'Template content indicates priority considerations',
|
|
610
|
+
'confidence': 75,
|
|
611
|
+
'category': 'prioritization'
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
# Department/Team fields
|
|
615
|
+
dept_keywords = ['department', 'team', 'division', 'group', 'unit', 'office', 'branch']
|
|
616
|
+
if any(keyword in combined_content for keyword in dept_keywords):
|
|
617
|
+
if 'department' not in existing_labels and 'team' not in existing_labels:
|
|
618
|
+
suggestions.append({
|
|
619
|
+
'label': 'Department',
|
|
620
|
+
'type': 'text',
|
|
621
|
+
'required': False,
|
|
622
|
+
'description': 'Department or team responsible for this process',
|
|
623
|
+
'reasoning': 'Template mentions departments or teams',
|
|
624
|
+
'confidence': 70,
|
|
625
|
+
'category': 'organization'
|
|
626
|
+
})
|
|
627
|
+
|
|
628
|
+
# Document/File fields
|
|
629
|
+
doc_keywords = ['document', 'file', 'attachment', 'upload', 'report', 'contract', 'agreement']
|
|
630
|
+
if any(keyword in combined_content for keyword in doc_keywords):
|
|
631
|
+
if 'supporting documents' not in existing_labels and 'attachments' not in existing_labels:
|
|
632
|
+
suggestions.append({
|
|
633
|
+
'label': 'Supporting Documents',
|
|
634
|
+
'type': 'file',
|
|
635
|
+
'required': False,
|
|
636
|
+
'description': 'Upload any relevant documents for this process',
|
|
637
|
+
'reasoning': 'Template involves document handling',
|
|
638
|
+
'confidence': 65,
|
|
639
|
+
'category': 'documentation'
|
|
640
|
+
})
|
|
641
|
+
|
|
642
|
+
# Financial fields
|
|
643
|
+
financial_keywords = ['cost', 'price', 'amount', 'budget', 'expense', 'fee', 'payment', 'invoice']
|
|
644
|
+
if any(keyword in combined_content for keyword in financial_keywords):
|
|
645
|
+
if 'estimated cost' not in existing_labels and 'cost' not in existing_labels:
|
|
646
|
+
suggestions.append({
|
|
647
|
+
'label': 'Estimated Cost',
|
|
648
|
+
'type': 'number',
|
|
649
|
+
'required': False,
|
|
650
|
+
'description': 'Estimated cost for this process',
|
|
651
|
+
'reasoning': 'Template involves financial considerations',
|
|
652
|
+
'confidence': 75,
|
|
653
|
+
'category': 'financial'
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
# Generic description field
|
|
657
|
+
if 'description' not in existing_labels and 'details' not in existing_labels:
|
|
658
|
+
suggestions.append({
|
|
659
|
+
'label': 'Additional Details',
|
|
660
|
+
'type': 'textarea',
|
|
661
|
+
'required': False,
|
|
662
|
+
'description': 'Any additional information or special requirements',
|
|
663
|
+
'reasoning': 'Provides flexibility for process-specific information',
|
|
664
|
+
'confidence': 60,
|
|
665
|
+
'category': 'general'
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
# Sort suggestions by confidence score
|
|
669
|
+
suggestions.sort(key=lambda x: x['confidence'], reverse=True)
|
|
670
|
+
|
|
671
|
+
return suggestions
|
|
672
|
+
|
|
673
|
+
except TallyfyError:
|
|
674
|
+
raise
|
|
675
|
+
except Exception as e:
|
|
676
|
+
self._handle_api_error(e, "suggest kickoff fields", org_id=org_id, template_id=template_id)
|
|
677
|
+
|
|
678
|
+
def suggest_automation_consolidation(self, org_id: str, template_id: str) -> List[Dict[str, Any]]:
|
|
679
|
+
"""
|
|
680
|
+
AI analysis with consolidation recommendations.
|
|
681
|
+
|
|
682
|
+
Args:
|
|
683
|
+
org_id: Organization ID
|
|
684
|
+
template_id: Template ID to analyze
|
|
685
|
+
|
|
686
|
+
Returns:
|
|
687
|
+
List of consolidation recommendations with detailed analysis
|
|
688
|
+
|
|
689
|
+
Raises:
|
|
690
|
+
TallyfyError: If the request fails
|
|
691
|
+
"""
|
|
692
|
+
self._validate_org_id(org_id)
|
|
693
|
+
self._validate_template_id(template_id)
|
|
694
|
+
|
|
695
|
+
try:
|
|
696
|
+
# Use the existing analyze_template_automations method
|
|
697
|
+
automation_analysis = self.analyze_template_automations(org_id, template_id)
|
|
698
|
+
|
|
699
|
+
recommendations = []
|
|
700
|
+
|
|
701
|
+
# Extract automation data from analysis
|
|
702
|
+
automations = automation_analysis.get('automations', [])
|
|
703
|
+
redundancies = automation_analysis.get('analysis', {}).get('redundant_rules', [])
|
|
704
|
+
conflicts = automation_analysis.get('analysis', {}).get('conflicting_rules', [])
|
|
705
|
+
|
|
706
|
+
# Critical issues first
|
|
707
|
+
if conflicts:
|
|
708
|
+
recommendations.append({
|
|
709
|
+
'type': 'critical',
|
|
710
|
+
'priority': 'high',
|
|
711
|
+
'title': 'Resolve Conflicting Automation Rules',
|
|
712
|
+
'description': f'Found {len(conflicts)} conflicting automation rules that may cause unpredictable behavior',
|
|
713
|
+
'affected_automations': [conflict['automation_ids'] for conflict in conflicts],
|
|
714
|
+
'action_required': 'Review and modify conflicting rules to ensure consistent behavior',
|
|
715
|
+
'impact': 'High - Can cause workflow failures or unexpected results',
|
|
716
|
+
'effort': 'Medium - Requires careful analysis of rule interactions',
|
|
717
|
+
'details': conflicts
|
|
718
|
+
})
|
|
719
|
+
|
|
720
|
+
# Redundancy consolidation
|
|
721
|
+
if redundancies:
|
|
722
|
+
recommendations.append({
|
|
723
|
+
'type': 'optimization',
|
|
724
|
+
'priority': 'medium',
|
|
725
|
+
'title': 'Consolidate Redundant Automation Rules',
|
|
726
|
+
'description': f'Found {len(redundancies)} sets of redundant rules that can be consolidated',
|
|
727
|
+
'affected_automations': [red['automation_ids'] for red in redundancies],
|
|
728
|
+
'action_required': 'Combine similar rules to reduce complexity and improve maintainability',
|
|
729
|
+
'impact': 'Medium - Improves template performance and reduces maintenance overhead',
|
|
730
|
+
'effort': 'Low - Simple consolidation of similar rules',
|
|
731
|
+
'details': redundancies
|
|
732
|
+
})
|
|
733
|
+
|
|
734
|
+
# Complex rule simplification
|
|
735
|
+
complex_rules = [auto for auto in automations if len(auto.get('conditions', [])) > 3]
|
|
736
|
+
if complex_rules:
|
|
737
|
+
recommendations.append({
|
|
738
|
+
'type': 'simplification',
|
|
739
|
+
'priority': 'low',
|
|
740
|
+
'title': 'Simplify Complex Automation Rules',
|
|
741
|
+
'description': f'Found {len(complex_rules)} automation rules with high complexity',
|
|
742
|
+
'affected_automations': [rule.get('id') for rule in complex_rules],
|
|
743
|
+
'action_required': 'Break down complex rules into simpler, more manageable components',
|
|
744
|
+
'impact': 'Low - Improves rule understanding and debugging',
|
|
745
|
+
'effort': 'Medium - Requires careful rule restructuring',
|
|
746
|
+
'details': [{
|
|
747
|
+
'automation_id': rule.get('id'),
|
|
748
|
+
'condition_count': len(rule.get('conditions', [])),
|
|
749
|
+
'action_count': len(rule.get('actions', []))
|
|
750
|
+
} for rule in complex_rules]
|
|
751
|
+
})
|
|
752
|
+
|
|
753
|
+
# Unused automation detection
|
|
754
|
+
all_step_ids = set()
|
|
755
|
+
for auto in automations:
|
|
756
|
+
for condition in auto.get('conditions', []):
|
|
757
|
+
if condition.get('step_id'):
|
|
758
|
+
all_step_ids.add(condition.get('step_id'))
|
|
759
|
+
for action in auto.get('actions', []):
|
|
760
|
+
if action.get('step_id'):
|
|
761
|
+
all_step_ids.add(action.get('step_id'))
|
|
762
|
+
|
|
763
|
+
# Get template steps to check for unused automations
|
|
764
|
+
template_endpoint = f"organizations/{org_id}/checklists/{template_id}"
|
|
765
|
+
template_params = {'with': 'steps'}
|
|
766
|
+
template_response = self.sdk._make_request('GET', template_endpoint, params=template_params)
|
|
767
|
+
template_data = self._extract_data(template_response)
|
|
768
|
+
|
|
769
|
+
if template_data:
|
|
770
|
+
actual_step_ids = {step.get('id') for step in template_data.get('steps', [])}
|
|
771
|
+
orphaned_step_refs = all_step_ids - actual_step_ids
|
|
772
|
+
|
|
773
|
+
if orphaned_step_refs:
|
|
774
|
+
recommendations.append({
|
|
775
|
+
'type': 'cleanup',
|
|
776
|
+
'priority': 'low',
|
|
777
|
+
'title': 'Remove References to Deleted Steps',
|
|
778
|
+
'description': f'Found automation rules referencing {len(orphaned_step_refs)} non-existent steps',
|
|
779
|
+
'affected_automations': [], # Would need deeper analysis to identify
|
|
780
|
+
'action_required': 'Remove or update automation rules that reference deleted steps',
|
|
781
|
+
'impact': 'Low - Prevents potential errors and improves template cleanliness',
|
|
782
|
+
'effort': 'Low - Simple cleanup of outdated references',
|
|
783
|
+
'details': {'orphaned_step_ids': list(orphaned_step_refs)}
|
|
784
|
+
})
|
|
785
|
+
|
|
786
|
+
# Performance optimization suggestions
|
|
787
|
+
if len(automations) > 10:
|
|
788
|
+
recommendations.append({
|
|
789
|
+
'type': 'performance',
|
|
790
|
+
'priority': 'medium',
|
|
791
|
+
'title': 'Optimize Automation Performance',
|
|
792
|
+
'description': f'Template has {len(automations)} automation rules - consider optimization',
|
|
793
|
+
'affected_automations': [auto.get('id') for auto in automations],
|
|
794
|
+
'action_required': 'Review automation necessity and consolidate where possible',
|
|
795
|
+
'impact': 'Medium - Improves template execution speed',
|
|
796
|
+
'effort': 'Medium - Requires comprehensive automation review',
|
|
797
|
+
'details': {
|
|
798
|
+
'total_automations': len(automations),
|
|
799
|
+
'recommendation': 'Consider if all automations are necessary or if some can be combined'
|
|
800
|
+
}
|
|
801
|
+
})
|
|
802
|
+
|
|
803
|
+
# If no specific issues found, provide general optimization advice
|
|
804
|
+
if not recommendations:
|
|
805
|
+
recommendations.append({
|
|
806
|
+
'type': 'maintenance',
|
|
807
|
+
'priority': 'low',
|
|
808
|
+
'title': 'Automation Health Check Passed',
|
|
809
|
+
'description': 'No critical automation issues detected',
|
|
810
|
+
'affected_automations': [],
|
|
811
|
+
'action_required': 'Continue monitoring automation performance',
|
|
812
|
+
'impact': 'Low - Template automations are functioning well',
|
|
813
|
+
'effort': 'None - No immediate action required',
|
|
814
|
+
'details': {
|
|
815
|
+
'total_automations': len(automations),
|
|
816
|
+
'status': 'healthy'
|
|
817
|
+
}
|
|
818
|
+
})
|
|
819
|
+
|
|
820
|
+
return recommendations
|
|
821
|
+
|
|
822
|
+
except TallyfyError:
|
|
823
|
+
raise
|
|
824
|
+
except Exception as e:
|
|
825
|
+
self._handle_api_error(e, "suggest automation consolidation", org_id=org_id, template_id=template_id)
|
|
826
|
+
|
|
827
|
+
def _calculate_field_similarity(self, field1_label: str, field2_label: str) -> float:
|
|
828
|
+
"""
|
|
829
|
+
Helper for field similarity calculation.
|
|
830
|
+
|
|
831
|
+
Args:
|
|
832
|
+
field1_label: First field label
|
|
833
|
+
field2_label: Second field label
|
|
834
|
+
|
|
835
|
+
Returns:
|
|
836
|
+
Float representing similarity score (0.0 to 1.0)
|
|
837
|
+
"""
|
|
838
|
+
# Simple word-based similarity calculation
|
|
839
|
+
words1 = set(field1_label.lower().split())
|
|
840
|
+
words2 = set(field2_label.lower().split())
|
|
841
|
+
|
|
842
|
+
if not words1 or not words2:
|
|
843
|
+
return 0.0
|
|
844
|
+
|
|
845
|
+
intersection = words1.intersection(words2)
|
|
846
|
+
union = words1.union(words2)
|
|
847
|
+
|
|
848
|
+
return len(intersection) / len(union)
|
|
849
|
+
|
|
850
|
+
def analyze_template_automations(self, org_id: str, template_id: str) -> Dict[str, Any]:
|
|
851
|
+
"""
|
|
852
|
+
Core automation analysis method used by multiple functions.
|
|
853
|
+
|
|
854
|
+
Args:
|
|
855
|
+
org_id: Organization ID
|
|
856
|
+
template_id: Template ID to analyze
|
|
857
|
+
|
|
858
|
+
Returns:
|
|
859
|
+
Dictionary containing comprehensive automation analysis
|
|
860
|
+
|
|
861
|
+
Raises:
|
|
862
|
+
TallyfyError: If the request fails
|
|
863
|
+
"""
|
|
864
|
+
self._validate_org_id(org_id)
|
|
865
|
+
self._validate_template_id(template_id)
|
|
866
|
+
|
|
867
|
+
try:
|
|
868
|
+
# Get template with automation data
|
|
869
|
+
template_endpoint = f"organizations/{org_id}/checklists/{template_id}"
|
|
870
|
+
template_params = {'with': 'steps,automated_actions,prerun'}
|
|
871
|
+
template_response = self.sdk._make_request('GET', template_endpoint, params=template_params)
|
|
872
|
+
|
|
873
|
+
template_data = self._extract_data(template_response)
|
|
874
|
+
if not template_data:
|
|
875
|
+
raise TallyfyError("Unable to retrieve template data for automation analysis")
|
|
876
|
+
|
|
877
|
+
automations = template_data.get('automated_actions', [])
|
|
878
|
+
steps = template_data.get('steps', [])
|
|
879
|
+
|
|
880
|
+
# Create step lookup
|
|
881
|
+
step_lookup = {step.get('id'): step.get('title', 'Unknown') for step in steps}
|
|
882
|
+
|
|
883
|
+
analysis = {
|
|
884
|
+
'redundant_rules': [],
|
|
885
|
+
'conflicting_rules': [],
|
|
886
|
+
'complex_rules': [],
|
|
887
|
+
'simple_rules': [],
|
|
888
|
+
'statistics': {
|
|
889
|
+
'total_automations': len(automations),
|
|
890
|
+
'avg_conditions_per_rule': 0,
|
|
891
|
+
'avg_actions_per_rule': 0
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
# Analyze each automation
|
|
896
|
+
total_conditions = 0
|
|
897
|
+
total_actions = 0
|
|
898
|
+
|
|
899
|
+
for i, automation in enumerate(automations):
|
|
900
|
+
conditions = automation.get('conditions', [])
|
|
901
|
+
actions = automation.get('actions', [])
|
|
902
|
+
|
|
903
|
+
total_conditions += len(conditions)
|
|
904
|
+
total_actions += len(actions)
|
|
905
|
+
|
|
906
|
+
# Classify by complexity
|
|
907
|
+
if len(conditions) > 3 or len(actions) > 2:
|
|
908
|
+
analysis['complex_rules'].append(automation.get('id'))
|
|
909
|
+
else:
|
|
910
|
+
analysis['simple_rules'].append(automation.get('id'))
|
|
911
|
+
|
|
912
|
+
# Check for redundancy with other automations
|
|
913
|
+
for j, other_automation in enumerate(automations[i+1:], i+1):
|
|
914
|
+
similarity = self._analyze_condition_similarity(
|
|
915
|
+
conditions, other_automation.get('conditions', [])
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
if similarity > 0.8: # High similarity threshold
|
|
919
|
+
analysis['redundant_rules'].append({
|
|
920
|
+
'automation_ids': [automation.get('id'), other_automation.get('id')],
|
|
921
|
+
'similarity_score': similarity,
|
|
922
|
+
'recommendation': 'Consider consolidating these similar rules'
|
|
923
|
+
})
|
|
924
|
+
|
|
925
|
+
# Calculate statistics
|
|
926
|
+
if automations:
|
|
927
|
+
analysis['statistics']['avg_conditions_per_rule'] = total_conditions / len(automations)
|
|
928
|
+
analysis['statistics']['avg_actions_per_rule'] = total_actions / len(automations)
|
|
929
|
+
|
|
930
|
+
return {
|
|
931
|
+
'template_id': template_id,
|
|
932
|
+
'automations': automations,
|
|
933
|
+
'analysis': analysis,
|
|
934
|
+
'step_lookup': step_lookup
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
except TallyfyError:
|
|
938
|
+
raise
|
|
939
|
+
except Exception as e:
|
|
940
|
+
self._handle_api_error(e, "analyze template automations", org_id=org_id, template_id=template_id)
|
|
941
|
+
|
|
942
|
+
def _analyze_condition_similarity(self, conditions1: List[Dict], conditions2: List[Dict]) -> float:
|
|
943
|
+
"""
|
|
944
|
+
Helper for analyzing similarity between condition sets.
|
|
945
|
+
|
|
946
|
+
Args:
|
|
947
|
+
conditions1: First set of conditions
|
|
948
|
+
conditions2: Second set of conditions
|
|
949
|
+
|
|
950
|
+
Returns:
|
|
951
|
+
Float representing similarity score (0.0 to 1.0)
|
|
952
|
+
"""
|
|
953
|
+
if not conditions1 or not conditions2:
|
|
954
|
+
return 0.0
|
|
955
|
+
|
|
956
|
+
# Convert conditions to comparable strings
|
|
957
|
+
set1 = set()
|
|
958
|
+
set2 = set()
|
|
959
|
+
|
|
960
|
+
for condition in conditions1:
|
|
961
|
+
condition_str = f"{condition.get('type', '')}:{condition.get('step_id', '')}:{condition.get('value', '')}"
|
|
962
|
+
set1.add(condition_str)
|
|
963
|
+
|
|
964
|
+
for condition in conditions2:
|
|
965
|
+
condition_str = f"{condition.get('type', '')}:{condition.get('step_id', '')}:{condition.get('value', '')}"
|
|
966
|
+
set2.add(condition_str)
|
|
967
|
+
|
|
968
|
+
if not set1 or not set2:
|
|
969
|
+
return 0.0
|
|
970
|
+
|
|
971
|
+
intersection = set1.intersection(set2)
|
|
972
|
+
union = set1.union(set2)
|
|
973
|
+
|
|
974
|
+
return len(intersection) / len(union)
|
|
975
|
+
|
|
976
|
+
def _summarize_conditions(self, conditions: List[Dict], step_lookup: Dict[str, str]) -> List[str]:
|
|
977
|
+
"""
|
|
978
|
+
Creates human-readable condition summaries.
|
|
979
|
+
|
|
980
|
+
Args:
|
|
981
|
+
conditions: List of condition dictionaries
|
|
982
|
+
step_lookup: Mapping of step IDs to step titles
|
|
983
|
+
|
|
984
|
+
Returns:
|
|
985
|
+
List of human-readable condition descriptions
|
|
986
|
+
"""
|
|
987
|
+
summaries = []
|
|
988
|
+
|
|
989
|
+
for condition in conditions:
|
|
990
|
+
condition_type = condition.get('type', '')
|
|
991
|
+
step_id = condition.get('step_id', '')
|
|
992
|
+
value = condition.get('value', '')
|
|
993
|
+
|
|
994
|
+
step_name = step_lookup.get(step_id, f"Step {step_id}")
|
|
995
|
+
|
|
996
|
+
if condition_type == 'step_completed':
|
|
997
|
+
summaries.append(f"'{step_name}' is completed")
|
|
998
|
+
elif condition_type == 'step_started':
|
|
999
|
+
summaries.append(f"'{step_name}' is started")
|
|
1000
|
+
elif condition_type == 'field_value':
|
|
1001
|
+
field_label = condition.get('field_label', 'Unknown Field')
|
|
1002
|
+
summaries.append(f"Field '{field_label}' equals '{value}'")
|
|
1003
|
+
else:
|
|
1004
|
+
summaries.append(f"{condition_type} condition")
|
|
1005
|
+
|
|
1006
|
+
return summaries
|
|
1007
|
+
|
|
1008
|
+
def _summarize_condition(self, condition: Dict, step_lookup: Dict[str, str]) -> str:
|
|
1009
|
+
"""
|
|
1010
|
+
Creates a human-readable summary of a single condition.
|
|
1011
|
+
|
|
1012
|
+
Args:
|
|
1013
|
+
condition: Condition dictionary
|
|
1014
|
+
step_lookup: Mapping of step IDs to step titles
|
|
1015
|
+
|
|
1016
|
+
Returns:
|
|
1017
|
+
Human-readable condition description
|
|
1018
|
+
"""
|
|
1019
|
+
condition_type = condition.get('type', '')
|
|
1020
|
+
step_id = condition.get('step_id', '')
|
|
1021
|
+
value = condition.get('value', '')
|
|
1022
|
+
|
|
1023
|
+
step_name = step_lookup.get(step_id, f"Step {step_id}")
|
|
1024
|
+
|
|
1025
|
+
if condition_type == 'step_completed':
|
|
1026
|
+
return f"'{step_name}' is completed"
|
|
1027
|
+
elif condition_type == 'step_started':
|
|
1028
|
+
return f"'{step_name}' is started"
|
|
1029
|
+
elif condition_type == 'field_value':
|
|
1030
|
+
field_label = condition.get('field_label', 'Unknown Field')
|
|
1031
|
+
return f"Field '{field_label}' equals '{value}'"
|
|
1032
|
+
else:
|
|
1033
|
+
return f"{condition_type} condition"
|
|
1034
|
+
|
|
1035
|
+
def _generate_visibility_recommendations(self, visibility_rules: Dict, complexity_level: str) -> List[str]:
|
|
1036
|
+
"""
|
|
1037
|
+
Generates visibility-specific recommendations.
|
|
1038
|
+
|
|
1039
|
+
Args:
|
|
1040
|
+
visibility_rules: Dictionary containing visibility rules
|
|
1041
|
+
complexity_level: String indicating complexity level
|
|
1042
|
+
|
|
1043
|
+
Returns:
|
|
1044
|
+
List of recommendation strings
|
|
1045
|
+
"""
|
|
1046
|
+
recommendations = []
|
|
1047
|
+
|
|
1048
|
+
show_rules = visibility_rules.get('show_rules', [])
|
|
1049
|
+
hide_rules = visibility_rules.get('hide_rules', [])
|
|
1050
|
+
|
|
1051
|
+
if complexity_level == "Complex":
|
|
1052
|
+
recommendations.append("Consider simplifying visibility conditions to improve workflow clarity")
|
|
1053
|
+
|
|
1054
|
+
if show_rules and hide_rules:
|
|
1055
|
+
recommendations.append("Review show/hide rule interactions to prevent conflicts")
|
|
1056
|
+
|
|
1057
|
+
if len(show_rules) > 2:
|
|
1058
|
+
recommendations.append("Multiple show conditions may create user confusion - consider consolidation")
|
|
1059
|
+
|
|
1060
|
+
if not show_rules and not hide_rules:
|
|
1061
|
+
recommendations.append("Step has simple visibility - no optimization needed")
|
|
1062
|
+
|
|
1063
|
+
return recommendations
|
|
1064
|
+
|
|
1065
|
+
def _generate_dependency_recommendations(self, dependencies: Dict, complexity_level: str) -> List[str]:
|
|
1066
|
+
"""
|
|
1067
|
+
Generates dependency-specific recommendations.
|
|
1068
|
+
|
|
1069
|
+
Args:
|
|
1070
|
+
dependencies: Dictionary containing dependency information
|
|
1071
|
+
complexity_level: String indicating complexity level
|
|
1072
|
+
|
|
1073
|
+
Returns:
|
|
1074
|
+
List of recommendation strings
|
|
1075
|
+
"""
|
|
1076
|
+
recommendations = []
|
|
1077
|
+
|
|
1078
|
+
incoming_count = len(dependencies.get('incoming', []))
|
|
1079
|
+
outgoing_count = len(dependencies.get('outgoing', []))
|
|
1080
|
+
|
|
1081
|
+
if complexity_level == "High":
|
|
1082
|
+
recommendations.append("High dependency complexity - consider breaking step into smaller components")
|
|
1083
|
+
|
|
1084
|
+
if incoming_count > 3:
|
|
1085
|
+
recommendations.append("Step has many dependencies - ensure all are necessary")
|
|
1086
|
+
|
|
1087
|
+
if outgoing_count > 3:
|
|
1088
|
+
recommendations.append("Step affects many other steps - verify all impacts are intentional")
|
|
1089
|
+
|
|
1090
|
+
if not incoming_count and not outgoing_count:
|
|
1091
|
+
recommendations.append("Step is independent - good for parallel execution")
|
|
1092
|
+
|
|
1093
|
+
return recommendations
|