edsl 0.1.51__py3-none-any.whl → 0.1.53__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 +45 -34
- edsl/__version__.py +1 -1
- edsl/conversation/Conversation.py +2 -1
- edsl/coop/coop.py +2 -0
- edsl/interviews/answering_function.py +20 -21
- edsl/interviews/exception_tracking.py +4 -3
- edsl/interviews/interview_task_manager.py +5 -2
- edsl/interviews/request_token_estimator.py +104 -2
- edsl/invigilators/invigilators.py +37 -4
- edsl/jobs/html_table_job_logger.py +494 -257
- edsl/jobs/jobs_status_enums.py +1 -0
- edsl/jobs/remote_inference.py +46 -12
- edsl/language_models/language_model.py +148 -146
- edsl/results/results.py +31 -2
- edsl/scenarios/file_store.py +73 -23
- edsl/tasks/task_history.py +45 -8
- edsl/templates/error_reporting/base.html +37 -4
- edsl/templates/error_reporting/exceptions_table.html +105 -33
- edsl/templates/error_reporting/interview_details.html +130 -126
- edsl/templates/error_reporting/overview.html +21 -25
- edsl/templates/error_reporting/report.css +215 -46
- edsl/templates/error_reporting/report.js +122 -20
- {edsl-0.1.51.dist-info → edsl-0.1.53.dist-info}/METADATA +1 -1
- {edsl-0.1.51.dist-info → edsl-0.1.53.dist-info}/RECORD +27 -27
- {edsl-0.1.51.dist-info → edsl-0.1.53.dist-info}/LICENSE +0 -0
- {edsl-0.1.51.dist-info → edsl-0.1.53.dist-info}/WHEEL +0 -0
- {edsl-0.1.51.dist-info → edsl-0.1.53.dist-info}/entry_points.txt +0 -0
@@ -10,7 +10,7 @@ Key concepts and terminology:
|
|
10
10
|
- raw_response: The complete JSON response returned directly from the model API.
|
11
11
|
Contains all model metadata and response information.
|
12
12
|
|
13
|
-
- edsl_augmented_response: The raw model response augmented with EDSL-specific
|
13
|
+
- edsl_augmented_response: The raw model response augmented with EDSL-specific
|
14
14
|
information, such as cache keys, token usage statistics, and cost data.
|
15
15
|
|
16
16
|
- generated_tokens: The actual text output generated by the model in response
|
@@ -62,19 +62,21 @@ from ..key_management import KeyLookupCollection
|
|
62
62
|
from .registry import RegisterLanguageModelsMeta
|
63
63
|
from .raw_response_handler import RawResponseHandler
|
64
64
|
|
65
|
+
|
65
66
|
def handle_key_error(func):
|
66
67
|
"""Decorator to catch and provide user-friendly error messages for KeyError exceptions.
|
67
|
-
|
68
|
+
|
68
69
|
This decorator gracefully handles KeyError exceptions that may occur when parsing
|
69
70
|
model responses with unexpected structures, providing a clear error message to
|
70
71
|
help users understand what went wrong.
|
71
|
-
|
72
|
+
|
72
73
|
Args:
|
73
74
|
func: The function to decorate
|
74
|
-
|
75
|
+
|
75
76
|
Returns:
|
76
77
|
Decorated function that catches KeyError exceptions
|
77
78
|
"""
|
79
|
+
|
78
80
|
@wraps(func)
|
79
81
|
def wrapper(*args, **kwargs):
|
80
82
|
try:
|
@@ -89,20 +91,21 @@ def handle_key_error(func):
|
|
89
91
|
|
90
92
|
class classproperty:
|
91
93
|
"""Descriptor that combines @classmethod and @property behaviors.
|
92
|
-
|
94
|
+
|
93
95
|
This descriptor allows defining properties that work on the class itself
|
94
96
|
rather than on instances, making it possible to have computed attributes
|
95
97
|
at the class level.
|
96
|
-
|
98
|
+
|
97
99
|
Usage:
|
98
100
|
class MyClass:
|
99
101
|
@classproperty
|
100
102
|
def my_prop(cls):
|
101
103
|
return cls.__name__
|
102
104
|
"""
|
105
|
+
|
103
106
|
def __init__(self, method):
|
104
107
|
"""Initialize with the decorated method.
|
105
|
-
|
108
|
+
|
106
109
|
Args:
|
107
110
|
method: The class method to be accessed as a property
|
108
111
|
"""
|
@@ -110,19 +113,17 @@ class classproperty:
|
|
110
113
|
|
111
114
|
def __get__(self, instance, cls):
|
112
115
|
"""Return the result of calling the method on the class.
|
113
|
-
|
116
|
+
|
114
117
|
Args:
|
115
118
|
instance: The instance (if called on an instance)
|
116
119
|
cls: The class (always provided)
|
117
|
-
|
120
|
+
|
118
121
|
Returns:
|
119
122
|
The result of calling the method with the class as argument
|
120
123
|
"""
|
121
124
|
return self.method(cls)
|
122
125
|
|
123
126
|
|
124
|
-
|
125
|
-
|
126
127
|
class LanguageModel(
|
127
128
|
PersistenceMixin,
|
128
129
|
RepresentationMixin,
|
@@ -131,19 +132,19 @@ class LanguageModel(
|
|
131
132
|
metaclass=RegisterLanguageModelsMeta,
|
132
133
|
):
|
133
134
|
"""Abstract base class for all language model implementations in EDSL.
|
134
|
-
|
135
|
+
|
135
136
|
This class defines the common interface and functionality for interacting with
|
136
137
|
various language model providers (OpenAI, Anthropic, etc.). It handles caching,
|
137
138
|
response parsing, token usage tracking, and cost calculation, providing a
|
138
139
|
consistent interface regardless of the underlying model.
|
139
|
-
|
140
|
+
|
140
141
|
Subclasses must implement the async_execute_model_call method to handle the
|
141
142
|
actual API call to the model provider. Other methods may also be overridden
|
142
143
|
to customize behavior for specific models.
|
143
|
-
|
144
|
+
|
144
145
|
The class uses several mixins to provide serialization, pretty printing, and
|
145
146
|
hashing functionality, and a metaclass to automatically register model implementations.
|
146
|
-
|
147
|
+
|
147
148
|
Attributes:
|
148
149
|
_model_: The default model identifier (set by subclasses)
|
149
150
|
key_sequence: Path to extract generated text from model responses
|
@@ -162,11 +163,11 @@ class LanguageModel(
|
|
162
163
|
@classproperty
|
163
164
|
def response_handler(cls):
|
164
165
|
"""Get a handler for processing raw model responses.
|
165
|
-
|
166
|
+
|
166
167
|
This property creates a RawResponseHandler configured for the specific
|
167
168
|
model implementation, using the class's key_sequence and usage_sequence
|
168
169
|
attributes to know how to extract information from the model's response format.
|
169
|
-
|
170
|
+
|
170
171
|
Returns:
|
171
172
|
RawResponseHandler: Handler configured for this model type
|
172
173
|
"""
|
@@ -183,31 +184,31 @@ class LanguageModel(
|
|
183
184
|
**kwargs,
|
184
185
|
):
|
185
186
|
"""Initialize a new language model instance.
|
186
|
-
|
187
|
+
|
187
188
|
Args:
|
188
189
|
tpm: Optional tokens per minute rate limit override
|
189
190
|
rpm: Optional requests per minute rate limit override
|
190
191
|
omit_system_prompt_if_empty_string: Whether to omit the system prompt when empty
|
191
192
|
key_lookup: Optional custom key lookup for API credentials
|
192
193
|
**kwargs: Additional parameters to pass to the model provider
|
193
|
-
|
194
|
+
|
194
195
|
The initialization process:
|
195
196
|
1. Sets up the model identifier from the class attribute
|
196
197
|
2. Configures model parameters by merging defaults with provided values
|
197
198
|
3. Sets up API key lookup and rate limits
|
198
199
|
4. Applies all parameters as instance attributes
|
199
|
-
|
200
|
+
|
200
201
|
For subclasses that define _parameters_ class attribute, these will be
|
201
202
|
used as default parameters that can be overridden by kwargs.
|
202
203
|
"""
|
203
204
|
# Get the model identifier from the class attribute
|
204
205
|
self.model = getattr(self, "_model_", None)
|
205
|
-
|
206
|
+
|
206
207
|
# Set up model parameters by combining defaults with provided values
|
207
208
|
default_parameters = getattr(self, "_parameters_", None)
|
208
209
|
parameters = self._overide_default_parameters(kwargs, default_parameters)
|
209
210
|
self.parameters = parameters
|
210
|
-
|
211
|
+
|
211
212
|
# Initialize basic settings
|
212
213
|
self.remote = False
|
213
214
|
self.omit_system_prompt_if_empty = omit_system_prompt_if_empty_string
|
@@ -231,7 +232,7 @@ class LanguageModel(
|
|
231
232
|
for key, value in kwargs.items():
|
232
233
|
if key not in parameters:
|
233
234
|
setattr(self, key, value)
|
234
|
-
|
235
|
+
|
235
236
|
# Handle API key check skip for testing
|
236
237
|
if kwargs.get("skip_api_key_check", False):
|
237
238
|
# Skip the API key check. Sometimes this is useful for testing.
|
@@ -243,13 +244,13 @@ class LanguageModel(
|
|
243
244
|
|
244
245
|
def _set_key_lookup(self, key_lookup: "KeyLookup") -> "KeyLookup":
|
245
246
|
"""Set up the API key lookup mechanism.
|
246
|
-
|
247
|
+
|
247
248
|
This method either uses the provided key lookup object or creates a default
|
248
249
|
one that looks for API keys in config files and environment variables.
|
249
|
-
|
250
|
+
|
250
251
|
Args:
|
251
252
|
key_lookup: Optional custom key lookup object
|
252
|
-
|
253
|
+
|
253
254
|
Returns:
|
254
255
|
KeyLookup: The key lookup object to use for API credentials
|
255
256
|
"""
|
@@ -262,10 +263,10 @@ class LanguageModel(
|
|
262
263
|
|
263
264
|
def set_key_lookup(self, key_lookup: "KeyLookup") -> None:
|
264
265
|
"""Update the key lookup mechanism after initialization.
|
265
|
-
|
266
|
+
|
266
267
|
This method allows changing the API key lookup after the model has been
|
267
268
|
created, clearing any cached API tokens.
|
268
|
-
|
269
|
+
|
269
270
|
Args:
|
270
271
|
key_lookup: The new key lookup object to use
|
271
272
|
"""
|
@@ -275,13 +276,13 @@ class LanguageModel(
|
|
275
276
|
|
276
277
|
def ask_question(self, question: "QuestionBase") -> str:
|
277
278
|
"""Ask a question using this language model and return the response.
|
278
|
-
|
279
|
+
|
279
280
|
This is a convenience method that extracts the necessary prompts from a
|
280
281
|
question object and makes a model call.
|
281
|
-
|
282
|
+
|
282
283
|
Args:
|
283
284
|
question: The EDSL question object to ask
|
284
|
-
|
285
|
+
|
285
286
|
Returns:
|
286
287
|
str: The model's response to the question
|
287
288
|
"""
|
@@ -292,10 +293,10 @@ class LanguageModel(
|
|
292
293
|
@property
|
293
294
|
def rpm(self):
|
294
295
|
"""Get the requests per minute rate limit for this model.
|
295
|
-
|
296
|
+
|
296
297
|
This property provides the rate limit either from an explicitly set value,
|
297
298
|
from the model info in the key lookup, or from the default value.
|
298
|
-
|
299
|
+
|
299
300
|
Returns:
|
300
301
|
float: The requests per minute rate limit
|
301
302
|
"""
|
@@ -309,10 +310,10 @@ class LanguageModel(
|
|
309
310
|
@property
|
310
311
|
def tpm(self):
|
311
312
|
"""Get the tokens per minute rate limit for this model.
|
312
|
-
|
313
|
+
|
313
314
|
This property provides the rate limit either from an explicitly set value,
|
314
315
|
from the model info in the key lookup, or from the default value.
|
315
|
-
|
316
|
+
|
316
317
|
Returns:
|
317
318
|
float: The tokens per minute rate limit
|
318
319
|
"""
|
@@ -327,7 +328,7 @@ class LanguageModel(
|
|
327
328
|
@tpm.setter
|
328
329
|
def tpm(self, value):
|
329
330
|
"""Set the tokens per minute rate limit.
|
330
|
-
|
331
|
+
|
331
332
|
Args:
|
332
333
|
value: The new tokens per minute limit
|
333
334
|
"""
|
@@ -336,7 +337,7 @@ class LanguageModel(
|
|
336
337
|
@rpm.setter
|
337
338
|
def rpm(self, value):
|
338
339
|
"""Set the requests per minute rate limit.
|
339
|
-
|
340
|
+
|
340
341
|
Args:
|
341
342
|
value: The new requests per minute limit
|
342
343
|
"""
|
@@ -345,13 +346,13 @@ class LanguageModel(
|
|
345
346
|
@property
|
346
347
|
def api_token(self) -> str:
|
347
348
|
"""Get the API token for this model's service.
|
348
|
-
|
349
|
+
|
349
350
|
This property lazily fetches the API token from the key lookup
|
350
351
|
mechanism when first accessed, caching it for subsequent uses.
|
351
|
-
|
352
|
+
|
352
353
|
Returns:
|
353
354
|
str: The API token for authenticating with the model provider
|
354
|
-
|
355
|
+
|
355
356
|
Raises:
|
356
357
|
ValueError: If no API key is found for this model's service
|
357
358
|
"""
|
@@ -366,10 +367,10 @@ class LanguageModel(
|
|
366
367
|
|
367
368
|
def __getitem__(self, key):
|
368
369
|
"""Allow dictionary-style access to model attributes.
|
369
|
-
|
370
|
+
|
370
371
|
Args:
|
371
372
|
key: The attribute name to access
|
372
|
-
|
373
|
+
|
373
374
|
Returns:
|
374
375
|
The value of the specified attribute
|
375
376
|
"""
|
@@ -377,13 +378,13 @@ class LanguageModel(
|
|
377
378
|
|
378
379
|
def hello(self, verbose=False):
|
379
380
|
"""Run a simple test to verify the model connection is working.
|
380
|
-
|
381
|
+
|
381
382
|
This method makes a basic model call to check if the API credentials
|
382
383
|
are valid and the model is responsive.
|
383
|
-
|
384
|
+
|
384
385
|
Args:
|
385
386
|
verbose: If True, prints the masked API token
|
386
|
-
|
387
|
+
|
387
388
|
Returns:
|
388
389
|
str: The model's response to a simple greeting
|
389
390
|
"""
|
@@ -397,14 +398,14 @@ class LanguageModel(
|
|
397
398
|
|
398
399
|
def has_valid_api_key(self) -> bool:
|
399
400
|
"""Check if the model has a valid API key available.
|
400
|
-
|
401
|
+
|
401
402
|
This method verifies if the necessary API key is available in
|
402
403
|
environment variables or configuration for this model's service.
|
403
404
|
Test models always return True.
|
404
|
-
|
405
|
+
|
405
406
|
Returns:
|
406
407
|
bool: True if a valid API key is available, False otherwise
|
407
|
-
|
408
|
+
|
408
409
|
Examples:
|
409
410
|
>>> LanguageModel.example().has_valid_api_key() : # doctest: +SKIP
|
410
411
|
True
|
@@ -420,14 +421,14 @@ class LanguageModel(
|
|
420
421
|
|
421
422
|
def __hash__(self) -> int:
|
422
423
|
"""Generate a hash value based on model identity and parameters.
|
423
|
-
|
424
|
+
|
424
425
|
This method allows language model instances to be used as dictionary
|
425
426
|
keys or in sets by providing a stable hash value based on the
|
426
427
|
model's essential characteristics.
|
427
|
-
|
428
|
+
|
428
429
|
Returns:
|
429
430
|
int: A hash value for the model instance
|
430
|
-
|
431
|
+
|
431
432
|
Examples:
|
432
433
|
>>> m = LanguageModel.example()
|
433
434
|
>>> hash(m) # Actual value may vary
|
@@ -437,17 +438,17 @@ class LanguageModel(
|
|
437
438
|
|
438
439
|
def __eq__(self, other) -> bool:
|
439
440
|
"""Check if two language model instances are functionally equivalent.
|
440
|
-
|
441
|
+
|
441
442
|
Two models are considered equal if they have the same model identifier
|
442
443
|
and the same parameter settings, meaning they would produce the same
|
443
444
|
outputs given the same inputs.
|
444
|
-
|
445
|
+
|
445
446
|
Args:
|
446
447
|
other: Another model to compare with
|
447
|
-
|
448
|
+
|
448
449
|
Returns:
|
449
450
|
bool: True if the models are functionally equivalent
|
450
|
-
|
451
|
+
|
451
452
|
Examples:
|
452
453
|
>>> m1 = LanguageModel.example()
|
453
454
|
>>> m2 = LanguageModel.example()
|
@@ -459,33 +460,33 @@ class LanguageModel(
|
|
459
460
|
@staticmethod
|
460
461
|
def _overide_default_parameters(passed_parameter_dict, default_parameter_dict):
|
461
462
|
"""Merge default parameters with user-specified parameters.
|
462
|
-
|
463
|
+
|
463
464
|
This method creates a parameter dictionary where explicitly passed
|
464
465
|
parameters take precedence over default values, while ensuring all
|
465
466
|
required parameters have a value.
|
466
|
-
|
467
|
+
|
467
468
|
Args:
|
468
469
|
passed_parameter_dict: Dictionary of user-specified parameters
|
469
470
|
default_parameter_dict: Dictionary of default parameter values
|
470
|
-
|
471
|
+
|
471
472
|
Returns:
|
472
473
|
dict: Combined parameter dictionary with defaults and overrides
|
473
|
-
|
474
|
+
|
474
475
|
Examples:
|
475
476
|
>>> LanguageModel._overide_default_parameters(
|
476
|
-
... passed_parameter_dict={"temperature": 0.5},
|
477
|
+
... passed_parameter_dict={"temperature": 0.5},
|
477
478
|
... default_parameter_dict={"temperature": 0.9})
|
478
479
|
{'temperature': 0.5}
|
479
|
-
|
480
|
+
|
480
481
|
>>> LanguageModel._overide_default_parameters(
|
481
|
-
... passed_parameter_dict={"temperature": 0.5},
|
482
|
+
... passed_parameter_dict={"temperature": 0.5},
|
482
483
|
... default_parameter_dict={"temperature": 0.9, "max_tokens": 1000})
|
483
484
|
{'temperature': 0.5, 'max_tokens': 1000}
|
484
485
|
"""
|
485
486
|
# Handle the case when data is loaded from a dict after serialization
|
486
487
|
if "parameters" in passed_parameter_dict:
|
487
488
|
passed_parameter_dict = passed_parameter_dict["parameters"]
|
488
|
-
|
489
|
+
|
489
490
|
# Create new dict with defaults, overridden by passed parameters
|
490
491
|
return {
|
491
492
|
parameter_name: passed_parameter_dict.get(parameter_name, default_value)
|
@@ -494,14 +495,14 @@ class LanguageModel(
|
|
494
495
|
|
495
496
|
def __call__(self, user_prompt: str, system_prompt: str):
|
496
497
|
"""Allow the model to be called directly as a function.
|
497
|
-
|
498
|
+
|
498
499
|
This method provides a convenient way to use the model by calling
|
499
500
|
it directly with prompts, like `response = model(user_prompt, system_prompt)`.
|
500
|
-
|
501
|
+
|
501
502
|
Args:
|
502
503
|
user_prompt: The user message or input prompt
|
503
504
|
system_prompt: The system message or context
|
504
|
-
|
505
|
+
|
505
506
|
Returns:
|
506
507
|
The response from the model
|
507
508
|
"""
|
@@ -510,17 +511,17 @@ class LanguageModel(
|
|
510
511
|
@abstractmethod
|
511
512
|
async def async_execute_model_call(self, user_prompt: str, system_prompt: str):
|
512
513
|
"""Execute the model call asynchronously.
|
513
|
-
|
514
|
+
|
514
515
|
This abstract method must be implemented by all model subclasses
|
515
516
|
to handle the actual API call to the language model provider.
|
516
|
-
|
517
|
+
|
517
518
|
Args:
|
518
519
|
user_prompt: The user message or input prompt
|
519
520
|
system_prompt: The system message or context
|
520
|
-
|
521
|
+
|
521
522
|
Returns:
|
522
523
|
Coroutine that resolves to the model response
|
523
|
-
|
524
|
+
|
524
525
|
Note:
|
525
526
|
Implementations should handle the actual API communication,
|
526
527
|
including authentication, request formatting, and response parsing.
|
@@ -531,15 +532,15 @@ class LanguageModel(
|
|
531
532
|
self, user_prompt: str, system_prompt: str
|
532
533
|
):
|
533
534
|
"""Execute the model call remotely through the EDSL Coop service.
|
534
|
-
|
535
|
+
|
535
536
|
This method allows offloading the model call to a remote server,
|
536
537
|
which can be useful for models not available in the local environment
|
537
538
|
or to avoid rate limits.
|
538
|
-
|
539
|
+
|
539
540
|
Args:
|
540
541
|
user_prompt: The user message or input prompt
|
541
542
|
system_prompt: The system message or context
|
542
|
-
|
543
|
+
|
543
544
|
Returns:
|
544
545
|
Coroutine that resolves to the model response from the remote service
|
545
546
|
"""
|
@@ -554,18 +555,19 @@ class LanguageModel(
|
|
554
555
|
@jupyter_nb_handler
|
555
556
|
def execute_model_call(self, *args, **kwargs):
|
556
557
|
"""Execute a model call synchronously.
|
557
|
-
|
558
|
+
|
558
559
|
This method is a synchronous wrapper around the asynchronous execution,
|
559
560
|
making it easier to use the model in non-async contexts. It's decorated
|
560
561
|
with jupyter_nb_handler to ensure proper handling in notebook environments.
|
561
|
-
|
562
|
+
|
562
563
|
Args:
|
563
564
|
*args: Positional arguments to pass to async_execute_model_call
|
564
565
|
**kwargs: Keyword arguments to pass to async_execute_model_call
|
565
|
-
|
566
|
+
|
566
567
|
Returns:
|
567
568
|
The model response
|
568
569
|
"""
|
570
|
+
|
569
571
|
async def main():
|
570
572
|
results = await asyncio.gather(
|
571
573
|
self.async_execute_model_call(*args, **kwargs)
|
@@ -577,16 +579,16 @@ class LanguageModel(
|
|
577
579
|
@classmethod
|
578
580
|
def get_generated_token_string(cls, raw_response: dict[str, Any]) -> str:
|
579
581
|
"""Extract the generated text from a raw model response.
|
580
|
-
|
582
|
+
|
581
583
|
This method navigates the response structure using the model's key_sequence
|
582
584
|
to find and return just the generated text, without metadata.
|
583
|
-
|
585
|
+
|
584
586
|
Args:
|
585
587
|
raw_response: The complete response dictionary from the model API
|
586
|
-
|
588
|
+
|
587
589
|
Returns:
|
588
590
|
str: The generated text string
|
589
|
-
|
591
|
+
|
590
592
|
Examples:
|
591
593
|
>>> m = LanguageModel.example(test_model=True)
|
592
594
|
>>> raw_response = m.execute_model_call("Hello, model!", "You are a helpful agent.")
|
@@ -598,14 +600,14 @@ class LanguageModel(
|
|
598
600
|
@classmethod
|
599
601
|
def get_usage_dict(cls, raw_response: dict[str, Any]) -> dict[str, Any]:
|
600
602
|
"""Extract token usage statistics from a raw model response.
|
601
|
-
|
603
|
+
|
602
604
|
This method navigates the response structure to find and return
|
603
605
|
information about token usage, which is used for cost calculation
|
604
606
|
and monitoring.
|
605
|
-
|
607
|
+
|
606
608
|
Args:
|
607
609
|
raw_response: The complete response dictionary from the model API
|
608
|
-
|
610
|
+
|
609
611
|
Returns:
|
610
612
|
dict: Dictionary of token usage statistics (input tokens, output tokens, etc.)
|
611
613
|
"""
|
@@ -614,14 +616,14 @@ class LanguageModel(
|
|
614
616
|
@classmethod
|
615
617
|
def parse_response(cls, raw_response: dict[str, Any]) -> EDSLOutput:
|
616
618
|
"""Parse the raw API response into a standardized EDSL output format.
|
617
|
-
|
619
|
+
|
618
620
|
This method processes the model's response to extract the generated content
|
619
621
|
and format it according to EDSL's expected structure, making it consistent
|
620
622
|
across different model providers.
|
621
|
-
|
623
|
+
|
622
624
|
Args:
|
623
625
|
raw_response: The complete response dictionary from the model API
|
624
|
-
|
626
|
+
|
625
627
|
Returns:
|
626
628
|
EDSLOutput: Standardized output structure with answer and optional comment
|
627
629
|
"""
|
@@ -637,7 +639,7 @@ class LanguageModel(
|
|
637
639
|
invigilator=None,
|
638
640
|
) -> ModelResponse:
|
639
641
|
"""Handle model calls with caching for efficiency.
|
640
|
-
|
642
|
+
|
641
643
|
This method implements the caching logic for model calls, checking if a
|
642
644
|
response is already cached before making an actual API call. It handles
|
643
645
|
the complete workflow of:
|
@@ -646,7 +648,7 @@ class LanguageModel(
|
|
646
648
|
3. Making the API call if needed
|
647
649
|
4. Storing new responses in the cache
|
648
650
|
5. Adding metadata like cost and cache status
|
649
|
-
|
651
|
+
|
650
652
|
Args:
|
651
653
|
user_prompt: The user's message or input prompt
|
652
654
|
system_prompt: The system's message or context
|
@@ -654,10 +656,10 @@ class LanguageModel(
|
|
654
656
|
iteration: The iteration number, used for the cache key
|
655
657
|
files_list: Optional list of files to include in the prompt
|
656
658
|
invigilator: Optional invigilator object, not used in caching
|
657
|
-
|
659
|
+
|
658
660
|
Returns:
|
659
661
|
ModelResponse: Response object with the model output and metadata
|
660
|
-
|
662
|
+
|
661
663
|
Examples:
|
662
664
|
>>> from edsl import Cache
|
663
665
|
>>> m = LanguageModel.example(test_model=True)
|
@@ -679,10 +681,9 @@ class LanguageModel(
|
|
679
681
|
"user_prompt": user_prompt_with_hashes,
|
680
682
|
"iteration": iteration,
|
681
683
|
}
|
682
|
-
|
684
|
+
|
683
685
|
# Try to fetch from cache
|
684
686
|
cached_response, cache_key = cache.fetch(**cache_call_params)
|
685
|
-
|
686
687
|
if cache_used := cached_response is not None:
|
687
688
|
# Cache hit - use the cached response
|
688
689
|
response = json.loads(cached_response)
|
@@ -694,21 +695,22 @@ class LanguageModel(
|
|
694
695
|
if hasattr(self, "remote") and self.remote
|
695
696
|
else self.async_execute_model_call
|
696
697
|
)
|
697
|
-
|
698
|
+
|
698
699
|
# Prepare parameters for the model call
|
699
700
|
params = {
|
700
701
|
"user_prompt": user_prompt,
|
701
702
|
"system_prompt": system_prompt,
|
702
703
|
"files_list": files_list,
|
703
704
|
}
|
704
|
-
|
705
|
+
|
705
706
|
# Get timeout from configuration
|
706
707
|
from ..config import CONFIG
|
708
|
+
|
707
709
|
TIMEOUT = float(CONFIG.get("EDSL_API_TIMEOUT"))
|
708
|
-
|
710
|
+
|
709
711
|
# Execute the model call with timeout
|
710
712
|
response = await asyncio.wait_for(f(**params), timeout=TIMEOUT)
|
711
|
-
|
713
|
+
|
712
714
|
# Store the response in the cache
|
713
715
|
new_cache_key = cache.store(
|
714
716
|
**cache_call_params, response=response, service=self._inference_service_
|
@@ -717,7 +719,6 @@ class LanguageModel(
|
|
717
719
|
|
718
720
|
# Calculate cost for the response
|
719
721
|
cost = self.cost(response)
|
720
|
-
|
721
722
|
# Return a structured response with metadata
|
722
723
|
return ModelResponse(
|
723
724
|
response=response,
|
@@ -738,15 +739,15 @@ class LanguageModel(
|
|
738
739
|
top_logprobs=2,
|
739
740
|
):
|
740
741
|
"""Ask a simple question with log probability tracking.
|
741
|
-
|
742
|
+
|
742
743
|
This is a convenience method for getting responses with log probabilities,
|
743
744
|
which can be useful for analyzing model confidence and alternatives.
|
744
|
-
|
745
|
+
|
745
746
|
Args:
|
746
747
|
question: The EDSL question object to ask
|
747
748
|
system_prompt: System message to use (default is human agent instruction)
|
748
749
|
top_logprobs: Number of top alternative tokens to return probabilities for
|
749
|
-
|
750
|
+
|
750
751
|
Returns:
|
751
752
|
The model response, including log probabilities if supported
|
752
753
|
"""
|
@@ -766,14 +767,14 @@ class LanguageModel(
|
|
766
767
|
**kwargs,
|
767
768
|
) -> AgentResponseDict:
|
768
769
|
"""Get a complete response with all metadata and parsed format.
|
769
|
-
|
770
|
+
|
770
771
|
This method handles the complete pipeline for:
|
771
772
|
1. Making a model call (with caching)
|
772
773
|
2. Parsing the response
|
773
774
|
3. Constructing a full response object with inputs, outputs, and parsed data
|
774
|
-
|
775
|
+
|
775
776
|
It's the primary method used by higher-level components to interact with models.
|
776
|
-
|
777
|
+
|
777
778
|
Args:
|
778
779
|
user_prompt: The user's message or input prompt
|
779
780
|
system_prompt: The system's message or context
|
@@ -781,7 +782,7 @@ class LanguageModel(
|
|
781
782
|
iteration: The iteration number (default: 1)
|
782
783
|
files_list: Optional list of files to include in the prompt
|
783
784
|
**kwargs: Additional parameters (invigilator can be provided here)
|
784
|
-
|
785
|
+
|
785
786
|
Returns:
|
786
787
|
AgentResponseDict: Complete response object with inputs, raw outputs, and parsed data
|
787
788
|
"""
|
@@ -793,19 +794,19 @@ class LanguageModel(
|
|
793
794
|
"cache": cache,
|
794
795
|
"files_list": files_list,
|
795
796
|
}
|
796
|
-
|
797
|
+
|
797
798
|
# Add invigilator if provided
|
798
799
|
if "invigilator" in kwargs:
|
799
800
|
params.update({"invigilator": kwargs["invigilator"]})
|
800
801
|
|
801
802
|
# Create structured input record
|
802
803
|
model_inputs = ModelInputs(user_prompt=user_prompt, system_prompt=system_prompt)
|
803
|
-
|
804
|
+
|
804
805
|
# Get model response (using cache if available)
|
805
806
|
model_outputs: ModelResponse = (
|
806
807
|
await self._async_get_intended_model_call_outcome(**params)
|
807
808
|
)
|
808
|
-
|
809
|
+
|
809
810
|
# Parse the response into EDSL's standard format
|
810
811
|
edsl_dict: EDSLOutput = self.parse_response(model_outputs.response)
|
811
812
|
|
@@ -815,31 +816,31 @@ class LanguageModel(
|
|
815
816
|
model_outputs=model_outputs,
|
816
817
|
edsl_dict=edsl_dict,
|
817
818
|
)
|
818
|
-
|
819
819
|
return agent_response_dict
|
820
820
|
|
821
821
|
get_response = sync_wrapper(async_get_response)
|
822
822
|
|
823
823
|
def cost(self, raw_response: dict[str, Any]) -> Union[float, str]:
|
824
824
|
"""Calculate the monetary cost of a model API call.
|
825
|
-
|
825
|
+
|
826
826
|
This method extracts token usage information from the response and
|
827
827
|
uses the price manager to calculate the actual cost in dollars based
|
828
828
|
on the model's pricing structure and token counts.
|
829
|
-
|
829
|
+
|
830
830
|
Args:
|
831
831
|
raw_response: The complete response dictionary from the model API
|
832
|
-
|
832
|
+
|
833
833
|
Returns:
|
834
834
|
Union[float, str]: The calculated cost in dollars, or an error message
|
835
835
|
"""
|
836
836
|
# Extract token usage data from the response
|
837
837
|
usage = self.get_usage_dict(raw_response)
|
838
|
-
|
838
|
+
|
839
839
|
# Use the price manager to calculate the actual cost
|
840
840
|
from .price_manager import PriceManager
|
841
|
+
|
841
842
|
price_manager = PriceManager()
|
842
|
-
|
843
|
+
|
843
844
|
return price_manager.calculate_cost(
|
844
845
|
inference_service=self._inference_service_,
|
845
846
|
model=self.model,
|
@@ -850,17 +851,17 @@ class LanguageModel(
|
|
850
851
|
|
851
852
|
def to_dict(self, add_edsl_version: bool = True) -> dict[str, Any]:
|
852
853
|
"""Serialize the model instance to a dictionary representation.
|
853
|
-
|
854
|
+
|
854
855
|
This method creates a dictionary containing all the information needed
|
855
856
|
to recreate this model, including its identifier, parameters, and service.
|
856
857
|
Optionally includes EDSL version information for compatibility checking.
|
857
|
-
|
858
|
+
|
858
859
|
Args:
|
859
860
|
add_edsl_version: Whether to include EDSL version and class name (default: True)
|
860
|
-
|
861
|
+
|
861
862
|
Returns:
|
862
863
|
dict: Dictionary representation of this model instance
|
863
|
-
|
864
|
+
|
864
865
|
Examples:
|
865
866
|
>>> m = LanguageModel.example()
|
866
867
|
>>> m.to_dict()
|
@@ -872,30 +873,30 @@ class LanguageModel(
|
|
872
873
|
"parameters": self.parameters,
|
873
874
|
"inference_service": self._inference_service_,
|
874
875
|
}
|
875
|
-
|
876
|
+
|
876
877
|
# Add EDSL version and class information if requested
|
877
878
|
if add_edsl_version:
|
878
879
|
from edsl import __version__
|
879
880
|
|
880
881
|
d["edsl_version"] = __version__
|
881
882
|
d["edsl_class_name"] = self.__class__.__name__
|
882
|
-
|
883
|
+
|
883
884
|
return d
|
884
885
|
|
885
886
|
@classmethod
|
886
887
|
@remove_edsl_version
|
887
888
|
def from_dict(cls, data: dict) -> "LanguageModel":
|
888
889
|
"""Create a language model instance from a dictionary representation.
|
889
|
-
|
890
|
+
|
890
891
|
This class method deserializes a model from its dictionary representation,
|
891
892
|
finding the correct model class based on the model identifier and service.
|
892
|
-
|
893
|
+
|
893
894
|
Args:
|
894
895
|
data: Dictionary containing the model configuration
|
895
|
-
|
896
|
+
|
896
897
|
Returns:
|
897
898
|
LanguageModel: A new model instance of the appropriate type
|
898
|
-
|
899
|
+
|
899
900
|
Note:
|
900
901
|
This method does not use the stored inference_service directly but
|
901
902
|
fetches the model class based on the model name and service name.
|
@@ -906,24 +907,25 @@ class LanguageModel(
|
|
906
907
|
model_class = get_model_class(
|
907
908
|
data["model"], service_name=data.get("inference_service", None)
|
908
909
|
)
|
909
|
-
|
910
|
+
|
910
911
|
# Create and return a new instance
|
911
912
|
return model_class(**data)
|
912
913
|
|
913
914
|
def __repr__(self) -> str:
|
914
915
|
"""Generate a string representation of the model.
|
915
|
-
|
916
|
+
|
916
917
|
This representation includes the model identifier and all parameters,
|
917
918
|
providing a clear picture of how the model is configured.
|
918
|
-
|
919
|
+
|
919
920
|
Returns:
|
920
921
|
str: A string representation of the model
|
921
922
|
"""
|
922
923
|
# Format the parameters as a string
|
923
924
|
param_string = ", ".join(
|
924
|
-
f
|
925
|
+
f'{key} = """{value}"""' if key == "canned_response" else f"{key} = {value}"
|
926
|
+
for key, value in self.parameters.items()
|
925
927
|
)
|
926
|
-
|
928
|
+
|
927
929
|
# Combine model name and parameters
|
928
930
|
return (
|
929
931
|
f"Model(model_name = '{self.model}'"
|
@@ -933,16 +935,16 @@ class LanguageModel(
|
|
933
935
|
|
934
936
|
def __add__(self, other_model: "LanguageModel") -> "LanguageModel":
|
935
937
|
"""Define behavior when models are combined with the + operator.
|
936
|
-
|
937
|
-
This operator is used in survey builder contexts, but note that it
|
938
|
+
|
939
|
+
This operator is used in survey builder contexts, but note that it
|
938
940
|
replaces the left model with the right model rather than combining them.
|
939
|
-
|
941
|
+
|
940
942
|
Args:
|
941
943
|
other_model: Another model to combine with this one
|
942
|
-
|
944
|
+
|
943
945
|
Returns:
|
944
946
|
LanguageModel: The other model if provided, otherwise this model
|
945
|
-
|
947
|
+
|
946
948
|
Warning:
|
947
949
|
This doesn't truly combine models - it replaces one with the other.
|
948
950
|
For running multiple models, use a single 'by' call with multiple models.
|
@@ -961,37 +963,37 @@ class LanguageModel(
|
|
961
963
|
throw_exception: bool = False,
|
962
964
|
) -> "LanguageModel":
|
963
965
|
"""Create an example language model instance for testing and demonstration.
|
964
|
-
|
966
|
+
|
965
967
|
This method provides a convenient way to create a model instance for
|
966
968
|
examples, tests, and documentation. It can create either a real model
|
967
969
|
(with API key checking disabled) or a test model that returns predefined
|
968
970
|
responses.
|
969
|
-
|
971
|
+
|
970
972
|
Args:
|
971
973
|
test_model: If True, creates a test model that doesn't make real API calls
|
972
974
|
canned_response: For test models, the predefined response to return
|
973
975
|
throw_exception: For test models, whether to throw an exception instead of responding
|
974
|
-
|
976
|
+
|
975
977
|
Returns:
|
976
978
|
LanguageModel: An example model instance
|
977
|
-
|
979
|
+
|
978
980
|
Examples:
|
979
981
|
Create a test model with a custom response:
|
980
|
-
|
982
|
+
|
981
983
|
>>> from edsl.language_models import LanguageModel
|
982
984
|
>>> m = LanguageModel.example(test_model=True, canned_response="WOWZA!")
|
983
985
|
>>> isinstance(m, LanguageModel)
|
984
986
|
True
|
985
|
-
|
987
|
+
|
986
988
|
Use the test model to answer a question:
|
987
|
-
|
989
|
+
|
988
990
|
>>> from edsl import QuestionFreeText
|
989
991
|
>>> q = QuestionFreeText(question_text="What is your name?", question_name='example')
|
990
992
|
>>> q.by(m).run(cache=False, disable_remote_cache=True, disable_remote_inference=True).select('example').first()
|
991
993
|
'WOWZA!'
|
992
|
-
|
994
|
+
|
993
995
|
Create a test model that throws exceptions:
|
994
|
-
|
996
|
+
|
995
997
|
>>> m = LanguageModel.example(test_model=True, canned_response="WOWZA!", throw_exception=True)
|
996
998
|
>>> r = q.by(m).run(cache=False, disable_remote_cache=True, disable_remote_inference=True, print_exceptions=True)
|
997
999
|
Exception report saved to ...
|
@@ -1010,14 +1012,14 @@ class LanguageModel(
|
|
1010
1012
|
|
1011
1013
|
def from_cache(self, cache: "Cache") -> "LanguageModel":
|
1012
1014
|
"""Create a new model that only returns responses from the cache.
|
1013
|
-
|
1015
|
+
|
1014
1016
|
This method creates a modified copy of the model that will only use
|
1015
1017
|
cached responses, never making new API calls. This is useful for
|
1016
1018
|
offline operation or repeatable experiments.
|
1017
|
-
|
1019
|
+
|
1018
1020
|
Args:
|
1019
1021
|
cache: The cache object containing previously cached responses
|
1020
|
-
|
1022
|
+
|
1021
1023
|
Returns:
|
1022
1024
|
LanguageModel: A new model instance that only reads from cache
|
1023
1025
|
"""
|
@@ -1028,7 +1030,7 @@ class LanguageModel(
|
|
1028
1030
|
# Create a deep copy of this model instance
|
1029
1031
|
new_instance = deepcopy(self)
|
1030
1032
|
print("Cache entries", len(cache))
|
1031
|
-
|
1033
|
+
|
1032
1034
|
# Filter the cache to only include entries for this model
|
1033
1035
|
new_instance.cache = Cache(
|
1034
1036
|
data={k: v for k, v in cache.items() if v.model == self.model}
|