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.
@@ -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"{key} = {value}" for key, value in self.parameters.items()
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}