edsl 0.1.37.dev4__py3-none-any.whl → 0.1.37.dev6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. edsl/__version__.py +1 -1
  2. edsl/agents/Agent.py +86 -35
  3. edsl/agents/AgentList.py +5 -0
  4. edsl/agents/InvigilatorBase.py +2 -23
  5. edsl/agents/PromptConstructor.py +147 -106
  6. edsl/agents/descriptors.py +17 -4
  7. edsl/config.py +1 -1
  8. edsl/conjure/AgentConstructionMixin.py +11 -3
  9. edsl/conversation/Conversation.py +66 -14
  10. edsl/conversation/chips.py +95 -0
  11. edsl/coop/coop.py +134 -3
  12. edsl/data/Cache.py +1 -1
  13. edsl/exceptions/BaseException.py +21 -0
  14. edsl/exceptions/__init__.py +7 -3
  15. edsl/exceptions/agents.py +17 -19
  16. edsl/exceptions/results.py +11 -8
  17. edsl/exceptions/scenarios.py +22 -0
  18. edsl/exceptions/surveys.py +13 -10
  19. edsl/inference_services/InferenceServicesCollection.py +32 -9
  20. edsl/jobs/Jobs.py +265 -53
  21. edsl/jobs/interviews/InterviewExceptionEntry.py +5 -1
  22. edsl/jobs/tasks/TaskHistory.py +1 -0
  23. edsl/language_models/KeyLookup.py +30 -0
  24. edsl/language_models/LanguageModel.py +47 -59
  25. edsl/language_models/__init__.py +1 -0
  26. edsl/prompts/Prompt.py +8 -4
  27. edsl/questions/QuestionBase.py +53 -13
  28. edsl/questions/QuestionBasePromptsMixin.py +1 -33
  29. edsl/questions/QuestionFunctional.py +2 -2
  30. edsl/questions/descriptors.py +23 -28
  31. edsl/results/DatasetExportMixin.py +25 -1
  32. edsl/results/Result.py +16 -1
  33. edsl/results/Results.py +31 -120
  34. edsl/results/ResultsDBMixin.py +1 -1
  35. edsl/results/Selector.py +18 -1
  36. edsl/scenarios/Scenario.py +48 -12
  37. edsl/scenarios/ScenarioHtmlMixin.py +7 -2
  38. edsl/scenarios/ScenarioList.py +12 -1
  39. edsl/surveys/Rule.py +10 -4
  40. edsl/surveys/Survey.py +100 -77
  41. edsl/utilities/utilities.py +18 -0
  42. {edsl-0.1.37.dev4.dist-info → edsl-0.1.37.dev6.dist-info}/METADATA +1 -1
  43. {edsl-0.1.37.dev4.dist-info → edsl-0.1.37.dev6.dist-info}/RECORD +45 -41
  44. {edsl-0.1.37.dev4.dist-info → edsl-0.1.37.dev6.dist-info}/LICENSE +0 -0
  45. {edsl-0.1.37.dev4.dist-info → edsl-0.1.37.dev6.dist-info}/WHEEL +0 -0
@@ -17,9 +17,7 @@ import warnings
17
17
  from functools import wraps
18
18
  import asyncio
19
19
  import json
20
- import time
21
20
  import os
22
- import hashlib
23
21
  from typing import (
24
22
  Coroutine,
25
23
  Any,
@@ -30,6 +28,7 @@ from typing import (
30
28
  get_type_hints,
31
29
  TypedDict,
32
30
  Optional,
31
+ TYPE_CHECKING,
33
32
  )
34
33
  from abc import ABC, abstractmethod
35
34
 
@@ -49,34 +48,16 @@ from edsl.utilities.decorators import add_edsl_version, remove_edsl_version
49
48
  from edsl.language_models.repair import repair
50
49
  from edsl.enums import InferenceServiceType
51
50
  from edsl.Base import RichPrintingMixin, PersistenceMixin
52
- from edsl.enums import service_to_api_keyname
53
- from edsl.exceptions import MissingAPIKeyError
54
51
  from edsl.language_models.RegisterLanguageModelsMeta import RegisterLanguageModelsMeta
55
52
  from edsl.exceptions.language_models import LanguageModelBadResponseError
56
53
 
57
- TIMEOUT = float(CONFIG.get("EDSL_API_TIMEOUT"))
58
-
59
-
60
- def convert_answer(response_part):
61
- import json
62
-
63
- response_part = response_part.strip()
64
-
65
- if response_part == "None":
66
- return None
67
-
68
- repaired = repair_json(response_part)
69
- if repaired == '""':
70
- # it was a literal string
71
- return response_part
54
+ from edsl.language_models.KeyLookup import KeyLookup
72
55
 
73
- try:
74
- return json.loads(repaired)
75
- except json.JSONDecodeError as j:
76
- # last resort
77
- return response_part
56
+ TIMEOUT = float(CONFIG.get("EDSL_API_TIMEOUT"))
78
57
 
79
58
 
59
+ # you might be tempated to move this to be a static method of LanguageModel, but this doesn't work
60
+ # for reasons I don't understand. So leave it here.
80
61
  def extract_item_from_raw_response(data, key_sequence):
81
62
  if isinstance(data, str):
82
63
  try:
@@ -167,7 +148,12 @@ class LanguageModel(
167
148
  _safety_factor = 0.8
168
149
 
169
150
  def __init__(
170
- self, tpm=None, rpm=None, omit_system_prompt_if_empty_string=True, **kwargs
151
+ self,
152
+ tpm: float = None,
153
+ rpm: float = None,
154
+ omit_system_prompt_if_empty_string: bool = True,
155
+ key_lookup: Optional[KeyLookup] = None,
156
+ **kwargs,
171
157
  ):
172
158
  """Initialize the LanguageModel."""
173
159
  self.model = getattr(self, "_model_", None)
@@ -200,29 +186,26 @@ class LanguageModel(
200
186
  # Skip the API key check. Sometimes this is useful for testing.
201
187
  self._api_token = None
202
188
 
189
+ if key_lookup is not None:
190
+ self.key_lookup = key_lookup
191
+ else:
192
+ self.key_lookup = KeyLookup.from_os_environ()
193
+
203
194
  def ask_question(self, question):
204
195
  user_prompt = question.get_instructions().render(question.data).text
205
196
  system_prompt = "You are a helpful agent pretending to be a human."
206
197
  return self.execute_model_call(user_prompt, system_prompt)
207
198
 
199
+ def set_key_lookup(self, key_lookup: KeyLookup):
200
+ del self._api_token
201
+ self.key_lookup = key_lookup
202
+
208
203
  @property
209
204
  def api_token(self) -> str:
210
205
  if not hasattr(self, "_api_token"):
211
- key_name = service_to_api_keyname.get(self._inference_service_, "NOT FOUND")
212
- if self._inference_service_ == "bedrock":
213
- self._api_token = [os.getenv(key_name[0]), os.getenv(key_name[1])]
214
- # Check if any of the tokens are None
215
- missing_token = any(token is None for token in self._api_token)
216
- else:
217
- self._api_token = os.getenv(key_name)
218
- missing_token = self._api_token is None
219
- if missing_token and self._inference_service_ != "test" and not self.remote:
220
- print("raising error")
221
- raise MissingAPIKeyError(
222
- f"""The key for service: `{self._inference_service_}` is not set.
223
- Need a key with name {key_name} in your .env file."""
224
- )
225
-
206
+ self._api_token = self.key_lookup.get_api_token(
207
+ self._inference_service_, self.remote
208
+ )
226
209
  return self._api_token
227
210
 
228
211
  def __getitem__(self, key):
@@ -291,21 +274,6 @@ class LanguageModel(
291
274
  if tpm is not None:
292
275
  self._tpm = tpm
293
276
  return None
294
- # self._set_rate_limits(rpm=rpm, tpm=tpm)
295
-
296
- # def _set_rate_limits(self, rpm=None, tpm=None) -> None:
297
- # """Set the rate limits for the model.
298
-
299
- # If the model does not have rate limits, use the default rate limits."""
300
- # if rpm is not None and tpm is not None:
301
- # self.__rate_limits = {"rpm": rpm, "tpm": tpm}
302
- # return
303
-
304
- # if self.__rate_limits is None:
305
- # if hasattr(self, "get_rate_limits"):
306
- # self.__rate_limits = self.get_rate_limits()
307
- # else:
308
- # self.__rate_limits = self.__default_rate_limits
309
277
 
310
278
  @property
311
279
  def RPM(self):
@@ -416,6 +384,26 @@ class LanguageModel(
416
384
  )
417
385
  return extract_item_from_raw_response(raw_response, cls.usage_sequence)
418
386
 
387
+ @staticmethod
388
+ def convert_answer(response_part):
389
+ import json
390
+
391
+ response_part = response_part.strip()
392
+
393
+ if response_part == "None":
394
+ return None
395
+
396
+ repaired = repair_json(response_part)
397
+ if repaired == '""':
398
+ # it was a literal string
399
+ return response_part
400
+
401
+ try:
402
+ return json.loads(repaired)
403
+ except json.JSONDecodeError as j:
404
+ # last resort
405
+ return response_part
406
+
419
407
  @classmethod
420
408
  def parse_response(cls, raw_response: dict[str, Any]) -> EDSLOutput:
421
409
  """Parses the API response and returns the response text."""
@@ -425,13 +413,13 @@ class LanguageModel(
425
413
  if last_newline == -1:
426
414
  # There is no comment
427
415
  edsl_dict = {
428
- "answer": convert_answer(generated_token_string),
416
+ "answer": cls.convert_answer(generated_token_string),
429
417
  "generated_tokens": generated_token_string,
430
418
  "comment": None,
431
419
  }
432
420
  else:
433
421
  edsl_dict = {
434
- "answer": convert_answer(generated_token_string[:last_newline]),
422
+ "answer": cls.convert_answer(generated_token_string[:last_newline]),
435
423
  "comment": generated_token_string[last_newline + 1 :].strip(),
436
424
  "generated_tokens": generated_token_string,
437
425
  }
@@ -492,7 +480,7 @@ class LanguageModel(
492
480
  params = {
493
481
  "user_prompt": user_prompt,
494
482
  "system_prompt": system_prompt,
495
- "files_list": files_list
483
+ "files_list": files_list,
496
484
  # **({"encoded_image": encoded_image} if encoded_image else {}),
497
485
  }
498
486
  # response = await f(**params)
@@ -699,7 +687,7 @@ class LanguageModel(
699
687
  True
700
688
  >>> from edsl import QuestionFreeText
701
689
  >>> q = QuestionFreeText(question_text = "What is your name?", question_name = 'example')
702
- >>> q.by(m).run(cache = False).select('example').first()
690
+ >>> q.by(m).run(cache = False, disable_remote_cache = True, disable_remote_inference = True).select('example').first()
703
691
  'WOWZA!'
704
692
  """
705
693
  from edsl import Model
@@ -1,2 +1,3 @@
1
1
  from edsl.language_models.LanguageModel import LanguageModel
2
2
  from edsl.language_models.registry import Model
3
+ from edsl.language_models.KeyLookup import KeyLookup
edsl/prompts/Prompt.py CHANGED
@@ -240,10 +240,14 @@ class Prompt(PersistenceMixin, RichPrintingMixin):
240
240
  >>> p.render({"person": "Mr. {{last_name}}"})
241
241
  Prompt(text=\"""Hello, Mr. {{ last_name }}\""")
242
242
  """
243
- new_text = self._render(
244
- self.text, primary_replacement, **additional_replacements
245
- )
246
- return self.__class__(text=new_text)
243
+ try:
244
+ new_text = self._render(
245
+ self.text, primary_replacement, **additional_replacements
246
+ )
247
+ return self.__class__(text=new_text)
248
+ except Exception as e:
249
+ print(f"Error rendering prompt: {e}")
250
+ return self
247
251
 
248
252
  @staticmethod
249
253
  def _render(
@@ -150,14 +150,21 @@ class QuestionBase(
150
150
  "_include_comment",
151
151
  "_fake_data_factory",
152
152
  "_use_code",
153
- "_answering_instructions",
154
- "_question_presentation",
155
153
  "_model_instructions",
156
154
  ]
155
+ only_if_not_na_list = ["_answering_instructions", "_question_presentation"]
156
+
157
+ def ok(key, value):
158
+ if not key.startswith("_"):
159
+ return False
160
+ if key in exclude_list:
161
+ return False
162
+ if key in only_if_not_na_list and value is None:
163
+ return False
164
+ return True
165
+
157
166
  candidate_data = {
158
- k.replace("_", "", 1): v
159
- for k, v in self.__dict__.items()
160
- if k.startswith("_") and k not in exclude_list
167
+ k.replace("_", "", 1): v for k, v in self.__dict__.items() if ok(k, v)
161
168
  }
162
169
 
163
170
  if "func" in candidate_data:
@@ -176,7 +183,9 @@ class QuestionBase(
176
183
  """
177
184
  candidate_data = self.data.copy()
178
185
  candidate_data["question_type"] = self.question_type
179
- return candidate_data
186
+ return {
187
+ key: value for key, value in candidate_data.items() if value is not None
188
+ }
180
189
 
181
190
  @add_edsl_version
182
191
  def to_dict(self) -> dict[str, Any]:
@@ -239,6 +248,8 @@ class QuestionBase(
239
248
  show_answer: bool = True,
240
249
  model: Optional["LanguageModel"] = None,
241
250
  cache=False,
251
+ disable_remote_cache: bool = False,
252
+ disable_remote_inference: bool = False,
242
253
  **kwargs,
243
254
  ):
244
255
  """Run an example of the question.
@@ -247,7 +258,7 @@ class QuestionBase(
247
258
  >>> m = Q._get_test_model(canned_response = "Yo, what's up?")
248
259
  >>> m.execute_model_call("", "")
249
260
  {'message': [{'text': "Yo, what's up?"}], 'usage': {'prompt_tokens': 1, 'completion_tokens': 1}}
250
- >>> Q.run_example(show_answer = True, model = m)
261
+ >>> Q.run_example(show_answer = True, model = m, disable_remote_cache = True, disable_remote_inference = True)
251
262
  ┏━━━━━━━━━━━━━━━━┓
252
263
  ┃ answer ┃
253
264
  ┃ .how_are_you ┃
@@ -259,25 +270,48 @@ class QuestionBase(
259
270
  from edsl import Model
260
271
 
261
272
  model = Model()
262
- results = cls.example(**kwargs).by(model).run(cache=cache)
273
+ results = (
274
+ cls.example(**kwargs)
275
+ .by(model)
276
+ .run(
277
+ cache=cache,
278
+ disable_remote_cache=disable_remote_cache,
279
+ disable_remote_inference=disable_remote_inference,
280
+ )
281
+ )
263
282
  if show_answer:
264
283
  results.select("answer.*").print()
265
284
  else:
266
285
  return results
267
286
 
268
- def __call__(self, just_answer=True, model=None, agent=None, **kwargs):
287
+ def __call__(
288
+ self,
289
+ just_answer=True,
290
+ model=None,
291
+ agent=None,
292
+ disable_remote_cache: bool = False,
293
+ disable_remote_inference: bool = False,
294
+ **kwargs,
295
+ ):
269
296
  """Call the question.
270
297
 
271
298
 
272
299
  >>> from edsl import QuestionFreeText as Q
273
300
  >>> m = Q._get_test_model(canned_response = "Yo, what's up?")
274
301
  >>> q = Q(question_name = "color", question_text = "What is your favorite color?")
275
- >>> q(model = m)
302
+ >>> q(model = m, disable_remote_cache = True, disable_remote_inference = True)
276
303
  "Yo, what's up?"
277
304
 
278
305
  """
279
306
  survey = self.to_survey()
280
- results = survey(model=model, agent=agent, **kwargs, cache=False)
307
+ results = survey(
308
+ model=model,
309
+ agent=agent,
310
+ **kwargs,
311
+ cache=False,
312
+ disable_remote_cache=disable_remote_cache,
313
+ disable_remote_inference=disable_remote_inference,
314
+ )
281
315
  if just_answer:
282
316
  return results.select(f"answer.{self.question_name}").first()
283
317
  else:
@@ -295,6 +329,7 @@ class QuestionBase(
295
329
  just_answer: bool = True,
296
330
  model: Optional["Model"] = None,
297
331
  agent: Optional["Agent"] = None,
332
+ disable_remote_inference: bool = False,
298
333
  **kwargs,
299
334
  ) -> Union[Any, "Results"]:
300
335
  """Call the question asynchronously.
@@ -303,12 +338,17 @@ class QuestionBase(
303
338
  >>> from edsl import QuestionFreeText as Q
304
339
  >>> m = Q._get_test_model(canned_response = "Blue")
305
340
  >>> q = Q(question_name = "color", question_text = "What is your favorite color?")
306
- >>> async def test_run_async(): result = await q.run_async(model=m); print(result)
341
+ >>> async def test_run_async(): result = await q.run_async(model=m, disable_remote_inference = True); print(result)
307
342
  >>> asyncio.run(test_run_async())
308
343
  Blue
309
344
  """
310
345
  survey = self.to_survey()
311
- results = await survey.run_async(model=model, agent=agent, **kwargs)
346
+ results = await survey.run_async(
347
+ model=model,
348
+ agent=agent,
349
+ disable_remote_inference=disable_remote_inference,
350
+ **kwargs,
351
+ )
312
352
  if just_answer:
313
353
  return results.select(f"answer.{self.question_name}").first()
314
354
  else:
@@ -30,38 +30,6 @@ template_manager = TemplateManager()
30
30
 
31
31
 
32
32
  class QuestionBasePromptsMixin:
33
- # @classmethod
34
- # @lru_cache(maxsize=1)
35
- # def _read_template(cls, template_name):
36
- # with resources.open_text(
37
- # f"edsl.questions.templates.{cls.question_type}", template_name
38
- # ) as file:
39
- # return file.read()
40
-
41
- # @classmethod
42
- # def applicable_prompts(
43
- # cls, model: Optional[str] = None
44
- # ) -> list[type["PromptBase"]]:
45
- # """Get the prompts that are applicable to the question type.
46
-
47
- # :param model: The language model to use.
48
-
49
- # >>> from edsl.questions import QuestionFreeText
50
- # >>> QuestionFreeText.applicable_prompts()
51
- # [<class 'edsl.prompts.library.question_freetext.FreeText'>]
52
-
53
- # :param model: The language model to use. If None, assumes does not matter.
54
-
55
- # """
56
- # from edsl.prompts.registry import get_classes as prompt_lookup
57
-
58
- # applicable_prompts = prompt_lookup(
59
- # component_type="question_instructions",
60
- # question_type=cls.question_type,
61
- # model=model,
62
- # )
63
- # return applicable_prompts
64
-
65
33
  @property
66
34
  def model_instructions(self) -> dict:
67
35
  """Get the model-specific instructions for the question."""
@@ -231,7 +199,7 @@ class QuestionBasePromptsMixin:
231
199
  @property
232
200
  def new_default_instructions(self) -> "Prompt":
233
201
  "This is set up as a property because there are mutable question values that determine how it is rendered."
234
- return self.question_presentation + self.answering_instructions
202
+ return Prompt(self.question_presentation) + Prompt(self.answering_instructions)
235
203
 
236
204
  @property
237
205
  def parameters(self) -> set[str]:
@@ -19,7 +19,7 @@ class QuestionFunctional(QuestionBase):
19
19
  >>> question.activate()
20
20
  >>> scenario = Scenario({"numbers": [1, 2, 3, 4, 5]})
21
21
  >>> agent = Agent(traits={"multiplier": 10})
22
- >>> results = question.by(scenario).by(agent).run()
22
+ >>> results = question.by(scenario).by(agent).run(disable_remote_cache = True, disable_remote_inference = True)
23
23
  >>> results.select("answer.*").to_list()[0] == 150
24
24
  True
25
25
 
@@ -27,7 +27,7 @@ class QuestionFunctional(QuestionBase):
27
27
 
28
28
  >>> from edsl.questions.QuestionBase import QuestionBase
29
29
  >>> new_question = QuestionBase.from_dict(question.to_dict())
30
- >>> results = new_question.by(scenario).by(agent).run()
30
+ >>> results = new_question.by(scenario).by(agent).run(disable_remote_cache = True, disable_remote_inference = True)
31
31
  >>> results.select("answer.*").to_list()[0] == 150
32
32
  True
33
33
 
@@ -53,33 +53,12 @@ class BaseDescriptor(ABC):
53
53
 
54
54
  def __set__(self, instance, value: Any) -> None:
55
55
  """Set the value of the attribute."""
56
- self.validate(value, instance)
57
- # from edsl.prompts.registry import get_classes
58
-
59
- instance.__dict__[self.name] = value
60
- # if self.name == "_instructions":
61
- # instructions = value
62
- # if value is not None:
63
- # instance.__dict__[self.name] = instructions
64
- # instance.set_instructions = True
65
- # else:
66
- # potential_prompt_classes = get_classes(
67
- # question_type=instance.question_type
68
- # )
69
- # if len(potential_prompt_classes) > 0:
70
- # instructions = potential_prompt_classes[0]().text
71
- # instance.__dict__[self.name] = instructions
72
- # instance.set_instructions = False
73
- # else:
74
- # if not hasattr(instance, "default_instructions"):
75
- # raise Exception(
76
- # "No default instructions found and no matching prompts!"
77
- # )
78
- # instructions = instance.default_instructions
79
- # instance.__dict__[self.name] = instructions
80
- # instance.set_instructions = False
81
-
82
- # instance.set_instructions = value != instance.default_instructions
56
+ new_value = self.validate(value, instance)
57
+
58
+ if new_value is not None:
59
+ instance.__dict__[self.name] = new_value
60
+ else:
61
+ instance.__dict__[self.name] = value
83
62
 
84
63
  def __set_name__(self, owner, name: str) -> None:
85
64
  """Set the name of the attribute."""
@@ -400,10 +379,24 @@ class QuestionTextDescriptor(BaseDescriptor):
400
379
  if contains_single_braced_substring(value):
401
380
  import warnings
402
381
 
382
+ # # warnings.warn(
383
+ # # f"WARNING: Question text contains a single-braced substring: If you intended to parameterize the question with a Scenario this should be changed to a double-braced substring, e.g. {{variable}}.\nSee details on constructing Scenarios in the docs: https://docs.expectedparrot.com/en/latest/scenarios.html",
384
+ # # UserWarning,
385
+ # # )
403
386
  warnings.warn(
404
- f"WARNING: Question text contains a single-braced substring: If you intended to parameterize the question with a Scenario this should be changed to a double-braced substring, e.g. {{variable}}.\nSee details on constructing Scenarios in the docs: https://docs.expectedparrot.com/en/latest/scenarios.html",
387
+ "WARNING: Question text contains a single-braced substring. "
388
+ "If you intended to parameterize the question with a Scenario, this will "
389
+ "be changed to a double-braced substring, e.g. {{variable}}.\n"
390
+ "See details on constructing Scenarios in the docs: "
391
+ "https://docs.expectedparrot.com/en/latest/scenarios.html",
405
392
  UserWarning,
406
393
  )
394
+ # Automatically replace single braces with double braces
395
+ # This is here because if the user is using an f-string, the double brace will get converted to a single brace.
396
+ # This undoes that.
397
+ value = re.sub(r"\{([^\{\}]+)\}", r"{{\1}}", value)
398
+ return value
399
+
407
400
  # iterate through all doubles braces and check if they are valid python identifiers
408
401
  for match in re.finditer(r"\{\{([^\{\}]+)\}\}", value):
409
402
  if " " in match.group(1).strip():
@@ -411,6 +404,8 @@ class QuestionTextDescriptor(BaseDescriptor):
411
404
  f"Question text contains an invalid identifier: '{match.group(1)}'"
412
405
  )
413
406
 
407
+ return None
408
+
414
409
 
415
410
  if __name__ == "__main__":
416
411
  import doctest
@@ -437,7 +437,30 @@ class DatasetExportMixin:
437
437
  b64 = base64.b64encode(csv_string.encode()).decode()
438
438
  return f'<a href="data:file/csv;base64,{b64}" download="my_data.csv">Download CSV file</a>'
439
439
 
440
- def to_pandas(self, remove_prefix: bool = False) -> "pd.DataFrame":
440
+ def to_pandas(
441
+ self, remove_prefix: bool = False, lists_as_strings=False
442
+ ) -> "DataFrame":
443
+ """Convert the results to a pandas DataFrame, ensuring that lists remain as lists.
444
+
445
+ :param remove_prefix: Whether to remove the prefix from the column names.
446
+
447
+ """
448
+ return self._to_pandas_strings(remove_prefix)
449
+ # if lists_as_strings:
450
+ # return self._to_pandas_strings(remove_prefix=remove_prefix)
451
+
452
+ # import pandas as pd
453
+
454
+ # df = pd.DataFrame(self.data)
455
+
456
+ # if remove_prefix:
457
+ # # Optionally remove prefixes from column names
458
+ # df.columns = [col.split(".")[-1] for col in df.columns]
459
+
460
+ # df_sorted = df.sort_index(axis=1) # Sort columns alphabetically
461
+ # return df_sorted
462
+
463
+ def _to_pandas_strings(self, remove_prefix: bool = False) -> "pd.DataFrame":
441
464
  """Convert the results to a pandas DataFrame.
442
465
 
443
466
  :param remove_prefix: Whether to remove the prefix from the column names.
@@ -451,6 +474,7 @@ class DatasetExportMixin:
451
474
  2 Terrible
452
475
  3 OK
453
476
  """
477
+
454
478
  import pandas as pd
455
479
 
456
480
  csv_string = self.to_csv(remove_prefix=remove_prefix)
edsl/results/Result.py CHANGED
@@ -257,10 +257,25 @@ class Result(Base, UserDict):
257
257
 
258
258
  """
259
259
  d = {}
260
- data_types = self.sub_dicts.keys()
260
+ problem_keys = []
261
+ data_types = sorted(self.sub_dicts.keys())
261
262
  for data_type in data_types:
262
263
  for key in self.sub_dicts[data_type]:
264
+ if key in d:
265
+ import warnings
266
+
267
+ warnings.warn(
268
+ f"Key '{key}' of data type '{data_type}' is already in use. Renaming to {key}_{data_type}"
269
+ )
270
+ problem_keys.append((key, data_type))
271
+ key = f"{key}_{data_type}"
272
+ # raise ValueError(f"Key '{key}' is already in the dictionary")
263
273
  d[key] = data_type
274
+
275
+ for key, data_type in problem_keys:
276
+ self.sub_dicts[data_type][f"{key}_{data_type}"] = self.sub_dicts[
277
+ data_type
278
+ ].pop(key)
264
279
  return d
265
280
 
266
281
  def rows(self, index) -> tuple[int, str, str, str]: