edsl 0.1.38__py3-none-any.whl → 0.1.38.dev2__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/Base.py +31 -60
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +9 -18
- edsl/agents/AgentList.py +8 -59
- edsl/agents/Invigilator.py +7 -18
- edsl/agents/InvigilatorBase.py +19 -0
- edsl/agents/PromptConstructor.py +4 -5
- edsl/config.py +0 -8
- edsl/coop/coop.py +7 -74
- edsl/data/Cache.py +2 -27
- edsl/data/CacheEntry.py +3 -8
- edsl/data/RemoteCacheSync.py +19 -0
- edsl/enums.py +0 -2
- edsl/inference_services/GoogleService.py +15 -7
- edsl/inference_services/registry.py +0 -2
- edsl/jobs/Jobs.py +548 -88
- edsl/jobs/interviews/Interview.py +11 -11
- edsl/jobs/runners/JobsRunnerAsyncio.py +35 -140
- edsl/jobs/runners/JobsRunnerStatus.py +2 -0
- edsl/jobs/tasks/TaskHistory.py +16 -15
- edsl/language_models/LanguageModel.py +84 -44
- edsl/language_models/ModelList.py +1 -47
- edsl/language_models/registry.py +4 -57
- edsl/prompts/Prompt.py +3 -8
- edsl/questions/QuestionBase.py +16 -20
- edsl/questions/QuestionExtract.py +4 -3
- edsl/questions/question_registry.py +6 -36
- edsl/results/Dataset.py +15 -146
- edsl/results/DatasetExportMixin.py +217 -231
- edsl/results/DatasetTree.py +4 -134
- edsl/results/Result.py +9 -18
- edsl/results/Results.py +51 -145
- edsl/scenarios/FileStore.py +13 -187
- edsl/scenarios/Scenario.py +4 -61
- edsl/scenarios/ScenarioList.py +62 -237
- edsl/surveys/Survey.py +2 -16
- edsl/surveys/SurveyFlowVisualizationMixin.py +9 -67
- edsl/surveys/instructions/Instruction.py +0 -12
- edsl/templates/error_reporting/interview_details.html +3 -3
- edsl/templates/error_reporting/interviews.html +9 -18
- edsl/utilities/utilities.py +0 -15
- {edsl-0.1.38.dist-info → edsl-0.1.38.dev2.dist-info}/METADATA +1 -2
- {edsl-0.1.38.dist-info → edsl-0.1.38.dev2.dist-info}/RECORD +45 -53
- edsl/inference_services/PerplexityService.py +0 -163
- edsl/jobs/JobsChecks.py +0 -147
- edsl/jobs/JobsPrompts.py +0 -268
- edsl/jobs/JobsRemoteInferenceHandler.py +0 -239
- edsl/results/CSSParameterizer.py +0 -108
- edsl/results/TableDisplay.py +0 -198
- edsl/results/table_display.css +0 -78
- edsl/scenarios/ScenarioJoin.py +0 -127
- {edsl-0.1.38.dist-info → edsl-0.1.38.dev2.dist-info}/LICENSE +0 -0
- {edsl-0.1.38.dist-info → edsl-0.1.38.dev2.dist-info}/WHEEL +0 -0
edsl/scenarios/ScenarioList.py
CHANGED
@@ -31,10 +31,6 @@ class ScenarioListMixin(ScenarioListPdfMixin, ScenarioListExportMixin):
|
|
31
31
|
class ScenarioList(Base, UserList, ScenarioListMixin):
|
32
32
|
"""Class for creating a list of scenarios to be used in a survey."""
|
33
33
|
|
34
|
-
__documentation__ = (
|
35
|
-
"https://docs.expectedparrot.com/en/latest/scenarios.html#scenariolist"
|
36
|
-
)
|
37
|
-
|
38
34
|
def __init__(self, data: Optional[list] = None, codebook: Optional[dict] = None):
|
39
35
|
"""Initialize the ScenarioList class."""
|
40
36
|
if data is not None:
|
@@ -245,9 +241,6 @@ class ScenarioList(Base, UserList, ScenarioListMixin):
|
|
245
241
|
|
246
242
|
return dict_hash(self.to_dict(sort=True, add_edsl_version=False))
|
247
243
|
|
248
|
-
def __eq__(self, other: Any) -> bool:
|
249
|
-
return hash(self) == hash(other)
|
250
|
-
|
251
244
|
def __repr__(self):
|
252
245
|
return f"ScenarioList({self.data})"
|
253
246
|
|
@@ -289,49 +282,41 @@ class ScenarioList(Base, UserList, ScenarioListMixin):
|
|
289
282
|
random.shuffle(self.data)
|
290
283
|
return self
|
291
284
|
|
292
|
-
def _repr_html_(self):
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
# >>> s = ScenarioList([Scenario({'a': 1, 'b': 1}), Scenario({'a': 1, 'b': 2})])
|
321
|
-
# >>> s.tally('b')
|
322
|
-
# {1: 1, 2: 1}
|
323
|
-
# """
|
324
|
-
# return dict(Counter([scenario[field] for scenario in self]))
|
325
|
-
|
326
|
-
def sample(self, n: int, seed: Optional[str] = None) -> ScenarioList:
|
285
|
+
def _repr_html_(self) -> str:
|
286
|
+
from edsl.utilities.utilities import data_to_html
|
287
|
+
|
288
|
+
data = self.to_dict()
|
289
|
+
_ = data.pop("edsl_version")
|
290
|
+
_ = data.pop("edsl_class_name")
|
291
|
+
for s in data["scenarios"]:
|
292
|
+
_ = s.pop("edsl_version")
|
293
|
+
_ = s.pop("edsl_class_name")
|
294
|
+
for scenario in data["scenarios"]:
|
295
|
+
for key, value in scenario.items():
|
296
|
+
if hasattr(value, "to_dict"):
|
297
|
+
data[key] = value.to_dict()
|
298
|
+
return data_to_html(data)
|
299
|
+
|
300
|
+
def tally(self, field) -> dict:
|
301
|
+
"""Return a tally of the values in the field.
|
302
|
+
|
303
|
+
Example:
|
304
|
+
|
305
|
+
>>> s = ScenarioList([Scenario({'a': 1, 'b': 1}), Scenario({'a': 1, 'b': 2})])
|
306
|
+
>>> s.tally('b')
|
307
|
+
{1: 1, 2: 1}
|
308
|
+
"""
|
309
|
+
return dict(Counter([scenario[field] for scenario in self]))
|
310
|
+
|
311
|
+
def sample(self, n: int, seed="edsl") -> ScenarioList:
|
327
312
|
"""Return a random sample from the ScenarioList
|
328
313
|
|
329
314
|
>>> s = ScenarioList.from_list("a", [1,2,3,4,5,6])
|
330
|
-
>>> s.sample(3
|
315
|
+
>>> s.sample(3)
|
331
316
|
ScenarioList([Scenario({'a': 2}), Scenario({'a': 1}), Scenario({'a': 3})])
|
332
317
|
"""
|
333
|
-
|
334
|
-
|
318
|
+
|
319
|
+
random.seed(seed)
|
335
320
|
|
336
321
|
return ScenarioList(random.sample(self.data, n))
|
337
322
|
|
@@ -579,47 +564,6 @@ class ScenarioList(Base, UserList, ScenarioListMixin):
|
|
579
564
|
func = lambda x: x
|
580
565
|
return cls([Scenario({name: func(value)}) for value in values])
|
581
566
|
|
582
|
-
def table(self, *fields, tablefmt=None, pretty_labels=None) -> str:
|
583
|
-
"""Return the ScenarioList as a table."""
|
584
|
-
|
585
|
-
from tabulate import tabulate_formats
|
586
|
-
|
587
|
-
if tablefmt is not None and tablefmt not in tabulate_formats:
|
588
|
-
raise ValueError(
|
589
|
-
f"Invalid table format: {tablefmt}",
|
590
|
-
f"Valid formats are: {tabulate_formats}",
|
591
|
-
)
|
592
|
-
return self.to_dataset().table(
|
593
|
-
*fields, tablefmt=tablefmt, pretty_labels=pretty_labels
|
594
|
-
)
|
595
|
-
|
596
|
-
def tree(self, node_list: Optional[List[str]] = None) -> str:
|
597
|
-
"""Return the ScenarioList as a tree."""
|
598
|
-
return self.to_dataset().tree(node_list)
|
599
|
-
|
600
|
-
def _summary(self):
|
601
|
-
d = {
|
602
|
-
"EDSL Class name": "ScenarioList",
|
603
|
-
"# Scenarios": len(self),
|
604
|
-
"Scenario Keys": list(self.parameters),
|
605
|
-
}
|
606
|
-
return d
|
607
|
-
|
608
|
-
def reorder_keys(self, new_order):
|
609
|
-
"""Reorder the keys in the scenarios.
|
610
|
-
|
611
|
-
Example:
|
612
|
-
|
613
|
-
>>> s = ScenarioList([Scenario({'a': 1, 'b': 2}), Scenario({'a': 3, 'b': 4})])
|
614
|
-
>>> s.reorder_keys(['b', 'a'])
|
615
|
-
ScenarioList([Scenario({'b': 2, 'a': 1}), Scenario({'b': 4, 'a': 3})])
|
616
|
-
"""
|
617
|
-
new_scenarios = []
|
618
|
-
for scenario in self:
|
619
|
-
new_scenario = Scenario({key: scenario[key] for key in new_order})
|
620
|
-
new_scenarios.append(new_scenario)
|
621
|
-
return ScenarioList(new_scenarios)
|
622
|
-
|
623
567
|
def to_dataset(self) -> "Dataset":
|
624
568
|
"""
|
625
569
|
>>> s = ScenarioList.from_list("a", [1,2,3])
|
@@ -635,32 +579,16 @@ class ScenarioList(Base, UserList, ScenarioListMixin):
|
|
635
579
|
data = [{key: [scenario[key] for scenario in self.data]} for key in keys]
|
636
580
|
return Dataset(data)
|
637
581
|
|
638
|
-
def
|
639
|
-
self, field: str,
|
582
|
+
def split(
|
583
|
+
self, field: str, split_on: str, index: int, new_name: Optional[str] = None
|
640
584
|
) -> ScenarioList:
|
641
|
-
"""
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
>>> s = ScenarioList([Scenario({'a': 1, 'b': [2, True]}), Scenario({'a': 3, 'b': [3, False]})])
|
646
|
-
>>> s.unpack('b')
|
647
|
-
ScenarioList([Scenario({'a': 1, 'b': [2, True], 'b_0': 2, 'b_1': True}), Scenario({'a': 3, 'b': [3, False], 'b_0': 3, 'b_1': False})])
|
648
|
-
>>> s.unpack('b', new_names=['c', 'd'], keep_original=False)
|
649
|
-
ScenarioList([Scenario({'a': 1, 'c': 2, 'd': True}), Scenario({'a': 3, 'c': 3, 'd': False})])
|
650
|
-
|
651
|
-
"""
|
652
|
-
new_names = new_names or [f"{field}_{i}" for i in range(len(self[0][field]))]
|
585
|
+
"""Split a scenario fiel in multiple fields."""
|
586
|
+
if new_name is None:
|
587
|
+
new_name = field + "_split_" + str(index)
|
653
588
|
new_scenarios = []
|
654
589
|
for scenario in self:
|
655
590
|
new_scenario = scenario.copy()
|
656
|
-
|
657
|
-
new_scenario[new_names[0]] = scenario[field]
|
658
|
-
else:
|
659
|
-
for i, new_name in enumerate(new_names):
|
660
|
-
new_scenario[new_name] = scenario[field][i]
|
661
|
-
|
662
|
-
if not keep_original:
|
663
|
-
del new_scenario[field]
|
591
|
+
new_scenario[new_name] = scenario[field].split(split_on)[index]
|
664
592
|
new_scenarios.append(new_scenario)
|
665
593
|
return ScenarioList(new_scenarios)
|
666
594
|
|
@@ -973,32 +901,33 @@ class ScenarioList(Base, UserList, ScenarioListMixin):
|
|
973
901
|
return cls.from_excel(temp_filename, sheet_name=sheet_name)
|
974
902
|
|
975
903
|
@classmethod
|
976
|
-
def
|
977
|
-
|
978
|
-
) -> ScenarioList:
|
979
|
-
"""Create a ScenarioList from a delimited file (CSV/TSV) or URL.
|
904
|
+
def from_csv(cls, source: Union[str, urllib.parse.ParseResult]) -> ScenarioList:
|
905
|
+
"""Create a ScenarioList from a CSV file or URL.
|
980
906
|
|
981
907
|
Args:
|
982
|
-
source: A string representing either a local file path or a URL to a
|
908
|
+
source: A string representing either a local file path or a URL to a CSV file,
|
983
909
|
or a urllib.parse.ParseResult object for a URL.
|
984
|
-
delimiter: The delimiter used in the file. Defaults to ',' for CSV files.
|
985
|
-
Use '\t' for TSV files.
|
986
910
|
|
987
911
|
Returns:
|
988
|
-
ScenarioList: A ScenarioList object containing the data from the
|
912
|
+
ScenarioList: A ScenarioList object containing the data from the CSV.
|
989
913
|
|
990
914
|
Example:
|
991
|
-
# For CSV files
|
992
915
|
|
993
|
-
|
994
|
-
|
995
|
-
|
996
|
-
|
997
|
-
|
998
|
-
|
999
|
-
|
1000
|
-
|
916
|
+
>>> import tempfile
|
917
|
+
>>> import os
|
918
|
+
>>> with tempfile.NamedTemporaryFile(delete=False, mode='w', suffix='.csv') as f:
|
919
|
+
... _ = f.write("name,age,location\\nAlice,30,New York\\nBob,25,Los Angeles\\n")
|
920
|
+
... temp_filename = f.name
|
921
|
+
>>> scenario_list = ScenarioList.from_csv(temp_filename)
|
922
|
+
>>> len(scenario_list)
|
923
|
+
2
|
924
|
+
>>> scenario_list[0]['name']
|
925
|
+
'Alice'
|
926
|
+
>>> scenario_list[1]['age']
|
927
|
+
'25'
|
1001
928
|
|
929
|
+
>>> url = "https://example.com/data.csv"
|
930
|
+
>>> ## scenario_list_from_url = ScenarioList.from_csv(url)
|
1002
931
|
"""
|
1003
932
|
from edsl.scenarios.Scenario import Scenario
|
1004
933
|
|
@@ -1011,111 +940,24 @@ class ScenarioList(Base, UserList, ScenarioListMixin):
|
|
1011
940
|
|
1012
941
|
if isinstance(source, str) and is_url(source):
|
1013
942
|
with urllib.request.urlopen(source) as response:
|
1014
|
-
|
1015
|
-
|
943
|
+
csv_content = response.read().decode("utf-8")
|
944
|
+
csv_file = StringIO(csv_content)
|
1016
945
|
elif isinstance(source, urllib.parse.ParseResult):
|
1017
946
|
with urllib.request.urlopen(source.geturl()) as response:
|
1018
|
-
|
1019
|
-
|
947
|
+
csv_content = response.read().decode("utf-8")
|
948
|
+
csv_file = StringIO(csv_content)
|
1020
949
|
else:
|
1021
|
-
|
950
|
+
csv_file = open(source, "r")
|
1022
951
|
|
1023
952
|
try:
|
1024
|
-
reader = csv.reader(
|
953
|
+
reader = csv.reader(csv_file)
|
1025
954
|
header = next(reader)
|
1026
955
|
observations = [Scenario(dict(zip(header, row))) for row in reader]
|
1027
956
|
finally:
|
1028
|
-
|
957
|
+
csv_file.close()
|
1029
958
|
|
1030
959
|
return cls(observations)
|
1031
960
|
|
1032
|
-
# Convenience methods for specific file types
|
1033
|
-
@classmethod
|
1034
|
-
def from_csv(cls, source: Union[str, urllib.parse.ParseResult]) -> ScenarioList:
|
1035
|
-
"""Create a ScenarioList from a CSV file or URL."""
|
1036
|
-
return cls.from_delimited_file(source, delimiter=",")
|
1037
|
-
|
1038
|
-
def left_join(self, other: ScenarioList, by: Union[str, list[str]]) -> ScenarioList:
|
1039
|
-
"""Perform a left join with another ScenarioList, following SQL join semantics.
|
1040
|
-
|
1041
|
-
Args:
|
1042
|
-
other: The ScenarioList to join with
|
1043
|
-
by: String or list of strings representing the key(s) to join on. Cannot be empty.
|
1044
|
-
|
1045
|
-
>>> s1 = ScenarioList([Scenario({'name': 'Alice', 'age': 30}), Scenario({'name': 'Bob', 'age': 25})])
|
1046
|
-
>>> s2 = ScenarioList([Scenario({'name': 'Alice', 'location': 'New York'}), Scenario({'name': 'Charlie', 'location': 'Los Angeles'})])
|
1047
|
-
>>> s3 = s1.left_join(s2, 'name')
|
1048
|
-
>>> s3 == ScenarioList([Scenario({'age': 30, 'location': 'New York', 'name': 'Alice'}), Scenario({'age': 25, 'location': None, 'name': 'Bob'})])
|
1049
|
-
True
|
1050
|
-
"""
|
1051
|
-
from edsl.scenarios.ScenarioJoin import ScenarioJoin
|
1052
|
-
|
1053
|
-
sj = ScenarioJoin(self, other)
|
1054
|
-
return sj.left_join(by)
|
1055
|
-
# # Validate join keys
|
1056
|
-
# if not by:
|
1057
|
-
# raise ValueError(
|
1058
|
-
# "Join keys cannot be empty. Please specify at least one key to join on."
|
1059
|
-
# )
|
1060
|
-
|
1061
|
-
# # Convert single string to list for consistent handling
|
1062
|
-
# by_keys = [by] if isinstance(by, str) else by
|
1063
|
-
|
1064
|
-
# # Verify all join keys exist in both ScenarioLists
|
1065
|
-
# left_keys = set(next(iter(self)).keys()) if self else set()
|
1066
|
-
# right_keys = set(next(iter(other)).keys()) if other else set()
|
1067
|
-
|
1068
|
-
# missing_left = set(by_keys) - left_keys
|
1069
|
-
# missing_right = set(by_keys) - right_keys
|
1070
|
-
# if missing_left or missing_right:
|
1071
|
-
# missing = missing_left | missing_right
|
1072
|
-
# raise ValueError(f"Join key(s) {missing} not found in both ScenarioLists")
|
1073
|
-
|
1074
|
-
# # Create lookup dictionary from the other ScenarioList
|
1075
|
-
# def get_key_tuple(scenario: Scenario, keys: list[str]) -> tuple:
|
1076
|
-
# return tuple(scenario[k] for k in keys)
|
1077
|
-
|
1078
|
-
# other_dict = {get_key_tuple(scenario, by_keys): scenario for scenario in other}
|
1079
|
-
|
1080
|
-
# # Collect all possible keys (like SQL combining all columns)
|
1081
|
-
# all_keys = set()
|
1082
|
-
# for scenario in self:
|
1083
|
-
# all_keys.update(scenario.keys())
|
1084
|
-
# for scenario in other:
|
1085
|
-
# all_keys.update(scenario.keys())
|
1086
|
-
|
1087
|
-
# new_scenarios = []
|
1088
|
-
# for scenario in self:
|
1089
|
-
# new_scenario = {
|
1090
|
-
# key: None for key in all_keys
|
1091
|
-
# } # Start with nulls (like SQL)
|
1092
|
-
# new_scenario.update(scenario) # Add all left values
|
1093
|
-
|
1094
|
-
# key_tuple = get_key_tuple(scenario, by_keys)
|
1095
|
-
# if matching_scenario := other_dict.get(key_tuple):
|
1096
|
-
# # Check for overlapping keys with different values
|
1097
|
-
# overlapping_keys = set(scenario.keys()) & set(matching_scenario.keys())
|
1098
|
-
# for key in overlapping_keys:
|
1099
|
-
# if key not in by_keys and scenario[key] != matching_scenario[key]:
|
1100
|
-
# join_conditions = [f"{k}='{scenario[k]}'" for k in by_keys]
|
1101
|
-
# print(
|
1102
|
-
# f"Warning: Conflicting values for key '{key}' where {' AND '.join(join_conditions)}. "
|
1103
|
-
# f"Keeping left value: {scenario[key]} (discarding: {matching_scenario[key]})"
|
1104
|
-
# )
|
1105
|
-
|
1106
|
-
# # Only update with non-overlapping keys from matching scenario
|
1107
|
-
# new_keys = set(matching_scenario.keys()) - set(scenario.keys())
|
1108
|
-
# new_scenario.update({k: matching_scenario[k] for k in new_keys})
|
1109
|
-
|
1110
|
-
# new_scenarios.append(Scenario(new_scenario))
|
1111
|
-
|
1112
|
-
# return ScenarioList(new_scenarios)
|
1113
|
-
|
1114
|
-
@classmethod
|
1115
|
-
def from_tsv(cls, source: Union[str, urllib.parse.ParseResult]) -> ScenarioList:
|
1116
|
-
"""Create a ScenarioList from a TSV file or URL."""
|
1117
|
-
return cls.from_delimited_file(source, delimiter="\t")
|
1118
|
-
|
1119
961
|
def to_dict(self, sort=False, add_edsl_version=True) -> dict:
|
1120
962
|
"""
|
1121
963
|
>>> s = ScenarioList([Scenario({'food': 'wood chips'}), Scenario({'food': 'wood-fired pizza'})])
|
@@ -1232,25 +1074,8 @@ class ScenarioList(Base, UserList, ScenarioListMixin):
|
|
1232
1074
|
"""
|
1233
1075
|
from edsl.agents.AgentList import AgentList
|
1234
1076
|
from edsl.agents.Agent import Agent
|
1235
|
-
import warnings
|
1236
|
-
|
1237
|
-
agents = []
|
1238
|
-
for scenario in self:
|
1239
|
-
new_scenario = scenario.copy().data
|
1240
|
-
if "name" in new_scenario:
|
1241
|
-
name = new_scenario.pop("name")
|
1242
|
-
proposed_agent_name = "agent_name"
|
1243
|
-
while proposed_agent_name not in new_scenario:
|
1244
|
-
proposed_agent_name += "_"
|
1245
|
-
warnings.warn(
|
1246
|
-
f"The 'name' field is reserved for the agent's name---putting this value in {proposed_agent_name}"
|
1247
|
-
)
|
1248
|
-
new_scenario[proposed_agent_name] = name
|
1249
|
-
agents.append(Agent(traits=new_scenario, name=name))
|
1250
|
-
else:
|
1251
|
-
agents.append(Agent(traits=new_scenario))
|
1252
1077
|
|
1253
|
-
return AgentList(
|
1078
|
+
return AgentList([Agent(traits=s.data) for s in self])
|
1254
1079
|
|
1255
1080
|
def chunk(
|
1256
1081
|
self,
|
edsl/surveys/Survey.py
CHANGED
@@ -41,8 +41,6 @@ class ValidatedString(str):
|
|
41
41
|
class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
42
42
|
"""A collection of questions that supports skip logic."""
|
43
43
|
|
44
|
-
__documentation__ = """https://docs.expectedparrot.com/en/latest/surveys.html"""
|
45
|
-
|
46
44
|
questions = QuestionsDescriptor()
|
47
45
|
"""
|
48
46
|
A collection of questions that supports skip logic.
|
@@ -1589,22 +1587,10 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1589
1587
|
# question_names_string = ", ".join([repr(name) for name in self.question_names])
|
1590
1588
|
return f"Survey(questions=[{questions_string}], memory_plan={self.memory_plan}, rule_collection={self.rule_collection}, question_groups={self.question_groups})"
|
1591
1589
|
|
1592
|
-
def _summary(self) -> dict:
|
1593
|
-
return {
|
1594
|
-
"EDSL Class": "Survey",
|
1595
|
-
"Number of Questions": len(self),
|
1596
|
-
"Question Names": self.question_names,
|
1597
|
-
}
|
1598
|
-
|
1599
1590
|
def _repr_html_(self) -> str:
|
1600
|
-
|
1601
|
-
return str(self.summary(format="html")) + footer
|
1602
|
-
|
1603
|
-
def tree(self, node_list: Optional[List[str]] = None):
|
1604
|
-
return self.to_scenario_list().tree(node_list=node_list)
|
1591
|
+
from edsl.utilities.utilities import data_to_html
|
1605
1592
|
|
1606
|
-
|
1607
|
-
return self.to_scenario_list().to_dataset().table(*fields, tablefmt=tablefmt)
|
1593
|
+
return data_to_html(self.to_dict())
|
1608
1594
|
|
1609
1595
|
def rich_print(self) -> Table:
|
1610
1596
|
"""Print the survey in a rich format.
|
@@ -1,85 +1,27 @@
|
|
1
|
-
"""A mixin for visualizing the flow of a survey
|
1
|
+
"""A mixin for visualizing the flow of a survey."""
|
2
2
|
|
3
3
|
from typing import Optional
|
4
4
|
from edsl.surveys.base import RulePriority, EndOfSurvey
|
5
5
|
import tempfile
|
6
|
-
import os
|
7
6
|
|
8
7
|
|
9
8
|
class SurveyFlowVisualizationMixin:
|
10
|
-
"""A mixin for visualizing the flow of a survey
|
9
|
+
"""A mixin for visualizing the flow of a survey."""
|
11
10
|
|
12
11
|
def show_flow(self, filename: Optional[str] = None):
|
13
|
-
"""Create an image showing the flow of users through the survey
|
12
|
+
"""Create an image showing the flow of users through the survey."""
|
14
13
|
# Create a graph object
|
15
14
|
import pydot
|
16
15
|
|
17
16
|
graph = pydot.Dot(graph_type="digraph")
|
18
17
|
|
19
|
-
#
|
20
|
-
params_and_refs = set()
|
21
|
-
param_to_questions = {} # Keep track of which questions use each parameter
|
22
|
-
answer_refs = set() # Track answer references between questions
|
23
|
-
|
24
|
-
# First pass: collect parameters and their question associations
|
18
|
+
# Add nodes for each question
|
25
19
|
for index, question in enumerate(self.questions):
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
)
|
30
|
-
graph.add_node(question_node)
|
31
|
-
|
32
|
-
if hasattr(question, "parameters"):
|
33
|
-
for param in question.parameters:
|
34
|
-
# Check if this is an answer reference (contains '.answer')
|
35
|
-
if ".answer" in param:
|
36
|
-
answer_refs.add((param.split(".")[0], index))
|
37
|
-
else:
|
38
|
-
params_and_refs.add(param)
|
39
|
-
if param not in param_to_questions:
|
40
|
-
param_to_questions[param] = []
|
41
|
-
param_to_questions[param].append(index)
|
42
|
-
|
43
|
-
# Create parameter nodes and connect them to questions
|
44
|
-
for param in params_and_refs:
|
45
|
-
param_node_name = f"param_{param}"
|
46
|
-
param_node = pydot.Node(
|
47
|
-
param_node_name,
|
48
|
-
label=f"{{{{ {param} }}}}",
|
49
|
-
shape="box",
|
50
|
-
style="filled",
|
51
|
-
fillcolor="lightgrey",
|
52
|
-
fontsize="10",
|
53
|
-
)
|
54
|
-
graph.add_node(param_node)
|
55
|
-
|
56
|
-
# Connect this parameter to all questions that use it
|
57
|
-
for q_index in param_to_questions[param]:
|
58
|
-
param_edge = pydot.Edge(
|
59
|
-
param_node_name,
|
60
|
-
f"Q{q_index}",
|
61
|
-
style="dotted",
|
62
|
-
color="grey",
|
63
|
-
arrowsize="0.5",
|
20
|
+
graph.add_node(
|
21
|
+
pydot.Node(
|
22
|
+
f"Q{index}", label=f"{question.question_name}", shape="ellipse"
|
64
23
|
)
|
65
|
-
graph.add_edge(param_edge)
|
66
|
-
|
67
|
-
# Add edges for answer references
|
68
|
-
for source_q_name, target_q_index in answer_refs:
|
69
|
-
# Find the source question index by name
|
70
|
-
source_q_index = next(
|
71
|
-
i
|
72
|
-
for i, q in enumerate(self.questions)
|
73
|
-
if q.question_name == source_q_name
|
74
|
-
)
|
75
|
-
ref_edge = pydot.Edge(
|
76
|
-
f"Q{source_q_index}",
|
77
|
-
f"Q{target_q_index}",
|
78
|
-
style="dashed",
|
79
|
-
color="purple",
|
80
|
-
label="answer reference",
|
81
24
|
)
|
82
|
-
graph.add_edge(ref_edge)
|
83
25
|
|
84
26
|
# Add an "EndOfSurvey" node
|
85
27
|
graph.add_node(
|
@@ -88,7 +30,7 @@ class SurveyFlowVisualizationMixin:
|
|
88
30
|
|
89
31
|
# Add edges for normal flow through the survey
|
90
32
|
num_questions = len(self.questions)
|
91
|
-
for index in range(num_questions - 1):
|
33
|
+
for index in range(num_questions - 1): # From Q1 to Q3
|
92
34
|
graph.add_edge(pydot.Edge(f"Q{index}", f"Q{index+1}"))
|
93
35
|
|
94
36
|
graph.add_edge(pydot.Edge(f"Q{num_questions-1}", "EndOfSurvey"))
|
@@ -122,7 +64,7 @@ class SurveyFlowVisualizationMixin:
|
|
122
64
|
if rule.next_q != EndOfSurvey and rule.next_q < num_questions
|
123
65
|
else "EndOfSurvey"
|
124
66
|
)
|
125
|
-
if rule.before_rule:
|
67
|
+
if rule.before_rule: # Assume skip rules have an attribute `is_skip`
|
126
68
|
edge = pydot.Edge(
|
127
69
|
source_node,
|
128
70
|
target_node,
|
@@ -18,18 +18,6 @@ class Instruction:
|
|
18
18
|
def __repr__(self):
|
19
19
|
return """Instruction(name="{}", text="{}")""".format(self.name, self.text)
|
20
20
|
|
21
|
-
def _repr_html_(self):
|
22
|
-
d = self.to_dict(add_edsl_version=False)
|
23
|
-
data = [[k, v] for k, v in d.items()]
|
24
|
-
from tabulate import tabulate
|
25
|
-
|
26
|
-
table = str(tabulate(data, headers=["keys", "values"], tablefmt="html"))
|
27
|
-
return f"<pre>{table}</pre>"
|
28
|
-
|
29
|
-
@classmethod
|
30
|
-
def example(cls) -> "Instruction":
|
31
|
-
return cls(name="example", text="This is an example instruction.")
|
32
|
-
|
33
21
|
def to_dict(self, add_edsl_version=True):
|
34
22
|
d = {
|
35
23
|
"name": self.name,
|
@@ -40,11 +40,11 @@
|
|
40
40
|
</tr>
|
41
41
|
<tr>
|
42
42
|
<td>Scenario</td>
|
43
|
-
<td>{{ interview.scenario.
|
43
|
+
<td>{{ interview.scenario._repr_html_() }}</td>
|
44
44
|
</tr>
|
45
45
|
<tr>
|
46
46
|
<td>Agent</td>
|
47
|
-
<td>{{ interview.agent.
|
47
|
+
<td>{{ interview.agent._repr_html_() }}</td>
|
48
48
|
</tr>
|
49
49
|
<tr>
|
50
50
|
<td>Model name</td>
|
@@ -56,7 +56,7 @@
|
|
56
56
|
</tr>
|
57
57
|
<tr>
|
58
58
|
<td>Model parameters</td>
|
59
|
-
<td>{{ interview.model.
|
59
|
+
<td>{{ interview.model._repr_html_() }}</td>
|
60
60
|
</tr>
|
61
61
|
<tr>
|
62
62
|
<td>User Prompt</td>
|
@@ -1,19 +1,10 @@
|
|
1
|
-
|
2
|
-
{% if interviews|length > max_interviews %}
|
3
|
-
<h1>Only showing the first {{ max_interviews }} interviews with errors</h1>
|
4
|
-
{% else %}
|
5
|
-
<h1>Showing all interviews</h1>
|
6
|
-
{% endif %}
|
7
|
-
|
8
1
|
{% for index, interview in interviews.items() %}
|
9
|
-
{% if
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
{%
|
16
|
-
|
17
|
-
|
18
|
-
{% endif %}
|
19
|
-
{% endfor %}
|
2
|
+
{% if interview.exceptions != {} %}
|
3
|
+
<div class="interview">Interview: {{ index }} </div>
|
4
|
+
Model: {{ interview.model.model }}
|
5
|
+
<h1>Failing questions</h1>
|
6
|
+
{% endif %}
|
7
|
+
{% for question, exceptions in interview.exceptions.items() %}
|
8
|
+
{% include 'interview_details.html' %}
|
9
|
+
{% endfor %}
|
10
|
+
{% endfor %}
|
edsl/utilities/utilities.py
CHANGED
@@ -207,21 +207,6 @@ def is_notebook() -> bool:
|
|
207
207
|
return False # Probably standard Python interpreter
|
208
208
|
|
209
209
|
|
210
|
-
def file_notice(file_name):
|
211
|
-
"""Print a notice about the file being created."""
|
212
|
-
if is_notebook():
|
213
|
-
from IPython.display import HTML, display
|
214
|
-
|
215
|
-
link_text = "Download file"
|
216
|
-
display(
|
217
|
-
HTML(
|
218
|
-
f'<p>File created: {file_name}</p>.<a href="{file_name}" download>{link_text}</a>'
|
219
|
-
)
|
220
|
-
)
|
221
|
-
else:
|
222
|
-
print(f"File created: {file_name}")
|
223
|
-
|
224
|
-
|
225
210
|
class HTMLSnippet(str):
|
226
211
|
"""Create an object with html content (`value`).
|
227
212
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: edsl
|
3
|
-
Version: 0.1.38
|
3
|
+
Version: 0.1.38.dev2
|
4
4
|
Summary: Create and analyze LLM-based surveys
|
5
5
|
Home-page: https://www.expectedparrot.com/
|
6
6
|
License: MIT
|
@@ -45,7 +45,6 @@ Requires-Dist: rich (>=13.7.0,<14.0.0)
|
|
45
45
|
Requires-Dist: setuptools (<72.0)
|
46
46
|
Requires-Dist: simpleeval (>=0.9.13,<0.10.0)
|
47
47
|
Requires-Dist: sqlalchemy (>=2.0.23,<3.0.0)
|
48
|
-
Requires-Dist: tabulate (>=0.9.0,<0.10.0)
|
49
48
|
Requires-Dist: tenacity (>=8.2.3,<9.0.0)
|
50
49
|
Requires-Dist: urllib3 (>=1.25.4,<1.27)
|
51
50
|
Project-URL: Documentation, https://docs.expectedparrot.com
|