edsl 0.1.38__py3-none-any.whl → 0.1.38.dev1__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 +34 -63
  2. edsl/BaseDiff.py +7 -7
  3. edsl/__init__.py +1 -2
  4. edsl/__version__.py +1 -1
  5. edsl/agents/Agent.py +11 -23
  6. edsl/agents/AgentList.py +23 -86
  7. edsl/agents/Invigilator.py +7 -18
  8. edsl/agents/InvigilatorBase.py +19 -0
  9. edsl/agents/PromptConstructor.py +4 -5
  10. edsl/auto/SurveyCreatorPipeline.py +1 -1
  11. edsl/auto/utilities.py +1 -1
  12. edsl/base/Base.py +13 -3
  13. edsl/config.py +0 -8
  14. edsl/conjure/AgentConstructionMixin.py +160 -0
  15. edsl/conjure/Conjure.py +62 -0
  16. edsl/conjure/InputData.py +659 -0
  17. edsl/conjure/InputDataCSV.py +48 -0
  18. edsl/conjure/InputDataMixinQuestionStats.py +182 -0
  19. edsl/conjure/InputDataPyRead.py +91 -0
  20. edsl/conjure/InputDataSPSS.py +8 -0
  21. edsl/conjure/InputDataStata.py +8 -0
  22. edsl/conjure/QuestionOptionMixin.py +76 -0
  23. edsl/conjure/QuestionTypeMixin.py +23 -0
  24. edsl/conjure/RawQuestion.py +65 -0
  25. edsl/conjure/SurveyResponses.py +7 -0
  26. edsl/conjure/__init__.py +9 -0
  27. edsl/conjure/examples/placeholder.txt +0 -0
  28. edsl/{utilities → conjure}/naming_utilities.py +1 -1
  29. edsl/conjure/utilities.py +201 -0
  30. edsl/coop/coop.py +7 -77
  31. edsl/data/Cache.py +17 -45
  32. edsl/data/CacheEntry.py +3 -8
  33. edsl/data/RemoteCacheSync.py +19 -0
  34. edsl/enums.py +0 -2
  35. edsl/exceptions/agents.py +0 -4
  36. edsl/inference_services/GoogleService.py +15 -7
  37. edsl/inference_services/registry.py +0 -2
  38. edsl/jobs/Jobs.py +559 -110
  39. edsl/jobs/buckets/TokenBucket.py +0 -3
  40. edsl/jobs/interviews/Interview.py +7 -7
  41. edsl/jobs/runners/JobsRunnerAsyncio.py +28 -156
  42. edsl/jobs/runners/JobsRunnerStatus.py +196 -194
  43. edsl/jobs/tasks/TaskHistory.py +19 -27
  44. edsl/language_models/LanguageModel.py +90 -52
  45. edsl/language_models/ModelList.py +14 -67
  46. edsl/language_models/registry.py +4 -57
  47. edsl/notebooks/Notebook.py +8 -7
  48. edsl/prompts/Prompt.py +3 -8
  49. edsl/questions/QuestionBase.py +30 -38
  50. edsl/questions/QuestionBaseGenMixin.py +1 -1
  51. edsl/questions/QuestionBasePromptsMixin.py +17 -0
  52. edsl/questions/QuestionExtract.py +4 -3
  53. edsl/questions/QuestionFunctional.py +3 -10
  54. edsl/questions/derived/QuestionTopK.py +0 -2
  55. edsl/questions/question_registry.py +6 -36
  56. edsl/results/Dataset.py +15 -146
  57. edsl/results/DatasetExportMixin.py +217 -231
  58. edsl/results/DatasetTree.py +4 -134
  59. edsl/results/Result.py +16 -31
  60. edsl/results/Results.py +65 -159
  61. edsl/scenarios/FileStore.py +13 -187
  62. edsl/scenarios/Scenario.py +18 -73
  63. edsl/scenarios/ScenarioList.py +76 -251
  64. edsl/surveys/MemoryPlan.py +1 -1
  65. edsl/surveys/Rule.py +5 -1
  66. edsl/surveys/RuleCollection.py +1 -1
  67. edsl/surveys/Survey.py +19 -25
  68. edsl/surveys/SurveyFlowVisualizationMixin.py +9 -67
  69. edsl/surveys/instructions/ChangeInstruction.py +7 -9
  70. edsl/surveys/instructions/Instruction.py +7 -21
  71. edsl/templates/error_reporting/interview_details.html +3 -3
  72. edsl/templates/error_reporting/interviews.html +9 -18
  73. edsl/utilities/utilities.py +0 -15
  74. {edsl-0.1.38.dist-info → edsl-0.1.38.dev1.dist-info}/METADATA +1 -2
  75. {edsl-0.1.38.dist-info → edsl-0.1.38.dev1.dist-info}/RECORD +77 -71
  76. edsl/exceptions/cache.py +0 -5
  77. edsl/inference_services/PerplexityService.py +0 -163
  78. edsl/jobs/JobsChecks.py +0 -147
  79. edsl/jobs/JobsPrompts.py +0 -268
  80. edsl/jobs/JobsRemoteInferenceHandler.py +0 -239
  81. edsl/results/CSSParameterizer.py +0 -108
  82. edsl/results/TableDisplay.py +0 -198
  83. edsl/results/table_display.css +0 -78
  84. edsl/scenarios/ScenarioJoin.py +0 -127
  85. {edsl-0.1.38.dist-info → edsl-0.1.38.dev1.dist-info}/LICENSE +0 -0
  86. {edsl-0.1.38.dist-info → edsl-0.1.38.dev1.dist-info}/WHEEL +0 -0
@@ -1,21 +1,33 @@
1
1
  from __future__ import annotations
2
2
 
3
- import os
4
3
  import time
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
-
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
14
20
  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
15
24
 
16
25
  InterviewTokenUsageMapping = DefaultDict[str, InterviewTokenUsage]
17
26
 
18
27
  from edsl.jobs.interviews.InterviewStatistic import InterviewStatistic
28
+ from edsl.jobs.interviews.InterviewStatisticsCollection import (
29
+ InterviewStatisticsCollection,
30
+ )
19
31
  from edsl.jobs.tokens.InterviewTokenUsage import InterviewTokenUsage
20
32
 
21
33
 
@@ -35,23 +47,16 @@ class ModelTokenUsageStats:
35
47
  cost: str
36
48
 
37
49
 
38
- class JobsRunnerStatusBase(ABC):
50
+ class Stats:
51
+ def elapsed_time(self):
52
+ InterviewStatistic("elapsed_time", value=elapsed_time, digits=1, units="sec.")
53
+
54
+
55
+ class JobsRunnerStatus:
39
56
  def __init__(
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,
57
+ self, jobs_runner: "JobsRunnerAsyncio", n: int, refresh_rate: float = 0.25
47
58
  ):
48
59
  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
-
55
60
  self.start_time = time.time()
56
61
  self.completed_interviews = []
57
62
  self.refresh_rate = refresh_rate
@@ -75,99 +80,6 @@ class JobsRunnerStatusBase(ABC):
75
80
 
76
81
  self.completed_interview_by_model = defaultdict(list)
77
82
 
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
-
171
83
  def add_completed_interview(self, result):
172
84
  self.completed_interviews.append(result.interview_hash)
173
85
 
@@ -238,90 +150,180 @@ class JobsRunnerStatusBase(ABC):
238
150
  }
239
151
  return stat_definitions[stat_name]()
240
152
 
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)
245
-
246
- self.send_status_update()
247
-
248
-
249
- class JobsRunnerStatus(JobsRunnerStatusBase):
250
- @property
251
- def create_url(self) -> str:
252
- return f"{self.base_url}/api/v0/local-job"
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
+ )
253
160
 
254
- @property
255
- def viewing_url(self) -> str:
256
- return f"{self.base_url}/home/local-job-progress/{str(self.job_uuid)}"
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
+ )
257
232
 
258
- @property
259
- def update_url(self) -> str:
260
- return f"{self.base_url}/api/v0/local-job/{str(self.job_uuid)}"
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
+ )
261
254
 
262
- def setup(self) -> None:
263
- """
264
- Creates a local job on Coop if one does not already exist.
265
- """
255
+ return layout, progress, task_ids
266
256
 
267
- headers = {"Content-Type": "application/json"}
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")
268
261
 
269
- if self.api_key:
270
- headers["Authorization"] = f"Bearer {self.api_key}"
271
- else:
272
- headers["Authorization"] = f"Bearer None"
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
273
267
 
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,
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
+ )
311
324
  )
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
325
+ live.update(layout)
326
+ time.sleep(1) # Show final state for 1 second
325
327
 
326
328
 
327
329
  if __name__ == "__main__":
@@ -8,12 +8,7 @@ from edsl.jobs.tasks.task_status_enum import TaskStatus
8
8
 
9
9
 
10
10
  class TaskHistory:
11
- def __init__(
12
- self,
13
- interviews: List["Interview"],
14
- include_traceback: bool = False,
15
- max_interviews: int = 10,
16
- ):
11
+ def __init__(self, interviews: List["Interview"], include_traceback: bool = False):
17
12
  """
18
13
  The structure of a TaskHistory exception
19
14
 
@@ -27,7 +22,6 @@ class TaskHistory:
27
22
  self.include_traceback = include_traceback
28
23
 
29
24
  self._interviews = {index: i for index, i in enumerate(self.total_interviews)}
30
- self.max_interviews = max_interviews
31
25
 
32
26
  @classmethod
33
27
  def example(cls):
@@ -79,21 +73,19 @@ class TaskHistory:
79
73
  """Return a string representation of the TaskHistory."""
80
74
  return f"TaskHistory(interviews={self.total_interviews})."
81
75
 
82
- def to_dict(self, add_edsl_version=True):
76
+ def to_dict(self):
83
77
  """Return the TaskHistory as a dictionary."""
84
- d = {
85
- "interviews": [
86
- i.to_dict(add_edsl_version=add_edsl_version)
87
- for i in self.total_interviews
88
- ],
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],
89
87
  "include_traceback": self.include_traceback,
90
88
  }
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
97
89
 
98
90
  @classmethod
99
91
  def from_dict(cls, data: dict):
@@ -123,11 +115,10 @@ class TaskHistory:
123
115
 
124
116
  def _repr_html_(self):
125
117
  """Return an HTML representation of the TaskHistory."""
126
- d = self.to_dict(add_edsl_version=False)
127
- data = [[k, v] for k, v in d.items()]
128
- from tabulate import tabulate
118
+ from edsl.utilities.utilities import data_to_html
129
119
 
130
- return tabulate(data, headers=["keys", "values"], tablefmt="html")
120
+ newdata = self.to_dict()["exceptions"]
121
+ return data_to_html(newdata, replace_new_lines=True)
131
122
 
132
123
  def show_exceptions(self, tracebacks=False):
133
124
  """Print the exceptions."""
@@ -257,6 +248,8 @@ class TaskHistory:
257
248
  for question_name, exceptions in interview.exceptions.items():
258
249
  for exception in exceptions:
259
250
  exception_type = exception.exception.__class__.__name__
251
+ # exception_type = exception["exception"]
252
+ # breakpoint()
260
253
  if exception_type in exceptions_by_type:
261
254
  exceptions_by_type[exception_type] += 1
262
255
  else:
@@ -343,9 +336,9 @@ class TaskHistory:
343
336
 
344
337
  env = Environment(loader=TemplateLoader("edsl", "templates/error_reporting"))
345
338
 
346
- # Get current memory usage at this point
347
-
339
+ # Load and render a template
348
340
  template = env.get_template("base.html")
341
+ # rendered_template = template.render(your_data=your_data)
349
342
 
350
343
  # Render the template with data
351
344
  output = template.render(
@@ -359,7 +352,6 @@ class TaskHistory:
359
352
  exceptions_by_model=self.exceptions_by_model,
360
353
  exceptions_by_service=self.exceptions_by_service,
361
354
  models_used=models_used,
362
- max_interviews=self.max_interviews,
363
355
  )
364
356
  return output
365
357
 
@@ -369,7 +361,7 @@ class TaskHistory:
369
361
  return_link=False,
370
362
  css=None,
371
363
  cta="Open Report in New Tab",
372
- open_in_browser=False,
364
+ open_in_browser=True,
373
365
  ):
374
366
  """Return an HTML report."""
375
367