edsl 0.1.54__py3-none-any.whl → 0.1.56__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.
- edsl/__init__.py +8 -1
- edsl/__init__original.py +134 -0
- edsl/__version__.py +1 -1
- edsl/agents/agent.py +29 -0
- edsl/agents/agent_list.py +36 -1
- edsl/base/base_class.py +281 -151
- edsl/base/data_transfer_models.py +15 -4
- edsl/buckets/__init__.py +8 -3
- edsl/buckets/bucket_collection.py +9 -3
- edsl/buckets/model_buckets.py +4 -2
- edsl/buckets/token_bucket.py +2 -2
- edsl/buckets/token_bucket_client.py +5 -3
- edsl/caching/cache.py +131 -62
- edsl/caching/cache_entry.py +70 -58
- edsl/caching/sql_dict.py +17 -0
- edsl/cli.py +99 -0
- edsl/config/config_class.py +16 -0
- edsl/conversation/__init__.py +31 -0
- edsl/coop/coop.py +276 -242
- edsl/coop/coop_jobs_objects.py +59 -0
- edsl/coop/coop_objects.py +29 -0
- edsl/coop/coop_regular_objects.py +26 -0
- edsl/coop/utils.py +24 -19
- edsl/dataset/dataset.py +338 -101
- edsl/dataset/dataset_operations_mixin.py +216 -180
- edsl/db_list/sqlite_list.py +349 -0
- edsl/inference_services/__init__.py +40 -5
- edsl/inference_services/exceptions.py +11 -0
- edsl/inference_services/services/anthropic_service.py +5 -2
- edsl/inference_services/services/aws_bedrock.py +6 -2
- edsl/inference_services/services/azure_ai.py +6 -2
- edsl/inference_services/services/google_service.py +7 -3
- edsl/inference_services/services/mistral_ai_service.py +6 -2
- edsl/inference_services/services/open_ai_service.py +6 -2
- edsl/inference_services/services/perplexity_service.py +6 -2
- edsl/inference_services/services/test_service.py +94 -5
- edsl/interviews/answering_function.py +167 -59
- edsl/interviews/interview.py +124 -72
- edsl/interviews/interview_task_manager.py +10 -0
- edsl/interviews/request_token_estimator.py +8 -0
- edsl/invigilators/invigilators.py +35 -13
- edsl/jobs/async_interview_runner.py +146 -104
- edsl/jobs/data_structures.py +6 -4
- edsl/jobs/decorators.py +61 -0
- edsl/jobs/fetch_invigilator.py +61 -18
- edsl/jobs/html_table_job_logger.py +14 -2
- edsl/jobs/jobs.py +180 -104
- edsl/jobs/jobs_component_constructor.py +2 -2
- edsl/jobs/jobs_interview_constructor.py +2 -0
- edsl/jobs/jobs_pricing_estimation.py +154 -113
- edsl/jobs/jobs_remote_inference_logger.py +4 -0
- edsl/jobs/jobs_runner_status.py +30 -25
- edsl/jobs/progress_bar_manager.py +79 -0
- edsl/jobs/remote_inference.py +35 -1
- edsl/key_management/key_lookup_builder.py +6 -1
- edsl/language_models/language_model.py +110 -12
- edsl/language_models/model.py +10 -3
- edsl/language_models/price_manager.py +176 -71
- edsl/language_models/registry.py +5 -0
- edsl/notebooks/notebook.py +77 -10
- edsl/questions/VALIDATION_README.md +134 -0
- edsl/questions/__init__.py +24 -1
- edsl/questions/exceptions.py +21 -0
- edsl/questions/question_dict.py +201 -16
- edsl/questions/question_multiple_choice_with_other.py +624 -0
- edsl/questions/question_registry.py +2 -1
- edsl/questions/templates/multiple_choice_with_other/__init__.py +0 -0
- edsl/questions/templates/multiple_choice_with_other/answering_instructions.jinja +15 -0
- edsl/questions/templates/multiple_choice_with_other/question_presentation.jinja +17 -0
- edsl/questions/validation_analysis.py +185 -0
- edsl/questions/validation_cli.py +131 -0
- edsl/questions/validation_html_report.py +404 -0
- edsl/questions/validation_logger.py +136 -0
- edsl/results/result.py +115 -46
- edsl/results/results.py +702 -171
- edsl/scenarios/construct_download_link.py +16 -3
- edsl/scenarios/directory_scanner.py +226 -226
- edsl/scenarios/file_methods.py +5 -0
- edsl/scenarios/file_store.py +150 -9
- edsl/scenarios/handlers/__init__.py +5 -1
- edsl/scenarios/handlers/mp4_file_store.py +104 -0
- edsl/scenarios/handlers/webm_file_store.py +104 -0
- edsl/scenarios/scenario.py +120 -101
- edsl/scenarios/scenario_list.py +800 -727
- edsl/scenarios/scenario_list_gc_test.py +146 -0
- edsl/scenarios/scenario_list_memory_test.py +214 -0
- edsl/scenarios/scenario_list_source_refactor.md +35 -0
- edsl/scenarios/scenario_selector.py +5 -4
- edsl/scenarios/scenario_source.py +1990 -0
- edsl/scenarios/tests/test_scenario_list_sources.py +52 -0
- edsl/surveys/survey.py +22 -0
- edsl/tasks/__init__.py +4 -2
- edsl/tasks/task_history.py +198 -36
- edsl/tests/scenarios/test_ScenarioSource.py +51 -0
- edsl/tests/scenarios/test_scenario_list_sources.py +51 -0
- edsl/utilities/__init__.py +2 -1
- edsl/utilities/decorators.py +121 -0
- edsl/utilities/memory_debugger.py +1010 -0
- {edsl-0.1.54.dist-info → edsl-0.1.56.dist-info}/METADATA +51 -76
- {edsl-0.1.54.dist-info → edsl-0.1.56.dist-info}/RECORD +103 -79
- edsl/jobs/jobs_runner_asyncio.py +0 -281
- edsl/language_models/unused/fake_openai_service.py +0 -60
- {edsl-0.1.54.dist-info → edsl-0.1.56.dist-info}/LICENSE +0 -0
- {edsl-0.1.54.dist-info → edsl-0.1.56.dist-info}/WHEEL +0 -0
- {edsl-0.1.54.dist-info → edsl-0.1.56.dist-info}/entry_points.txt +0 -0
@@ -5,8 +5,9 @@ This module provides functionality to run multiple interviews in parallel
|
|
5
5
|
with controlled concurrency, supporting both error handling and result collection.
|
6
6
|
"""
|
7
7
|
|
8
|
-
from collections.abc import AsyncGenerator
|
9
|
-
from
|
8
|
+
from collections.abc import AsyncGenerator, AsyncIterator
|
9
|
+
from contextlib import asynccontextmanager
|
10
|
+
from typing import List, Generator, Tuple, TYPE_CHECKING, AsyncIterator
|
10
11
|
from dataclasses import dataclass
|
11
12
|
import asyncio
|
12
13
|
from ..data_transfer_models import EDSLResultObjectInput
|
@@ -22,19 +23,15 @@ if TYPE_CHECKING:
|
|
22
23
|
from ..jobs import Jobs
|
23
24
|
|
24
25
|
@dataclass
|
25
|
-
class
|
26
|
-
"""Container for
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
interview: The Interview object used to conduct the interview
|
31
|
-
order: The original position of this interview in the processing queue
|
32
|
-
"""
|
33
|
-
|
34
|
-
result: Result
|
35
|
-
interview: Interview
|
36
|
-
order: int
|
26
|
+
class InterviewBatch:
|
27
|
+
"""Container for a batch of interviews being processed."""
|
28
|
+
chunks: List[Tuple[int, Interview]]
|
29
|
+
results: List[Tuple[Result, Interview, int]]
|
30
|
+
failed: List[Tuple[int, Interview, Exception]]
|
37
31
|
|
32
|
+
@classmethod
|
33
|
+
def create(cls, chunks: List[Tuple[int, Interview]]) -> 'InterviewBatch':
|
34
|
+
return cls(chunks=chunks, results=[], failed=[])
|
38
35
|
|
39
36
|
class AsyncInterviewRunner:
|
40
37
|
"""
|
@@ -68,6 +65,110 @@ class AsyncInterviewRunner:
|
|
68
65
|
self.run_config = run_config
|
69
66
|
self._initialized = asyncio.Event()
|
70
67
|
|
68
|
+
@asynccontextmanager
|
69
|
+
async def _manage_tasks(self, tasks: List[asyncio.Task]) -> AsyncIterator[None]:
|
70
|
+
"""Context manager for handling task lifecycle and cleanup."""
|
71
|
+
try:
|
72
|
+
yield
|
73
|
+
finally:
|
74
|
+
for task in tasks:
|
75
|
+
if not task.done():
|
76
|
+
task.cancel()
|
77
|
+
|
78
|
+
@asynccontextmanager
|
79
|
+
async def _interview_batch_processor(self) -> AsyncIterator[AsyncGenerator[tuple[Result, Interview, int], None]]:
|
80
|
+
"""Context manager for processing batches of interviews.
|
81
|
+
|
82
|
+
Handles initialization, cleanup, and error management for the entire
|
83
|
+
interview processing lifecycle.
|
84
|
+
"""
|
85
|
+
self._initialized.set()
|
86
|
+
self._current_idx = 0
|
87
|
+
interview_generator = self._expand_interviews()
|
88
|
+
|
89
|
+
try:
|
90
|
+
async def process_batches() -> AsyncGenerator[tuple[Result, Interview, int], None]:
|
91
|
+
while True:
|
92
|
+
chunk = self._get_next_chunk(interview_generator)
|
93
|
+
if not chunk:
|
94
|
+
break
|
95
|
+
|
96
|
+
async with self._process_chunk(chunk) as results:
|
97
|
+
for result_tuple in results:
|
98
|
+
# Yield the full tuple (result, interview, idx)
|
99
|
+
yield result_tuple
|
100
|
+
|
101
|
+
# Clean up chunk to help with garbage collection
|
102
|
+
for idx, interview in chunk:
|
103
|
+
# Explicitly clear any interview references when done with the chunk
|
104
|
+
if hasattr(interview, 'clear_references'):
|
105
|
+
interview.clear_references()
|
106
|
+
del chunk
|
107
|
+
|
108
|
+
yield process_batches()
|
109
|
+
|
110
|
+
finally:
|
111
|
+
# Cleanup code to help garbage collection
|
112
|
+
self._current_idx = 0
|
113
|
+
self._initialized.clear()
|
114
|
+
# Clear the generator to avoid references
|
115
|
+
if 'interview_generator' in locals():
|
116
|
+
del interview_generator
|
117
|
+
|
118
|
+
async def _run_single_interview(
|
119
|
+
self, interview: Interview, idx: int
|
120
|
+
) -> Tuple[Result, Interview, int]:
|
121
|
+
"""Execute a single interview with error handling."""
|
122
|
+
try:
|
123
|
+
await interview.async_conduct_interview(self.run_config)
|
124
|
+
# Create result and explicitly break reference to interview
|
125
|
+
result = Result.from_interview(interview)
|
126
|
+
# Update the status
|
127
|
+
self.run_config.environment.jobs_runner_status.add_completed_interview(
|
128
|
+
interview
|
129
|
+
)
|
130
|
+
# Return tuple that keeps the interview reference
|
131
|
+
return (result, interview, idx)
|
132
|
+
except Exception as e:
|
133
|
+
if self.run_config.parameters.stop_on_exception:
|
134
|
+
raise
|
135
|
+
# Could log the error here if needed
|
136
|
+
return None
|
137
|
+
|
138
|
+
@asynccontextmanager
|
139
|
+
async def _process_chunk(
|
140
|
+
self, chunk: List[Tuple[int, Interview]]
|
141
|
+
) -> AsyncIterator[List[Tuple[Result, Interview, int]]]:
|
142
|
+
"""Process a chunk of interviews concurrently."""
|
143
|
+
tasks = [
|
144
|
+
asyncio.create_task(self._run_single_interview(interview, idx))
|
145
|
+
for idx, interview in chunk
|
146
|
+
]
|
147
|
+
|
148
|
+
async with self._manage_tasks(tasks):
|
149
|
+
results = await asyncio.gather(
|
150
|
+
*tasks,
|
151
|
+
return_exceptions=not self.run_config.parameters.stop_on_exception
|
152
|
+
)
|
153
|
+
# Filter out None results and yield a new list to avoid keeping the original tuple references
|
154
|
+
valid_results = []
|
155
|
+
for r in results:
|
156
|
+
if r is not None:
|
157
|
+
result, interview, idx = r
|
158
|
+
# Create a new tuple to break reference to the original
|
159
|
+
new_tuple = (result, interview, idx)
|
160
|
+
valid_results.append(new_tuple)
|
161
|
+
|
162
|
+
# Clear original tuple to help GC
|
163
|
+
del r
|
164
|
+
|
165
|
+
yield valid_results
|
166
|
+
|
167
|
+
# Manually clean up the valid_results list and its contents to help garbage collection
|
168
|
+
for tup in valid_results:
|
169
|
+
del tup
|
170
|
+
del valid_results
|
171
|
+
|
71
172
|
def _expand_interviews(self) -> Generator["Interview", None, None]:
|
72
173
|
"""
|
73
174
|
Create multiple copies of each interview based on the run configuration.
|
@@ -101,104 +202,45 @@ class AsyncInterviewRunner:
|
|
101
202
|
interview.cache = self.run_config.environment.cache
|
102
203
|
yield interview
|
103
204
|
|
104
|
-
|
105
|
-
self, interview: "Interview"
|
106
|
-
) -> Tuple["Result", "Interview"]:
|
107
|
-
"""
|
108
|
-
Asynchronously conduct a single interview.
|
109
|
-
|
110
|
-
This method performs the interview and creates a Result object with
|
111
|
-
the extracted answers and model responses.
|
112
|
-
|
113
|
-
Args:
|
114
|
-
interview: The interview to conduct
|
115
|
-
|
116
|
-
Returns:
|
117
|
-
Tuple containing the Result object and the Interview object
|
118
|
-
|
119
|
-
Notes:
|
120
|
-
'extracted_answers' contains the processed and validated answers
|
121
|
-
from the interview, which may differ from the raw model output.
|
122
|
-
"""
|
123
|
-
extracted_answers: dict[str, str]
|
124
|
-
model_response_objects: List[EDSLResultObjectInput]
|
125
|
-
|
126
|
-
extracted_answers, model_response_objects = (
|
127
|
-
await interview.async_conduct_interview(self.run_config)
|
128
|
-
)
|
129
|
-
result = Result.from_interview(
|
130
|
-
interview=interview,
|
131
|
-
extracted_answers=extracted_answers,
|
132
|
-
model_response_objects=model_response_objects,
|
133
|
-
)
|
134
|
-
return result, interview
|
135
|
-
|
136
|
-
async def run(
|
205
|
+
def _get_next_chunk(
|
137
206
|
self,
|
138
|
-
|
207
|
+
gen: Generator[Interview, None, None]
|
208
|
+
) -> List[Tuple[int, Interview]]:
|
209
|
+
"""Take interviews from the generator up to MAX_CONCURRENT."""
|
210
|
+
chunk = []
|
211
|
+
while len(chunk) < self.MAX_CONCURRENT:
|
212
|
+
try:
|
213
|
+
interview = next(gen)
|
214
|
+
chunk.append((self._current_idx, interview))
|
215
|
+
self._current_idx += 1
|
216
|
+
except StopIteration:
|
217
|
+
break
|
218
|
+
return chunk
|
219
|
+
|
220
|
+
async def run(self) -> AsyncGenerator[tuple[Result, Interview, int], None]:
|
139
221
|
"""
|
140
222
|
Run all interviews asynchronously and yield results as they complete.
|
141
223
|
|
142
|
-
This method
|
143
|
-
maintaining controlled concurrency
|
224
|
+
This method orchestrates the parallel execution of interviews while
|
225
|
+
maintaining controlled concurrency. Results are yielded as soon as
|
144
226
|
they become available.
|
145
227
|
|
146
228
|
Yields:
|
147
|
-
Tuples of (Result, Interview) as interviews complete
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
- Ensures task cleanup even in case of failures
|
229
|
+
Tuples of (Result, Interview, idx) as interviews complete, where idx is the
|
230
|
+
original position index of the interview.
|
231
|
+
|
232
|
+
Raises:
|
233
|
+
Exception: If stop_on_exception is True and any interview fails
|
153
234
|
"""
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
interview
|
164
|
-
)
|
165
|
-
result.order = idx
|
166
|
-
return InterviewResult(result, interview, idx)
|
167
|
-
except Exception:
|
168
|
-
if self.run_config.parameters.stop_on_exception:
|
169
|
-
raise
|
170
|
-
return None
|
171
|
-
|
172
|
-
# Process interviews in chunks
|
173
|
-
for i in range(0, len(interviews), self.MAX_CONCURRENT):
|
174
|
-
chunk = interviews[i : i + self.MAX_CONCURRENT]
|
175
|
-
tasks = [
|
176
|
-
asyncio.create_task(_process_single_interview(interview, idx))
|
177
|
-
for idx, interview in enumerate(chunk, start=i)
|
178
|
-
]
|
179
|
-
|
180
|
-
try:
|
181
|
-
# Wait for all tasks in the chunk to complete
|
182
|
-
results = await asyncio.gather(
|
183
|
-
*tasks,
|
184
|
-
return_exceptions=not self.run_config.parameters.stop_on_exception
|
185
|
-
)
|
186
|
-
|
187
|
-
# Process successful results
|
188
|
-
for result in (r for r in results if r is not None):
|
189
|
-
yield result.result, result.interview
|
190
|
-
|
191
|
-
except Exception:
|
192
|
-
if self.run_config.parameters.stop_on_exception:
|
193
|
-
raise
|
194
|
-
continue
|
195
|
-
|
196
|
-
finally:
|
197
|
-
# Clean up any remaining tasks
|
198
|
-
for task in tasks:
|
199
|
-
if not task.done():
|
200
|
-
task.cancel()
|
201
|
-
|
235
|
+
async with self._interview_batch_processor() as processor:
|
236
|
+
async for result_tuple in processor:
|
237
|
+
# For each result tuple in the processor
|
238
|
+
result, interview, idx = result_tuple
|
239
|
+
# Yield a new tuple to break reference to the original tuple
|
240
|
+
yield result, interview, idx
|
241
|
+
|
242
|
+
# Help garbage collection by removing references
|
243
|
+
del result_tuple
|
202
244
|
|
203
245
|
if __name__ == "__main__":
|
204
246
|
import doctest
|
edsl/jobs/data_structures.py
CHANGED
@@ -1,11 +1,11 @@
|
|
1
|
-
from typing import Optional, Literal, TYPE_CHECKING
|
1
|
+
from typing import Optional, Literal, TYPE_CHECKING, Any
|
2
2
|
from dataclasses import dataclass, asdict
|
3
3
|
from collections import UserDict
|
4
4
|
from ..data_transfer_models import EDSLResultObjectInput
|
5
5
|
|
6
6
|
# from edsl.data_transfer_models import VisibilityType
|
7
7
|
from ..caching import Cache
|
8
|
-
|
8
|
+
# Import BucketCollection lazily to avoid circular imports
|
9
9
|
from ..key_management import KeyLookup
|
10
10
|
from ..base import Base
|
11
11
|
|
@@ -14,6 +14,7 @@ from .jobs_runner_status import JobsRunnerStatus
|
|
14
14
|
if TYPE_CHECKING:
|
15
15
|
from ..questions.question_base import QuestionBase
|
16
16
|
from ..surveys import Survey
|
17
|
+
from ..buckets import BucketCollection
|
17
18
|
|
18
19
|
VisibilityType = Literal["private", "public", "unlisted"]
|
19
20
|
|
@@ -33,7 +34,7 @@ class RunEnvironment:
|
|
33
34
|
jobs_runner_status (JobsRunnerStatus, optional): Tracker for job execution progress
|
34
35
|
"""
|
35
36
|
cache: Optional[Cache] = None
|
36
|
-
bucket_collection: Optional[
|
37
|
+
bucket_collection: Optional[Any] = None # Using Any to avoid circular import of BucketCollection
|
37
38
|
key_lookup: Optional[KeyLookup] = None
|
38
39
|
jobs_runner_status: Optional["JobsRunnerStatus"] = None
|
39
40
|
|
@@ -82,6 +83,7 @@ class RunParameters(Base):
|
|
82
83
|
disable_remote_inference: bool = False
|
83
84
|
job_uuid: Optional[str] = None
|
84
85
|
fresh: bool = False # if True, will not use cache and will save new results to cache
|
86
|
+
memory_threshold: Optional[int] = None # Threshold in bytes for Results SQLList memory management
|
85
87
|
|
86
88
|
def to_dict(self, add_edsl_version=False) -> dict:
|
87
89
|
d = asdict(self)
|
@@ -131,7 +133,7 @@ class RunConfig:
|
|
131
133
|
"""
|
132
134
|
self.environment = environment
|
133
135
|
|
134
|
-
def add_bucket_collection(self, bucket_collection: BucketCollection) -> None:
|
136
|
+
def add_bucket_collection(self, bucket_collection: "BucketCollection") -> None:
|
135
137
|
"""
|
136
138
|
Set or replace the bucket collection in the environment.
|
137
139
|
|
edsl/jobs/decorators.py
CHANGED
@@ -1,6 +1,67 @@
|
|
1
1
|
from functools import wraps
|
2
2
|
from threading import RLock
|
3
3
|
import inspect
|
4
|
+
from typing import Optional, Union, TypeVar, Callable, cast
|
5
|
+
|
6
|
+
try:
|
7
|
+
from typing import ParamSpec
|
8
|
+
except ImportError:
|
9
|
+
from typing_extensions import ParamSpec
|
10
|
+
|
11
|
+
from ..jobs.data_structures import RunEnvironment, RunParameters, RunConfig
|
12
|
+
|
13
|
+
|
14
|
+
P = ParamSpec("P")
|
15
|
+
T = TypeVar("T")
|
16
|
+
|
17
|
+
|
18
|
+
def with_config(f: Callable[P, T]) -> Callable[P, T]:
|
19
|
+
"""
|
20
|
+
Decorator that processes function parameters to match the RunConfig dataclass structure.
|
21
|
+
|
22
|
+
This decorator is used primarily with the run() and run_async() methods to provide
|
23
|
+
a consistent interface for job configuration while maintaining a clean API.
|
24
|
+
|
25
|
+
The decorator:
|
26
|
+
1. Extracts environment-related parameters into a RunEnvironment instance
|
27
|
+
2. Extracts execution-related parameters into a RunParameters instance
|
28
|
+
3. Combines both into a single RunConfig object
|
29
|
+
4. Passes this RunConfig to the decorated function as a keyword argument
|
30
|
+
|
31
|
+
Parameters:
|
32
|
+
f (Callable): The function to decorate, typically run() or run_async()
|
33
|
+
|
34
|
+
Returns:
|
35
|
+
Callable: A wrapped function that accepts all RunConfig parameters directly
|
36
|
+
|
37
|
+
Example:
|
38
|
+
@with_config
|
39
|
+
def run(self, *, config: RunConfig) -> Results:
|
40
|
+
# Function can now access config.parameters and config.environment
|
41
|
+
"""
|
42
|
+
parameter_fields = {
|
43
|
+
name: field.default
|
44
|
+
for name, field in RunParameters.__dataclass_fields__.items()
|
45
|
+
}
|
46
|
+
environment_fields = {
|
47
|
+
name: field.default
|
48
|
+
for name, field in RunEnvironment.__dataclass_fields__.items()
|
49
|
+
}
|
50
|
+
# Combined fields dict used for reference during development
|
51
|
+
# combined = {**parameter_fields, **environment_fields}
|
52
|
+
|
53
|
+
@wraps(f)
|
54
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
55
|
+
environment = RunEnvironment(
|
56
|
+
**{k: v for k, v in kwargs.items() if k in environment_fields}
|
57
|
+
)
|
58
|
+
parameters = RunParameters(
|
59
|
+
**{k: v for k, v in kwargs.items() if k in parameter_fields}
|
60
|
+
)
|
61
|
+
config = RunConfig(environment=environment, parameters=parameters)
|
62
|
+
return f(*args, config=config)
|
63
|
+
|
64
|
+
return cast(Callable[P, T], wrapper)
|
4
65
|
|
5
66
|
|
6
67
|
def synchronized_class(wrapped_class):
|
edsl/jobs/fetch_invigilator.py
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
from typing import Dict, Any, Optional, TYPE_CHECKING
|
2
|
+
import weakref
|
2
3
|
|
3
4
|
if TYPE_CHECKING:
|
4
5
|
from ..questions import QuestionBase
|
5
6
|
from ..agents import InvigilatorBase
|
6
|
-
from ..
|
7
|
-
from
|
7
|
+
from ..key_management import KeyLookup
|
8
|
+
from ..interviews import Interview
|
8
9
|
|
9
10
|
|
10
11
|
class FetchInvigilator:
|
@@ -14,34 +15,76 @@ class FetchInvigilator:
|
|
14
15
|
current_answers: Optional[Dict[str, Any]] = None,
|
15
16
|
key_lookup: Optional["KeyLookup"] = None,
|
16
17
|
):
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
18
|
+
# Store a weak reference to the interview instead of a strong reference
|
19
|
+
self._interview_ref = weakref.ref(interview)
|
20
|
+
|
21
|
+
# Store external parameters that don't create reference cycles
|
22
|
+
self._current_answers = current_answers
|
22
23
|
self.key_lookup = key_lookup
|
23
24
|
|
25
|
+
@property
|
26
|
+
def interview(self):
|
27
|
+
"""Access the interview via weak reference if it still exists."""
|
28
|
+
interview = self._interview_ref()
|
29
|
+
if interview is None:
|
30
|
+
raise RuntimeError("Interview has been garbage collected")
|
31
|
+
return interview
|
32
|
+
|
33
|
+
@property
|
34
|
+
def _scenario(self):
|
35
|
+
return self.interview.scenario
|
36
|
+
|
37
|
+
@property
|
38
|
+
def _model(self):
|
39
|
+
return self.interview.model
|
40
|
+
|
41
|
+
@property
|
42
|
+
def _survey(self):
|
43
|
+
return self.interview.survey
|
44
|
+
|
45
|
+
@property
|
46
|
+
def _agent(self):
|
47
|
+
return self.interview.agent
|
48
|
+
|
49
|
+
@property
|
50
|
+
def _iteration(self):
|
51
|
+
return self.interview.iteration
|
52
|
+
|
53
|
+
@property
|
54
|
+
def _cache(self):
|
55
|
+
return self.interview.cache
|
56
|
+
|
57
|
+
@property
|
58
|
+
def _raise_validation_errors(self):
|
59
|
+
return self.interview.raise_validation_errors
|
60
|
+
|
61
|
+
@property
|
62
|
+
def current_answers(self):
|
63
|
+
if self._current_answers is not None:
|
64
|
+
return self._current_answers
|
65
|
+
return self.interview.answers
|
66
|
+
|
24
67
|
def get_invigilator(self, question: "QuestionBase") -> "InvigilatorBase":
|
25
68
|
"""Return an invigilator for the given question.
|
26
69
|
|
27
70
|
:param question: the question to be answered
|
28
71
|
:param debug: whether to use debug mode, in which case `InvigilatorDebug` is used.
|
29
72
|
"""
|
30
|
-
|
31
|
-
invigilator = self.
|
73
|
+
# Use cached properties instead of accessing through the interview reference
|
74
|
+
invigilator = self._agent.create_invigilator(
|
32
75
|
question=question,
|
33
|
-
scenario=self.
|
34
|
-
model=self.
|
35
|
-
survey=self.
|
36
|
-
memory_plan=self.
|
37
|
-
current_answers=self.current_answers,
|
38
|
-
iteration=self.
|
39
|
-
cache=self.
|
40
|
-
raise_validation_errors=self.
|
76
|
+
scenario=self._scenario,
|
77
|
+
model=self._model,
|
78
|
+
survey=self._survey,
|
79
|
+
memory_plan=self._survey.memory_plan,
|
80
|
+
current_answers=self.current_answers,
|
81
|
+
iteration=self._iteration,
|
82
|
+
cache=self._cache,
|
83
|
+
raise_validation_errors=self._raise_validation_errors,
|
41
84
|
key_lookup=self.key_lookup,
|
42
85
|
)
|
43
|
-
"""Return an invigilator for the given question."""
|
44
86
|
return invigilator
|
45
87
|
|
46
88
|
def __call__(self, question):
|
47
89
|
return self.get_invigilator(question)
|
90
|
+
|
@@ -362,7 +362,11 @@ class HTMLTableJobLogger(JobLogger):
|
|
362
362
|
other_fields = []
|
363
363
|
|
364
364
|
for field, _ in self.jobs_info.__annotations__.items():
|
365
|
-
if field
|
365
|
+
if field not in [
|
366
|
+
"pretty_names",
|
367
|
+
"completed_interviews",
|
368
|
+
"failed_interviews",
|
369
|
+
]:
|
366
370
|
value = getattr(self.jobs_info, field)
|
367
371
|
if not value:
|
368
372
|
continue
|
@@ -522,6 +526,14 @@ class HTMLTableJobLogger(JobLogger):
|
|
522
526
|
|
523
527
|
display_style = "block" if self.is_expanded else "none"
|
524
528
|
|
529
|
+
header_status_text = status_text
|
530
|
+
if (
|
531
|
+
current_status == JobsStatus.PARTIALLY_FAILED
|
532
|
+
and self.jobs_info.completed_interviews is not None
|
533
|
+
and self.jobs_info.failed_interviews is not None
|
534
|
+
):
|
535
|
+
header_status_text += f" ({self.jobs_info.completed_interviews:,} completed, {self.jobs_info.failed_interviews:,} failed)"
|
536
|
+
|
525
537
|
return f"""
|
526
538
|
{css}
|
527
539
|
<div class="jobs-container">
|
@@ -539,7 +551,7 @@ class HTMLTableJobLogger(JobLogger):
|
|
539
551
|
<span id="arrow-{self.log_id}" class="expand-toggle">{'⌃' if self.is_expanded else '⌄'}</span>
|
540
552
|
Job Status 🦜
|
541
553
|
</div>
|
542
|
-
<div class="{status_class}">{
|
554
|
+
<div class="{status_class}">{header_status_text}</div>
|
543
555
|
</div>
|
544
556
|
<div id="content-{self.log_id}" class="jobs-content" style="display: {display_style};">
|
545
557
|
{content_html}
|