edsl 0.1.37.dev5__py3-none-any.whl → 0.1.38__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 (86) hide show
  1. edsl/Base.py +63 -34
  2. edsl/BaseDiff.py +7 -7
  3. edsl/__init__.py +2 -1
  4. edsl/__version__.py +1 -1
  5. edsl/agents/Agent.py +23 -11
  6. edsl/agents/AgentList.py +86 -23
  7. edsl/agents/Invigilator.py +18 -7
  8. edsl/agents/InvigilatorBase.py +0 -19
  9. edsl/agents/PromptConstructor.py +5 -4
  10. edsl/auto/SurveyCreatorPipeline.py +1 -1
  11. edsl/auto/utilities.py +1 -1
  12. edsl/base/Base.py +3 -13
  13. edsl/config.py +8 -0
  14. edsl/coop/coop.py +89 -19
  15. edsl/data/Cache.py +45 -17
  16. edsl/data/CacheEntry.py +8 -3
  17. edsl/data/RemoteCacheSync.py +0 -19
  18. edsl/enums.py +2 -0
  19. edsl/exceptions/agents.py +4 -0
  20. edsl/exceptions/cache.py +5 -0
  21. edsl/inference_services/GoogleService.py +7 -15
  22. edsl/inference_services/PerplexityService.py +163 -0
  23. edsl/inference_services/registry.py +2 -0
  24. edsl/jobs/Jobs.py +110 -559
  25. edsl/jobs/JobsChecks.py +147 -0
  26. edsl/jobs/JobsPrompts.py +268 -0
  27. edsl/jobs/JobsRemoteInferenceHandler.py +239 -0
  28. edsl/jobs/buckets/TokenBucket.py +3 -0
  29. edsl/jobs/interviews/Interview.py +7 -7
  30. edsl/jobs/runners/JobsRunnerAsyncio.py +156 -28
  31. edsl/jobs/runners/JobsRunnerStatus.py +194 -196
  32. edsl/jobs/tasks/TaskHistory.py +27 -19
  33. edsl/language_models/LanguageModel.py +52 -90
  34. edsl/language_models/ModelList.py +67 -14
  35. edsl/language_models/registry.py +57 -4
  36. edsl/notebooks/Notebook.py +7 -8
  37. edsl/prompts/Prompt.py +8 -3
  38. edsl/questions/QuestionBase.py +38 -30
  39. edsl/questions/QuestionBaseGenMixin.py +1 -1
  40. edsl/questions/QuestionBasePromptsMixin.py +0 -17
  41. edsl/questions/QuestionExtract.py +3 -4
  42. edsl/questions/QuestionFunctional.py +10 -3
  43. edsl/questions/derived/QuestionTopK.py +2 -0
  44. edsl/questions/question_registry.py +36 -6
  45. edsl/results/CSSParameterizer.py +108 -0
  46. edsl/results/Dataset.py +146 -15
  47. edsl/results/DatasetExportMixin.py +231 -217
  48. edsl/results/DatasetTree.py +134 -4
  49. edsl/results/Result.py +31 -16
  50. edsl/results/Results.py +159 -65
  51. edsl/results/TableDisplay.py +198 -0
  52. edsl/results/table_display.css +78 -0
  53. edsl/scenarios/FileStore.py +187 -13
  54. edsl/scenarios/Scenario.py +73 -18
  55. edsl/scenarios/ScenarioJoin.py +127 -0
  56. edsl/scenarios/ScenarioList.py +251 -76
  57. edsl/surveys/MemoryPlan.py +1 -1
  58. edsl/surveys/Rule.py +1 -5
  59. edsl/surveys/RuleCollection.py +1 -1
  60. edsl/surveys/Survey.py +25 -19
  61. edsl/surveys/SurveyFlowVisualizationMixin.py +67 -9
  62. edsl/surveys/instructions/ChangeInstruction.py +9 -7
  63. edsl/surveys/instructions/Instruction.py +21 -7
  64. edsl/templates/error_reporting/interview_details.html +3 -3
  65. edsl/templates/error_reporting/interviews.html +18 -9
  66. edsl/{conjure → utilities}/naming_utilities.py +1 -1
  67. edsl/utilities/utilities.py +15 -0
  68. {edsl-0.1.37.dev5.dist-info → edsl-0.1.38.dist-info}/METADATA +2 -1
  69. {edsl-0.1.37.dev5.dist-info → edsl-0.1.38.dist-info}/RECORD +71 -77
  70. edsl/conjure/AgentConstructionMixin.py +0 -160
  71. edsl/conjure/Conjure.py +0 -62
  72. edsl/conjure/InputData.py +0 -659
  73. edsl/conjure/InputDataCSV.py +0 -48
  74. edsl/conjure/InputDataMixinQuestionStats.py +0 -182
  75. edsl/conjure/InputDataPyRead.py +0 -91
  76. edsl/conjure/InputDataSPSS.py +0 -8
  77. edsl/conjure/InputDataStata.py +0 -8
  78. edsl/conjure/QuestionOptionMixin.py +0 -76
  79. edsl/conjure/QuestionTypeMixin.py +0 -23
  80. edsl/conjure/RawQuestion.py +0 -65
  81. edsl/conjure/SurveyResponses.py +0 -7
  82. edsl/conjure/__init__.py +0 -9
  83. edsl/conjure/examples/placeholder.txt +0 -0
  84. edsl/conjure/utilities.py +0 -201
  85. {edsl-0.1.37.dev5.dist-info → edsl-0.1.38.dist-info}/LICENSE +0 -0
  86. {edsl-0.1.37.dev5.dist-info → edsl-0.1.38.dist-info}/WHEEL +0 -0
@@ -1,33 +1,21 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import os
3
4
  import time
4
- from dataclasses import dataclass, asdict
5
-
6
- from typing import List, DefaultDict, Optional, Type, Literal
7
- from collections import UserDict, defaultdict
8
-
9
- from rich.text import Text
10
- from rich.box import SIMPLE
11
- from rich.table import Table
12
- from rich.live import Live
13
- from rich.panel import Panel
14
- from rich.progress import Progress, TextColumn, BarColumn, TaskProgressColumn
15
- from rich.layout import Layout
16
- from rich.console import Group
17
- from rich import box
18
-
19
- from edsl.jobs.interviews.InterviewStatusDictionary import InterviewStatusDictionary
5
+ import requests
6
+ import warnings
7
+ from abc import ABC, abstractmethod
8
+ from dataclasses import dataclass
9
+
10
+ from typing import Any, List, DefaultDict, Optional, Dict
11
+ from collections import defaultdict
12
+ from uuid import UUID
13
+
20
14
  from edsl.jobs.tokens.InterviewTokenUsage import InterviewTokenUsage
21
- from edsl.jobs.tokens.TokenUsage import TokenUsage
22
- from edsl.enums import get_token_pricing
23
- from edsl.jobs.tasks.task_status_enum import TaskStatus
24
15
 
25
16
  InterviewTokenUsageMapping = DefaultDict[str, InterviewTokenUsage]
26
17
 
27
18
  from edsl.jobs.interviews.InterviewStatistic import InterviewStatistic
28
- from edsl.jobs.interviews.InterviewStatisticsCollection import (
29
- InterviewStatisticsCollection,
30
- )
31
19
  from edsl.jobs.tokens.InterviewTokenUsage import InterviewTokenUsage
32
20
 
33
21
 
@@ -47,16 +35,23 @@ class ModelTokenUsageStats:
47
35
  cost: str
48
36
 
49
37
 
50
- class Stats:
51
- def elapsed_time(self):
52
- InterviewStatistic("elapsed_time", value=elapsed_time, digits=1, units="sec.")
53
-
54
-
55
- class JobsRunnerStatus:
38
+ class JobsRunnerStatusBase(ABC):
56
39
  def __init__(
57
- self, jobs_runner: "JobsRunnerAsyncio", n: int, refresh_rate: float = 0.25
40
+ self,
41
+ jobs_runner: "JobsRunnerAsyncio",
42
+ n: int,
43
+ refresh_rate: float = 1,
44
+ endpoint_url: Optional[str] = "http://localhost:8000",
45
+ job_uuid: Optional[UUID] = None,
46
+ api_key: str = None,
58
47
  ):
59
48
  self.jobs_runner = jobs_runner
49
+
50
+ # The uuid of the job on Coop
51
+ self.job_uuid = job_uuid
52
+
53
+ self.base_url = f"{endpoint_url}"
54
+
60
55
  self.start_time = time.time()
61
56
  self.completed_interviews = []
62
57
  self.refresh_rate = refresh_rate
@@ -80,6 +75,99 @@ class JobsRunnerStatus:
80
75
 
81
76
  self.completed_interview_by_model = defaultdict(list)
82
77
 
78
+ self.api_key = api_key or os.getenv("EXPECTED_PARROT_API_KEY")
79
+
80
+ @abstractmethod
81
+ def has_ep_api_key(self):
82
+ """
83
+ Checks if the user has an Expected Parrot API key.
84
+ """
85
+ pass
86
+
87
+ def get_status_dict(self) -> Dict[str, Any]:
88
+ """
89
+ Converts current status into a JSON-serializable dictionary.
90
+ """
91
+ # Get all statistics
92
+ stats = {}
93
+ for stat_name in self.statistics:
94
+ stat = self._compute_statistic(stat_name)
95
+ name, value = list(stat.items())[0]
96
+ stats[name] = value
97
+
98
+ # Calculate overall progress
99
+ total_interviews = len(self.jobs_runner.total_interviews)
100
+ completed = len(self.completed_interviews)
101
+
102
+ # Get model-specific progress
103
+ model_progress = {}
104
+ for model in self.distinct_models:
105
+ completed_for_model = len(self.completed_interview_by_model[model])
106
+ target_for_model = int(
107
+ self.num_total_interviews / len(self.distinct_models)
108
+ )
109
+ model_progress[model] = {
110
+ "completed": completed_for_model,
111
+ "total": target_for_model,
112
+ "percent": (
113
+ (completed_for_model / target_for_model * 100)
114
+ if target_for_model > 0
115
+ else 0
116
+ ),
117
+ }
118
+
119
+ status_dict = {
120
+ "overall_progress": {
121
+ "completed": completed,
122
+ "total": total_interviews,
123
+ "percent": (
124
+ (completed / total_interviews * 100) if total_interviews > 0 else 0
125
+ ),
126
+ },
127
+ "language_model_progress": model_progress,
128
+ "statistics": stats,
129
+ "status": "completed" if completed >= total_interviews else "running",
130
+ }
131
+
132
+ model_queues = {}
133
+ for model, bucket in self.jobs_runner.bucket_collection.items():
134
+ model_name = model.model
135
+ model_queues[model_name] = {
136
+ "language_model_name": model_name,
137
+ "requests_bucket": {
138
+ "completed": bucket.requests_bucket.num_released,
139
+ "requested": bucket.requests_bucket.num_requests,
140
+ "tokens_returned": bucket.requests_bucket.tokens_returned,
141
+ "target_rate": round(bucket.requests_bucket.target_rate, 1),
142
+ "current_rate": round(bucket.requests_bucket.get_throughput(), 1),
143
+ },
144
+ "tokens_bucket": {
145
+ "completed": bucket.tokens_bucket.num_released,
146
+ "requested": bucket.tokens_bucket.num_requests,
147
+ "tokens_returned": bucket.tokens_bucket.tokens_returned,
148
+ "target_rate": round(bucket.tokens_bucket.target_rate, 1),
149
+ "current_rate": round(bucket.tokens_bucket.get_throughput(), 1),
150
+ },
151
+ }
152
+ status_dict["language_model_queues"] = model_queues
153
+ return status_dict
154
+
155
+ @abstractmethod
156
+ def setup(self):
157
+ """
158
+ Conducts any setup that needs to happen prior to sending status updates.
159
+
160
+ Ex. For a local job, creates a job in the Coop database.
161
+ """
162
+ pass
163
+
164
+ @abstractmethod
165
+ def send_status_update(self):
166
+ """
167
+ Updates the current status of the job.
168
+ """
169
+ pass
170
+
83
171
  def add_completed_interview(self, result):
84
172
  self.completed_interviews.append(result.interview_hash)
85
173
 
@@ -150,180 +238,90 @@ class JobsRunnerStatus:
150
238
  }
151
239
  return stat_definitions[stat_name]()
152
240
 
153
- def create_progress_bar(self):
154
- return Progress(
155
- TextColumn("[progress.description]{task.description}"),
156
- BarColumn(),
157
- TaskProgressColumn(),
158
- TextColumn("{task.completed}/{task.total}"),
159
- )
241
+ def update_progress(self, stop_event):
242
+ while not stop_event.is_set():
243
+ self.send_status_update()
244
+ time.sleep(self.refresh_rate)
160
245
 
161
- def generate_model_queues_table(self):
162
- table = Table(show_header=False, box=box.SIMPLE)
163
- table.add_column("Info", style="cyan")
164
- table.add_column("Value", style="magenta")
165
- # table.add_row("Bucket collection", str(self.jobs_runner.bucket_collection))
166
- for model, bucket in self.jobs_runner.bucket_collection.items():
167
- table.add_row(Text(model.model, style="bold blue"), "")
168
- bucket_types = ["requests_bucket", "tokens_bucket"]
169
- for bucket_type in bucket_types:
170
- table.add_row(Text(" " + bucket_type, style="green"), "")
171
- # table.add_row(
172
- # f" Current level (capacity = {round(getattr(bucket, bucket_type).capacity, 3)})",
173
- # str(round(getattr(bucket, bucket_type).tokens, 3)),
174
- # )
175
- num_requests = getattr(bucket, bucket_type).num_requests
176
- num_released = getattr(bucket, bucket_type).num_released
177
- tokens_returned = getattr(bucket, bucket_type).tokens_returned
178
- # table.add_row(
179
- # f" Requested",
180
- # str(num_requests),
181
- # )
182
- # table.add_row(
183
- # f" Completed",
184
- # str(num_released),
185
- # )
186
- table.add_row(
187
- " Completed vs. Requested", f"{num_released} vs. {num_requests}"
188
- )
189
- table.add_row(
190
- " Added tokens (from cache)",
191
- str(tokens_returned),
192
- )
193
- if bucket_type == "tokens_bucket":
194
- rate_name = "TPM"
195
- else:
196
- rate_name = "RPM"
197
- target_rate = round(getattr(bucket, bucket_type).target_rate, 1)
198
- table.add_row(
199
- f" Empirical {rate_name} (target = {target_rate})",
200
- str(round(getattr(bucket, bucket_type).get_throughput(), 0)),
201
- )
202
-
203
- return table
204
-
205
- def generate_layout(self):
206
- progress = self.create_progress_bar()
207
- task_ids = []
208
- for model in self.distinct_models:
209
- task_id = progress.add_task(
210
- f"[cyan]{model}...",
211
- total=int(self.num_total_interviews / len(self.distinct_models)),
212
- )
213
- task_ids.append((model, task_id))
214
-
215
- progress_height = min(5, 2 + len(self.distinct_models))
216
- layout = Layout()
217
-
218
- # Create the top row with only the progress panel
219
- layout.split_column(
220
- Layout(
221
- Panel(
222
- progress,
223
- title="Interview Progress",
224
- border_style="cyan",
225
- box=box.ROUNDED,
226
- ),
227
- name="progress",
228
- size=progress_height, # Adjusted size
229
- ),
230
- Layout(name="bottom_row"), # Adjusted size
231
- )
246
+ self.send_status_update()
232
247
 
233
- # Split the bottom row into two columns for metrics and model queues
234
- layout["bottom_row"].split_row(
235
- Layout(
236
- Panel(
237
- self.generate_metrics_table(),
238
- title="Metrics",
239
- border_style="magenta",
240
- box=box.ROUNDED,
241
- ),
242
- name="metrics",
243
- ),
244
- Layout(
245
- Panel(
246
- self.generate_model_queues_table(),
247
- title="Model Queues",
248
- border_style="yellow",
249
- box=box.ROUNDED,
250
- ),
251
- name="model_queues",
252
- ),
253
- )
254
248
 
255
- return layout, progress, task_ids
249
+ class JobsRunnerStatus(JobsRunnerStatusBase):
250
+ @property
251
+ def create_url(self) -> str:
252
+ return f"{self.base_url}/api/v0/local-job"
256
253
 
257
- def generate_metrics_table(self):
258
- table = Table(show_header=True, header_style="bold magenta", box=box.SIMPLE)
259
- table.add_column("Metric", style="cyan", no_wrap=True)
260
- table.add_column("Value", justify="right")
254
+ @property
255
+ def viewing_url(self) -> str:
256
+ return f"{self.base_url}/home/local-job-progress/{str(self.job_uuid)}"
261
257
 
262
- for stat_name in self.statistics:
263
- pretty_name, value = list(self._compute_statistic(stat_name).items())[0]
264
- # breakpoint()
265
- table.add_row(pretty_name, value)
266
- return table
258
+ @property
259
+ def update_url(self) -> str:
260
+ return f"{self.base_url}/api/v0/local-job/{str(self.job_uuid)}"
267
261
 
268
- def update_progress(self, stop_event):
269
- layout, progress, task_ids = self.generate_layout()
270
-
271
- with Live(
272
- layout, refresh_per_second=int(1 / self.refresh_rate), transient=True
273
- ) as live:
274
- while (
275
- len(self.completed_interviews) < len(self.jobs_runner.total_interviews)
276
- and not stop_event.is_set()
277
- ):
278
- completed_tasks = len(self.completed_interviews)
279
- total_tasks = len(self.jobs_runner.total_interviews)
280
-
281
- for model, task_id in task_ids:
282
- completed_tasks = len(self.completed_interview_by_model[model])
283
- progress.update(
284
- task_id,
285
- completed=completed_tasks,
286
- description=f"[cyan]Conducting interviews for {model}...",
287
- )
288
-
289
- layout["metrics"].update(
290
- Panel(
291
- self.generate_metrics_table(),
292
- title="Metrics",
293
- border_style="magenta",
294
- box=box.ROUNDED,
295
- )
296
- )
297
- layout["model_queues"].update(
298
- Panel(
299
- self.generate_model_queues_table(),
300
- title="Final Model Queues",
301
- border_style="yellow",
302
- box=box.ROUNDED,
303
- )
304
- )
305
-
306
- time.sleep(self.refresh_rate)
307
-
308
- # Final update
309
- for model, task_id in task_ids:
310
- completed_tasks = len(self.completed_interview_by_model[model])
311
- progress.update(
312
- task_id,
313
- completed=completed_tasks,
314
- description=f"[cyan]Conducting interviews for {model}...",
315
- )
316
-
317
- layout["metrics"].update(
318
- Panel(
319
- self.generate_metrics_table(),
320
- title="Final Metrics",
321
- border_style="magenta",
322
- box=box.ROUNDED,
323
- )
262
+ def setup(self) -> None:
263
+ """
264
+ Creates a local job on Coop if one does not already exist.
265
+ """
266
+
267
+ headers = {"Content-Type": "application/json"}
268
+
269
+ if self.api_key:
270
+ headers["Authorization"] = f"Bearer {self.api_key}"
271
+ else:
272
+ headers["Authorization"] = f"Bearer None"
273
+
274
+ if self.job_uuid is None:
275
+ # Create a new local job
276
+ response = requests.post(
277
+ self.create_url,
278
+ headers=headers,
279
+ timeout=1,
280
+ )
281
+ response.raise_for_status()
282
+ data = response.json()
283
+ self.job_uuid = data.get("job_uuid")
284
+
285
+ print(f"Running with progress bar. View progress at {self.viewing_url}")
286
+
287
+ def send_status_update(self) -> None:
288
+ """
289
+ Sends current status to the web endpoint using the instance's job_uuid.
290
+ """
291
+ try:
292
+ # Get the status dictionary and add the job_id
293
+ status_dict = self.get_status_dict()
294
+
295
+ # Make the UUID JSON serializable
296
+ status_dict["job_id"] = str(self.job_uuid)
297
+
298
+ headers = {"Content-Type": "application/json"}
299
+
300
+ if self.api_key:
301
+ headers["Authorization"] = f"Bearer {self.api_key}"
302
+ else:
303
+ headers["Authorization"] = f"Bearer None"
304
+
305
+ # Send the update
306
+ response = requests.patch(
307
+ self.update_url,
308
+ json=status_dict,
309
+ headers=headers,
310
+ timeout=1,
324
311
  )
325
- live.update(layout)
326
- time.sleep(1) # Show final state for 1 second
312
+ response.raise_for_status()
313
+ except requests.exceptions.RequestException as e:
314
+ print(f"Failed to send status update for job {self.job_uuid}: {e}")
315
+
316
+ def has_ep_api_key(self) -> bool:
317
+ """
318
+ Returns True if the user has an Expected Parrot API key. Otherwise, returns False.
319
+ """
320
+
321
+ if self.api_key is not None:
322
+ return True
323
+ else:
324
+ return False
327
325
 
328
326
 
329
327
  if __name__ == "__main__":
@@ -8,7 +8,12 @@ from edsl.jobs.tasks.task_status_enum import TaskStatus
8
8
 
9
9
 
10
10
  class TaskHistory:
11
- def __init__(self, interviews: List["Interview"], include_traceback: bool = False):
11
+ def __init__(
12
+ self,
13
+ interviews: List["Interview"],
14
+ include_traceback: bool = False,
15
+ max_interviews: int = 10,
16
+ ):
12
17
  """
13
18
  The structure of a TaskHistory exception
14
19
 
@@ -22,6 +27,7 @@ class TaskHistory:
22
27
  self.include_traceback = include_traceback
23
28
 
24
29
  self._interviews = {index: i for index, i in enumerate(self.total_interviews)}
30
+ self.max_interviews = max_interviews
25
31
 
26
32
  @classmethod
27
33
  def example(cls):
@@ -73,19 +79,21 @@ class TaskHistory:
73
79
  """Return a string representation of the TaskHistory."""
74
80
  return f"TaskHistory(interviews={self.total_interviews})."
75
81
 
76
- def to_dict(self):
82
+ def to_dict(self, add_edsl_version=True):
77
83
  """Return the TaskHistory as a dictionary."""
78
- # return {
79
- # "exceptions": [
80
- # e.to_dict(include_traceback=self.include_traceback)
81
- # for e in self.exceptions
82
- # ],
83
- # "indices": self.indices,
84
- # }
85
- return {
86
- "interviews": [i._to_dict() for i in self.total_interviews],
84
+ d = {
85
+ "interviews": [
86
+ i.to_dict(add_edsl_version=add_edsl_version)
87
+ for i in self.total_interviews
88
+ ],
87
89
  "include_traceback": self.include_traceback,
88
90
  }
91
+ if add_edsl_version:
92
+ from edsl import __version__
93
+
94
+ d["edsl_version"] = __version__
95
+ d["edsl_class_name"] = "TaskHistory"
96
+ return d
89
97
 
90
98
  @classmethod
91
99
  def from_dict(cls, data: dict):
@@ -115,10 +123,11 @@ class TaskHistory:
115
123
 
116
124
  def _repr_html_(self):
117
125
  """Return an HTML representation of the TaskHistory."""
118
- from edsl.utilities.utilities import data_to_html
126
+ d = self.to_dict(add_edsl_version=False)
127
+ data = [[k, v] for k, v in d.items()]
128
+ from tabulate import tabulate
119
129
 
120
- newdata = self.to_dict()["exceptions"]
121
- return data_to_html(newdata, replace_new_lines=True)
130
+ return tabulate(data, headers=["keys", "values"], tablefmt="html")
122
131
 
123
132
  def show_exceptions(self, tracebacks=False):
124
133
  """Print the exceptions."""
@@ -248,8 +257,6 @@ class TaskHistory:
248
257
  for question_name, exceptions in interview.exceptions.items():
249
258
  for exception in exceptions:
250
259
  exception_type = exception.exception.__class__.__name__
251
- # exception_type = exception["exception"]
252
- # breakpoint()
253
260
  if exception_type in exceptions_by_type:
254
261
  exceptions_by_type[exception_type] += 1
255
262
  else:
@@ -336,9 +343,9 @@ class TaskHistory:
336
343
 
337
344
  env = Environment(loader=TemplateLoader("edsl", "templates/error_reporting"))
338
345
 
339
- # Load and render a template
346
+ # Get current memory usage at this point
347
+
340
348
  template = env.get_template("base.html")
341
- # rendered_template = template.render(your_data=your_data)
342
349
 
343
350
  # Render the template with data
344
351
  output = template.render(
@@ -352,6 +359,7 @@ class TaskHistory:
352
359
  exceptions_by_model=self.exceptions_by_model,
353
360
  exceptions_by_service=self.exceptions_by_service,
354
361
  models_used=models_used,
362
+ max_interviews=self.max_interviews,
355
363
  )
356
364
  return output
357
365
 
@@ -361,7 +369,7 @@ class TaskHistory:
361
369
  return_link=False,
362
370
  css=None,
363
371
  cta="Open Report in New Tab",
364
- open_in_browser=True,
372
+ open_in_browser=False,
365
373
  ):
366
374
  """Return an HTML report."""
367
375