speedy-utils 1.1.6__py3-none-any.whl → 1.1.7__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.
- llm_utils/__init__.py +1 -5
- llm_utils/chat_format/transform.py +9 -9
- llm_utils/group_messages.py +1 -1
- llm_utils/lm/async_lm/__init__.py +6 -1
- llm_utils/lm/async_lm/_utils.py +7 -4
- llm_utils/lm/async_lm/async_llm_task.py +465 -110
- llm_utils/lm/async_lm/async_lm.py +273 -665
- llm_utils/lm/async_lm/async_lm_base.py +405 -0
- llm_utils/lm/async_lm/lm_specific.py +136 -0
- llm_utils/lm/utils.py +1 -3
- llm_utils/scripts/vllm_load_balancer.py +49 -37
- speedy_utils/__init__.py +3 -1
- speedy_utils/common/notebook_utils.py +4 -4
- speedy_utils/common/report_manager.py +2 -3
- speedy_utils/common/utils_cache.py +233 -3
- speedy_utils/common/utils_io.py +2 -0
- speedy_utils/scripts/mpython.py +1 -3
- {speedy_utils-1.1.6.dist-info → speedy_utils-1.1.7.dist-info}/METADATA +1 -1
- speedy_utils-1.1.7.dist-info/RECORD +39 -0
- llm_utils/lm/chat_html.py +0 -246
- llm_utils/lm/lm_json.py +0 -68
- llm_utils/lm/sync_lm.py +0 -943
- speedy_utils-1.1.6.dist-info/RECORD +0 -40
- {speedy_utils-1.1.6.dist-info → speedy_utils-1.1.7.dist-info}/WHEEL +0 -0
- {speedy_utils-1.1.6.dist-info → speedy_utils-1.1.7.dist-info}/entry_points.txt +0 -0
|
@@ -1,154 +1,509 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Async LLM Task module for handling language model interactions with structured input/output.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
import pathlib
|
|
1
7
|
from abc import ABC
|
|
2
|
-
from
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
List,
|
|
7
|
-
Optional,
|
|
8
|
-
TypeVar,
|
|
9
|
-
Union,
|
|
10
|
-
cast,
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
# from openai.pagination import AsyncSyncPage
|
|
14
|
-
from openai.types.chat import (
|
|
15
|
-
ChatCompletionMessageParam,
|
|
16
|
-
)
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any, Dict, Generic, List, Literal, Optional, TypeVar, Union, cast
|
|
10
|
+
|
|
11
|
+
from openai.types.chat import ChatCompletionMessageParam
|
|
17
12
|
from pydantic import BaseModel
|
|
13
|
+
from pytest import Cache
|
|
14
|
+
from speedy_utils import jdumps
|
|
15
|
+
from speedy_utils.all import dump_json_or_pickle, identify
|
|
18
16
|
|
|
19
17
|
from llm_utils.chat_format.display import get_conversation_one_turn
|
|
18
|
+
from llm_utils.lm.async_lm._utils import InputModelType, OutputModelType, ParsedOutput
|
|
19
|
+
from llm_utils.lm.async_lm.async_lm import AsyncLM
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
# --------------------------------------------------------------------------- #
|
|
24
|
-
# type helpers
|
|
25
|
-
# --------------------------------------------------------------------------- #
|
|
21
|
+
# Type aliases for better readability
|
|
26
22
|
TModel = TypeVar("TModel", bound=BaseModel)
|
|
27
23
|
Messages = List[ChatCompletionMessageParam]
|
|
28
24
|
LegacyMsgs = List[Dict[str, str]]
|
|
29
25
|
RawMsgs = Union[Messages, LegacyMsgs]
|
|
30
26
|
|
|
27
|
+
# Default configuration constants
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class LMConfiguration:
|
|
32
|
+
"""Configuration class for language model parameters."""
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
model: Optional[str] = None
|
|
35
|
+
temperature: Optional[float] = None
|
|
36
|
+
max_tokens: Optional[int] = None
|
|
37
|
+
host: Optional[str] = None
|
|
38
|
+
port: Optional[Union[int, str]] = None
|
|
39
|
+
base_url: Optional[str] = None
|
|
40
|
+
api_key: Optional[str] = None
|
|
41
|
+
cache: Optional[bool] = True
|
|
42
|
+
think: Optional[Literal[True, False]] = None
|
|
43
|
+
add_json_schema_to_instruction: Optional[bool] = None
|
|
44
|
+
use_beta: Optional[bool] = False
|
|
45
|
+
ports: Optional[List[int]] = None
|
|
46
|
+
top_p: Optional[float] = None
|
|
47
|
+
presence_penalty: Optional[float] = None
|
|
48
|
+
top_k: Optional[int] = None
|
|
49
|
+
repetition_penalty: Optional[float] = None
|
|
35
50
|
|
|
36
|
-
|
|
37
|
-
|
|
51
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
52
|
+
"""Convert configuration to dictionary format."""
|
|
53
|
+
return {
|
|
54
|
+
"model": self.model,
|
|
55
|
+
"temperature": self.temperature,
|
|
56
|
+
"max_tokens": self.max_tokens,
|
|
57
|
+
"host": self.host,
|
|
58
|
+
"port": self.port,
|
|
59
|
+
"base_url": self.base_url,
|
|
60
|
+
"api_key": self.api_key,
|
|
61
|
+
"cache": self.cache,
|
|
62
|
+
"think": self.think,
|
|
63
|
+
"add_json_schema_to_instruction": self.add_json_schema_to_instruction,
|
|
64
|
+
"use_beta": self.use_beta,
|
|
65
|
+
"ports": self.ports,
|
|
66
|
+
"top_p": self.top_p,
|
|
67
|
+
"presence_penalty": self.presence_penalty,
|
|
68
|
+
"top_k": self.top_k,
|
|
69
|
+
"repetition_penalty": self.repetition_penalty,
|
|
70
|
+
}
|
|
38
71
|
|
|
39
72
|
|
|
40
73
|
class AsyncLLMTask(ABC, Generic[InputModelType, OutputModelType]):
|
|
41
74
|
"""
|
|
42
|
-
|
|
75
|
+
Abstract base class for asynchronous language model tasks with structured I/O.
|
|
43
76
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
• OutputModel – a Pydantic output class
|
|
77
|
+
This class provides a framework for creating LLM tasks with strongly typed
|
|
78
|
+
input and output models, automatic training data collection, and support
|
|
79
|
+
for both thinking and non-thinking modes.
|
|
48
80
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
81
|
+
Type Parameters:
|
|
82
|
+
InputModelType: Pydantic model type for task input
|
|
83
|
+
OutputModelType: Pydantic model type for task output
|
|
84
|
+
"""
|
|
53
85
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
```python
|
|
57
|
-
class DemoTask(AsyncLLMTask):
|
|
58
|
-
"TODO: SYSTEM_PROMPT_INSTURCTION HERE"
|
|
86
|
+
InputModel: InputModelType
|
|
87
|
+
OutputModel: OutputModelType
|
|
59
88
|
|
|
60
|
-
|
|
89
|
+
# default class attributes for configuration
|
|
90
|
+
DEFAULT_MODEL: Optional[str] = None
|
|
91
|
+
DEFAULT_CACHE_DIR: Optional[pathlib.Path] = None
|
|
92
|
+
DEFAULT_TEMPERATURE: Optional[float] = None
|
|
93
|
+
DEFAULT_MAX_TOKENS: Optional[int] = None
|
|
94
|
+
DEFAULT_HOST: Optional[str] = None
|
|
95
|
+
DEFAULT_PORT: Optional[Union[int, str]] = None
|
|
96
|
+
DEFAULT_TOP_P: Optional[float] = None
|
|
97
|
+
DEFAULT_PRESENCE_PENALTY: Optional[float] = None
|
|
98
|
+
DEFAULT_TOP_K: Optional[int] = None
|
|
99
|
+
DEFAULT_REPETITION_PENALTY: Optional[float] = None
|
|
100
|
+
DEFAULT_CACHE: Optional[bool] = True
|
|
101
|
+
DEFAULT_THINK: Optional[Literal[True, False]] = None
|
|
102
|
+
DEFAULT_PORTS: Optional[List[int]] = None
|
|
103
|
+
DEFAULT_USE_BETA: Optional[bool] = False
|
|
104
|
+
DEFAULT_ADD_JSON_SCHEMA_TO_INSTRUCTION: Optional[bool] = True
|
|
105
|
+
DEFAULT_COLLECT_DATA: Optional[bool] = None
|
|
106
|
+
DEFAULT_BASE_URL: Optional[str] = None
|
|
107
|
+
DEFAULT_API_KEY: Optional[str] = None
|
|
61
108
|
|
|
62
|
-
|
|
63
|
-
|
|
109
|
+
IS_DATA_COLLECTION: bool = False
|
|
110
|
+
|
|
111
|
+
def __init__(
|
|
112
|
+
self,
|
|
113
|
+
model: Optional[str] = None,
|
|
114
|
+
temperature: Optional[float] = None,
|
|
115
|
+
max_tokens: Optional[int] = None,
|
|
116
|
+
host: Optional[str] = None,
|
|
117
|
+
port: Optional[Union[int, str]] = None,
|
|
118
|
+
base_url: Optional[str] = None,
|
|
119
|
+
api_key: Optional[str] = None,
|
|
120
|
+
cache: Optional[bool] = None,
|
|
121
|
+
think: Optional[Literal[True, False]] = None,
|
|
122
|
+
add_json_schema_to_instruction: Optional[bool] = None,
|
|
123
|
+
use_beta: Optional[bool] = None,
|
|
124
|
+
ports: Optional[List[int]] = None,
|
|
125
|
+
top_p: Optional[float] = None,
|
|
126
|
+
presence_penalty: Optional[float] = None,
|
|
127
|
+
top_k: Optional[int] = None,
|
|
128
|
+
repetition_penalty: Optional[float] = None,
|
|
129
|
+
) -> None:
|
|
130
|
+
"""
|
|
131
|
+
Initialize the AsyncLLMTask with language model configuration.
|
|
64
132
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
133
|
+
All arguments are optional; defaults are taken from class attributes if not provided.
|
|
134
|
+
"""
|
|
135
|
+
self._config = LMConfiguration(
|
|
136
|
+
model=model if model is not None else self.DEFAULT_MODEL,
|
|
137
|
+
temperature=temperature
|
|
138
|
+
if temperature is not None
|
|
139
|
+
else self.DEFAULT_TEMPERATURE,
|
|
140
|
+
max_tokens=max_tokens
|
|
141
|
+
if max_tokens is not None
|
|
142
|
+
else self.DEFAULT_MAX_TOKENS,
|
|
143
|
+
host=host if host is not None else self.DEFAULT_HOST,
|
|
144
|
+
port=port if port is not None else self.DEFAULT_PORT,
|
|
145
|
+
base_url=base_url if base_url is not None else self.DEFAULT_BASE_URL,
|
|
146
|
+
api_key=api_key if api_key is not None else self.DEFAULT_API_KEY,
|
|
147
|
+
cache=cache if cache is not None else self.DEFAULT_CACHE,
|
|
148
|
+
think=think if think is not None else self.DEFAULT_THINK,
|
|
149
|
+
add_json_schema_to_instruction=add_json_schema_to_instruction
|
|
150
|
+
if add_json_schema_to_instruction is not None
|
|
151
|
+
else self.DEFAULT_ADD_JSON_SCHEMA_TO_INSTRUCTION,
|
|
152
|
+
use_beta=use_beta if use_beta is not None else self.DEFAULT_USE_BETA,
|
|
153
|
+
ports=ports if ports is not None else self.DEFAULT_PORTS,
|
|
154
|
+
top_p=top_p if top_p is not None else self.DEFAULT_TOP_P,
|
|
155
|
+
presence_penalty=presence_penalty
|
|
156
|
+
if presence_penalty is not None
|
|
157
|
+
else self.DEFAULT_PRESENCE_PENALTY,
|
|
158
|
+
top_k=top_k if top_k is not None else self.DEFAULT_TOP_K,
|
|
159
|
+
repetition_penalty=repetition_penalty
|
|
160
|
+
if repetition_penalty is not None
|
|
161
|
+
else self.DEFAULT_REPETITION_PENALTY,
|
|
162
|
+
)
|
|
163
|
+
self._lm: Optional[AsyncLM] = None
|
|
68
164
|
|
|
69
|
-
|
|
70
|
-
|
|
165
|
+
@property
|
|
166
|
+
def lm(self) -> AsyncLM:
|
|
167
|
+
"""
|
|
168
|
+
Lazy-loaded AsyncLM instance with proper configuration.
|
|
71
169
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
170
|
+
Returns:
|
|
171
|
+
Configured AsyncLM instance for this task
|
|
172
|
+
"""
|
|
173
|
+
if self._lm is None:
|
|
174
|
+
self._lm = AsyncLM(
|
|
175
|
+
**self._config.to_dict(),
|
|
176
|
+
response_model=self._get_output_model_type(),
|
|
177
|
+
)
|
|
178
|
+
return self._lm
|
|
76
179
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
180
|
+
def _get_output_model_type(self) -> type[OutputModelType]:
|
|
181
|
+
"""
|
|
182
|
+
Extract the output model type from generic type arguments.
|
|
80
183
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
add_json_schema: bool = False
|
|
84
|
-
cache: bool = False
|
|
184
|
+
Returns:
|
|
185
|
+
The OutputModelType class
|
|
85
186
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
think: Optional[bool] = None, # if not None, overrides self.think
|
|
92
|
-
) -> tuple[OutputModelType, List[Dict[str, Any]]]:
|
|
93
|
-
# Get the input and output model types from the generic parameters
|
|
94
|
-
type_args = getattr(self.__class__, "__orig_bases__", None)
|
|
187
|
+
Raises:
|
|
188
|
+
TypeError: If output model type cannot be determined
|
|
189
|
+
"""
|
|
190
|
+
# Try to get type from generic base classes
|
|
191
|
+
orig_bases = getattr(self.__class__, "__orig_bases__", None)
|
|
95
192
|
if (
|
|
96
|
-
|
|
97
|
-
and hasattr(
|
|
98
|
-
and len(
|
|
193
|
+
orig_bases
|
|
194
|
+
and hasattr(orig_bases[0], "__args__")
|
|
195
|
+
and len(orig_bases[0].__args__) >= 2
|
|
99
196
|
):
|
|
100
|
-
|
|
101
|
-
output_model = type_args[0].__args__[1]
|
|
102
|
-
else:
|
|
103
|
-
# Fallback to the old way if type introspection fails
|
|
104
|
-
if (
|
|
105
|
-
not hasattr(self, "InputModel")
|
|
106
|
-
or not hasattr(self, "OutputModel")
|
|
107
|
-
or not hasattr(self, "lm")
|
|
108
|
-
):
|
|
109
|
-
raise NotImplementedError(
|
|
110
|
-
f"{self.__class__.__name__} must define lm, InputModel, and OutputModel as class attributes or use proper generic typing."
|
|
111
|
-
)
|
|
112
|
-
input_model = self.InputModel
|
|
113
|
-
output_model = self.OutputModel
|
|
197
|
+
return orig_bases[0].__args__[1]
|
|
114
198
|
|
|
115
|
-
#
|
|
116
|
-
if
|
|
117
|
-
|
|
118
|
-
elif isinstance(input_model, type) and issubclass(input_model, BaseModel):
|
|
119
|
-
item = input_model(**data)
|
|
120
|
-
else:
|
|
121
|
-
raise TypeError("InputModel must be a subclass of BaseModel")
|
|
199
|
+
# Fallback to class attribute
|
|
200
|
+
if hasattr(self, "OutputModel"):
|
|
201
|
+
return self.OutputModel # type: ignore
|
|
122
202
|
|
|
123
|
-
|
|
124
|
-
"
|
|
203
|
+
raise TypeError(
|
|
204
|
+
f"{self.__class__.__name__} must define OutputModel as a class attribute "
|
|
205
|
+
"or use proper generic typing with AsyncLLMTask[InputModel, OutputModel]"
|
|
125
206
|
)
|
|
126
207
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
208
|
+
def _get_input_model_type(self) -> type[InputModelType]:
|
|
209
|
+
"""
|
|
210
|
+
Extract the input model type from generic type arguments.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
The InputModelType class
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
TypeError: If input model type cannot be determined
|
|
217
|
+
"""
|
|
218
|
+
# Try to get type from generic base classes
|
|
219
|
+
orig_bases = getattr(self.__class__, "__orig_bases__", None)
|
|
220
|
+
if (
|
|
221
|
+
orig_bases
|
|
222
|
+
and hasattr(orig_bases[0], "__args__")
|
|
223
|
+
and len(orig_bases[0].__args__) >= 2
|
|
224
|
+
):
|
|
225
|
+
return orig_bases[0].__args__[0]
|
|
226
|
+
|
|
227
|
+
raise TypeError(
|
|
228
|
+
f"{self.__class__.__name__} must define InputModel as a class attribute "
|
|
229
|
+
"or use proper generic typing with AsyncLLMTask[InputModel, OutputModel]"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
def _validate_and_convert_input(self, data: Union[BaseModel, dict]) -> BaseModel:
|
|
233
|
+
"""
|
|
234
|
+
Validate and convert input data to the expected input model type.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
data: Input data as BaseModel instance or dictionary
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Validated BaseModel instance
|
|
241
|
+
|
|
242
|
+
Raises:
|
|
243
|
+
TypeError: If input data cannot be converted to InputModel
|
|
244
|
+
"""
|
|
245
|
+
if isinstance(data, BaseModel):
|
|
246
|
+
return data
|
|
247
|
+
|
|
248
|
+
input_model_type = self._get_input_model_type()
|
|
249
|
+
if isinstance(input_model_type, type) and issubclass(
|
|
250
|
+
input_model_type, BaseModel
|
|
251
|
+
):
|
|
252
|
+
try:
|
|
253
|
+
return input_model_type(**data)
|
|
254
|
+
except Exception as e:
|
|
255
|
+
raise TypeError(
|
|
256
|
+
f"Failed to convert input data to {input_model_type.__name__}: {e}"
|
|
257
|
+
) from e
|
|
258
|
+
|
|
259
|
+
raise TypeError("InputModel must be a subclass of BaseModel")
|
|
260
|
+
|
|
261
|
+
def _validate_output_model(self) -> type[BaseModel]:
|
|
262
|
+
"""
|
|
263
|
+
Validate that the output model is properly configured.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
The validated output model type
|
|
267
|
+
|
|
268
|
+
Raises:
|
|
269
|
+
TypeError: If output model is not a valid BaseModel subclass
|
|
270
|
+
"""
|
|
271
|
+
output_model_type = self._get_output_model_type()
|
|
272
|
+
if not (
|
|
273
|
+
isinstance(output_model_type, type)
|
|
274
|
+
and issubclass(output_model_type, BaseModel)
|
|
275
|
+
):
|
|
276
|
+
raise TypeError("OutputModel must be a subclass of BaseModel")
|
|
277
|
+
return output_model_type
|
|
278
|
+
|
|
279
|
+
async def _base_call(
|
|
280
|
+
self, data: Union[BaseModel, dict]
|
|
281
|
+
) -> ParsedOutput[OutputModelType]:
|
|
282
|
+
"""
|
|
283
|
+
Core method that handles language model interaction with type safety.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
data: Input data as BaseModel instance or dictionary
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Parsed output from the language model
|
|
290
|
+
|
|
291
|
+
Raises:
|
|
292
|
+
TypeError: If input/output models are not properly configured
|
|
293
|
+
"""
|
|
294
|
+
# Validate input and output models
|
|
295
|
+
validated_input = self._validate_and_convert_input(data)
|
|
296
|
+
self._validate_output_model()
|
|
297
|
+
|
|
298
|
+
# Execute the language model call
|
|
299
|
+
return cast(
|
|
300
|
+
ParsedOutput[OutputModelType],
|
|
301
|
+
await self.lm.parse(
|
|
302
|
+
instruction=self.__doc__ or "",
|
|
303
|
+
prompt=validated_input.model_dump_json(),
|
|
304
|
+
),
|
|
135
305
|
)
|
|
136
306
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
307
|
+
def _create_no_think_messages(self, think_messages: Messages) -> Messages:
|
|
308
|
+
"""
|
|
309
|
+
Convert thinking mode messages to non-thinking mode.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
think_messages: Original messages with thinking mode enabled
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Messages converted to non-thinking mode
|
|
316
|
+
"""
|
|
317
|
+
if not think_messages:
|
|
318
|
+
return think_messages
|
|
319
|
+
|
|
320
|
+
# Create deep copy to avoid modifying original
|
|
321
|
+
no_think_messages = copy.deepcopy(think_messages)
|
|
322
|
+
|
|
323
|
+
# Update system message
|
|
324
|
+
if no_think_messages and "content" in no_think_messages[0]:
|
|
325
|
+
system_content = no_think_messages[0]["content"]
|
|
326
|
+
if isinstance(system_content, str):
|
|
327
|
+
no_think_messages[0]["content"] = system_content.replace(
|
|
328
|
+
"/think", "/no_think"
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Update assistant message (last message)
|
|
332
|
+
if len(no_think_messages) > 1 and "content" in no_think_messages[-1]:
|
|
333
|
+
assistant_content = no_think_messages[-1]["content"]
|
|
334
|
+
if isinstance(assistant_content, str) and "</think>" in assistant_content:
|
|
335
|
+
# Extract content after thinking block
|
|
336
|
+
post_think_content = assistant_content.split("</think>", 1)[1].strip()
|
|
337
|
+
no_think_messages[-1]["content"] = (
|
|
338
|
+
f"<think>\n\n</think>\n\n{post_think_content}"
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
return no_think_messages
|
|
342
|
+
|
|
343
|
+
def _save_training_data(
|
|
344
|
+
self,
|
|
345
|
+
input_data: InputModelType,
|
|
346
|
+
think_messages: Messages,
|
|
347
|
+
no_think_messages: Messages,
|
|
348
|
+
model_kwargs: Dict[str, Any],
|
|
349
|
+
cache_dir: pathlib.Path,
|
|
350
|
+
expected_response: Optional[OutputModelType] = None,
|
|
351
|
+
label: Optional[str] = None,
|
|
352
|
+
) -> None:
|
|
353
|
+
"""
|
|
354
|
+
Save training data to cache directory.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
input_data: Input data for the task
|
|
358
|
+
think_messages: Messages with thinking mode
|
|
359
|
+
no_think_messages: Messages without thinking mode
|
|
360
|
+
model_kwargs: Model configuration used
|
|
361
|
+
cache_dir: Directory to save training data
|
|
362
|
+
expected_response: Expected response for validation
|
|
363
|
+
label: Optional label for the training data
|
|
364
|
+
"""
|
|
365
|
+
# Create unique identifier for this input
|
|
366
|
+
input_id = identify(input_data.model_dump())
|
|
367
|
+
class_cache_dir = cache_dir / self.__class__.__name__
|
|
368
|
+
class_cache_dir.mkdir(parents=True, exist_ok=True)
|
|
369
|
+
|
|
370
|
+
# Prepare combined training data
|
|
371
|
+
training_data = {
|
|
372
|
+
"think_messages": think_messages,
|
|
373
|
+
"no_think_messages": no_think_messages,
|
|
374
|
+
"model_kwargs": model_kwargs,
|
|
375
|
+
"input_data": input_data.model_dump(),
|
|
376
|
+
"label": label,
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if expected_response is not None:
|
|
380
|
+
training_data["expected_response"] = expected_response.model_dump()
|
|
381
|
+
|
|
382
|
+
# Save to file
|
|
383
|
+
training_file = class_cache_dir / f"{input_id}.json"
|
|
384
|
+
dump_json_or_pickle(training_data, str(training_file))
|
|
385
|
+
|
|
386
|
+
async def _generate_training_data_with_thinking_mode(
|
|
387
|
+
self,
|
|
388
|
+
input_data: InputModelType,
|
|
389
|
+
expected_response: Optional[OutputModelType] = None,
|
|
390
|
+
label: Optional[str] = None,
|
|
391
|
+
cache_dir: pathlib.Path = DEFAULT_CACHE_DIR,
|
|
392
|
+
) -> OutputModelType:
|
|
393
|
+
"""
|
|
394
|
+
Generate training data for both thinking and non-thinking modes.
|
|
395
|
+
|
|
396
|
+
This method executes the task in thinking mode, then creates equivalent
|
|
397
|
+
non-thinking mode data for training purposes. Both versions are saved
|
|
398
|
+
to the cache directory for later use in model training.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
input_data: Input data for the task
|
|
402
|
+
expected_response: Expected response for validation
|
|
403
|
+
label: Optional label for the training data
|
|
404
|
+
cache_dir: Directory to save training data
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
Parsed output from the language model
|
|
408
|
+
"""
|
|
409
|
+
# Execute the base call to get thinking mode data
|
|
410
|
+
output = await self._base_call(input_data)
|
|
411
|
+
parsed_result = output["parsed"]
|
|
412
|
+
think_messages = output["messages"]
|
|
413
|
+
|
|
414
|
+
# Create non-thinking mode equivalent
|
|
415
|
+
no_think_messages = self._create_no_think_messages(think_messages)
|
|
416
|
+
|
|
417
|
+
# Save training data
|
|
418
|
+
self._save_training_data(
|
|
419
|
+
input_data=input_data,
|
|
420
|
+
think_messages=think_messages,
|
|
421
|
+
no_think_messages=no_think_messages,
|
|
422
|
+
model_kwargs=output["model_kwargs"],
|
|
423
|
+
cache_dir=cache_dir,
|
|
424
|
+
expected_response=expected_response,
|
|
425
|
+
label=label,
|
|
140
426
|
)
|
|
141
427
|
|
|
428
|
+
return parsed_result
|
|
429
|
+
|
|
430
|
+
def _should_collect_data(self) -> bool:
|
|
431
|
+
"""
|
|
432
|
+
Determine if training data should be collected for this call.
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
True if data collection is enabled
|
|
436
|
+
"""
|
|
437
|
+
return self.IS_DATA_COLLECTION
|
|
438
|
+
|
|
439
|
+
async def __call__(
|
|
440
|
+
self,
|
|
441
|
+
input_data: InputModelType,
|
|
442
|
+
expected_response: Optional[OutputModelType] = None,
|
|
443
|
+
label: Optional[str] = None,
|
|
444
|
+
**kwargs: Any,
|
|
445
|
+
) -> OutputModelType:
|
|
446
|
+
"""
|
|
447
|
+
Execute the LLM task with the provided input data.
|
|
448
|
+
|
|
449
|
+
This is the main entry point for task execution. If data collection
|
|
450
|
+
is enabled (either via instance configuration or environment variable),
|
|
451
|
+
training data will be automatically generated and saved.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
input_data: Input data conforming to InputModelType
|
|
455
|
+
expected_response: Expected response for validation during data collection
|
|
456
|
+
label: Optional label for training data categorization
|
|
457
|
+
**kwargs: Additional keyword arguments (for future extensibility)
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
Parsed output conforming to OutputModelType
|
|
461
|
+
"""
|
|
462
|
+
if self._should_collect_data():
|
|
463
|
+
return await self._generate_training_data_with_thinking_mode(
|
|
464
|
+
input_data=input_data,
|
|
465
|
+
expected_response=expected_response,
|
|
466
|
+
label=label,
|
|
467
|
+
)
|
|
468
|
+
else:
|
|
469
|
+
output = await self._base_call(input_data)
|
|
470
|
+
return output["parsed"]
|
|
471
|
+
|
|
142
472
|
def generate_training_data(
|
|
143
|
-
self,
|
|
473
|
+
self, input_json: str, output_json: str
|
|
144
474
|
) -> Dict[str, Any]:
|
|
145
|
-
"""
|
|
475
|
+
"""
|
|
476
|
+
Generate training data in ShareGPT format for the given input/output pair.
|
|
477
|
+
|
|
478
|
+
This method is useful for creating training datasets from existing
|
|
479
|
+
input/output pairs without executing the language model.
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
input_dict: Input data as dictionary
|
|
483
|
+
output: Output data as dictionary
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
Training data in ShareGPT message format
|
|
487
|
+
|
|
488
|
+
Raises:
|
|
489
|
+
AttributeError: If InputModel or OutputModel are not properly defined
|
|
490
|
+
"""
|
|
491
|
+
# if not hasattr(self, "InputModel") or not hasattr(self, "OutputModel"):
|
|
492
|
+
# raise AttributeError(
|
|
493
|
+
# f"{self.__class__.__name__} must define InputModel and OutputModel "
|
|
494
|
+
# "as class attributes to use generate_training_data"
|
|
495
|
+
# )
|
|
496
|
+
|
|
146
497
|
system_prompt = self.__doc__ or ""
|
|
147
|
-
|
|
148
|
-
|
|
498
|
+
assert isinstance(input_json, str), "Input must be a JSON string"
|
|
499
|
+
assert isinstance(output_json, str), "Output must be a JSON string"
|
|
149
500
|
messages = get_conversation_one_turn(
|
|
150
|
-
system_msg=system_prompt,
|
|
501
|
+
system_msg=system_prompt,
|
|
502
|
+
user_msg=input_json,
|
|
503
|
+
assistant_msg=output_json,
|
|
151
504
|
)
|
|
505
|
+
|
|
152
506
|
return {"messages": messages}
|
|
153
507
|
|
|
154
|
-
|
|
508
|
+
# Compatibility alias for other LLMTask implementations
|
|
509
|
+
arun = __call__
|