sdg-hub 0.2.2__py3-none-any.whl → 0.3.1__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.
Files changed (35) hide show
  1. sdg_hub/_version.py +2 -2
  2. sdg_hub/core/blocks/llm/client_manager.py +63 -26
  3. sdg_hub/core/blocks/llm/llm_chat_block.py +12 -9
  4. sdg_hub/core/blocks/llm/text_parser_block.py +88 -21
  5. sdg_hub/core/blocks/transform/__init__.py +2 -0
  6. sdg_hub/core/blocks/transform/json_structure_block.py +142 -0
  7. sdg_hub/core/flow/base.py +199 -56
  8. sdg_hub/core/utils/datautils.py +45 -2
  9. sdg_hub/core/utils/flow_metrics.py +261 -0
  10. sdg_hub/core/utils/logger_config.py +50 -9
  11. sdg_hub/flows/qa_generation/document_grounded_qa/enhanced_multi_summary_qa/__init__.py +0 -0
  12. sdg_hub/flows/qa_generation/document_grounded_qa/enhanced_multi_summary_qa/detailed_summary/__init__.py +0 -0
  13. sdg_hub/flows/qa_generation/document_grounded_qa/enhanced_multi_summary_qa/detailed_summary/detailed_summary.yaml +11 -0
  14. sdg_hub/flows/qa_generation/document_grounded_qa/enhanced_multi_summary_qa/detailed_summary/flow.yaml +159 -0
  15. sdg_hub/flows/qa_generation/document_grounded_qa/enhanced_multi_summary_qa/extractive_summary/__init__.py +0 -0
  16. sdg_hub/flows/qa_generation/document_grounded_qa/enhanced_multi_summary_qa/extractive_summary/extractive_summary.yaml +65 -0
  17. sdg_hub/flows/qa_generation/document_grounded_qa/enhanced_multi_summary_qa/extractive_summary/flow.yaml +161 -0
  18. sdg_hub/flows/qa_generation/document_grounded_qa/enhanced_multi_summary_qa/generate_answers.yaml +15 -0
  19. sdg_hub/flows/qa_generation/document_grounded_qa/enhanced_multi_summary_qa/generate_multiple_qa.yaml +21 -0
  20. sdg_hub/flows/qa_generation/document_grounded_qa/enhanced_multi_summary_qa/generate_question_list.yaml +44 -0
  21. sdg_hub/flows/qa_generation/document_grounded_qa/enhanced_multi_summary_qa/key_facts/__init__.py +0 -0
  22. sdg_hub/flows/qa_generation/document_grounded_qa/enhanced_multi_summary_qa/key_facts/flow.yaml +104 -0
  23. sdg_hub/flows/qa_generation/document_grounded_qa/enhanced_multi_summary_qa/key_facts/key_facts_summary.yaml +61 -0
  24. sdg_hub/flows/text_analysis/__init__.py +2 -0
  25. sdg_hub/flows/text_analysis/structured_insights/__init__.py +6 -0
  26. sdg_hub/flows/text_analysis/structured_insights/analyze_sentiment.yaml +27 -0
  27. sdg_hub/flows/text_analysis/structured_insights/extract_entities.yaml +38 -0
  28. sdg_hub/flows/text_analysis/structured_insights/extract_keywords.yaml +21 -0
  29. sdg_hub/flows/text_analysis/structured_insights/flow.yaml +153 -0
  30. sdg_hub/flows/text_analysis/structured_insights/summarize.yaml +21 -0
  31. {sdg_hub-0.2.2.dist-info → sdg_hub-0.3.1.dist-info}/METADATA +3 -1
  32. {sdg_hub-0.2.2.dist-info → sdg_hub-0.3.1.dist-info}/RECORD +35 -13
  33. {sdg_hub-0.2.2.dist-info → sdg_hub-0.3.1.dist-info}/WHEEL +0 -0
  34. {sdg_hub-0.2.2.dist-info → sdg_hub-0.3.1.dist-info}/licenses/LICENSE +0 -0
  35. {sdg_hub-0.2.2.dist-info → sdg_hub-0.3.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,261 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Flow execution metrics utilities for display and export."""
3
+
4
+ # Standard
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from typing import Any, Optional
8
+ import json
9
+ import time
10
+
11
+ # Third Party
12
+ from datasets import Dataset
13
+ from rich.console import Console
14
+ from rich.panel import Panel
15
+ from rich.table import Table
16
+
17
+
18
+ def aggregate_block_metrics(entries: list[dict[str, Any]]) -> list[dict[str, Any]]:
19
+ """Aggregate per-block metrics, coalescing chunked runs.
20
+
21
+ Parameters
22
+ ----------
23
+ entries : list[dict[str, Any]]
24
+ Raw block metrics entries from flow execution.
25
+
26
+ Returns
27
+ -------
28
+ list[dict[str, Any]]
29
+ Aggregated metrics with combined execution times and data changes.
30
+ """
31
+ agg: dict[tuple[str, str], dict[str, Any]] = {}
32
+ for m in entries:
33
+ key = (m.get("block_name"), m.get("block_type"))
34
+ a = agg.setdefault(
35
+ key,
36
+ {
37
+ "block_name": key[0],
38
+ "block_type": key[1],
39
+ "execution_time": 0.0,
40
+ "input_rows": 0,
41
+ "output_rows": 0,
42
+ "added_cols": set(),
43
+ "removed_cols": set(),
44
+ "status": "success",
45
+ "error_type": None,
46
+ "error": None,
47
+ },
48
+ )
49
+ a["execution_time"] += float(m.get("execution_time", 0.0))
50
+ a["input_rows"] += int(m.get("input_rows", 0))
51
+ a["output_rows"] += int(m.get("output_rows", 0))
52
+ a["added_cols"].update(m.get("added_cols", []))
53
+ a["removed_cols"].update(m.get("removed_cols", []))
54
+ if m.get("status") == "failed":
55
+ a["status"] = "failed"
56
+ a["error_type"] = m.get("error_type") or a["error_type"]
57
+ a["error"] = m.get("error") or a["error"]
58
+ # normalize
59
+ result = []
60
+ for a in agg.values():
61
+ a["added_cols"] = sorted(a["added_cols"])
62
+ a["removed_cols"] = sorted(a["removed_cols"])
63
+ # drop empty error fields
64
+ if a["status"] == "success":
65
+ a.pop("error_type", None)
66
+ a.pop("error", None)
67
+ result.append(a)
68
+ return result
69
+
70
+
71
+ def display_metrics_summary(
72
+ block_metrics: list[dict[str, Any]],
73
+ flow_name: str,
74
+ final_dataset: Optional[Dataset] = None,
75
+ ) -> None:
76
+ """Display a rich table summarizing block execution metrics.
77
+
78
+ Parameters
79
+ ----------
80
+ block_metrics : list[dict[str, Any]]
81
+ Raw block metrics from flow execution.
82
+ flow_name : str
83
+ Name of the flow for display title.
84
+ final_dataset : Optional[Dataset], optional
85
+ Final dataset from flow execution. None if flow failed.
86
+ """
87
+ if not block_metrics:
88
+ return
89
+
90
+ console = Console()
91
+
92
+ # Create the metrics table
93
+ table = Table(
94
+ show_header=True,
95
+ header_style="bold bright_white",
96
+ title="Flow Execution Summary",
97
+ )
98
+ table.add_column("Block Name", style="bright_cyan", width=20)
99
+ table.add_column("Type", style="bright_green", width=15)
100
+ table.add_column("Duration", justify="right", style="bright_yellow", width=10)
101
+ table.add_column("Rows", justify="center", style="bright_blue", width=12)
102
+ table.add_column("Columns", justify="center", style="bright_magenta", width=15)
103
+ table.add_column("Status", justify="center", width=10)
104
+
105
+ total_time = 0.0
106
+ successful_blocks = 0
107
+
108
+ for metrics in block_metrics:
109
+ # Format duration
110
+ duration = f"{metrics['execution_time']:.2f}s"
111
+ total_time += metrics["execution_time"]
112
+
113
+ # Format row changes
114
+ if metrics["status"] == "success":
115
+ row_change = f"{metrics['input_rows']:,} → {metrics['output_rows']:,}"
116
+ successful_blocks += 1
117
+ else:
118
+ row_change = f"{metrics['input_rows']:,} → ❌"
119
+
120
+ # Format column changes
121
+ added = len(metrics["added_cols"])
122
+ removed = len(metrics["removed_cols"])
123
+ if added > 0 and removed > 0:
124
+ col_change = f"+{added}/-{removed}"
125
+ elif added > 0:
126
+ col_change = f"+{added}"
127
+ elif removed > 0:
128
+ col_change = f"-{removed}"
129
+ else:
130
+ col_change = "—"
131
+
132
+ # Format status with color
133
+ if metrics["status"] == "success":
134
+ status = "[green]✓[/green]"
135
+ else:
136
+ status = "[red]✗[/red]"
137
+
138
+ table.add_row(
139
+ metrics["block_name"],
140
+ metrics["block_type"],
141
+ duration,
142
+ row_change,
143
+ col_change,
144
+ status,
145
+ )
146
+
147
+ # Add summary row
148
+ table.add_section()
149
+ final_row_count = len(final_dataset) if final_dataset else 0
150
+ final_col_count = len(final_dataset.column_names) if final_dataset else 0
151
+
152
+ table.add_row(
153
+ "[bold]TOTAL[/bold]",
154
+ f"[bold]{len(block_metrics)} blocks[/bold]",
155
+ f"[bold]{total_time:.2f}s[/bold]",
156
+ f"[bold]{final_row_count:,} final[/bold]",
157
+ f"[bold]{final_col_count} final[/bold]",
158
+ f"[bold][green]{successful_blocks}/{len(block_metrics)}[/green][/bold]",
159
+ )
160
+
161
+ # Display the table with panel
162
+ console.print()
163
+
164
+ # Determine panel title and border color based on execution status
165
+ failed_blocks = len(block_metrics) - successful_blocks
166
+ if final_dataset is None:
167
+ # Flow failed completely
168
+ title = (
169
+ f"[bold bright_white]{flow_name}[/bold bright_white] - [red]Failed[/red]"
170
+ )
171
+ border_style = "bright_red"
172
+ elif failed_blocks == 0:
173
+ # All blocks succeeded
174
+ title = f"[bold bright_white]{flow_name}[/bold bright_white] - [green]Complete[/green]"
175
+ border_style = "bright_green"
176
+ else:
177
+ # Some blocks failed but flow completed
178
+ title = f"[bold bright_white]{flow_name}[/bold bright_white] - [yellow]Partial[/yellow]"
179
+ border_style = "bright_yellow"
180
+
181
+ console.print(
182
+ Panel(
183
+ table,
184
+ title=title,
185
+ border_style=border_style,
186
+ )
187
+ )
188
+ console.print()
189
+
190
+
191
+ def save_metrics_to_json(
192
+ block_metrics: list[dict[str, Any]],
193
+ flow_name: str,
194
+ flow_version: str,
195
+ execution_successful: bool,
196
+ run_start_time: float,
197
+ log_dir: str,
198
+ timestamp: Optional[str] = None,
199
+ flow_name_normalized: Optional[str] = None,
200
+ logger=None,
201
+ ) -> None:
202
+ """Save flow execution metrics to JSON file.
203
+
204
+ Parameters
205
+ ----------
206
+ block_metrics : list[dict[str, Any]]
207
+ Raw block metrics from flow execution.
208
+ flow_name : str
209
+ Human-readable flow name.
210
+ flow_version : str
211
+ Flow version string.
212
+ execution_successful : bool
213
+ Whether the flow execution completed successfully.
214
+ run_start_time : float
215
+ Start time from time.perf_counter() for wall time calculation.
216
+ log_dir : str
217
+ Directory to save metrics JSON file.
218
+ timestamp : Optional[str], optional
219
+ Timestamp string for filename. Generated if not provided.
220
+ flow_name_normalized : Optional[str], optional
221
+ Normalized flow name for filename. Generated if not provided.
222
+ logger : Optional[logging.Logger], optional
223
+ Logger instance for status messages.
224
+ """
225
+ try:
226
+ # Generate timestamp and normalized name if not provided
227
+ if timestamp is None:
228
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
229
+ if flow_name_normalized is None:
230
+ flow_name_normalized = flow_name.replace(" ", "_").lower()
231
+
232
+ # Aggregate metrics per block (coalesce chunked runs)
233
+ aggregated = aggregate_block_metrics(block_metrics)
234
+
235
+ metrics_data = {
236
+ "flow_name": flow_name,
237
+ "flow_version": flow_version,
238
+ "execution_timestamp": timestamp,
239
+ "execution_successful": execution_successful,
240
+ "total_execution_time": sum(m["execution_time"] for m in aggregated),
241
+ "total_wall_time": time.perf_counter() - run_start_time, # end-to-end
242
+ "total_blocks": len(aggregated),
243
+ "successful_blocks": sum(1 for m in aggregated if m["status"] == "success"),
244
+ "failed_blocks": sum(1 for m in aggregated if m["status"] == "failed"),
245
+ "block_metrics": aggregated,
246
+ }
247
+
248
+ metrics_filename = f"{flow_name_normalized}_{timestamp}_metrics.json"
249
+ metrics_path = Path(log_dir) / metrics_filename
250
+ metrics_path.parent.mkdir(parents=True, exist_ok=True)
251
+
252
+ with open(metrics_path, "w", encoding="utf-8") as f:
253
+ json.dump(metrics_data, f, indent=2, sort_keys=True)
254
+
255
+ if logger:
256
+ logger.info(f"Metrics saved to: {metrics_path}")
257
+
258
+ except Exception as e:
259
+ # Metrics saving failed, warn but do not break flow
260
+ if logger:
261
+ logger.warning(f"Failed to save metrics: {e}")
@@ -7,14 +7,55 @@ import os
7
7
  from rich.logging import RichHandler
8
8
 
9
9
 
10
- def setup_logger(name):
11
- # Set up the logger
12
- log_level = os.getenv("LOG_LEVEL", "INFO")
13
- logging.basicConfig(
14
- level=log_level,
15
- format="%(message)s",
16
- datefmt="[%X]",
17
- handlers=[RichHandler()],
18
- )
10
+ def setup_logger(name, log_dir=None, log_filename="sdg_hub.log"):
11
+ """
12
+ Set up a logger with optional file logging.
13
+
14
+ Parameters
15
+ ----------
16
+ name : str
17
+ Logger name.
18
+ log_dir : str, optional
19
+ Directory to save log files. If None, logs are not saved to file.
20
+ log_filename : str, optional
21
+ Name of the log file (default: "sdg_hub.log").
22
+
23
+ Returns
24
+ -------
25
+ logging.Logger
26
+ Configured logger.
27
+ """
28
+ log_level = os.getenv("LOG_LEVEL", "INFO").upper()
19
29
  logger = logging.getLogger(name)
30
+ logger.setLevel(log_level)
31
+
32
+ # Suppress litellm logs to reduce noise
33
+ litellm_log_level = os.getenv("LITELLM_LOG_LEVEL", "WARNING").upper()
34
+ logging.getLogger("litellm").setLevel(litellm_log_level)
35
+ logging.getLogger("litellm.proxy").setLevel(litellm_log_level)
36
+ logging.getLogger("litellm.router").setLevel(litellm_log_level)
37
+
38
+ # Prevent duplicate handlers if setup_logger is called multiple times
39
+ if not logger.handlers:
40
+ # Rich console handler
41
+ rich_handler = RichHandler()
42
+ rich_handler.setLevel(log_level)
43
+ formatter = logging.Formatter("%(message)s", datefmt="[%X]")
44
+ rich_handler.setFormatter(formatter)
45
+ logger.addHandler(rich_handler)
46
+
47
+ # Optional file handler
48
+ if log_dir is not None:
49
+ os.makedirs(log_dir, exist_ok=True)
50
+ file_path = os.path.join(log_dir, log_filename)
51
+ file_handler = logging.FileHandler(file_path, encoding="utf-8")
52
+ file_handler.setLevel(log_level)
53
+ file_formatter = logging.Formatter(
54
+ "%(asctime)s | %(levelname)s | %(name)s | %(message)s",
55
+ datefmt="[%Y-%m-%d %H:%M:%S]",
56
+ )
57
+ file_handler.setFormatter(file_formatter)
58
+ logger.addHandler(file_handler)
59
+
60
+ # logger.info(f"Logger setup complete: {name}")
20
61
  return logger
@@ -0,0 +1,11 @@
1
+ - role: system
2
+ content: You are an expert at summarizing text.
3
+
4
+ - role: user
5
+ content: |
6
+ Summarize the given document in a Abstract Conceptual Layer representation such that it captures overarching themes, main arguments, and core principles.
7
+ Make sure to include all the details from the document in the summary.
8
+
9
+ Document:
10
+ {{document_outline}}
11
+ {{document}}
@@ -0,0 +1,159 @@
1
+ metadata:
2
+ name: Detailed Summary Knowledge Tuning Dataset Generation Flow
3
+ description: Generates high level summaries of the document focusing on overarching themes, main arguments, and core principles. This is then converted into Question-Answer pairs.
4
+ version: 2.0.0
5
+ author: SDG Hub Contributors
6
+ recommended_models:
7
+ default: openai/gpt-oss-120b
8
+ compatible:
9
+ - meta-llama/Llama-3.3-70B-Instruct
10
+ - microsoft/phi-4
11
+ - mistralai/Mixtral-8x7B-Instruct-v0.1
12
+ experimental: []
13
+ tags:
14
+ - knowledge-tuning
15
+ - document-internalization
16
+ - question-generation
17
+ - qa-pairs
18
+ - detailed-summaries
19
+ license: Apache-2.0
20
+ min_sdg_hub_version: 0.2.0
21
+ dataset_requirements:
22
+ required_columns:
23
+ - document
24
+ - document_outline
25
+ - domain
26
+ - icl_document
27
+ - icl_query_1
28
+ - icl_query_2
29
+ - icl_query_3
30
+ description: 'Input dataset should contain documents with text content and domain classification. Each document should be substantial enough for meaningful question generation (minimum 100 words recommended). The flow generates three types
31
+ of summaries: detailed (n=20), extractive (n=10), and key facts (n=50), each producing corresponding QA pairs designed to help LLMs internalize document knowledge for knowledge tuning.'
32
+ output_columns:
33
+ - summary
34
+ - question
35
+ - response
36
+ - raw_document
37
+ - faithfulness_explanation
38
+ - faithfulness_judgment
39
+ id: mild-thunder-748
40
+ blocks:
41
+ - block_type: DuplicateColumnsBlock
42
+ block_config:
43
+ block_name: duplicate_document_col
44
+ input_cols:
45
+ document: base_document
46
+ - block_type: PromptBuilderBlock
47
+ block_config:
48
+ block_name: detailed_summary_prompt
49
+ input_cols:
50
+ - document
51
+ - document_outline
52
+ output_cols: summary_prompt
53
+ prompt_config_path: detailed_summary.yaml
54
+ format_as_messages: true
55
+ - block_type: LLMChatBlock
56
+ block_config:
57
+ block_name: gen_detailed_summary
58
+ input_cols: summary_prompt
59
+ output_cols: raw_summary
60
+ max_tokens: 4096
61
+ temperature: 0.7
62
+ n: 50
63
+ async_mode: true
64
+ - block_type: TextParserBlock
65
+ block_config:
66
+ block_name: parse_detailed_summary
67
+ input_cols: raw_summary
68
+ output_cols: summary
69
+ start_tags:
70
+ - ''
71
+ end_tags:
72
+ - ''
73
+ - block_type: RenameColumnsBlock
74
+ block_config:
75
+ block_name: rename_to_document_column
76
+ input_cols:
77
+ document: raw_document
78
+ summary: document
79
+ - block_type: PromptBuilderBlock
80
+ block_config:
81
+ block_name: question_generation_prompt
82
+ input_cols:
83
+ - domain
84
+ - document
85
+ - document_outline
86
+ - icl_document
87
+ - icl_query_1
88
+ - icl_query_2
89
+ - icl_query_3
90
+ output_cols: question_generation_prompt
91
+ prompt_config_path: ../generate_question_list.yaml
92
+ format_as_messages: true
93
+ - block_type: LLMChatBlock
94
+ block_config:
95
+ block_name: question_generation
96
+ input_cols: question_generation_prompt
97
+ output_cols: question_list
98
+ max_tokens: 256
99
+ temperature: 0.7
100
+ n: 1
101
+ async_mode: true
102
+ - block_type: TextParserBlock
103
+ block_config:
104
+ block_name: parse_question_list
105
+ input_cols: question_list
106
+ output_cols: question
107
+ start_tags:
108
+ - '[QUESTION]'
109
+ end_tags:
110
+ - '[END]'
111
+ - block_type: PromptBuilderBlock
112
+ block_config:
113
+ block_name: answer_generation_prompt
114
+ input_cols:
115
+ - question
116
+ - document
117
+ - document_outline
118
+ output_cols: answer_generation_prompt
119
+ prompt_config_path: ../generate_answers.yaml
120
+ format_as_messages: true
121
+ - block_type: LLMChatBlock
122
+ block_config:
123
+ block_name: answer_generation
124
+ input_cols: answer_generation_prompt
125
+ output_cols: response_dict
126
+ max_tokens: 4096
127
+ temperature: 0.7
128
+ n: 1
129
+ async_mode: true
130
+ - block_type: TextParserBlock
131
+ block_config:
132
+ block_name: parse_response_dict
133
+ input_cols: response_dict
134
+ output_cols: response
135
+ start_tags:
136
+ - ''
137
+ end_tags:
138
+ - ''
139
+ save_reasoning_content: true
140
+ - block_type: EvaluateFaithfulnessBlock
141
+ block_config:
142
+ block_name: eval_faithfulness
143
+ input_cols:
144
+ - document
145
+ - response
146
+ output_cols:
147
+ - faithfulness_explanation
148
+ - faithfulness_judgment
149
+ prompt_config_path: ../../multi_summary_qa/instructlab/evaluate_faithfulness.yaml
150
+ filter_value: 'YES'
151
+ operation: eq
152
+ async_mode: true
153
+ format_as_messages: true
154
+ start_tags:
155
+ - '[Start of Explanation]'
156
+ - '[Start of Answer]'
157
+ end_tags:
158
+ - '[End of Explanation]'
159
+ - '[End of Answer]'
@@ -0,0 +1,65 @@
1
+ - role: system
2
+ content: You are an expert at summarizing text.
3
+
4
+ - role: user
5
+ content: |
6
+ You will create an Enhanced Extractive Summary from the provided document.
7
+ Unlike a standard extractive summary that simply pulls key sentences, an Enhanced Extractive Summary adds rich contextual information and cognitive classification to each extracted segment.
8
+ For each significant section of the document, extract 2-4 key passages, and list them. Then for each extract annotate. Structure your response as follows:
9
+ ### Extracts from the Passage:
10
+ [List of extracts]
11
+
12
+ ### Annotations:
13
+
14
+ ### Extract [Number]
15
+ > "[Direct quote from the original text]"
16
+
17
+ **Context Marker**: [Brief description of where this extract fits within the document's narrative or argument structure]
18
+ **Relevance**: [Rate as Low, Medium, Medium-High, High, or Very High and briefly explain importance to main themes]
19
+ **Relationship**: [Explain how this extract connects to other extracts, by specifying extract number, or concepts in the document]
20
+
21
+ To help you understand the task, here is an example:
22
+ Document:
23
+ Remote work has grown by over 150% since 2020 due to the pandemic. Companies found that productivity remained stable, while employee satisfaction increased. However, challenges like communication gaps and team cohesion issues emerged. Firms are now adopting hybrid models to balance flexibility with collaboration.
24
+
25
+ ### Extracts from the Passage:
26
+ 1. > "Remote work has grown by over 150% since 2020 due to the pandemic."
27
+ 2. > "Companies found that productivity remained stable, while employee satisfaction increased."
28
+ 3. > "However, challenges like communication gaps and team cohesion issues emerged."
29
+ 4. > "Firms are now adopting hybrid models to balance flexibility with collaboration."
30
+
31
+ ### Annotations:
32
+
33
+ ### Extract 1
34
+ > "Remote work has grown by over 150% since 2020 due to the pandemic."
35
+
36
+ * **Context Marker**: This is the opening factual statement, providing temporal context and the catalyst for the changes discussed later.
37
+ * **Relevance**: **Very High** – It introduces the main subject and quantifies the scale of the transformation, anchoring the entire discussion.
38
+ * **Relationship**: Establishes the cause for changes in work patterns; leads directly to the evaluations in Extracts 2 and 3, and the resulting shift in Extract 4.
39
+
40
+
41
+ ### Extract 2
42
+ > "Companies found that productivity remained stable, while employee satisfaction increased."
43
+
44
+ * **Context Marker**: Positioned after the growth in remote work, this extract summarizes key benefits observed by firms.
45
+ * **Relevance**: **High** – Highlights why remote work gained support: it delivered business continuity and improved employee morale.
46
+ * **Relationship**: Works in tandem with Extract 3 to present a balanced view of remote work’s outcomes; explains part of the motivation behind hybrid models in Extract 4.
47
+
48
+ ### Extract 3
49
+ > "However, challenges like communication gaps and team cohesion issues emerged."
50
+
51
+ * **Context Marker**: Marks a turning point in the narrative, shifting from benefits to complications of remote work.
52
+ * **Relevance**: **High** – Adds nuance by introducing critical downsides that companies faced.
53
+ * **Relationship**: Contrasts with Extract 2 and sets up the rationale for the hybrid solution in Extract 4.
54
+
55
+ ### Extract 4
56
+ > "Firms are now adopting hybrid models to balance flexibility with collaboration."
57
+
58
+ * **Context Marker**: Concluding insight, presenting the emerging consensus or strategy being adopted in response to earlier findings.
59
+ * **Relevance**: **Very High** – Synthesizes the document’s insights into a forward-looking solution.
60
+ * **Relationship**: Resolves the tension highlighted in Extracts 2 and 3; represents the evolution sparked by the situation in Extract 1.
61
+
62
+ Now it's your turn to create an Enhanced Extractive Summary from the provided document.
63
+ Document:
64
+ {{document_outline}}
65
+ {{document}}