tallyfy 1.0.3__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.

Files changed (34) hide show
  1. tallyfy/__init__.py +8 -4
  2. tallyfy/core.py +8 -8
  3. tallyfy/form_fields_management/__init__.py +70 -0
  4. tallyfy/form_fields_management/base.py +109 -0
  5. tallyfy/form_fields_management/crud_operations.py +234 -0
  6. tallyfy/form_fields_management/options_management.py +222 -0
  7. tallyfy/form_fields_management/suggestions.py +411 -0
  8. tallyfy/task_management/__init__.py +81 -0
  9. tallyfy/task_management/base.py +125 -0
  10. tallyfy/task_management/creation.py +221 -0
  11. tallyfy/task_management/retrieval.py +211 -0
  12. tallyfy/task_management/search.py +196 -0
  13. tallyfy/template_management/__init__.py +85 -0
  14. tallyfy/template_management/analysis.py +1093 -0
  15. tallyfy/template_management/automation.py +469 -0
  16. tallyfy/template_management/base.py +56 -0
  17. tallyfy/template_management/basic_operations.py +477 -0
  18. tallyfy/template_management/health_assessment.py +763 -0
  19. tallyfy/user_management/__init__.py +69 -0
  20. tallyfy/user_management/base.py +146 -0
  21. tallyfy/user_management/invitation.py +286 -0
  22. tallyfy/user_management/retrieval.py +339 -0
  23. {tallyfy-1.0.3.dist-info → tallyfy-1.0.5.dist-info}/METADATA +120 -56
  24. tallyfy-1.0.5.dist-info/RECORD +28 -0
  25. tallyfy/BUILD.md +0 -5
  26. tallyfy/README.md +0 -634
  27. tallyfy/form_fields_management.py +0 -582
  28. tallyfy/task_management.py +0 -356
  29. tallyfy/template_management.py +0 -2607
  30. tallyfy/user_management.py +0 -235
  31. tallyfy-1.0.3.dist-info/RECORD +0 -14
  32. {tallyfy-1.0.3.dist-info → tallyfy-1.0.5.dist-info}/WHEEL +0 -0
  33. {tallyfy-1.0.3.dist-info → tallyfy-1.0.5.dist-info}/licenses/LICENSE +0 -0
  34. {tallyfy-1.0.3.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