speedy-utils 1.1.16__py3-none-any.whl → 1.1.18__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.
@@ -0,0 +1,288 @@
1
+ # type: ignore
2
+
3
+ """
4
+ Simplified LLM Task module for handling language model interactions with structured input/output.
5
+ """
6
+
7
+ from typing import Any, Dict, List, Optional, Type, Union, cast
8
+
9
+ from openai import OpenAI
10
+ from openai.types.chat import ChatCompletionMessageParam
11
+ from pydantic import BaseModel
12
+ from pydantic import create_model
13
+ from typing import Callable, Tuple
14
+ from abc import ABC, abstractmethod
15
+
16
+ # Type aliases for better readability
17
+ Messages = List[ChatCompletionMessageParam]
18
+
19
+ import json
20
+ from typing import Type, TypeVar
21
+
22
+ B = TypeVar("B", bound="BasePromptBuilder")
23
+
24
+
25
+ class BasePromptBuilder(BaseModel, ABC):
26
+ """
27
+ Abstract base class for prompt builders.
28
+ Provides a consistent interface for:
29
+ - input/output key declaration
30
+ - prompt building
31
+ - schema enforcement via auto-built modget_io_keysels
32
+ """
33
+
34
+ # ------------------------------------------------------------------ #
35
+ # Abstract methods
36
+ # ------------------------------------------------------------------ #
37
+ @abstractmethod
38
+ def get_instruction(self) -> str:
39
+ """Return the system instruction string (role of the model)."""
40
+ raise NotImplementedError
41
+
42
+ @abstractmethod
43
+ def get_io_keys(self) -> Tuple[List[str], List[Union[str, Tuple[str, str]]]]:
44
+ """
45
+ Return (input_keys, output_keys).
46
+ Each key must match a field of the subclass.
47
+ For output_keys, you can use:
48
+ - str: Use the field name as-is
49
+ - tuple[str, str]: (original_field_name, renamed_field_name)
50
+ Input keys are always strings.
51
+ """
52
+ raise NotImplementedError
53
+
54
+ # ------------------------------------------------------------------ #
55
+ # Auto-build models from keys
56
+ # ------------------------------------------------------------------ #
57
+ def _build_model_from_keys(self, keys: Union[List[str], List[Union[str, Tuple[str, str]]]], name: str) -> Type[BaseModel]:
58
+ fields: Dict[str, tuple[Any, Any]] = {}
59
+ for key in keys:
60
+ if isinstance(key, tuple):
61
+ # Handle tuple: (original_field_name, renamed_field_name)
62
+ original_key, renamed_key = key
63
+ if original_key not in self.model_fields:
64
+ raise ValueError(f"Key '{original_key}' not found in model fields")
65
+ field_info = self.model_fields[original_key]
66
+ field_type = field_info.annotation if field_info.annotation is not None else (Any,)
67
+ default = field_info.default if field_info.default is not None else ...
68
+ fields[renamed_key] = (field_type, default)
69
+ else:
70
+ # Handle string key
71
+ if key not in self.model_fields:
72
+ raise ValueError(f"Key '{key}' not found in model fields")
73
+ field_info = self.model_fields[key]
74
+ field_type = field_info.annotation if field_info.annotation is not None else (Any,)
75
+ default = field_info.default if field_info.default is not None else ...
76
+ fields[key] = (field_type, default)
77
+ return create_model(name, **fields) # type: ignore
78
+
79
+ def get_input_model(self) -> Type[BaseModel]:
80
+ input_keys, _ = self.get_io_keys()
81
+ return self._build_model_from_keys(input_keys, "InputModel")
82
+
83
+ def get_output_model(self) -> Type[BaseModel]:
84
+ _, output_keys = self.get_io_keys()
85
+ return self._build_model_from_keys(output_keys, "OutputModel")
86
+
87
+ # ------------------------------------------------------------------ #
88
+ # Dump methods (JSON)
89
+ # ------------------------------------------------------------------ #
90
+ def _dump_json_unique(self, schema_model: Type[BaseModel], keys: Union[List[str], List[Union[str, Tuple[str, str]]]], **kwargs) -> str:
91
+ allowed = list(schema_model.model_fields.keys())
92
+ seen = set()
93
+ unique_keys = [k for k in allowed if not (k in seen or seen.add(k))]
94
+ data = self.model_dump()
95
+
96
+ # Handle key mapping for renamed fields
97
+ filtered = {}
98
+ for key in keys:
99
+ if isinstance(key, tuple):
100
+ original_key, renamed_key = key
101
+ if original_key in data and renamed_key in unique_keys:
102
+ filtered[renamed_key] = data[original_key]
103
+ else:
104
+ if key in data and key in unique_keys:
105
+ filtered[key] = data[key]
106
+
107
+ return schema_model(**filtered).model_dump_json(**kwargs)
108
+
109
+ def model_dump_json_input(self, **kwargs) -> str:
110
+ input_keys, _ = self.get_io_keys()
111
+ return self._dump_json_unique(self.get_input_model(), input_keys, **kwargs)
112
+
113
+ def model_dump_json_output(self, **kwargs) -> str:
114
+ _, output_keys = self.get_io_keys()
115
+ return self._dump_json_unique(self.get_output_model(), output_keys, **kwargs)
116
+
117
+ # ------------------------------------------------------------------ #
118
+ # Markdown helpers
119
+ # ------------------------------------------------------------------ #
120
+ def _to_markdown(self, obj: Any, level: int = 1, title: Optional[str] = None) -> str:
121
+ """
122
+ Recursively convert dict/list/primitive into clean, generic Markdown.
123
+ """
124
+ md: List[str] = []
125
+
126
+ # Format title if provided
127
+ if title is not None:
128
+ formatted_title = title.replace('_', ' ').title()
129
+ if level <= 2:
130
+ md.append(f"{'#' * level} {formatted_title}")
131
+ else:
132
+ md.append(f"**{formatted_title}:**")
133
+
134
+ if isinstance(obj, dict):
135
+ if not obj: # Empty dict
136
+ md.append("None")
137
+ else:
138
+ for k, v in obj.items():
139
+ if isinstance(v, (str, int, float, bool)) and len(str(v)) < 100:
140
+ # Short values inline
141
+ key_name = k.replace('_', ' ').title()
142
+ if level <= 2:
143
+ md.append(f"**{key_name}:** {v}")
144
+ else:
145
+ md.append(f"- **{key_name}:** {v}")
146
+ else:
147
+ # Complex values get recursive handling
148
+ md.append(self._to_markdown(v, level=level + 1, title=k))
149
+ elif isinstance(obj, list):
150
+ if not obj: # Empty list
151
+ md.append("None")
152
+ elif all(isinstance(i, dict) for i in obj):
153
+ # List of objects
154
+ for i, item in enumerate(obj, 1):
155
+ if level <= 2:
156
+ md.append(f"### {title or 'Item'} {i}")
157
+ else:
158
+ md.append(f"**{title or 'Item'} {i}:**")
159
+ # Process dict items inline for cleaner output
160
+ for k, v in item.items():
161
+ key_name = k.replace('_', ' ').title()
162
+ md.append(f"- **{key_name}:** {v}")
163
+ if i < len(obj): # Add spacing between items
164
+ md.append("")
165
+ else:
166
+ # Simple list
167
+ for item in obj:
168
+ md.append(f"- {item}")
169
+ else:
170
+ # Primitive value
171
+ value_str = str(obj) if obj is not None else "None"
172
+ if title is None:
173
+ md.append(value_str)
174
+ else:
175
+ md.append(value_str)
176
+
177
+ return "\n".join(md)
178
+
179
+ def _dump_markdown_unique(self, keys: Union[List[str], List[Union[str, Tuple[str, str]]]]) -> str:
180
+ data = self.model_dump()
181
+ filtered: Dict[str, Any] = {}
182
+ for key in keys:
183
+ if isinstance(key, tuple):
184
+ original_key, renamed_key = key
185
+ if original_key in data:
186
+ filtered[renamed_key] = data[original_key]
187
+ else:
188
+ if key in data:
189
+ filtered[key] = data[key]
190
+
191
+ # Generate markdown without top-level headers to avoid duplication
192
+ parts = []
193
+ for key, value in filtered.items():
194
+ if value is None:
195
+ continue
196
+ formatted_key = key.replace('_', ' ').title()
197
+ if isinstance(value, (str, int, float, bool)) and len(str(value)) < 200:
198
+ parts.append(f"**{formatted_key}:** {value}")
199
+ else:
200
+ parts.append(self._to_markdown(value, level=2, title=key))
201
+
202
+ return '\n'.join(parts)
203
+
204
+ def model_dump_markdown_input(self) -> str:
205
+ input_keys, _ = self.get_io_keys()
206
+ return self._dump_markdown_unique(input_keys)
207
+
208
+ def model_dump_markdown_output(self) -> str:
209
+ _, output_keys = self.get_io_keys()
210
+ return self._dump_markdown_unique(output_keys)
211
+
212
+ # ------------------------------------------------------------------ #
213
+ # Training & preview (JSON or Markdown)
214
+ # ------------------------------------------------------------------ #
215
+ def build_training_data(self, format: str = "json", indent=None) -> dict[str, Any]:
216
+ """
217
+ Build training data in either JSON (dict for OpenAI-style messages)
218
+ or Markdown (clean format without role prefixes).
219
+ """
220
+ if format == "json":
221
+ return {
222
+ "messages": [
223
+ {"role": "system", "content": self.get_instruction()},
224
+ {"role": "user", "content": self.model_dump_json_input(indent=indent)},
225
+ {"role": "assistant", "content": self.model_dump_json_output(indent=indent)},
226
+ ]
227
+ }
228
+ elif format == "markdown":
229
+ system_content = self.get_instruction()
230
+
231
+ return {
232
+ 'messages': [
233
+ {"role": "system", "content": system_content},
234
+ {"role": "user", "content": self.model_dump_markdown_input()},
235
+ {"role": "assistant", "content": self.model_dump_markdown_output()},
236
+ ]
237
+ }
238
+ raise ValueError("format must be either 'json' or 'markdown'")
239
+
240
+ def __str__(self) -> str:
241
+ # Return clean format without explicit role prefixes
242
+ training_data = self.build_training_data(format="markdown")
243
+ messages = training_data['messages'] # type: ignore[index]
244
+
245
+ parts = []
246
+ for msg in messages:
247
+ content = msg['content']
248
+ if msg['role'] == 'system':
249
+ parts.append(content)
250
+ elif msg['role'] == 'user':
251
+ parts.append(content)
252
+ elif msg['role'] == 'assistant':
253
+ # Get output keys to determine the main output field name
254
+ _, output_keys = self.get_io_keys()
255
+ main_output = output_keys[0] if output_keys else 'response'
256
+ if isinstance(main_output, tuple):
257
+ main_output = main_output[1] # Use renamed key
258
+ title = main_output.replace('_', ' ').title()
259
+ parts.append(f"## {title}\n{content}")
260
+
261
+ return '\n\n'.join(parts)
262
+
263
+ @classmethod
264
+ def from_messages(cls: Type[B], messages: list[dict]) -> B:
265
+ """
266
+ Reconstruct a prompt builder instance from OpenAI-style messages.
267
+ """
268
+ user_msg = next((m for m in messages if m.get("role") == "user"), None)
269
+ assistant_msg = next((m for m in messages if m.get("role") == "assistant"), None)
270
+
271
+ if user_msg is None:
272
+ raise ValueError("No user message found")
273
+ if assistant_msg is None:
274
+ raise ValueError("No assistant message found")
275
+
276
+ try:
277
+ user_data = json.loads(user_msg["content"]) # type: ignore[index]
278
+ except Exception as e:
279
+ raise ValueError(f"Invalid user JSON content: {e}")
280
+
281
+ try:
282
+ assistant_data = json.loads(assistant_msg["content"]) # type: ignore[index]
283
+ except Exception as e:
284
+ raise ValueError(f"Invalid assistant JSON content: {e}")
285
+
286
+ combined_data = {**user_data, **assistant_data}
287
+ return cast(B, cls(**combined_data))
288
+
@@ -0,0 +1,400 @@
1
+ # type: ignore
2
+
3
+ """
4
+ Simplified LLM Task module for handling language model interactions with structured input/output.
5
+ """
6
+
7
+ from typing import Any, Dict, List, Optional, Type, Union, cast
8
+
9
+ from loguru import logger
10
+ from openai import OpenAI
11
+ from openai.types.chat import ChatCompletionMessageParam
12
+ from pydantic import BaseModel
13
+
14
+ from .base_prompt_builder import BasePromptBuilder
15
+
16
+ # Type aliases for better readability
17
+ Messages = List[ChatCompletionMessageParam]
18
+
19
+
20
+ def get_base_client(
21
+ client: Union[OpenAI, int, str, None] = None, cache: bool = True, api_key="abc"
22
+ ) -> OpenAI:
23
+ """Get OpenAI client from port number, base_url string, or existing client."""
24
+ from llm_utils import MOpenAI
25
+
26
+ open_ai_class = OpenAI if not cache else MOpenAI
27
+ if client is None:
28
+ return open_ai_class()
29
+ elif isinstance(client, int):
30
+ return open_ai_class(base_url=f"http://localhost:{client}/v1", api_key=api_key)
31
+ elif isinstance(client, str):
32
+ return open_ai_class(base_url=client, api_key=api_key)
33
+ elif isinstance(client, OpenAI):
34
+ return client
35
+ else:
36
+ raise ValueError(
37
+ "Invalid client type. Must be OpenAI instance, port number (int), base_url (str), or None."
38
+ )
39
+
40
+
41
+ class LLMTask:
42
+ """
43
+ Language model task with structured input/output and optional system instruction.
44
+
45
+ Supports str or Pydantic models for both input and output. Automatically handles
46
+ message formatting and response parsing.
47
+
48
+ Two main APIs:
49
+ - text(): Returns raw text responses as list of dicts (alias for text_completion)
50
+ - parse(): Returns parsed Pydantic model responses as list of dicts (alias for pydantic_parse)
51
+ - __call__(): Backward compatibility method that delegates based on output_model
52
+
53
+ Example:
54
+ ```python
55
+ from pydantic import BaseModel
56
+ from llm_utils.lm.llm_task import LLMTask
57
+
58
+ class EmailOutput(BaseModel):
59
+ content: str
60
+ estimated_read_time: int
61
+
62
+ # Set up task with Pydantic output model
63
+ task = LLMTask(
64
+ instruction="Generate professional email content.",
65
+ output_model=EmailOutput,
66
+ client=OpenAI(),
67
+ temperature=0.7
68
+ )
69
+
70
+ # Use parse() for structured output
71
+ results = task.parse("Write a meeting follow-up email")
72
+ result = results[0]
73
+ print(result["parsed"].content, result["parsed"].estimated_read_time)
74
+
75
+ # Use text() for plain text output
76
+ results = task.text("Write a meeting follow-up email")
77
+ text_result = results[0]
78
+ print(text_result["parsed"])
79
+
80
+ # Multiple responses
81
+ results = task.parse("Write a meeting follow-up email", n=3)
82
+ for result in results:
83
+ print(f"Content: {result['parsed'].content}")
84
+
85
+ # Override parameters at runtime
86
+ results = task.text(
87
+ "Write a meeting follow-up email",
88
+ temperature=0.9,
89
+ n=2,
90
+ max_tokens=500
91
+ )
92
+ for result in results:
93
+ print(result["parsed"])
94
+
95
+ # Backward compatibility (uses output_model to choose method)
96
+ results = task("Write a meeting follow-up email") # Calls parse()
97
+ result = results[0]
98
+ print(result["parsed"].content)
99
+ ```
100
+ """
101
+
102
+ def __init__(
103
+ self,
104
+ instruction: Optional[str] = None,
105
+ input_model: Union[Type[BaseModel], type[str]] = str,
106
+ output_model: Type[BaseModel] | Type[str] = None,
107
+ client: Union[OpenAI, int, str, None] = None,
108
+ cache=True,
109
+ **model_kwargs,
110
+ ):
111
+ """
112
+ Initialize the LLMTask.
113
+
114
+ Args:
115
+ instruction: Optional system instruction for the task
116
+ input_model: Input type (str or BaseModel subclass)
117
+ output_model: Output BaseModel type
118
+ client: OpenAI client, port number, or base_url string
119
+ cache: Whether to use cached responses (default True)
120
+ **model_kwargs: Additional model parameters including:
121
+ - temperature: Controls randomness (0.0 to 2.0)
122
+ - n: Number of responses to generate (when n > 1, returns list)
123
+ - max_tokens: Maximum tokens in response
124
+ - model: Model name (auto-detected if not provided)
125
+ """
126
+ self.instruction = instruction
127
+ self.input_model = input_model
128
+ self.output_model = output_model
129
+ self.model_kwargs = model_kwargs
130
+
131
+ # if cache:
132
+ # print("Caching is enabled will use llm_utils.MOpenAI")
133
+
134
+ # self.client = MOpenAI(base_url=base_url, api_key=api_key)
135
+ # else:
136
+ # self.client = OpenAI(base_url=base_url, api_key=api_key)
137
+ self.client = get_base_client(client, cache=cache)
138
+
139
+ if not self.model_kwargs.get("model", ""):
140
+ self.model_kwargs["model"] = self.client.models.list().data[0].id
141
+ print(self.model_kwargs)
142
+
143
+ def _prepare_input(self, input_data: Union[str, BaseModel, List[Dict]]) -> Messages:
144
+ """Convert input to messages format."""
145
+ if isinstance(input_data, list):
146
+ assert isinstance(input_data[0], dict) and "role" in input_data[0], (
147
+ "If input_data is a list, it must be a list of messages with 'role' and 'content' keys."
148
+ )
149
+ return cast(Messages, input_data)
150
+ else:
151
+ # Convert input to string format
152
+ if isinstance(input_data, str):
153
+ user_content = input_data
154
+ elif hasattr(input_data, "model_dump_json"):
155
+ user_content = input_data.model_dump_json()
156
+ elif isinstance(input_data, dict):
157
+ user_content = str(input_data)
158
+ else:
159
+ user_content = str(input_data)
160
+
161
+ # Build messages
162
+ messages = (
163
+ [
164
+ {"role": "system", "content": self.instruction},
165
+ ]
166
+ if self.instruction is not None
167
+ else []
168
+ )
169
+
170
+ messages.append({"role": "user", "content": user_content})
171
+ return cast(Messages, messages)
172
+
173
+ def text_completion(
174
+ self, input_data: Union[str, BaseModel, list[Dict]], **runtime_kwargs
175
+ ) -> List[Dict[str, Any]]:
176
+ """
177
+ Execute the LLM task and return text responses.
178
+
179
+ Args:
180
+ input_data: Input as string or BaseModel
181
+ **runtime_kwargs: Runtime model parameters that override defaults
182
+ - temperature: Controls randomness (0.0 to 2.0)
183
+ - n: Number of responses to generate
184
+ - max_tokens: Maximum tokens in response
185
+ - model: Model name override
186
+ - Any other model parameters supported by OpenAI API
187
+
188
+ Returns:
189
+ List of dicts [{'parsed': text_response, 'messages': messages}, ...]
190
+ When n=1: List contains one dict
191
+ When n>1: List contains multiple dicts
192
+ """
193
+ # Prepare messages
194
+ messages = self._prepare_input(input_data)
195
+
196
+ # Merge runtime kwargs with default model kwargs (runtime takes precedence)
197
+ effective_kwargs = {**self.model_kwargs, **runtime_kwargs}
198
+ model_name = effective_kwargs.get("model", self.model_kwargs["model"])
199
+
200
+ # Extract model name from kwargs for API call
201
+ api_kwargs = {k: v for k, v in effective_kwargs.items() if k != "model"}
202
+
203
+ completion = self.client.chat.completions.create(
204
+ model=model_name, messages=messages, **api_kwargs
205
+ )
206
+ # print(completion)
207
+
208
+ results: List[Dict[str, Any]] = []
209
+ for choice in completion.choices:
210
+ choice_messages = cast(
211
+ Messages,
212
+ messages + [{"role": "assistant", "content": choice.message.content}],
213
+ )
214
+ results.append(
215
+ {"parsed": choice.message.content, "messages": choice_messages}
216
+ )
217
+ return results
218
+
219
+ def pydantic_parse(
220
+ self,
221
+ input_data: Union[str, BaseModel, list[Dict]],
222
+ response_model: Optional[Type[BaseModel]] | Type[str] = None,
223
+ **runtime_kwargs,
224
+ ) -> List[Dict[str, Any]]:
225
+ """
226
+ Execute the LLM task and return parsed Pydantic model responses.
227
+
228
+ Args:
229
+ input_data: Input as string or BaseModel
230
+ response_model: Pydantic model for response parsing (overrides default)
231
+ **runtime_kwargs: Runtime model parameters that override defaults
232
+ - temperature: Controls randomness (0.0 to 2.0)
233
+ - n: Number of responses to generate
234
+ - max_tokens: Maximum tokens in response
235
+ - model: Model name override
236
+ - Any other model parameters supported by OpenAI API
237
+
238
+ Returns:
239
+ List of dicts [{'parsed': parsed_model, 'messages': messages}, ...]
240
+ When n=1: List contains one dict
241
+ When n>1: List contains multiple dicts
242
+ """
243
+ # Prepare messages
244
+ messages = self._prepare_input(input_data)
245
+
246
+ # Merge runtime kwargs with default model kwargs (runtime takes precedence)
247
+ effective_kwargs = {**self.model_kwargs, **runtime_kwargs}
248
+ model_name = effective_kwargs.get("model", self.model_kwargs["model"])
249
+
250
+ # Extract model name from kwargs for API call
251
+ api_kwargs = {k: v for k, v in effective_kwargs.items() if k != "model"}
252
+
253
+ pydantic_model_to_use_opt = response_model or self.output_model
254
+ if pydantic_model_to_use_opt is None:
255
+ raise ValueError(
256
+ "No response model specified. Either set output_model in constructor or pass response_model parameter."
257
+ )
258
+ pydantic_model_to_use: Type[BaseModel] = cast(
259
+ Type[BaseModel], pydantic_model_to_use_opt
260
+ )
261
+ try:
262
+ completion = self.client.chat.completions.parse(
263
+ model=model_name,
264
+ messages=messages,
265
+ response_format=pydantic_model_to_use,
266
+ **api_kwargs,
267
+ )
268
+ except Exception as e:
269
+ is_length_error = "Length" in str(e) or "maximum context length" in str(e)
270
+ if is_length_error:
271
+ raise ValueError(
272
+ f"Input too long for model {model_name}. Error: {str(e)[:100]}..."
273
+ )
274
+
275
+ results: List[Dict[str, Any]] = []
276
+ for choice in completion.choices: # type: ignore[attr-defined]
277
+ choice_messages = cast(
278
+ Messages,
279
+ messages + [{"role": "assistant", "content": choice.message.content}],
280
+ )
281
+ results.append(
282
+ {"parsed": choice.message.parsed, "messages": choice_messages}
283
+ ) # type: ignore[attr-defined]
284
+ return results
285
+
286
+ def __call__(
287
+ self,
288
+ input_data: Union[str, BaseModel, list[Dict]],
289
+ response_model: Optional[Type[BaseModel] | Type[str]] = None,
290
+ two_step_parse_pydantic=False,
291
+ **runtime_kwargs,
292
+ ) -> List[Dict[str, Any]]:
293
+ """
294
+ Execute the LLM task. Delegates to text() or parse() based on output_model.
295
+
296
+ This method maintains backward compatibility by automatically choosing
297
+ between text and parse methods based on the output_model configuration.
298
+
299
+ Args:
300
+ input_data: Input as string or BaseModel
301
+ response_model: Optional override for output model
302
+ **runtime_kwargs: Runtime model parameters
303
+
304
+ Returns:
305
+ List of dicts [{'parsed': response, 'messages': messages}, ...]
306
+ """
307
+ pydantic_model_to_use = response_model or self.output_model
308
+
309
+ if pydantic_model_to_use is str or pydantic_model_to_use is None:
310
+ return self.text_completion(input_data, **runtime_kwargs)
311
+ elif two_step_parse_pydantic:
312
+ # step 1: get text completions
313
+ results = self.text_completion(input_data, **runtime_kwargs)
314
+ parsed_results = []
315
+ for result in results:
316
+ response_text = result["parsed"]
317
+ messages = result["messages"]
318
+ # check if the pydantic_model_to_use is validated
319
+ if "</think>" in response_text:
320
+ response_text = response_text.split("</think>")[1]
321
+ try:
322
+ parsed = pydantic_model_to_use.model_validate_json(response_text)
323
+ except Exception as e:
324
+ # logger.info(
325
+ # f"Warning: Failed to parsed JSON, Falling back to LLM parsing. Error: {str(e)[:100]}..."
326
+ # )
327
+ # use model to parse the response_text
328
+ _parsed_messages = [
329
+ {
330
+ "role": "system",
331
+ "content": "You are a helpful assistant that extracts JSON from text.",
332
+ },
333
+ {
334
+ "role": "user",
335
+ "content": f"Extract JSON from the following text:\n{response_text}",
336
+ },
337
+ ]
338
+ parsed_result = self.pydantic_parse(
339
+ _parsed_messages,
340
+ response_model=pydantic_model_to_use,
341
+ **runtime_kwargs,
342
+ )[0]
343
+ parsed = parsed_result["parsed"]
344
+ # ---
345
+ parsed_results.append({"parsed": parsed, "messages": messages})
346
+ return parsed_results
347
+
348
+ else:
349
+ return self.pydantic_parse(
350
+ input_data, response_model=response_model, **runtime_kwargs
351
+ )
352
+
353
+ # Backward compatibility aliases
354
+ def text(self, *args, **kwargs) -> List[Dict[str, Any]]:
355
+ """Alias for text_completion() for backward compatibility."""
356
+ return self.text_completion(*args, **kwargs)
357
+
358
+ def parse(self, *args, **kwargs) -> List[Dict[str, Any]]:
359
+ """Alias for pydantic_parse() for backward compatibility."""
360
+ return self.pydantic_parse(*args, **kwargs)
361
+
362
+ @classmethod
363
+ def from_prompt_builder(
364
+ builder: BasePromptBuilder,
365
+ client: Union[OpenAI, int, str, None] = None,
366
+ cache=True,
367
+ **model_kwargs,
368
+ ) -> "LLMTask":
369
+ """
370
+ Create an LLMTask instance from a BasePromptBuilder instance.
371
+
372
+ This method extracts the instruction, input model, and output model
373
+ from the provided builder and initializes an LLMTask accordingly.
374
+ """
375
+ instruction = builder.get_instruction()
376
+ input_model = builder.get_input_model()
377
+ output_model = builder.get_output_model()
378
+
379
+ # Extract data from the builder to initialize LLMTask
380
+ return LLMTask(
381
+ instruction=instruction,
382
+ input_model=input_model,
383
+ output_model=output_model,
384
+ client=client,
385
+ )
386
+
387
+ @staticmethod
388
+ def list_models(client: Union[OpenAI, int, str, None] = None) -> List[str]:
389
+ """
390
+ List available models from the OpenAI client.
391
+
392
+ Args:
393
+ client: OpenAI client, port number, or base_url string
394
+
395
+ Returns:
396
+ List of available model names.
397
+ """
398
+ client = get_base_client(client, cache=False)
399
+ models = client.models.list().data
400
+ return [m.id for m in models]