edsl 0.1.59__py3-none-any.whl → 0.1.60__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/__version__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.59"
1
+ __version__ = "0.1.60"
@@ -17,6 +17,7 @@ class EDSLOutput(NamedTuple):
17
17
  answer: Any
18
18
  generated_tokens: str
19
19
  comment: Optional[str] = None
20
+ reasoning_summary: Optional[Any] = None
20
21
 
21
22
 
22
23
  class ModelResponse(NamedTuple):
@@ -49,6 +50,7 @@ class EDSLResultObjectInput(NamedTuple):
49
50
  cache_key: str
50
51
  answer: Any
51
52
  comment: str
53
+ reasoning_summary: Optional[Any] = None
52
54
  validated: bool = False
53
55
  exception_occurred: Exception = None
54
56
  input_tokens: Optional[int] = None
@@ -96,12 +98,15 @@ class Answers(UserDict):
96
98
  answer = response.answer
97
99
  comment = response.comment
98
100
  generated_tokens = response.generated_tokens
101
+ reasoning_summary = response.reasoning_summary
99
102
  # record the answer
100
103
  if generated_tokens:
101
104
  self[question.question_name + "_generated_tokens"] = generated_tokens
102
105
  self[question.question_name] = answer
103
106
  if comment:
104
107
  self[question.question_name + "_comment"] = comment
108
+ if reasoning_summary:
109
+ self[question.question_name + "_reasoning_summary"] = reasoning_summary
105
110
 
106
111
  def replace_missing_answers_with_none(self, survey: "Survey") -> None:
107
112
  """Replace missing answers with None. Answers can be missing if the agent skips a question."""
edsl/base/enums.py CHANGED
@@ -57,6 +57,7 @@ class InferenceServiceType(EnumWithChecks):
57
57
  DEEP_INFRA = "deep_infra"
58
58
  REPLICATE = "replicate"
59
59
  OPENAI = "openai"
60
+ OPENAI_V2 = "openai_v2"
60
61
  GOOGLE = "google"
61
62
  TEST = "test"
62
63
  ANTHROPIC = "anthropic"
@@ -77,6 +78,7 @@ InferenceServiceLiteral = Literal[
77
78
  "deep_infra",
78
79
  "replicate",
79
80
  "openai",
81
+ "openai_v2",
80
82
  "google",
81
83
  "test",
82
84
  "anthropic",
@@ -93,6 +95,7 @@ InferenceServiceLiteral = Literal[
93
95
  available_models_urls = {
94
96
  "anthropic": "https://docs.anthropic.com/en/docs/about-claude/models",
95
97
  "openai": "https://platform.openai.com/docs/models/gp",
98
+ "openai_v2": "https://platform.openai.com/docs/models/gp",
96
99
  "groq": "https://console.groq.com/docs/models",
97
100
  "google": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models",
98
101
  }
@@ -102,6 +105,7 @@ service_to_api_keyname = {
102
105
  InferenceServiceType.DEEP_INFRA.value: "DEEP_INFRA_API_KEY",
103
106
  InferenceServiceType.REPLICATE.value: "TBD",
104
107
  InferenceServiceType.OPENAI.value: "OPENAI_API_KEY",
108
+ InferenceServiceType.OPENAI_V2.value: "OPENAI_API_KEY",
105
109
  InferenceServiceType.GOOGLE.value: "GOOGLE_API_KEY",
106
110
  InferenceServiceType.TEST.value: "TBD",
107
111
  InferenceServiceType.ANTHROPIC.value: "ANTHROPIC_API_KEY",
@@ -135,7 +139,7 @@ class TokenPricing:
135
139
  and self.prompt_token_price == other.prompt_token_price
136
140
  and self.completion_token_price == other.completion_token_price
137
141
  )
138
-
142
+
139
143
  @classmethod
140
144
  def example(cls) -> "TokenPricing":
141
145
  """Return an example TokenPricing object."""
@@ -145,6 +149,7 @@ class TokenPricing:
145
149
  completion_token_price_per_k=0.03,
146
150
  )
147
151
 
152
+
148
153
  pricing = {
149
154
  "dbrx-instruct": TokenPricing(
150
155
  model_name="dbrx-instruct",
@@ -212,4 +217,4 @@ def get_token_pricing(model_name):
212
217
  model_name=model_name,
213
218
  prompt_token_price_per_k=0.0,
214
219
  completion_token_price_per_k=0.0,
215
- )
220
+ )
@@ -357,7 +357,7 @@ class DataOperationsBase:
357
357
  4
358
358
  >>> engine = Results.example()._db(shape = "long")
359
359
  >>> len(engine.execute(text("SELECT * FROM self")).fetchall())
360
- 204
360
+ 212
361
361
  """
362
362
  # Import needed for database connection
363
363
  from sqlalchemy import create_engine
@@ -442,7 +442,7 @@ class DataOperationsBase:
442
442
 
443
443
  # Using long format
444
444
  >>> len(r.sql("SELECT * FROM self", shape="long"))
445
- 204
445
+ 212
446
446
  """
447
447
  import pandas as pd
448
448
 
@@ -8,6 +8,7 @@ from .groq_service import GroqService
8
8
  from .mistral_ai_service import MistralAIService
9
9
  from .ollama_service import OllamaService
10
10
  from .open_ai_service import OpenAIService
11
+ from .open_ai_service_v2 import OpenAIServiceV2
11
12
  from .perplexity_service import PerplexityService
12
13
  from .test_service import TestService
13
14
  from .together_ai_service import TogetherAIService
@@ -24,8 +25,9 @@ __all__ = [
24
25
  "MistralAIService",
25
26
  "OllamaService",
26
27
  "OpenAIService",
28
+ "OpenAIServiceV2",
27
29
  "PerplexityService",
28
30
  "TestService",
29
31
  "TogetherAIService",
30
32
  "XAIService",
31
- ]
33
+ ]
@@ -0,0 +1,243 @@
1
+ from __future__ import annotations
2
+ from typing import Any, List, Optional, Dict, NewType, TYPE_CHECKING
3
+ import os
4
+
5
+ import openai
6
+
7
+ from ..inference_service_abc import InferenceServiceABC
8
+
9
+ # Use TYPE_CHECKING to avoid circular imports at runtime
10
+ if TYPE_CHECKING:
11
+ from ...language_models import LanguageModel
12
+ from ..rate_limits_cache import rate_limits
13
+
14
+ # Default to completions API but can use responses API with parameter
15
+
16
+ if TYPE_CHECKING:
17
+ from ....scenarios.file_store import FileStore as Files
18
+ from ....invigilators.invigilator_base import InvigilatorBase as InvigilatorAI
19
+
20
+
21
+ APIToken = NewType("APIToken", str)
22
+
23
+
24
+ class OpenAIServiceV2(InferenceServiceABC):
25
+ """OpenAI service class using the Responses API."""
26
+
27
+ _inference_service_ = "openai_v2"
28
+ _env_key_name_ = "OPENAI_API_KEY"
29
+ _base_url_ = None
30
+
31
+ _sync_client_ = openai.OpenAI
32
+ _async_client_ = openai.AsyncOpenAI
33
+
34
+ _sync_client_instances: Dict[APIToken, openai.OpenAI] = {}
35
+ _async_client_instances: Dict[APIToken, openai.AsyncOpenAI] = {}
36
+
37
+ # sequence to extract text from response.output
38
+ key_sequence = ["output", 1, "content", 0, "text"]
39
+ usage_sequence = ["usage"]
40
+ # sequence to extract reasoning summary from response.output
41
+ reasoning_sequence = ["output", 0, "summary"]
42
+ input_token_name = "prompt_tokens"
43
+ output_token_name = "completion_tokens"
44
+
45
+ available_models_url = "https://platform.openai.com/docs/models/gp"
46
+
47
+ def __init_subclass__(cls, **kwargs):
48
+ super().__init_subclass__(**kwargs)
49
+ cls._sync_client_instances = {}
50
+ cls._async_client_instances = {}
51
+
52
+ @classmethod
53
+ def sync_client(cls, api_key: str) -> openai.OpenAI:
54
+ if api_key not in cls._sync_client_instances:
55
+ client = cls._sync_client_(
56
+ api_key=api_key,
57
+ base_url=cls._base_url_,
58
+ )
59
+ cls._sync_client_instances[api_key] = client
60
+ return cls._sync_client_instances[api_key]
61
+
62
+ @classmethod
63
+ def async_client(cls, api_key: str) -> openai.AsyncOpenAI:
64
+ if api_key not in cls._async_client_instances:
65
+ client = cls._async_client_(
66
+ api_key=api_key,
67
+ base_url=cls._base_url_,
68
+ )
69
+ cls._async_client_instances[api_key] = client
70
+ return cls._async_client_instances[api_key]
71
+
72
+ model_exclude_list = [
73
+ "whisper-1",
74
+ "davinci-002",
75
+ "dall-e-2",
76
+ "tts-1-hd-1106",
77
+ "tts-1-hd",
78
+ "dall-e-3",
79
+ "tts-1",
80
+ "babbage-002",
81
+ "tts-1-1106",
82
+ "text-embedding-3-large",
83
+ "text-embedding-3-small",
84
+ "text-embedding-ada-002",
85
+ "ft:davinci-002:mit-horton-lab::8OfuHgoo",
86
+ "gpt-3.5-turbo-instruct-0914",
87
+ "gpt-3.5-turbo-instruct",
88
+ ]
89
+ _models_list_cache: List[str] = []
90
+
91
+ @classmethod
92
+ def get_model_list(cls, api_key: str | None = None) -> List[str]:
93
+ if api_key is None:
94
+ api_key = os.getenv(cls._env_key_name_)
95
+ raw = cls.sync_client(api_key).models.list()
96
+ return raw.data if hasattr(raw, "data") else raw
97
+
98
+ @classmethod
99
+ def available(cls, api_token: str | None = None) -> List[str]:
100
+ if api_token is None:
101
+ api_token = os.getenv(cls._env_key_name_)
102
+ if not cls._models_list_cache:
103
+ data = cls.get_model_list(api_key=api_token)
104
+ cls._models_list_cache = [
105
+ m.id for m in data if m.id not in cls.model_exclude_list
106
+ ]
107
+ return cls._models_list_cache
108
+
109
+ @classmethod
110
+ def create_model(
111
+ cls,
112
+ model_name: str,
113
+ model_class_name: str | None = None,
114
+ ) -> LanguageModel:
115
+ if model_class_name is None:
116
+ model_class_name = cls.to_class_name(model_name)
117
+
118
+ from ...language_models import LanguageModel
119
+
120
+ class LLM(LanguageModel):
121
+ """Child class for OpenAI Responses API"""
122
+
123
+ key_sequence = cls.key_sequence
124
+ usage_sequence = cls.usage_sequence
125
+ reasoning_sequence = cls.reasoning_sequence
126
+ input_token_name = cls.input_token_name
127
+ output_token_name = cls.output_token_name
128
+ _inference_service_ = cls._inference_service_
129
+ _model_ = model_name
130
+ _parameters_ = {
131
+ "temperature": 0.5,
132
+ "max_tokens": 2000,
133
+ "top_p": 1,
134
+ "frequency_penalty": 0,
135
+ "presence_penalty": 0,
136
+ "logprobs": False,
137
+ "top_logprobs": 3,
138
+ }
139
+
140
+ def sync_client(self) -> openai.OpenAI:
141
+ return cls.sync_client(api_key=self.api_token)
142
+
143
+ def async_client(self) -> openai.AsyncOpenAI:
144
+ return cls.async_client(api_key=self.api_token)
145
+
146
+ @classmethod
147
+ def available(cls) -> list[str]:
148
+ return cls.sync_client().models.list().data
149
+
150
+ def get_headers(self) -> dict[str, Any]:
151
+ client = self.sync_client()
152
+ response = client.responses.with_raw_response.create(
153
+ model=self.model,
154
+ input=[{"role": "user", "content": "Say this is a test"}],
155
+ store=False,
156
+ )
157
+ return dict(response.headers)
158
+
159
+ def get_rate_limits(self) -> dict[str, Any]:
160
+ try:
161
+ headers = rate_limits.get("openai", self.get_headers())
162
+ except Exception:
163
+ return {"rpm": 10000, "tpm": 2000000}
164
+ return {
165
+ "rpm": int(headers["x-ratelimit-limit-requests"]),
166
+ "tpm": int(headers["x-ratelimit-limit-tokens"]),
167
+ }
168
+
169
+ async def async_execute_model_call(
170
+ self,
171
+ user_prompt: str,
172
+ system_prompt: str = "",
173
+ files_list: Optional[List[Files]] = None,
174
+ invigilator: Optional[InvigilatorAI] = None,
175
+ ) -> dict[str, Any]:
176
+ content = user_prompt
177
+ if files_list:
178
+ # embed files as separate inputs
179
+ content = [{"type": "text", "text": user_prompt}]
180
+ for f in files_list:
181
+ content.append(
182
+ {
183
+ "type": "image_url",
184
+ "image_url": {
185
+ "url": f"data:{f.mime_type};base64,{f.base64_string}"
186
+ },
187
+ }
188
+ )
189
+ # build input sequence
190
+ messages: Any
191
+ if system_prompt and not self.omit_system_prompt_if_empty:
192
+ messages = [
193
+ {"role": "system", "content": system_prompt},
194
+ {"role": "user", "content": content},
195
+ ]
196
+ else:
197
+ messages = [{"role": "user", "content": content}]
198
+
199
+ # All OpenAI models with the responses API use these base parameters
200
+ params = {
201
+ "model": self.model,
202
+ "input": messages,
203
+ "temperature": self.temperature,
204
+ "top_p": self.top_p,
205
+ "store": False,
206
+ }
207
+
208
+ # Check if this is a reasoning model (o-series models)
209
+ is_reasoning_model = any(tag in self.model for tag in ["o1", "o1-mini", "o3", "o3-mini", "o1-pro", "o4-mini"])
210
+
211
+ # Only add reasoning parameter for reasoning models
212
+ if is_reasoning_model:
213
+ params["reasoning"] = {"summary": "auto"}
214
+
215
+ # For all models using the responses API, use max_output_tokens
216
+ # instead of max_tokens (which is for the completions API)
217
+ params["max_output_tokens"] = self.max_tokens
218
+
219
+ # Specifically for o-series, we also set temperature to 1
220
+ if is_reasoning_model:
221
+ params["temperature"] = 1
222
+
223
+ client = self.async_client()
224
+ try:
225
+ response = await client.responses.create(**params)
226
+
227
+ except Exception as e:
228
+ return {"message": str(e)}
229
+
230
+ # convert to dict
231
+ response_dict = response.model_dump()
232
+ return response_dict
233
+
234
+ LLM.__name__ = model_class_name
235
+ return LLM
236
+
237
+ @staticmethod
238
+ def _create_reasoning_sequence():
239
+ """Create the reasoning sequence for extracting reasoning summaries from model responses."""
240
+ # For OpenAI responses, the reasoning summary is typically found at:
241
+ # ["output", 0, "summary"]
242
+ # This is the path to the 'summary' field in the first item of the 'output' array
243
+ return ["output", 0, "summary"]
@@ -213,6 +213,9 @@ class Answers(UserDict):
213
213
  if comment:
214
214
  self[question.question_name + "_comment"] = comment
215
215
 
216
+ if getattr(response, "reasoning_summary", None):
217
+ self[question.question_name + "_reasoning_summary"] = response.reasoning_summary
218
+
216
219
  def replace_missing_answers_with_none(self, survey: "Survey") -> None:
217
220
  """
218
221
  Replace missing answers with None for all questions in the survey.
@@ -363,13 +363,35 @@ class KeyLookupBuilder:
363
363
  >>> builder._add_api_key("OPENAI_API_KEY", "sk-1234", "env")
364
364
  >>> 'sk-1234' == builder.key_data["openai"][-1].value
365
365
  True
366
+ >>> 'sk-1234' == builder.key_data["openai_v2"][-1].value
367
+ True
366
368
  """
367
369
  service = api_keyname_to_service[key]
368
370
  new_entry = APIKeyEntry(service=service, name=key, value=value, source=source)
369
- if service not in self.key_data:
370
- self.key_data[service] = [new_entry]
371
+
372
+ # Special case for OPENAI_API_KEY - add to both openai and openai_v2
373
+ if key == "OPENAI_API_KEY":
374
+ # Add to openai service
375
+ openai_service = "openai"
376
+ openai_entry = APIKeyEntry(service=openai_service, name=key, value=value, source=source)
377
+ if openai_service not in self.key_data:
378
+ self.key_data[openai_service] = [openai_entry]
379
+ else:
380
+ self.key_data[openai_service].append(openai_entry)
381
+
382
+ # Add to openai_v2 service
383
+ openai_v2_service = "openai_v2"
384
+ openai_v2_entry = APIKeyEntry(service=openai_v2_service, name=key, value=value, source=source)
385
+ if openai_v2_service not in self.key_data:
386
+ self.key_data[openai_v2_service] = [openai_v2_entry]
387
+ else:
388
+ self.key_data[openai_v2_service].append(openai_v2_entry)
371
389
  else:
372
- self.key_data[service].append(new_entry)
390
+ # Normal case for all other API keys
391
+ if service not in self.key_data:
392
+ self.key_data[service] = [new_entry]
393
+ else:
394
+ self.key_data[service].append(new_entry)
373
395
 
374
396
  def update_from_dict(self, d: dict) -> None:
375
397
  """
@@ -174,7 +174,8 @@ class LanguageModel(
174
174
  """
175
175
  key_sequence = cls.key_sequence
176
176
  usage_sequence = cls.usage_sequence if hasattr(cls, "usage_sequence") else None
177
- return RawResponseHandler(key_sequence, usage_sequence)
177
+ reasoning_sequence = cls.reasoning_sequence if hasattr(cls, "reasoning_sequence") else None
178
+ return RawResponseHandler(key_sequence, usage_sequence, reasoning_sequence)
178
179
 
179
180
  def __init__(
180
181
  self,
@@ -1,5 +1,5 @@
1
1
  import json
2
- from typing import Optional, Any
2
+ from typing import Optional, Any, List
3
3
  from .exceptions import (
4
4
  LanguageModelBadResponseError,
5
5
  LanguageModelTypeError,
@@ -41,10 +41,13 @@ def _extract_item_from_raw_response(data, sequence):
41
41
  current_data = current_data[key]
42
42
  except Exception as e:
43
43
  path = " -> ".join(map(str, sequence[: i + 1]))
44
- if "error" in data:
45
- msg = data["error"]
44
+
45
+ # Create a safe error message that won't be None
46
+ if "error" in data and data["error"] is not None:
47
+ msg = str(data["error"])
46
48
  else:
47
49
  msg = f"Error accessing path: {path}. {str(e)}. Full response is: '{data}'"
50
+
48
51
  raise LanguageModelBadResponseError(message=msg, response_json=data)
49
52
  if isinstance(current_data, str):
50
53
  return current_data.strip()
@@ -55,17 +58,127 @@ def _extract_item_from_raw_response(data, sequence):
55
58
  class RawResponseHandler:
56
59
  """Class to handle raw responses from language models."""
57
60
 
58
- def __init__(self, key_sequence: list, usage_sequence: Optional[list] = None):
61
+ def __init__(self, key_sequence: list, usage_sequence: Optional[list] = None, reasoning_sequence: Optional[list] = None):
59
62
  self.key_sequence = key_sequence
60
63
  self.usage_sequence = usage_sequence
64
+ self.reasoning_sequence = reasoning_sequence
61
65
 
62
66
  def get_generated_token_string(self, raw_response):
63
- return _extract_item_from_raw_response(raw_response, self.key_sequence)
67
+ try:
68
+ return _extract_item_from_raw_response(raw_response, self.key_sequence)
69
+ except (LanguageModelKeyError, LanguageModelIndexError, LanguageModelTypeError, LanguageModelBadResponseError) as e:
70
+ # For non-reasoning models or reasoning models with different response formats,
71
+ # try to extract text directly from common response formats
72
+ if isinstance(raw_response, dict):
73
+ # Responses API format for non-reasoning models
74
+ if 'output' in raw_response and isinstance(raw_response['output'], list):
75
+ # Try to get first message content
76
+ if len(raw_response['output']) > 0:
77
+ item = raw_response['output'][0]
78
+ if isinstance(item, dict) and 'content' in item:
79
+ if isinstance(item['content'], list) and len(item['content']) > 0:
80
+ first_content = item['content'][0]
81
+ if isinstance(first_content, dict) and 'text' in first_content:
82
+ return first_content['text']
83
+ elif isinstance(item['content'], str):
84
+ return item['content']
85
+
86
+ # OpenAI completions format
87
+ if 'choices' in raw_response and isinstance(raw_response['choices'], list) and len(raw_response['choices']) > 0:
88
+ choice = raw_response['choices'][0]
89
+ if isinstance(choice, dict):
90
+ if 'text' in choice:
91
+ return choice['text']
92
+ elif 'message' in choice and isinstance(choice['message'], dict) and 'content' in choice['message']:
93
+ return choice['message']['content']
94
+
95
+ # Text directly in response
96
+ if 'text' in raw_response:
97
+ return raw_response['text']
98
+ elif 'content' in raw_response:
99
+ return raw_response['content']
100
+
101
+ # Error message - try to return a coherent error for debugging
102
+ if 'message' in raw_response:
103
+ return f"[ERROR: {raw_response['message']}]"
104
+
105
+ # If we get a string directly, return it
106
+ if isinstance(raw_response, str):
107
+ return raw_response
108
+
109
+ # As a last resort, convert the whole response to string
110
+ try:
111
+ return f"[ERROR: Could not extract text. Raw response: {str(raw_response)}]"
112
+ except:
113
+ return "[ERROR: Could not extract text from response]"
64
114
 
65
115
  def get_usage_dict(self, raw_response):
66
116
  if self.usage_sequence is None:
67
117
  return {}
68
- return _extract_item_from_raw_response(raw_response, self.usage_sequence)
118
+ try:
119
+ return _extract_item_from_raw_response(raw_response, self.usage_sequence)
120
+ except (LanguageModelKeyError, LanguageModelIndexError, LanguageModelTypeError, LanguageModelBadResponseError):
121
+ # For non-reasoning models, try to extract usage from common response formats
122
+ if isinstance(raw_response, dict):
123
+ # Standard OpenAI usage format
124
+ if 'usage' in raw_response:
125
+ return raw_response['usage']
126
+
127
+ # Look for nested usage info
128
+ if 'choices' in raw_response and len(raw_response['choices']) > 0:
129
+ choice = raw_response['choices'][0]
130
+ if isinstance(choice, dict) and 'usage' in choice:
131
+ return choice['usage']
132
+
133
+ # If no usage info found, return empty dict
134
+ return {}
135
+
136
+ def get_reasoning_summary(self, raw_response):
137
+ """
138
+ Extract reasoning summary from the model response.
139
+
140
+ Handles various response structures:
141
+ 1. Standard path extraction using self.reasoning_sequence
142
+ 2. Direct access to output[0]['summary'] for OpenAI responses
143
+ 3. List responses where the first item contains the output structure
144
+ """
145
+ if self.reasoning_sequence is None:
146
+ return None
147
+
148
+ try:
149
+ # First try the standard extraction path
150
+ summary_data = _extract_item_from_raw_response(raw_response, self.reasoning_sequence)
151
+
152
+ # If summary_data is a list of dictionaries with 'text' and 'type' fields
153
+ # (as in OpenAI's response format), combine them into a single string
154
+ if isinstance(summary_data, list) and all(isinstance(item, dict) and 'text' in item for item in summary_data):
155
+ return '\n\n'.join(item['text'] for item in summary_data)
156
+
157
+ return summary_data
158
+ except Exception:
159
+ # Fallback approaches for different response structures
160
+ try:
161
+ # Case 1: Direct dict with 'output' field (common OpenAI format)
162
+ if isinstance(raw_response, dict) and 'output' in raw_response:
163
+ output = raw_response['output']
164
+ if isinstance(output, list) and len(output) > 0 and 'summary' in output[0]:
165
+ summary_data = output[0]['summary']
166
+ if isinstance(summary_data, list) and all(isinstance(item, dict) and 'text' in item for item in summary_data):
167
+ return '\n\n'.join(item['text'] for item in summary_data)
168
+
169
+ # Case 2: List where the first item is a dict with 'output' field
170
+ if isinstance(raw_response, list) and len(raw_response) > 0:
171
+ first_item = raw_response[0]
172
+ if isinstance(first_item, dict) and 'output' in first_item:
173
+ output = first_item['output']
174
+ if isinstance(output, list) and len(output) > 0 and 'summary' in output[0]:
175
+ summary_data = output[0]['summary']
176
+ if isinstance(summary_data, list) and all(isinstance(item, dict) and 'text' in item for item in summary_data):
177
+ return '\n\n'.join(item['text'] for item in summary_data)
178
+ except Exception:
179
+ pass
180
+
181
+ return None
69
182
 
70
183
  def parse_response(self, raw_response: dict[str, Any]) -> Any:
71
184
  """Parses the API response and returns the response text."""
@@ -73,7 +186,11 @@ class RawResponseHandler:
73
186
  from edsl.data_transfer_models import EDSLOutput
74
187
 
75
188
  generated_token_string = self.get_generated_token_string(raw_response)
189
+ # Ensure generated_token_string is a string before using string methods
190
+ if not isinstance(generated_token_string, str):
191
+ generated_token_string = str(generated_token_string)
76
192
  last_newline = generated_token_string.rfind("\n")
193
+ reasoning_summary = self.get_reasoning_summary(raw_response)
77
194
 
78
195
  if last_newline == -1:
79
196
  # There is no comment
@@ -81,12 +198,14 @@ class RawResponseHandler:
81
198
  "answer": self.convert_answer(generated_token_string),
82
199
  "generated_tokens": generated_token_string,
83
200
  "comment": None,
201
+ "reasoning_summary": reasoning_summary,
84
202
  }
85
203
  else:
86
204
  edsl_dict = {
87
205
  "answer": self.convert_answer(generated_token_string[:last_newline]),
88
- "comment": generated_token_string[last_newline + 1 :].strip(),
206
+ "comment": generated_token_string[last_newline + 1:].strip(),
89
207
  "generated_tokens": generated_token_string,
208
+ "reasoning_summary": reasoning_summary,
90
209
  }
91
210
  return EDSLOutput(**edsl_dict)
92
211
 
edsl/results/result.py CHANGED
@@ -95,6 +95,7 @@ class Result(Base, UserDict):
95
95
  question_to_attributes: Optional[dict[QuestionName, Any]] = None,
96
96
  generated_tokens: Optional[dict] = None,
97
97
  comments_dict: Optional[dict] = None,
98
+ reasoning_summaries_dict: Optional[dict] = None,
98
99
  cache_used_dict: Optional[dict[QuestionName, bool]] = None,
99
100
  indices: Optional[dict] = None,
100
101
  cache_keys: Optional[dict[QuestionName, str]] = None,
@@ -112,6 +113,7 @@ class Result(Base, UserDict):
112
113
  :param question_to_attributes: A dictionary of question attributes.
113
114
  :param generated_tokens: A dictionary of generated tokens.
114
115
  :param comments_dict: A dictionary of comments.
116
+ :param reasoning_summaries_dict: A dictionary of reasoning summaries.
115
117
  :param cache_used_dict: A dictionary of cache usage.
116
118
  :param indices: A dictionary of indices.
117
119
 
@@ -130,6 +132,7 @@ class Result(Base, UserDict):
130
132
  "question_to_attributes": self.question_to_attributes,
131
133
  "generated_tokens": generated_tokens or {},
132
134
  "comments_dict": comments_dict or {},
135
+ "reasoning_summaries_dict": reasoning_summaries_dict or {},
133
136
  "cache_used_dict": cache_used_dict or {},
134
137
  "cache_keys": cache_keys or {},
135
138
  }
@@ -236,6 +239,7 @@ class Result(Base, UserDict):
236
239
  "answer": self.data["answer"],
237
240
  "prompt": self.data["prompt"],
238
241
  "comment": self.data["comments_dict"],
242
+ "reasoning_summary": self.data["reasoning_summaries_dict"],
239
243
  "generated_tokens": self.data["generated_tokens"],
240
244
  "raw_model_response": self.data["raw_model_response"],
241
245
  "question_text": sub_dicts_needing_new_keys["question_text"],
@@ -497,6 +501,7 @@ class Result(Base, UserDict):
497
501
  question_to_attributes=json_dict.get("question_to_attributes", None),
498
502
  generated_tokens=json_dict.get("generated_tokens", {}),
499
503
  comments_dict=json_dict.get("comments_dict", {}),
504
+ reasoning_summaries_dict=json_dict.get("reasoning_summaries_dict", {}),
500
505
  cache_used_dict=json_dict.get("cache_used_dict", {}),
501
506
  cache_keys=json_dict.get("cache_keys", {}),
502
507
  indices=json_dict.get("indices", None),
@@ -631,6 +636,36 @@ class Result(Base, UserDict):
631
636
  }
632
637
  return comments_dict
633
638
 
639
+ def get_reasoning_summaries_dict(answer_key_names) -> dict[str, Any]:
640
+ reasoning_summaries_dict = {}
641
+ for k in answer_key_names:
642
+ reasoning_summary = question_results[k].reasoning_summary
643
+
644
+ # If reasoning summary is None but we have a raw model response, try to extract it
645
+ if reasoning_summary is None and hasattr(question_results[k], 'raw_model_response'):
646
+ try:
647
+ # Get the model class to access the reasoning_sequence
648
+ model_class = interview.model.__class__ if hasattr(interview, 'model') else None
649
+
650
+ if model_class and hasattr(model_class, 'reasoning_sequence'):
651
+ from ..language_models.raw_response_handler import RawResponseHandler
652
+
653
+ # Create a handler with the model's reasoning sequence
654
+ handler = RawResponseHandler(
655
+ key_sequence=model_class.key_sequence if hasattr(model_class, 'key_sequence') else None,
656
+ usage_sequence=model_class.usage_sequence if hasattr(model_class, 'usage_sequence') else None,
657
+ reasoning_sequence=model_class.reasoning_sequence
658
+ )
659
+
660
+ # Try to extract the reasoning summary
661
+ reasoning_summary = handler.get_reasoning_summary(question_results[k].raw_model_response)
662
+ except Exception:
663
+ # If extraction fails, keep it as None
664
+ pass
665
+
666
+ reasoning_summaries_dict[k + "_reasoning_summary"] = reasoning_summary
667
+ return reasoning_summaries_dict
668
+
634
669
  def get_question_name_to_prompts(
635
670
  model_response_objects,
636
671
  ) -> dict[str, dict[str, str]]:
@@ -705,6 +740,7 @@ class Result(Base, UserDict):
705
740
  answer_key_names = list(question_results.keys())
706
741
  generated_tokens_dict = get_generated_tokens_dict(answer_key_names) if answer_key_names else {}
707
742
  comments_dict = get_comments_dict(answer_key_names) if answer_key_names else {}
743
+ reasoning_summaries_dict = get_reasoning_summaries_dict(answer_key_names) if answer_key_names else {}
708
744
 
709
745
  # Get answers that are in the question results
710
746
  answer_dict = {}
@@ -735,6 +771,7 @@ class Result(Base, UserDict):
735
771
  survey=survey_copy,
736
772
  generated_tokens=generated_tokens_dict,
737
773
  comments_dict=comments_dict,
774
+ reasoning_summaries_dict=reasoning_summaries_dict,
738
775
  cache_used_dict=cache_used_dictionary,
739
776
  indices=indices_copy,
740
777
  cache_keys=cache_keys,
edsl/results/results.py CHANGED
@@ -273,6 +273,7 @@ class Results(MutableSequence, ResultsOperationsMixin, Base):
273
273
  "generated_tokens",
274
274
  "cache_used",
275
275
  "cache_keys",
276
+ "reasoning_summary",
276
277
  ]
277
278
 
278
279
  @classmethod
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: edsl
3
- Version: 0.1.59
3
+ Version: 0.1.60
4
4
  Summary: Create and analyze LLM-based surveys
5
5
  Home-page: https://www.expectedparrot.com/
6
6
  License: MIT
@@ -43,7 +43,7 @@ Requires-Dist: pydot (>=2.0.0,<3.0.0)
43
43
  Requires-Dist: pygments (>=2.17.2,<3.0.0)
44
44
  Requires-Dist: pymupdf (>=1.25.5,<2.0.0)
45
45
  Requires-Dist: pypdf2 (>=3.0.1,<4.0.0)
46
- Requires-Dist: pyreadstat (>=1.2.7,<2.0.0)
46
+ Requires-Dist: pyreadstat (==1.2.8)
47
47
  Requires-Dist: python-docx (>=1.1.0,<2.0.0)
48
48
  Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
49
49
  Requires-Dist: python-pptx (>=1.0.2,<2.0.0)
@@ -1,6 +1,6 @@
1
1
  edsl/__init__.py,sha256=EkpMsEKqKRbN9Qqcn_y8CjX8OjlWFyhxslLrt3SJY0Q,4827
2
2
  edsl/__init__original.py,sha256=PzMzANf98PrSleSThXT4anNkeVqZMdw0tfFonzsoiGk,4446
3
- edsl/__version__.py,sha256=6ejKyHgulDwYDrT6JBlbHrh83UmxgavrhSuOolniIfI,23
3
+ edsl/__version__.py,sha256=SH6dBtwKkjChke7DXHi0Y5HbtzV16-5wAfhv91d_D0A,23
4
4
  edsl/agents/__init__.py,sha256=AyhfXjygRHT1Pd9w16lcu5Bu0jnBmMPz86aKP1uRL3Y,93
5
5
  edsl/agents/agent.py,sha256=omq3lnEujOObKuDyr0seaTiRL7SbJxMjF6bZXqiTt7c,56296
6
6
  edsl/agents/agent_list.py,sha256=k29SMOP2trdYWJs5-tPIfpme97fcnanL1lDhhJK3zfg,24249
@@ -9,8 +9,8 @@ edsl/agents/exceptions.py,sha256=7KMAtAHKqlkVkd_iVZC_mWXQnzDPV0V_n2iXaGAQgzc,566
9
9
  edsl/base/__init__.py,sha256=h119NxrAJOV92jnX7ussXNjKFXqzySVGOjMG3G7Zkzc,992
10
10
  edsl/base/base_class.py,sha256=bpuKCf6OOl71OlhrInDLC4b8LxFfDnuMVaaEaSp7ECY,48158
11
11
  edsl/base/base_exception.py,sha256=gwk4mNoS3TBe6446NiQeSrUrjUqjlB3_fcDFgV90Dms,7644
12
- edsl/base/data_transfer_models.py,sha256=j_7qQIlP73WxFEPvU6lL4RSN_CV8xihpYAl0OM62dW4,3677
13
- edsl/base/enums.py,sha256=njo1lEsjB4Xf0loTerks8eWMTP0JResqzd5kZuclS-w,6447
12
+ edsl/base/data_transfer_models.py,sha256=JpEnlgdQ5_URixzZUr7MJuAY4U6obPo0rWfzDl39WNg,3934
13
+ edsl/base/enums.py,sha256=46mqtWjeiL6NTsN8j-zGfY8QNOVXO4sVb1p1MjmD1N4,6613
14
14
  edsl/base/exceptions.py,sha256=hEMu40lW1IsuarZiOJAL2sAUwuxsubxfR41J6BK5Ri8,3493
15
15
  edsl/base.py,sha256=9Jx5zXfWLtKAm0L7LD_kTF3rSIR-tlEuCEuXDbeqHxI,221
16
16
  edsl/buckets/__init__.py,sha256=g3VzxuhrC4wO1i6sljXAcJ_k6MNAu_OH-wAmSfzxBjI,1536
@@ -52,7 +52,7 @@ edsl/coop/utils.py,sha256=DON2ns5nWlUqqvlNVUsdgiPlz-6oEqFVOmjhnOwHQBs,8174
52
52
  edsl/data_transfer_models.py,sha256=pPaKsbo9pgNcBB9kX-U2O_dUtNkd0Xm4JNmv26jrbhI,265
53
53
  edsl/dataset/__init__.py,sha256=RIzfFIytKJfniKZ0VThMk8Z2fjejx91t9PZBct78xXw,422
54
54
  edsl/dataset/dataset.py,sha256=o1icaFSE2ipCj7FDqhXkPb-E42wBzn74hLD7QXg0qaE,42277
55
- edsl/dataset/dataset_operations_mixin.py,sha256=SDGqQRg0Zdy-VMHDF1z4bChCkZ6t5iT-tP2zydAdyYs,59344
55
+ edsl/dataset/dataset_operations_mixin.py,sha256=k0t4MF_nOIf7McLV6JNLUkGmQNoeDvP5kN0Z_aK9JHA,59344
56
56
  edsl/dataset/dataset_tree.py,sha256=mKLQhwo-gxDyJCwCH3gj6Os0Jk2JqfWd_PvUyuWqM6s,14268
57
57
  edsl/dataset/display/CSSParameterizer.py,sha256=vI3VTgTihJeCYGfmGp7fOhTitHZ17jrDGbq46Sa2rd8,3677
58
58
  edsl/dataset/display/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -81,7 +81,7 @@ edsl/inference_services/models_available_cache.py,sha256=bOvevfRn2HlmBcHalaDkjFL
81
81
  edsl/inference_services/rate_limits_cache.py,sha256=HYslviz7mxF9U4CUTPAkoyBsiXjSju-YCp4HHir6e34,1398
82
82
  edsl/inference_services/registry.py,sha256=KIs1GpGSrczqukm6QsKe9pPn9LnfSrpT_wVAthU3-HM,307
83
83
  edsl/inference_services/service_availability.py,sha256=z4rkonyD51Y-flKoxrpxBZIfOqjNrOXCxWCmA0mlUDU,4176
84
- edsl/inference_services/services/__init__.py,sha256=y28_A9Sbza0-kWC041ocLTBXsWvPObEFHTIbjFl2nBE,939
84
+ edsl/inference_services/services/__init__.py,sha256=_6F-qINiAj9spBmCRKgQR9tYDEek3x1uzX9CKX-3DLw,1010
85
85
  edsl/inference_services/services/anthropic_service.py,sha256=yeSdXbqZ0RxXd2NKOrgXM4V5K2ioV_W1h-qujsPQpRU,4212
86
86
  edsl/inference_services/services/aws_bedrock.py,sha256=tijqEp4IgKWeJiqR90T3zkeXaqA0Qat9h5eyhG1qY3M,4285
87
87
  edsl/inference_services/services/azure_ai.py,sha256=7tZzyOhvT0eRooO2hccTZNghykth1e05MWlUKZNovpo,9436
@@ -92,6 +92,7 @@ edsl/inference_services/services/groq_service.py,sha256=eSxVbQXzrc6rtgVyMgDOsdG0
92
92
  edsl/inference_services/services/mistral_ai_service.py,sha256=tvwIeqhwzT6kPjrUo_lO3QCSBYUGD7jHG010FPp72Z4,3925
93
93
  edsl/inference_services/services/ollama_service.py,sha256=quSKlgD0bHG9mO_s9verGePfqQi_rZWovHEQ6dy-Fe0,303
94
94
  edsl/inference_services/services/open_ai_service.py,sha256=WFcl9g7Y28hckdiD_bPxRL_yJqz9ukERL3h_znh6b80,8682
95
+ edsl/inference_services/services/open_ai_service_v2.py,sha256=KywwuZKeJA3zmnVU8EQC32xt0ICVPDp2suKpeAPxWAg,9027
95
96
  edsl/inference_services/services/perplexity_service.py,sha256=7bt5Mb6Dxkb7UOljNdTBpZuT_8ri4i6Sk_h5g8paKu4,5994
96
97
  edsl/inference_services/services/test_service.py,sha256=JUK2bch1uu5XefMhNnuAXCbTqgiMqQRAIN8xYCMNe1E,7394
97
98
  edsl/inference_services/services/together_ai_service.py,sha256=biUYs07jsrIHp19O81o0nJCwYdSWudMEXdGtmA1-y60,6151
@@ -127,7 +128,7 @@ edsl/invigilators/question_template_replacements_builder.py,sha256=a_-n0TWE4PLK_
127
128
  edsl/jobs/__init__.py,sha256=gBGDlPZiaTkKENGdGYaMKzk0BFf5R1Cv9yk2YMPvIqI,1183
128
129
  edsl/jobs/async_interview_runner.py,sha256=rj07EKRu4fjbBkTADn8RAxbMF6m3vZFOG1qtnn0g12U,9532
129
130
  edsl/jobs/check_survey_scenario_compatibility.py,sha256=9qD9qi6qjvC-4M3Mq2bSF8F5HMIbWilSVPSJ3wlFqmM,4022
130
- edsl/jobs/data_structures.py,sha256=i-XXq2zul1K1aOZDZXbPIO8l-0bJLqDL2t7pxITXbks,9869
131
+ edsl/jobs/data_structures.py,sha256=jQPl4KIv4WZ7rJ9bQD2_S14T_oyG7mN9nPiA2eqrYMo,10020
131
132
  edsl/jobs/decorators.py,sha256=0Eot9pFPsWmQIJAafNd0f5hdb9RUAFp_hGMmSUTJ_C8,3272
132
133
  edsl/jobs/exceptions.py,sha256=5lktTya2VgiBR5Bd977tG2xHdrMjDqhPhQO17O6jIdc,7220
133
134
  edsl/jobs/fetch_invigilator.py,sha256=nzXAIulvOvuDpRDEN5TDNmEfikUEwrnS_XCtnYG2uPQ,2795
@@ -146,16 +147,16 @@ edsl/jobs/results_exceptions_handler.py,sha256=VCtnd60xwdFznzGhtXPbxLmyVf3kIjR24
146
147
  edsl/key_management/__init__.py,sha256=JiOJ71Ly9aw-tVYbWZu-qRjsW4QETYMQ9IJjsKgW1DQ,1274
147
148
  edsl/key_management/exceptions.py,sha256=dDtoDh1UL52BUBrAlCIc_McgtZCAQkUx6onoSz26qeM,2158
148
149
  edsl/key_management/key_lookup.py,sha256=HfIntc_i_WWUDoMOLwAHHbNlwC-0HivOyf_djeKiPlo,6080
149
- edsl/key_management/key_lookup_builder.py,sha256=AlQxXbUYwyJc-3JjLddXBOBPVsYJ-B2grZRAZSIT7P4,14974
150
+ edsl/key_management/key_lookup_builder.py,sha256=s5H_DBGZpMJwaQc1fLh46GYfTpSUOOOsl_gsVCVkkKg,16050
150
151
  edsl/key_management/key_lookup_collection.py,sha256=b1STYU4FIqgCtCf90bRZh6IXf8kcoTC8ad8RSHPmw-w,3471
151
152
  edsl/key_management/models.py,sha256=z9TimNMnz47mnITM5SlJy2m2sk1aKKtt0ybV89rsaiY,6703
152
153
  edsl/language_models/__init__.py,sha256=WtefJs6XOCn5RSz22PgoAi3eTEr1NzGtnnBpDIie2mg,240
153
154
  edsl/language_models/exceptions.py,sha256=P9dMA8XfK_qcuXNJZ-Xsb_Ny-12Ldu3fPC133RB40Ek,13728
154
- edsl/language_models/language_model.py,sha256=gN3qW1NUK4kPl_CfgMKUd8ORdSB0iEZC0miuZDsCQUw,46462
155
+ edsl/language_models/language_model.py,sha256=e1RZLnLin3haLUYqfu5aQ0pLhopqqQuiQjxC2pVTW9E,46582
155
156
  edsl/language_models/model.py,sha256=oYZsfgvko_EH4EWT9XZPEgLcs9KA36SGEAKZwYRFjv8,12013
156
157
  edsl/language_models/model_list.py,sha256=Eb62xQdrlayqWYyJVgYxheMiNi14e1U9b_12qYzy1ws,4522
157
158
  edsl/language_models/price_manager.py,sha256=74XEkoVdQv06w7gMFZmXeeXGW6om4_ISr-qFnmX4lFE,10711
158
- edsl/language_models/raw_response_handler.py,sha256=i2Ye1WzjYq_2YJ1EKX946dx9m331GilwqC5qymGJlEI,4003
159
+ edsl/language_models/raw_response_handler.py,sha256=WynUO2q986ALb9QJ2IS6erAiDWFy8_Zr2lUAMdGWkaY,10708
159
160
  edsl/language_models/registry.py,sha256=io_Cp-7PtLpPuvZs_j8XaMxJiv-zSplbAQdrzPp2pzg,7308
160
161
  edsl/language_models/repair.py,sha256=ljm0xc9e1tMdyKc9b-v7ikpYRBh639xJ11SkDzI2vZE,5245
161
162
  edsl/language_models/unused/fake_openai_call.py,sha256=dxbL5e4NLF-eTk9IduPyGwLiVCX_-eGCJDaLYPlQTqc,364
@@ -278,8 +279,8 @@ edsl/questions/validation_logger.py,sha256=ru0y2uM3t9Hln2oaq-n-9d4zTKXQIQWiKincG
278
279
  edsl/results/__init__.py,sha256=RKbHY0g6s_k42VcdmTOZ2yB_nltiJnnbeQAkUY5WD9o,129
279
280
  edsl/results/exceptions.py,sha256=u-TQsazt_qj-G4eJKBnj0UtpnIiw6A2GcCLJ2wTYE_g,6536
280
281
  edsl/results/report.py,sha256=oHjMY981Gn8estqvoTk5SPiuEOIM0IR_QPBrRLdk5pM,7481
281
- edsl/results/result.py,sha256=5cT7ikHDoNASGINRLDRCpMokusz0Plx5iq7LJ9pgK5I,29723
282
- edsl/results/results.py,sha256=BOy_NfRAWu9Q_JeuMtfG04oQhE7hMuiJ-WAH6_ov6Vk,84973
282
+ edsl/results/result.py,sha256=SZekHBstRMhuvhz20cPaTREY7Rq4tQIF0Nc6tyWWyjE,32160
283
+ edsl/results/results.py,sha256=dd0MvTU0Rg4mLoyryKv8mreVmM1Eu6UPGPDJic16P_E,85002
283
284
  edsl/results/results_selector.py,sha256=4_XMS2Fb-3rcXEPUYaBRw52r1i66jttjttqNFe6PRc4,18050
284
285
  edsl/scenarios/DocxScenario.py,sha256=ul3nkX826m_T6LFptswqtnH5czP_yxMlLWgbTmFIZI4,482
285
286
  edsl/scenarios/PdfExtractor.py,sha256=6nPZ6v9x2RrU42EkqlEcW3MS-WIQpGfwg4--6WvEC8I,1972
@@ -382,8 +383,8 @@ edsl/utilities/restricted_python.py,sha256=248N2p5EWHDSpcK1G-q7DUoJeWy4sB6aO-RV0
382
383
  edsl/utilities/template_loader.py,sha256=SCAcnTnxNQ67MNSkmfz7F-S_u2peyGn2j1oRIqi1wfg,870
383
384
  edsl/utilities/utilities.py,sha256=irHheAGOnl_6RwI--Hi9StVzvsHcWCqB48PWsWJQYOw,12045
384
385
  edsl/utilities/wikipedia.py,sha256=I3Imbz3fzbaoA0ZLDsWUO2YpP_ovvaqtu-yd2Ye1BB0,6933
385
- edsl-0.1.59.dist-info/LICENSE,sha256=_qszBDs8KHShVYcYzdMz3HNMtH-fKN_p5zjoVAVumFc,1111
386
- edsl-0.1.59.dist-info/METADATA,sha256=FxWojFvdFs_p5nVxCwWquIeheS8yTHhAwXcGECq2ejg,12082
387
- edsl-0.1.59.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
388
- edsl-0.1.59.dist-info/entry_points.txt,sha256=JnG7xqMtHaQu9BU-yPATxdyCeA48XJpuclnWCqMfIMU,38
389
- edsl-0.1.59.dist-info/RECORD,,
386
+ edsl-0.1.60.dist-info/LICENSE,sha256=_qszBDs8KHShVYcYzdMz3HNMtH-fKN_p5zjoVAVumFc,1111
387
+ edsl-0.1.60.dist-info/METADATA,sha256=KodgK6MWkw8_QG3LVrIJFqbcQI3oMUowIfcrvdggkBU,12075
388
+ edsl-0.1.60.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
389
+ edsl-0.1.60.dist-info/entry_points.txt,sha256=JnG7xqMtHaQu9BU-yPATxdyCeA48XJpuclnWCqMfIMU,38
390
+ edsl-0.1.60.dist-info/RECORD,,
File without changes
File without changes