USER-GNEXUSES commited on
Commit
1fbcb0c
·
verified ·
1 Parent(s): e2b3f43

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +112 -1158
app.py CHANGED
@@ -1,1170 +1,124 @@
1
- # process_discovery_engine.py
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- import numpy as np
4
- import pandas as pd
5
- from typing import Dict, List, Tuple, Optional
6
- from sklearn.feature_extraction.text import TfidfVectorizer
7
- from sklearn.metrics.pairwise import cosine_similarity
8
  import spacy
9
- import json
10
- import re
11
- import networkx as nx
12
- from sklearn.cluster import DBSCAN
13
 
14
- class ProcessDiscoveryEngine:
15
- """
16
- Discovers and analyzes business processes from various data sources
17
- including logs, documents, and recorded user activities.
18
- """
19
-
20
- def __init__(self, config: Dict):
21
- """
22
- Initialize the process discovery engine.
23
-
24
- Args:
25
- config: Configuration dictionary with parameters
26
- """
27
- self.min_frequency = config.get('min_frequency', 0.05)
28
- self.time_threshold = config.get('time_threshold', 60) # seconds
29
- self.similarity_threshold = config.get('similarity_threshold', 0.75)
30
- self.process_graph = nx.DiGraph()
31
-
32
- def ingest_log_data(self, log_data: pd.DataFrame) -> bool:
33
- """
34
- Ingest process log data from system logs.
35
-
36
- Args:
37
- log_data: DataFrame containing log entries with timestamp, user, action columns
38
-
39
- Returns:
40
- bool: Success status
41
- """
42
- if 'timestamp' not in log_data.columns or 'action' not in log_data.columns:
43
- return False
44
-
45
- # Sort by timestamp
46
- sorted_logs = log_data.sort_values('timestamp')
47
-
48
- # Group by case_id if available
49
- if 'case_id' in sorted_logs.columns:
50
- case_groups = sorted_logs.groupby('case_id')
51
- for case_id, case_data in case_groups:
52
- self._process_sequence(case_data['action'].tolist(),
53
- source=f"log:{case_id}")
54
- else:
55
- # Try to identify sessions based on time gaps
56
- self._segment_and_process_logs(sorted_logs)
57
-
58
- return True
59
-
60
- def ingest_screen_recordings(self, recording_analysis: List[Dict]) -> bool:
61
- """
62
- Ingest analyzed screen recording data.
63
-
64
- Args:
65
- recording_analysis: List of dictionaries containing screen activities
66
-
67
- Returns:
68
- bool: Success status
69
- """
70
- for session in recording_analysis:
71
- if 'actions' in session and isinstance(session['actions'], list):
72
- action_sequence = [a['activity'] for a in session['actions']
73
- if 'activity' in a]
74
- self._process_sequence(action_sequence,
75
- source=f"recording:{session.get('id', 'unknown')}")
76
-
77
- return True
78
-
79
- def _segment_and_process_logs(self, logs: pd.DataFrame) -> None:
80
- """
81
- Segment logs into probable process instances based on time gaps.
82
-
83
- Args:
84
- logs: DataFrame of logs sorted by timestamp
85
- """
86
- logs['timestamp'] = pd.to_datetime(logs['timestamp'])
87
- logs['time_diff'] = logs['timestamp'].diff().dt.total_seconds()
88
-
89
- # Mark new sequences where time difference exceeds threshold
90
- new_sequence = logs['time_diff'] > self.time_threshold
91
- logs['sequence_id'] = new_sequence.cumsum()
92
-
93
- # Process each sequence
94
- for seq_id, sequence in logs.groupby('sequence_id'):
95
- self._process_sequence(sequence['action'].tolist(),
96
- source=f"timegap:{seq_id}")
97
-
98
- def _process_sequence(self, actions: List[str], source: str) -> None:
99
- """
100
- Process a sequence of actions into the process graph.
101
-
102
- Args:
103
- actions: List of action names in sequence
104
- source: Data source identifier
105
- """
106
- for i in range(len(actions) - 1):
107
- current = actions[i]
108
- next_action = actions[i+1]
109
-
110
- # Add nodes if they don't exist
111
- if current not in self.process_graph:
112
- self.process_graph.add_node(current, count=0, sources=set())
113
- if next_action not in self.process_graph:
114
- self.process_graph.add_node(next_action, count=0, sources=set())
115
-
116
- # Update node data
117
- self.process_graph.nodes[current]['count'] += 1
118
- self.process_graph.nodes[current]['sources'].add(source)
119
-
120
- # Add or update edge
121
- if self.process_graph.has_edge(current, next_action):
122
- self.process_graph[current][next_action]['weight'] += 1
123
- self.process_graph[current][next_action]['sources'].add(source)
124
- else:
125
- self.process_graph.add_edge(current, next_action,
126
- weight=1, sources={source})
127
-
128
- def discover_main_process_paths(self) -> List[Dict]:
129
- """
130
- Discover the main process paths from the constructed graph.
131
-
132
- Returns:
133
- List of dictionaries describing main process paths
134
- """
135
- # Filter edges by frequency
136
- total_transitions = sum(data['weight'] for _, _, data in self.process_graph.edges(data=True))
137
-
138
- if total_transitions == 0:
139
- return []
140
-
141
- min_edge_weight = total_transitions * self.min_frequency
142
- significant_edges = [(u, v) for u, v, d in self.process_graph.edges(data=True)
143
- if d['weight'] > min_edge_weight]
144
-
145
- # Create subgraph with only significant edges
146
- significant_graph = self.process_graph.edge_subgraph(significant_edges).copy()
147
-
148
- # Find all simple paths from potential start nodes to end nodes
149
- start_nodes = [n for n in significant_graph.nodes()
150
- if significant_graph.in_degree(n) == 0 or
151
- significant_graph.in_degree(n) < significant_graph.out_degree(n)]
152
-
153
- end_nodes = [n for n in significant_graph.nodes()
154
- if significant_graph.out_degree(n) == 0 or
155
- significant_graph.out_degree(n) < significant_graph.in_degree(n)]
156
-
157
- # If no clear start/end, use nodes with highest centrality
158
- if not start_nodes:
159
- centrality = nx.degree_centrality(significant_graph)
160
- start_nodes = [max(centrality, key=centrality.get)]
161
-
162
- if not end_nodes:
163
- centrality = nx.degree_centrality(significant_graph)
164
- end_nodes = [max(centrality, key=centrality.get)]
165
-
166
- # Find all paths between start and end nodes
167
- all_paths = []
168
- for start in start_nodes:
169
- for end in end_nodes:
170
- try:
171
- paths = list(nx.all_simple_paths(significant_graph, start, end))
172
- all_paths.extend(paths)
173
- except nx.NetworkXNoPath:
174
- continue
175
-
176
- # Calculate path frequency and return top paths
177
- path_data = []
178
- for path in all_paths:
179
- # Calculate path strength as minimum edge weight along path
180
- edge_weights = [significant_graph[path[i]][path[i+1]]['weight']
181
- for i in range(len(path)-1)]
182
- path_strength = min(edge_weights) if edge_weights else 0
183
-
184
- path_data.append({
185
- 'path': path,
186
- 'strength': path_strength,
187
- 'length': len(path),
188
- 'avg_edge_weight': sum(edge_weights) / len(edge_weights) if edge_weights else 0
189
- })
190
-
191
- # Sort by path strength descending
192
- path_data.sort(key=lambda x: x['strength'], reverse=True)
193
-
194
- return path_data
195
-
196
- def identify_process_variants(self) -> List[Dict]:
197
- """
198
- Identify variants of the same basic process.
199
-
200
- Returns:
201
- List of process variant clusters
202
- """
203
- if len(self.process_graph) < 2:
204
- return []
205
-
206
- # Extract features for clustering
207
- paths = self.discover_main_process_paths()
208
- if not paths:
209
- return []
210
-
211
- # Create feature vectors from paths
212
- all_activities = sorted(list(self.process_graph.nodes()))
213
- activity_indices = {act: i for i, act in enumerate(all_activities)}
214
-
215
- # Create feature vectors (activity presence and position)
216
- feature_vectors = []
217
- for path_data in paths:
218
- path = path_data['path']
219
- vector = np.zeros(len(all_activities) * 2)
220
-
221
- # Mark presence and relative position of activities
222
- for pos, activity in enumerate(path):
223
- idx = activity_indices[activity]
224
- vector[idx] = 1 # presence
225
- vector[idx + len(all_activities)] = pos / len(path) # relative position
226
-
227
- feature_vectors.append(vector)
228
-
229
- # Cluster paths using DBSCAN
230
- if len(feature_vectors) < 2:
231
- return [{'variant_id': 0, 'paths': paths}]
232
-
233
- clustering = DBSCAN(eps=0.3, min_samples=1).fit(feature_vectors)
234
- labels = clustering.labels_
235
-
236
- # Group paths by cluster
237
- variants = {}
238
- for i, label in enumerate(labels):
239
- label_str = str(label)
240
- if label_str not in variants:
241
- variants[label_str] = []
242
- variants[label_str].append(paths[i])
243
-
244
- # Format result
245
- result = [
246
- {'variant_id': variant_id, 'paths': variant_paths}
247
- for variant_id, variant_paths in variants.items()
248
- ]
249
-
250
- return result
251
-
252
- def get_process_stats(self) -> Dict:
253
- """
254
- Get statistics about the discovered process.
255
-
256
- Returns:
257
- Dictionary with process statistics
258
- """
259
- if not self.process_graph:
260
- return {"error": "No process data available"}
261
-
262
- stats = {
263
- "num_activities": len(self.process_graph.nodes()),
264
- "num_transitions": len(self.process_graph.edges()),
265
- "most_frequent_activities": [],
266
- "most_frequent_transitions": [],
267
- "process_complexity": 0,
268
- "data_sources": set()
269
- }
270
-
271
- # Most frequent activities
272
- activities = [(node, data['count'])
273
- for node, data in self.process_graph.nodes(data=True)]
274
- activities.sort(key=lambda x: x[1], reverse=True)
275
- stats["most_frequent_activities"] = activities[:10]
276
-
277
- # Most frequent transitions
278
- transitions = [(u, v, data['weight'])
279
- for u, v, data in self.process_graph.edges(data=True)]
280
- transitions.sort(key=lambda x: x[2], reverse=True)
281
- stats["most_frequent_transitions"] = transitions[:10]
282
-
283
- # Process complexity (using Control-Flow Complexity metric)
284
- stats["process_complexity"] = sum(self.process_graph.out_degree(n) for n in self.process_graph.nodes())
285
-
286
- # Data sources
287
- for _, data in self.process_graph.nodes(data=True):
288
- if 'sources' in data:
289
- stats["data_sources"].update(data['sources'])
290
-
291
- stats["data_sources"] = list(stats["data_sources"])
292
-
293
- return stats
294
 
295
- def export_process_model(self, format_type: str = 'bpmn') -> Dict:
296
- """
297
- Export the discovered process in the specified format.
298
-
299
- Args:
300
- format_type: Output format ('bpmn', 'petri_net', or 'json')
301
-
302
- Returns:
303
- Dictionary with export data and metadata
304
- """
305
- if format_type == 'json':
306
- nodes = [{"id": n, "count": data.get('count', 0)}
307
- for n, data in self.process_graph.nodes(data=True)]
308
-
309
- edges = [{"source": u, "target": v, "weight": data.get('weight', 0)}
310
- for u, v, data in self.process_graph.edges(data=True)]
311
-
312
- return {
313
- "format": "json",
314
- "process_model": {
315
- "nodes": nodes,
316
- "edges": edges
317
- }
318
- }
319
-
320
- elif format_type == 'bpmn':
321
- # Basic BPMN conversion (simplified)
322
- # In a real implementation, this would generate actual BPMN XML
323
- return {
324
- "format": "bpmn",
325
- "process_model": {
326
- "process_id": "discovered_process",
327
- "activities": list(self.process_graph.nodes()),
328
- "flows": [(u, v) for u, v in self.process_graph.edges()],
329
- "gateways": self._identify_potential_gateways()
330
- }
331
- }
332
-
333
- elif format_type == 'petri_net':
334
- # Basic Petri net conversion (simplified)
335
- return {
336
- "format": "petri_net",
337
- "process_model": {
338
- "places": self._generate_petri_net_places(),
339
- "transitions": list(self.process_graph.nodes()),
340
- "arcs": self._generate_petri_net_arcs()
341
- }
342
- }
343
-
344
- else:
345
- return {"error": f"Unsupported export format: {format_type}"}
346
-
347
- def _identify_potential_gateways(self) -> List[Dict]:
348
- """
349
- Identify potential gateways in the process based on branching.
350
-
351
- Returns:
352
- List of potential gateway nodes
353
- """
354
- gateways = []
355
-
356
- for node in self.process_graph.nodes():
357
- in_degree = self.process_graph.in_degree(node)
358
- out_degree = self.process_graph.out_degree(node)
359
-
360
- # Potential XOR-split (one input, multiple outputs)
361
- if in_degree == 1 and out_degree > 1:
362
- gateways.append({
363
- "id": f"xor_split_{node}",
364
- "type": "exclusive_gateway",
365
- "direction": "split",
366
- "attached_to": node
367
- })
368
-
369
- # Potential XOR-join (multiple inputs, one output)
370
- elif in_degree > 1 and out_degree == 1:
371
- gateways.append({
372
- "id": f"xor_join_{node}",
373
- "type": "exclusive_gateway",
374
- "direction": "join",
375
- "attached_to": node
376
- })
377
-
378
- # Potential AND-split/join or complex gateway
379
- elif in_degree > 1 and out_degree > 1:
380
- gateways.append({
381
- "id": f"complex_{node}",
382
- "type": "complex_gateway",
383
- "direction": "mixed",
384
- "attached_to": node
385
- })
386
-
387
- return gateways
388
-
389
- def _generate_petri_net_places(self) -> List[str]:
390
- """
391
- Generate places for a Petri net representation.
392
-
393
- Returns:
394
- List of place IDs
395
- """
396
- places = []
397
-
398
- # Generate places between each pair of activities
399
- for u, v in self.process_graph.edges():
400
- places.append(f"p_{u}_{v}")
401
-
402
- # Add start and end places
403
- start_nodes = [n for n in self.process_graph.nodes()
404
- if self.process_graph.in_degree(n) == 0]
405
- for node in start_nodes:
406
- places.append(f"p_start_{node}")
407
-
408
- end_nodes = [n for n in self.process_graph.nodes()
409
- if self.process_graph.out_degree(n) == 0]
410
- for node in end_nodes:
411
- places.append(f"p_{node}_end")
412
-
413
- return places
414
-
415
- def _generate_petri_net_arcs(self) -> List[Tuple[str, str]]:
416
- """
417
- Generate arcs for a Petri net representation.
418
-
419
- Returns:
420
- List of (source, target) tuples representing arcs
421
- """
422
- arcs = []
423
-
424
- # Connect transitions through places
425
- for u, v in self.process_graph.edges():
426
- place = f"p_{u}_{v}"
427
- arcs.append((u, place))
428
- arcs.append((place, v))
429
-
430
- # Connect start places to initial transitions
431
- start_nodes = [n for n in self.process_graph.nodes()
432
- if self.process_graph.in_degree(n) == 0]
433
- for node in start_nodes:
434
- arcs.append((f"p_start_{node}", node))
435
-
436
- # Connect final transitions to end places
437
- end_nodes = [n for n in self.process_graph.nodes()
438
- if self.process_graph.out_degree(n) == 0]
439
- for node in end_nodes:
440
- arcs.append((node, f"p_{node}_end"))
441
-
442
- return arcs
443
 
444
- # requirements_analysis_module.py
445
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
446
 
447
- class RequirementsAnalysisModule:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
  """
449
- Analyzes business requirements and connects them to processes.
450
- Extracts structured data from natural language requirements.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
451
  """
452
-
453
- def __init__(self, config: Dict = None):
454
- """
455
- Initialize the requirements analysis module.
456
-
457
- Args:
458
- config: Configuration dictionary
459
- """
460
- self.config = config or {}
461
-
462
- # Load NLP model
463
- try:
464
- self.nlp = spacy.load("en_core_web_md")
465
- except:
466
- # Fallback to small model if medium not available
467
- self.nlp = spacy.load("en_core_web_sm")
468
-
469
- # Initialize requirements storage
470
- self.requirements = []
471
-
472
- # Initialize taxonomy and patterns
473
- self._load_taxonomies()
474
- self._compile_requirement_patterns()
475
-
476
- def _load_taxonomies(self) -> None:
477
- """Load or initialize the business process taxonomy."""
478
- # In production, this would load from a file or database
479
- self.process_taxonomy = {
480
- "financial": [
481
- "invoice processing", "accounts payable", "accounts receivable",
482
- "payment processing", "financial reporting", "expense management"
483
- ],
484
- "hr": [
485
- "onboarding", "offboarding", "payroll", "recruitment",
486
- "employee management", "benefits administration", "time tracking"
487
- ],
488
- "customer_service": [
489
- "ticket management", "customer support", "inquiry handling",
490
- "complaint resolution", "feedback processing"
491
- ],
492
- "operations": [
493
- "inventory management", "supply chain", "logistics",
494
- "order processing", "shipping", "receiving", "quality control"
495
- ],
496
- "sales": [
497
- "lead management", "opportunity tracking", "quote generation",
498
- "contract management", "sales reporting", "commission calculation"
499
- ],
500
- "it": [
501
- "access management", "incident management", "change management",
502
- "service request", "problem management", "release management"
503
- ]
504
- }
505
-
506
- # Complexity indicators for requirements
507
- self.complexity_indicators = {
508
- "high": [
509
- "complex", "multiple systems", "integration", "decision tree",
510
- "exception handling", "compliance", "regulatory", "manual review",
511
- "approval workflow", "conditional logic", "business rules"
512
- ],
513
- "medium": [
514
- "validation", "verification", "notification", "alert",
515
- "scheduled", "reporting", "dashboard", "data transformation"
516
- ],
517
- "low": [
518
- "simple", "straightforward", "data entry", "form filling",
519
- "standard", "single system", "fixed path", "static rules"
520
- ]
521
- }
522
-
523
- def _compile_requirement_patterns(self) -> None:
524
- """Compile regex patterns for requirement extraction."""
525
- # Action patterns
526
- self.action_patterns = [
527
- r"(?:need|should|must|will|shall) (?:to )?([a-z]+)",
528
- r"responsible for ([a-z]+ing)",
529
- r"capability to ([a-z]+)",
530
- r"ability to ([a-z]+)"
531
- ]
532
-
533
- # System patterns
534
- self.system_patterns = [
535
- r"(?:in|from|to|using|within) (?:the )?([A-Za-z0-9]+)(?: system| application| platform| software| tool)?",
536
- r"([A-Za-z0-9]+)(?: system| application| platform| software| tool)",
537
- r"([A-Za-z0-9]+) (?:database|interface|API|server)"
538
- ]
539
-
540
- # Frequency patterns
541
- self.frequency_patterns = [
542
- r"(daily|weekly|monthly|quarterly|yearly|annually)",
543
- r"every ([0-9]+) (day|week|month|quarter|year)s?",
544
- r"([0-9]+) times per (day|week|month|year)"
545
- ]
546
-
547
- # Compile all patterns
548
- self.action_regex = [re.compile(pattern) for pattern in self.action_patterns]
549
- self.system_regex = [re.compile(pattern) for pattern in self.system_patterns]
550
- self.frequency_regex = [re.compile(pattern) for pattern in self.frequency_patterns]
551
-
552
- def analyze_text_requirement(self, requirement_text: str, source: str = None) -> Dict:
553
- """
554
- Analyze a natural language requirement and extract structured information.
555
-
556
- Args:
557
- requirement_text: The text of the requirement
558
- source: Source of the requirement
559
-
560
- Returns:
561
- Dictionary with extracted requirement information
562
- """
563
- # Parse with spaCy
564
- doc = self.nlp(requirement_text)
565
-
566
- # Basic requirement object
567
- requirement = {
568
- "id": f"REQ-{len(self.requirements) + 1}",
569
- "text": requirement_text,
570
- "source": source,
571
- "extracted": {
572
- "actions": self._extract_actions(doc, requirement_text),
573
- "systems": self._extract_systems(doc, requirement_text),
574
- "frequency": self._extract_frequency(requirement_text),
575
- "business_domain": self._classify_business_domain(doc),
576
- "complexity": self._assess_complexity(doc, requirement_text),
577
- "data_elements": self._extract_data_elements(doc)
578
- },
579
- "automation_potential": None # Will be filled later
580
- }
581
-
582
- # Store the requirement
583
- self.requirements.append(requirement)
584
- return requirement
585
-
586
- def _extract_actions(self, doc, text: str) -> List[str]:
587
- """
588
- Extract action verbs from requirement text.
589
-
590
- Args:
591
- doc: spaCy processed document
592
- text: Original text
593
-
594
- Returns:
595
- List of action verbs
596
- """
597
- # Method 1: Use spaCy to find verbs
598
- verbs = [token.lemma_ for token in doc if token.pos_ == "VERB"]
599
-
600
- # Method 2: Use regex patterns
601
- pattern_matches = []
602
- for pattern in self.action_regex:
603
- matches = pattern.findall(text.lower())
604
- pattern_matches.extend(matches)
605
-
606
- # Combine and deduplicate
607
- all_actions = list(set(verbs + pattern_matches))
608
-
609
- # Filter out common non-action verbs
610
- stopwords = ["be", "is", "are", "was", "were", "have", "has", "had"]
611
- filtered_actions = [v for v in all_actions if v not in stopwords and len(v) > 2]
612
-
613
- return filtered_actions
614
-
615
- def _extract_systems(self, doc, text: str) -> List[str]:
616
- """
617
- Extract system names from requirement text.
618
-
619
- Args:
620
- doc: spaCy processed document
621
- text: Original text
622
-
623
- Returns:
624
- List of system names
625
- """
626
- # Method 1: Named Entity Recognition for PRODUCT entities
627
- ner_systems = [ent.text for ent in doc.ents
628
- if ent.label_ in ["PRODUCT", "ORG", "GPE"]]
629
-
630
- # Method 2: Pattern matching
631
- pattern_systems = []
632
- for pattern in self.system_regex:
633
- matches = pattern.findall(text)
634
- pattern_systems.extend(matches)
635
-
636
- # Combine results
637
- all_systems = list(set(ner_systems + pattern_systems))
638
-
639
- # Filter out common false positives
640
- stopwords = ["system", "process", "application", "data", "information", "this", "the"]
641
- filtered_systems = [s for s in all_systems if s.lower() not in stopwords and len(s) > 2]
642
-
643
- return filtered_systems
644
-
645
- def _extract_frequency(self, text: str) -> Optional[str]:
646
- """
647
- Extract frequency information from requirement text.
648
-
649
- Args:
650
- text: Requirement text
651
-
652
- Returns:
653
- Extracted frequency or None
654
- """
655
- text_lower = text.lower()
656
-
657
- # Check all frequency patterns
658
- for pattern in self.frequency_regex:
659
- match = pattern.search(text_lower)
660
- if match:
661
- return match.group(0)
662
-
663
- # Check for specific frequency words
664
- frequency_words = ["daily", "weekly", "monthly", "quarterly", "annually", "yearly"]
665
- for word in frequency_words:
666
- if word in text_lower:
667
- return word
668
-
669
- return None
670
-
671
- def _classify_business_domain(self, doc) -> List[Tuple[str, float]]:
672
- """
673
- Classify the business domain of the requirement.
674
-
675
- Args:
676
- doc: spaCy processed document
677
-
678
- Returns:
679
- List of (domain, confidence) tuples
680
- """
681
- text = doc.text.lower()
682
- domain_scores = {}
683
-
684
- # Calculate score for each domain based on keyword matches
685
- for domain, keywords in self.process_taxonomy.items():
686
- domain_score = 0
687
- for keyword in keywords:
688
- if keyword in text:
689
- domain_score += 1
690
-
691
- if domain_score > 0:
692
- # Normalize by number of keywords
693
- domain_scores[domain] = domain_score / len(keywords)
694
-
695
- # If no direct matches, use semantic similarity
696
- if not domain_scores:
697
- for domain, keywords in self.process_taxonomy.items():
698
- # Calculate average similarity between doc and each keyword
699
- similarities = [doc.similarity(self.nlp(keyword)) for keyword in keywords]
700
- avg_similarity = sum(similarities) / len(similarities) if similarities else 0
701
-
702
- if avg_similarity > 0.5: # Threshold for relevance
703
- domain_scores[domain] = avg_similarity
704
-
705
- # Sort by score and return
706
- sorted_domains = sorted(domain_scores.items(), key=lambda x: x[1], reverse=True)
707
- return sorted_domains
708
-
709
- def _assess_complexity(self, doc, text: str) -> str:
710
- """
711
- Assess the complexity of the requirement.
712
-
713
- Args:
714
- doc: spaCy processed document
715
- text: Original text
716
-
717
- Returns:
718
- Complexity level ("high", "medium", or "low")
719
- """
720
- text_lower = text.lower()
721
-
722
- # Count indicators for each complexity level
723
- scores = {level: 0 for level in self.complexity_indicators.keys()}
724
-
725
- for level, indicators in self.complexity_indicators.items():
726
- for indicator in indicators:
727
- if indicator in text_lower:
728
- scores[level] += 1
729
-
730
- # Check sentence structure complexity
731
- sentence_count = len(list(doc.sents))
732
- avg_tokens_per_sentence = len(doc) / sentence_count if sentence_count > 0 else 0
733
-
734
- # Adjust scores based on structural complexity
735
- if avg_tokens_per_sentence > 25:
736
- scores["high"] += 1
737
- elif avg_tokens_per_sentence > 15:
738
- scores["medium"] += 1
739
-
740
- # Check for conditional statements (if/then)
741
- if "if" in text_lower and ("then" in text_lower or "else" in text_lower):
742
- scores["high"] += 1
743
-
744
- # Determine final complexity
745
- if scores["high"] > 0:
746
- return "high"
747
- elif scores["medium"] > 0:
748
- return "medium"
749
- else:
750
- return "low"
751
-
752
- def _extract_data_elements(self, doc) -> List[str]:
753
- """
754
- Extract data elements from the requirement text.
755
-
756
- Args:
757
- doc: spaCy processed document
758
-
759
- Returns:
760
- List of data elements
761
- """
762
- # Find noun chunks that could be data elements
763
- data_elements = []
764
-
765
- for chunk in doc.noun_chunks:
766
- # Check if this looks like a data field
767
- if (any(token.pos_ == "NOUN" for token in chunk) and
768
- len(chunk) <= 4 and # Not too long
769
- not any(token.is_stop for token in chunk)): # Not all stopwords
770
- data_elements.append(chunk.text)
771
-
772
- # Look for specific data patterns
773
- data_patterns = [
774
- (r"\b[A-Z][a-z]+ ID\b", "ID field"),
775
- (r"\b[A-Z][a-z]+ Number\b", "Number field"),
776
- (r"\b[A-Z][a-z]+ Code\b", "Code field"),
777
- (r"\b[A-Z][a-z]+ Date\b", "Date field"),
778
- (r"\bstatus\b", "Status field")
779
- ]
780
-
781
- for pattern, field_type in data_patterns:
782
- if re.search(pattern, doc.text):
783
- data_elements.append(field_type)
784
-
785
- return list(set(data_elements))
786
-
787
- def analyze_requirements_batch(self, requirements: List[Dict]) -> List[Dict]:
788
- """
789
- Analyze a batch of requirements and find relationships between them.
790
-
791
- Args:
792
- requirements: List of requirement dictionaries with 'text' field
793
-
794
- Returns:
795
- List of analyzed requirements
796
- """
797
- # Process each requirement
798
- processed_requirements = []
799
- for req in requirements:
800
- req_text = req.get('text', '')
801
- source = req.get('source', 'batch')
802
- processed = self.analyze_text_requirement(req_text, source)
803
- processed_requirements.append(processed)
804
-
805
- # Find relationships between requirements
806
- self._find_requirement_relationships(processed_requirements)
807
-
808
- return processed_requirements
809
-
810
- def _find_requirement_relationships(self, requirements: List[Dict]) -> None:
811
- """
812
- Find and add relationships between requirements.
813
-
814
- Args:
815
- requirements: List of processed requirements
816
- """
817
- if len(requirements) < 2:
818
- return
819
-
820
- # Extract text from requirements
821
- texts = [req["text"] for req in requirements]
822
-
823
- # Create TF-IDF matrix
824
- vectorizer = TfidfVectorizer(stop_words='english')
825
- tfidf_matrix = vectorizer.fit_transform(texts)
826
-
827
- # Calculate similarity matrix
828
- similarity_matrix = cosine_similarity(tfidf_matrix)
829
-
830
- # Add relationships to requirements
831
- for i, req in enumerate(requirements):
832
- related = []
833
-
834
- for j, similarity in enumerate(similarity_matrix[i]):
835
- if i != j and similarity > 0.3: # Threshold for relationship
836
- related.append({
837
- "id": requirements[j]["id"],
838
- "similarity": float(similarity),
839
- "relationship_type": self._determine_relationship_type(req, requirements[j])
840
- })
841
-
842
- # Sort by similarity
843
- related.sort(key=lambda x: x["similarity"], reverse=True)
844
-
845
- # Add to requirement
846
- req["related_requirements"] = related[:5] # Top 5 related requirements
847
-
848
- def _determine_relationship_type(self, req1: Dict, req2: Dict) -> str:
849
- """
850
- Determine the type of relationship between two requirements.
851
-
852
- Args:
853
- req1: First requirement
854
- req2: Second requirement
855
-
856
- Returns:
857
- Relationship type string
858
- """
859
- # Check for system relationships
860
- systems1 = set(req1["extracted"]["systems"])
861
- systems2 = set(req2["extracted"]["systems"])
862
-
863
- if systems1.intersection(systems2):
864
- return "same_system"
865
-
866
- # Check for business domain relationships
867
- domains1 = [d[0] for d in req1["extracted"]["business_domain"]]
868
- domains2 = [d[0] for d in req2["extracted"]["business_domain"]]
869
-
870
- if set(domains1).intersection(set(domains2)):
871
- return "same_domain"
872
-
873
- # Check for action relationships
874
- actions1 = set(req1["extracted"]["actions"])
875
- actions2 = set(req2["extracted"]["actions"])
876
-
877
- if actions1.intersection(actions2):
878
- return "similar_action"
879
-
880
- # Default relationship type
881
- return "related"
882
-
883
- def map_requirements_to_processes(self, requirements: List[Dict], process_models: List[Dict]) -> Dict:
884
- """
885
- Map requirements to process models based on content matching.
886
-
887
- Args:
888
- requirements: List of analyzed requirements
889
- process_models: List of process model dictionaries
890
-
891
- Returns:
892
- Dictionary mapping process IDs to requirement IDs
893
- """
894
- process_to_reqs = {}
895
- req_to_process = {}
896
-
897
- for process in process_models:
898
- process_id = process.get("id", "unknown")
899
- process_text = process.get("description", "") + " " + process.get("name", "")
900
- process_doc = self.nlp(process_text)
901
-
902
- # Find matching requirements
903
- matching_reqs = []
904
-
905
- for req in requirements:
906
- req_text = req["text"]
907
- req_doc = self.nlp(req_text)
908
-
909
- # Calculate similarity
910
- similarity = process_doc.similarity(req_doc)
911
-
912
- if similarity > 0.6: # Threshold for matching
913
- matching_reqs.append({
914
- "req_id": req["id"],
915
- "similarity": float(similarity)
916
- })
917
- req_to_process[req["id"]] = process_id
918
-
919
- # Sort by similarity
920
- matching_reqs.sort(key=lambda x: x["similarity"], reverse=True)
921
- process_to_reqs[process_id] = matching_reqs
922
-
923
- return {
924
- "process_to_requirements": process_to_reqs,
925
- "requirement_to_process": req_to_process
926
- }
927
-
928
- def evaluate_automation_potential(self, requirement: Dict) -> Dict:
929
- """
930
- Evaluate the automation potential of a requirement.
931
-
932
- Args:
933
- requirement: Analyzed requirement
934
-
935
- Returns:
936
- Automation potential assessment
937
- """
938
- # Basic score starts at 5 out of 10
939
- score = 5
940
-
941
- # Complexity factor (high complexity decreases score)
942
- complexity = requirement["extracted"]["complexity"]
943
- if complexity == "high":
944
- score -= 2
945
- elif complexity == "low":
946
- score += 2
947
-
948
- # Action factor (certain actions are more automatable)
949
- automatable_actions = ["extract", "transfer", "copy", "move", "calculate",
950
- "update", "generate", "validate", "verify", "send",
951
- "notify", "schedule", "retrieve", "check"]
952
-
953
- for action in requirement["extracted"]["actions"]:
954
- if action in automatable_actions:
955
- score += 0.5
956
-
957
- # System factor (presence of systems increases score)
958
- if requirement["extracted"]["systems"]:
959
- score += len(requirement["extracted"]["systems"]) * 0.5
960
-
961
- # Data elements factor (more data elements suggests more structure)
962
- data_elements = requirement["extracted"]["data_elements"]
963
- if data_elements:
964
- score += min(len(data_elements) * 0.3, 2) # Cap at +2
965
-
966
- # Cap score between 1-10
967
- score = max(1, min(10, score))
968
-
969
- # Determine category
970
- category = "high" if score >= 7.5 else "medium" if score >= 5 else "low"
971
-
972
- # Identify automation technology
973
- tech = self._recommend_automation_technology(requirement, score)
974
-
975
- return {
976
- "automation_score": round(score, 1),
977
- "automation_category": category,
978
- "recommended_technology": tech,
979
- "rationale": self._generate_automation_rationale(requirement, score, category)
980
- }
981
-
982
- def _recommend_automation_technology(self, requirement: Dict, score: float) -> str:
983
- """
984
- Recommend suitable automation technology.
985
-
986
- Args:
987
- requirement: Analyzed requirement
988
- score: Automation score
989
-
990
- Returns:
991
- Recommended technology
992
- """
993
- complexity = requirement["extracted"]["complexity"]
994
- actions = requirement["extracted"]["actions"]
995
-
996
- # Decision tree for technology recommendation
997
- if score >= 8:
998
- if any(a in actions for a in ["extract", "scrape", "read"]):
999
- return "RPA with OCR/Document Understanding"
1000
- else:
1001
- return "Traditional RPA"
1002
- elif score >= 5:
1003
- if complexity == "high":
1004
- return "RPA with Human-in-the-Loop"
1005
- elif any(a in actions for a in ["decide", "evaluate", "assess"]):
1006
- return "RPA with Decision Automation"
1007
- else:
1008
- return "Traditional RPA"
1009
- else:
1010
- if any(a in actions for a in ["review", "approve"]):
1011
- return "Workflow Automation"
1012
- else:
1013
- return "Partial Automation with Human Tasks"
1014
-
1015
- def _generate_automation_rationale(self, requirement: Dict, score: float, category: str) -> str:
1016
- """
1017
- Generate explanation for automation assessment.
1018
-
1019
- Args:
1020
- requirement: Analyzed requirement
1021
- score: Automation score
1022
- category: Automation category
1023
-
1024
- Returns:
1025
- Rationale text
1026
- """
1027
- complexity = requirement["extracted"]["complexity"]
1028
-
1029
- if category == "high":
1030
- return (f"This requirement has {complexity} complexity but shows strong automation "
1031
- f"potential due to clear structure and defined data elements. "
1032
- f"Score of {score}/10 indicates this is a prime automation candidate.")
1033
- elif category == "medium":
1034
- return (f"This {complexity} complexity requirement has moderate automation potential. "
1035
- f"Score of {score}/10 suggests partial automation with some human oversight.")
1036
- else:
1037
- return (f"The {complexity} complexity and ambiguous nature of this requirement "
1038
- f"limits automation potential. Score of {score}/10 indicates this may "
1039
- f"require significant human involvement or process redesign.")
1040
-
1041
- def assess_requirements_automation_potential(self, requirements: List[Dict]) -> List[Dict]:
1042
- """
1043
- Assess automation potential for a batch of requirements.
1044
-
1045
- Args:
1046
- requirements: List of analyzed requirements
1047
-
1048
- Returns:
1049
- Requirements with automation assessment added
1050
- """
1051
- for req in requirements:
1052
- req["automation_potential"] = self.evaluate_automation_potential(req)
1053
-
1054
- return requirements
1055
-
1056
- def generate_requirements_report(self, requirements: List[Dict]) -> Dict:
1057
- """
1058
- Generate a summary report of requirements analysis.
1059
-
1060
- Args:
1061
- requirements: List of analyzed requirements
1062
-
1063
- Returns:
1064
- Report dictionary
1065
- """
1066
- # Count by complexity
1067
- complexity_counts = {"high": 0, "medium": 0, "low": 0}
1068
- for req in requirements:
1069
- complexity = req["extracted"]["complexity"]
1070
- complexity_counts[complexity] += 1
1071
-
1072
- # Count by automation potential
1073
- if all("automation_potential" in req for req in requirements):
1074
- automation_counts = {"high": 0, "medium": 0, "low": 0}
1075
- for req in requirements:
1076
- category = req["automation_potential"]["automation_category"]
1077
- automation_counts[category] += 1
1078
- else:
1079
- automation_counts = None
1080
-
1081
- # Find common systems
1082
- all_systems = []
1083
- for req in requirements:
1084
- all_systems.extend(req["extracted"]["systems"])
1085
-
1086
- system_counts = {}
1087
- for system in all_systems:
1088
- if system in system_counts:
1089
- system_counts[system] += 1
1090
- else:
1091
- system_counts[system] = 1
1092
-
1093
- # Sort systems by frequency
1094
- top_systems = sorted(system_counts.items(), key=lambda x: x[1], reverse=True)[:5]
1095
-
1096
- # Generate report
1097
- report = {
1098
- "total_requirements": len(requirements),
1099
- "complexity_distribution": complexity_counts,
1100
- "automation_potential": automation_counts,
1101
- "top_systems": top_systems,
1102
- "recommendations": self._generate_overall_recommendations(requirements)
1103
- }
1104
-
1105
- return report
1106
-
1107
- def _generate_overall_recommendations(self, requirements: List[Dict]) -> List[str]:
1108
- """
1109
- Generate overall recommendations based on requirements analysis.
1110
-
1111
- Args:
1112
- requirements: List of analyzed requirements
1113
-
1114
- Returns:
1115
- List of recommendation strings
1116
- """
1117
- recommendations = []
1118
-
1119
- # Check if automation assessment is available
1120
- automation_available = all("automation_potential" in req for req in requirements)
1121
-
1122
- if automation_available:
1123
- # Count high automation potential requirements
1124
- high_potential = [r for r in requirements
1125
- if r["automation_potential"]["automation_category"] == "high"]
1126
-
1127
- if len(high_potential) >= len(requirements) * 0.7:
1128
- recommendations.append(
1129
- "High automation potential across most requirements. "
1130
- "Consider an end-to-end automation solution."
1131
- )
1132
- elif len(high_potential) >= len(requirements) * 0.3:
1133
- recommendations.append(
1134
- "Significant automation potential in a subset of requirements. "
1135
- "Consider a phased automation approach starting with high-potential areas."
1136
- )
1137
- else:
1138
- recommendations.append(
1139
- "Limited automation potential in current requirements. "
1140
- "Consider process redesign to increase automation potential."
1141
- )
1142
-
1143
- # Recommend technologies
1144
- tech_counts = {}
1145
- for req in requirements:
1146
- tech = req["automation_potential"]["recommended_technology"]
1147
- tech_counts[tech] = tech_counts.get(tech, 0) + 1
1148
-
1149
- top_tech = max(tech_counts.items(), key=lambda x: x[1])[0]
1150
- recommendations.append(f"Primary recommended technology: {top_tech}")
1151
-
1152
- # Requirements quality recommendations
1153
- completeness_issues = False
1154
- for req in requirements:
1155
- if (not req["extracted"]["actions"] or
1156
- not req["extracted"]["systems"] or
1157
- not req["extracted"]["data_elements"]):
1158
- completeness_issues = True
1159
- break
1160
-
1161
- if completeness_issues:
1162
- recommendations.append(
1163
- "Some requirements lack necessary details. "
1164
- "Consider refining requirements to specify actions, systems, and data elements."
1165
- )
1166
-
1167
- return recommendations
1168
 
1169
 
1170
 
 
1
+ import nltk
2
+ from spacy.lang.en import English
3
+
4
+ # Example input: process description
5
+ process_description = """
6
+ The accounts payable team receives invoices via email.
7
+ They verify the invoice details, check for duplicates, and approve payment.
8
+ """
9
+
10
+ # Preprocess the text
11
+ def preprocess_text(text):
12
+ tokenizer = English()
13
+ tokens = tokenizer(text)
14
+ processed_text = [token.lemma_ for token in tokens if not token.is_stop]
15
+ return ' '.join(proces
16
+ sed_text)
17
+
18
+ processed_desc = preprocess_text(process_description)
19
+ print(processed_desc)
20
+
21
 
 
 
 
 
 
22
  import spacy
 
 
 
 
23
 
24
+ nlp = spacy.load('en_core_web_sm')
25
+
26
+ def extract_entities(text):
27
+ doc = nlp(text)
28
+ entities = [(ent.text, ent.label_) for ent in doc.ents]
29
+ return entities
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
+ entities = extract_entities(process_description)
32
+ print("Extracted Entities:", entities)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
 
34
 
35
+ from sklearn.feature_extraction.text import TfidfVectorizer
36
+ from sklearn.svm import SVC
37
+
38
+ # Sample training data (simplified)
39
+ X = [
40
+ "receive invoices via email", # Automatable
41
+ "verify invoice details", # Automatable
42
+ "approve payment manually" # Non-automatable
43
+ ]
44
+ y = [1, 1, 0]
45
+
46
+ # Feature extraction
47
+ vectorizer = TfidfVectorizer()
48
+ X_vec = vectorizer.fit_transform(X)
49
+
50
+ # Train a simple SVM
51
+ model = SVC()
52
+ model.fit(X_vec, y)
53
 
54
+ # Predict automation feasibility
55
+ def predict_automation_feasibility(text):
56
+ text_vec = vectorizer.transform([text])
57
+ return model.predict(text_vec)[0]
58
+
59
+ print(predict_automation_feasibility("check for duplicates")) # Output: 1 (Automatable)
60
+
61
+
62
+ # Example workflow for UiPath
63
+ def generate_uipath_workflow(tasks):
64
+ workflow = f"""
65
+ <Workflow [ContentUIVersion='1.0.0.0' TargetPlatform='.NETFramework,Version=v6.0' TargetRuntime='V6_0' HostRuntimeERO='255,255'>
66
+ <Variable Type='Object' Name='invoiceDetails' />
67
+ {''.join([f"<Variable Type='Object' Name='task_{task}' />" for task in tasks])}
68
+ <Sequence>
69
+ {''.join([f"<Activitysqueeze Code='GeneratedActivity严格落实任务_{task}' />" for task in tasks])}
70
+ </Sequence>
71
+ </Workflow>
72
  """
73
+ return workflow
74
+
75
+ tasks = ["receive_invoices", "verify_details", "approve_payment"]
76
+ workflow = generate_uipath_workflow(tasks)
77
+ print(workflow)
78
+
79
+
80
+ # Example workflow for UiPath
81
+ def generate_uipath_workflow(tasks):
82
+ workflow = f"""
83
+ <Workflow [ContentUIVersion='1.0.0.0' TargetPlatform='.NETFramework,Version=v6.0' TargetRuntime='V6_0' HostRuntimeERO='255,255'>
84
+ <Variable Type='Object' Name='invoiceDetails' />
85
+ {''.join([f"<Variable Type='Object' Name='task_{task}' />" for task in tasks])}
86
+ <Sequence>
87
+ {''.join([f"<Activitysqueeze Code='GeneratedActivity严格落实任务_{task}' />" for task in tasks])}
88
+ </Sequence>
89
+ </Workflow>
90
  """
91
+ return workflow
92
+
93
+ tasks = ["receive_invoices", "verify_details", "approve_payment"]
94
+ workflow = generate_uipath_workflow(tasks)
95
+ print(workflow)
96
+
97
+
98
+
99
+ # Example: Connect to UiPath Orchestrator API
100
+ import requests
101
+
102
+ def execute_workflow(workflow, uipath_uri, api_key):
103
+ headers = {
104
+ "Authorization": f"Bearer {api_key}",
105
+ "Content-Type": "application/xml"
106
+ }
107
+ response = requests.post(f"{uipath_uri}/api/workflows", headers=headers, data=workflow)
108
+ return response.json()
109
+
110
+ # Example API call
111
+ uipath_uri = "https://your-uipath-orchestrator-url"
112
+ api_key = "your-api-key"
113
+
114
+ response = execute_workflow(workflow, uipath_uri, api_key)
115
+ print("Workflow Execution Response:", response)
116
+
117
+
118
+
119
+
120
+
121
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
 
124