sdg-hub 0.3.1__py3-none-any.whl → 0.4.1__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.
- sdg_hub/__init__.py +0 -2
- sdg_hub/_version.py +2 -2
- sdg_hub/core/__init__.py +1 -2
- sdg_hub/core/blocks/__init__.py +2 -4
- sdg_hub/core/blocks/base.py +61 -6
- sdg_hub/core/blocks/filtering/column_value_filter.py +3 -2
- sdg_hub/core/blocks/llm/__init__.py +2 -4
- sdg_hub/core/blocks/llm/llm_chat_block.py +251 -265
- sdg_hub/core/blocks/llm/llm_chat_with_parsing_retry_block.py +216 -98
- sdg_hub/core/blocks/llm/llm_parser_block.py +320 -0
- sdg_hub/core/blocks/llm/text_parser_block.py +53 -152
- sdg_hub/core/flow/__init__.py +3 -4
- sdg_hub/core/flow/base.py +11 -73
- sdg_hub/core/flow/metadata.py +1 -68
- sdg_hub/core/flow/registry.py +0 -1
- sdg_hub/flows/qa_generation/document_grounded_qa/enhanced_multi_summary_qa/detailed_summary/flow.yaml +51 -12
- sdg_hub/flows/qa_generation/document_grounded_qa/enhanced_multi_summary_qa/doc_direct_qa/__init__.py +0 -0
- sdg_hub/flows/qa_generation/document_grounded_qa/enhanced_multi_summary_qa/doc_direct_qa/flow.yaml +158 -0
- sdg_hub/flows/qa_generation/document_grounded_qa/enhanced_multi_summary_qa/extractive_summary/flow.yaml +51 -12
- sdg_hub/flows/qa_generation/document_grounded_qa/enhanced_multi_summary_qa/key_facts/flow.yaml +14 -3
- sdg_hub/flows/qa_generation/document_grounded_qa/multi_summary_qa/instructlab/flow.yaml +147 -28
- sdg_hub/flows/qa_generation/document_grounded_qa/multi_summary_qa/multilingual/japanese/README.md +0 -0
- sdg_hub/flows/qa_generation/document_grounded_qa/multi_summary_qa/multilingual/japanese/__init__.py +0 -0
- sdg_hub/flows/qa_generation/document_grounded_qa/multi_summary_qa/multilingual/japanese/atomic_facts_ja.yaml +41 -0
- sdg_hub/flows/qa_generation/document_grounded_qa/multi_summary_qa/multilingual/japanese/detailed_summary_ja.yaml +14 -0
- sdg_hub/flows/qa_generation/document_grounded_qa/multi_summary_qa/multilingual/japanese/extractive_summary_ja.yaml +14 -0
- sdg_hub/flows/qa_generation/document_grounded_qa/multi_summary_qa/multilingual/japanese/flow.yaml +303 -0
- sdg_hub/flows/qa_generation/document_grounded_qa/multi_summary_qa/multilingual/japanese/generate_questions_responses_ja.yaml +55 -0
- sdg_hub/flows/text_analysis/structured_insights/flow.yaml +28 -5
- {sdg_hub-0.3.1.dist-info → sdg_hub-0.4.1.dist-info}/METADATA +2 -1
- {sdg_hub-0.3.1.dist-info → sdg_hub-0.4.1.dist-info}/RECORD +34 -30
- sdg_hub/core/blocks/evaluation/__init__.py +0 -9
- sdg_hub/core/blocks/evaluation/evaluate_faithfulness_block.py +0 -323
- sdg_hub/core/blocks/evaluation/evaluate_relevancy_block.py +0 -323
- sdg_hub/core/blocks/evaluation/verify_question_block.py +0 -329
- sdg_hub/core/blocks/llm/client_manager.py +0 -472
- sdg_hub/core/blocks/llm/config.py +0 -337
- {sdg_hub-0.3.1.dist-info → sdg_hub-0.4.1.dist-info}/WHEEL +0 -0
- {sdg_hub-0.3.1.dist-info → sdg_hub-0.4.1.dist-info}/licenses/LICENSE +0 -0
- {sdg_hub-0.3.1.dist-info → sdg_hub-0.4.1.dist-info}/top_level.txt +0 -0
@@ -1,472 +0,0 @@
|
|
1
|
-
# SPDX-License-Identifier: Apache-2.0
|
2
|
-
"""Client manager for LLM operations supporting all providers via LiteLLM."""
|
3
|
-
|
4
|
-
# Standard
|
5
|
-
from typing import Any, Optional, Union
|
6
|
-
import asyncio
|
7
|
-
|
8
|
-
# Third Party
|
9
|
-
from litellm import acompletion, completion
|
10
|
-
import litellm
|
11
|
-
|
12
|
-
# Local
|
13
|
-
from ...utils.logger_config import setup_logger
|
14
|
-
from .config import LLMConfig
|
15
|
-
from .error_handler import LLMErrorHandler
|
16
|
-
|
17
|
-
logger = setup_logger(__name__)
|
18
|
-
|
19
|
-
|
20
|
-
class LLMClientManager:
|
21
|
-
"""Client manager for LLM operations using LiteLLM.
|
22
|
-
|
23
|
-
This class provides a unified interface for calling any LLM provider
|
24
|
-
supported by LiteLLM, with robust error handling and retry logic.
|
25
|
-
|
26
|
-
Parameters
|
27
|
-
----------
|
28
|
-
config : LLMConfig
|
29
|
-
Configuration for the LLM client.
|
30
|
-
error_handler : Optional[LLMErrorHandler], optional
|
31
|
-
Custom error handler. If None, a default one will be created.
|
32
|
-
"""
|
33
|
-
|
34
|
-
def __init__(
|
35
|
-
self, config: LLMConfig, error_handler: Optional[LLMErrorHandler] = None
|
36
|
-
) -> None:
|
37
|
-
self.config = config
|
38
|
-
self.error_handler = error_handler or LLMErrorHandler(
|
39
|
-
max_retries=config.max_retries
|
40
|
-
)
|
41
|
-
self._is_loaded = False
|
42
|
-
|
43
|
-
def load(self) -> None:
|
44
|
-
"""Load and configure the LLM client.
|
45
|
-
|
46
|
-
This method sets up LiteLLM configuration and validates the setup.
|
47
|
-
"""
|
48
|
-
if self._is_loaded:
|
49
|
-
return
|
50
|
-
|
51
|
-
# Configure LiteLLM
|
52
|
-
self._configure_litellm()
|
53
|
-
|
54
|
-
# Test the configuration
|
55
|
-
self._validate_setup()
|
56
|
-
|
57
|
-
self._is_loaded = True
|
58
|
-
|
59
|
-
# Only log when model is actually configured
|
60
|
-
if self.config.model:
|
61
|
-
logger.info(
|
62
|
-
f"Loaded LLM client for model '{self.config.model}'",
|
63
|
-
extra={
|
64
|
-
"model": self.config.model,
|
65
|
-
"provider": self.config.get_provider(),
|
66
|
-
"is_local": self.config.is_local_model(),
|
67
|
-
"api_base": self.config.api_base,
|
68
|
-
},
|
69
|
-
)
|
70
|
-
|
71
|
-
def unload(self) -> None:
|
72
|
-
"""Unload the client and clean up resources."""
|
73
|
-
self._is_loaded = False
|
74
|
-
try:
|
75
|
-
logger.info(f"Unloaded LLM client for model '{self.config.model}'")
|
76
|
-
except Exception:
|
77
|
-
# Ignore logging errors during cleanup to prevent issues during shutdown
|
78
|
-
pass
|
79
|
-
|
80
|
-
def _configure_litellm(self) -> None:
|
81
|
-
"""Configure LiteLLM settings."""
|
82
|
-
# Set global timeout for LiteLLM
|
83
|
-
litellm.request_timeout = self.config.timeout
|
84
|
-
|
85
|
-
# Note: API keys are now passed directly in completion calls
|
86
|
-
# instead of modifying environment variables for thread-safety
|
87
|
-
|
88
|
-
def _validate_setup(self) -> None:
|
89
|
-
"""Validate that the LLM setup is working."""
|
90
|
-
try:
|
91
|
-
# For testing/development, skip validation if using dummy API key
|
92
|
-
if self.config.api_key == "test-key":
|
93
|
-
logger.debug(
|
94
|
-
f"Skipping validation for model '{self.config.model}' (test mode)"
|
95
|
-
)
|
96
|
-
return
|
97
|
-
|
98
|
-
# TODO: Skip validation for now to avoid API calls during initialization
|
99
|
-
# we might want to make a minimal test call
|
100
|
-
logger.debug(
|
101
|
-
f"Setup configured for model '{self.config.model}'. "
|
102
|
-
f"Validation will occur on first actual call."
|
103
|
-
)
|
104
|
-
|
105
|
-
except Exception as e:
|
106
|
-
logger.warning(
|
107
|
-
f"Could not validate setup for model '{self.config.model}': {e}"
|
108
|
-
)
|
109
|
-
|
110
|
-
def _message_to_dict(self, message: Any) -> dict[str, Any]:
|
111
|
-
"""Convert a message to a dict."""
|
112
|
-
if hasattr(message, "to_dict"):
|
113
|
-
return message.to_dict()
|
114
|
-
elif hasattr(message, "__dict__"):
|
115
|
-
return message.__dict__
|
116
|
-
else:
|
117
|
-
return dict(message)
|
118
|
-
|
119
|
-
def create_completion(
|
120
|
-
self, messages: list[dict[str, Any]], **overrides: Any
|
121
|
-
) -> Union[dict, list[dict]]:
|
122
|
-
"""Create a completion using LiteLLM.
|
123
|
-
|
124
|
-
Parameters
|
125
|
-
----------
|
126
|
-
messages : List[Dict[str, Any]]
|
127
|
-
Messages in OpenAI format.
|
128
|
-
**overrides : Any
|
129
|
-
Runtime parameter overrides.
|
130
|
-
|
131
|
-
Returns
|
132
|
-
-------
|
133
|
-
Union[dict, List[dict]]
|
134
|
-
The completion response(s). Returns a single response when n=1 or n is None,
|
135
|
-
returns a list of responses when n>1. Response dicts contain 'content' and may contain 'reasoning_content'.
|
136
|
-
|
137
|
-
Raises
|
138
|
-
------
|
139
|
-
Exception
|
140
|
-
If the completion fails after all retries.
|
141
|
-
"""
|
142
|
-
if not self._is_loaded:
|
143
|
-
self.load()
|
144
|
-
|
145
|
-
# Merge configuration with overrides
|
146
|
-
final_config = self.config.merge_overrides(**overrides)
|
147
|
-
kwargs = self._build_completion_kwargs(messages, final_config)
|
148
|
-
|
149
|
-
# Create retry wrapper
|
150
|
-
context = {
|
151
|
-
"model": final_config.model,
|
152
|
-
"provider": final_config.get_provider(),
|
153
|
-
"message_count": len(messages),
|
154
|
-
}
|
155
|
-
|
156
|
-
completion_func = self.error_handler.wrap_completion(
|
157
|
-
self._call_litellm_completion, context=context
|
158
|
-
)
|
159
|
-
|
160
|
-
# Make the completion call
|
161
|
-
response = completion_func(kwargs)
|
162
|
-
|
163
|
-
# Extract message objects from response
|
164
|
-
# Check if n > 1 to determine return type
|
165
|
-
n_value = final_config.n or 1
|
166
|
-
if n_value > 1:
|
167
|
-
return [
|
168
|
-
self._message_to_dict(choice.message) for choice in response.choices
|
169
|
-
]
|
170
|
-
else:
|
171
|
-
return self._message_to_dict(response.choices[0].message)
|
172
|
-
|
173
|
-
async def acreate_completion(
|
174
|
-
self,
|
175
|
-
messages: Union[list[dict[str, Any]], list[list[dict[str, Any]]]],
|
176
|
-
max_concurrency: Optional[int] = None,
|
177
|
-
**overrides: Any,
|
178
|
-
) -> Union[dict, list[dict]] | list[Union[dict, list[dict]]]:
|
179
|
-
"""Create async completion(s) using LiteLLM with optional concurrency control.
|
180
|
-
|
181
|
-
Parameters
|
182
|
-
----------
|
183
|
-
messages : Union[List[Dict[str, Any]], List[List[Dict[str, Any]]]]
|
184
|
-
Single message list or list of message lists.
|
185
|
-
- For single: List[Dict[str, Any]] - returns Union[Any, List[Any]]
|
186
|
-
- For multiple: List[List[Dict[str, Any]]] - returns List[Union[Any, List[Any]]]
|
187
|
-
max_concurrency : Optional[int], optional
|
188
|
-
Maximum number of concurrent requests when processing multiple messages.
|
189
|
-
If None, all requests run concurrently.
|
190
|
-
**overrides : Any
|
191
|
-
Runtime parameter overrides.
|
192
|
-
|
193
|
-
Returns
|
194
|
-
-------
|
195
|
-
Union[dict, List[dict], List[Union[dict, List[dict]]]]
|
196
|
-
For single message: completion response (dict when n=1, List[dict] when n>1)
|
197
|
-
For multiple messages: list of completion responses (each element can be dict or List[dict])
|
198
|
-
|
199
|
-
Raises
|
200
|
-
------
|
201
|
-
Exception
|
202
|
-
If the completion fails after all retries.
|
203
|
-
"""
|
204
|
-
# Detect if we have single message or multiple messages
|
205
|
-
if not messages:
|
206
|
-
raise ValueError("messages cannot be empty")
|
207
|
-
|
208
|
-
# Check if first element is a dict (single message) or list (multiple messages)
|
209
|
-
if isinstance(messages[0], dict):
|
210
|
-
# Single message case
|
211
|
-
return await self._acreate_single(messages, **overrides)
|
212
|
-
else:
|
213
|
-
# Multiple messages case
|
214
|
-
messages_list = messages
|
215
|
-
|
216
|
-
if max_concurrency is not None:
|
217
|
-
if max_concurrency < 1:
|
218
|
-
raise ValueError(
|
219
|
-
"max_concurrency must be greater than 0, got {max_concurrency}"
|
220
|
-
)
|
221
|
-
# Adjust concurrency based on n parameter to avoid overwhelming API
|
222
|
-
# when n > 1 (multiple completions per request)
|
223
|
-
n_value = overrides.get("n") or self.config.n or 1
|
224
|
-
if n_value > 1:
|
225
|
-
# Warn if max_concurrency is less than n
|
226
|
-
if max_concurrency < n_value:
|
227
|
-
logger.warning(
|
228
|
-
f"max_concurrency ({max_concurrency}) is less than n ({n_value}). "
|
229
|
-
f"This may result in very low concurrency. Consider increasing max_concurrency "
|
230
|
-
f"or reducing n for better performance."
|
231
|
-
)
|
232
|
-
|
233
|
-
# Reduce concurrency when generating multiple completions per request
|
234
|
-
adjusted_concurrency = max(1, max_concurrency // n_value)
|
235
|
-
logger.debug(
|
236
|
-
f"Adjusted max_concurrency from {max_concurrency} to {adjusted_concurrency} "
|
237
|
-
f"for n={n_value} completions per request"
|
238
|
-
)
|
239
|
-
else:
|
240
|
-
adjusted_concurrency = max_concurrency
|
241
|
-
|
242
|
-
# Use semaphore for concurrency control
|
243
|
-
semaphore = asyncio.Semaphore(adjusted_concurrency)
|
244
|
-
|
245
|
-
async def _create_with_semaphore(msgs):
|
246
|
-
async with semaphore:
|
247
|
-
return await self._acreate_single(msgs, **overrides)
|
248
|
-
|
249
|
-
tasks = [_create_with_semaphore(msgs) for msgs in messages_list]
|
250
|
-
return await asyncio.gather(*tasks)
|
251
|
-
else:
|
252
|
-
# No concurrency limit - process all at once
|
253
|
-
tasks = [
|
254
|
-
self._acreate_single(msgs, **overrides) for msgs in messages_list
|
255
|
-
]
|
256
|
-
return await asyncio.gather(*tasks)
|
257
|
-
|
258
|
-
async def _acreate_single(
|
259
|
-
self, messages: list[dict[str, Any]], **overrides: Any
|
260
|
-
) -> Union[dict, list[dict]]:
|
261
|
-
"""Create a single async completion using LiteLLM.
|
262
|
-
|
263
|
-
Parameters
|
264
|
-
----------
|
265
|
-
messages : List[Dict[str, Any]]
|
266
|
-
Messages in OpenAI format.
|
267
|
-
**overrides : Any
|
268
|
-
Runtime parameter overrides.
|
269
|
-
|
270
|
-
Returns
|
271
|
-
-------
|
272
|
-
Union[dict, List[dict]]
|
273
|
-
List of completion message objects. Each element is a dict when n=1 or n is None,
|
274
|
-
or a list of dicts when n>1. Message dicts contain 'content' and may contain 'reasoning_content'.
|
275
|
-
Raises
|
276
|
-
------
|
277
|
-
Exception
|
278
|
-
If the completion fails after all retries.
|
279
|
-
"""
|
280
|
-
if not self._is_loaded:
|
281
|
-
self.load()
|
282
|
-
|
283
|
-
# Merge configuration with overrides
|
284
|
-
final_config = self.config.merge_overrides(**overrides)
|
285
|
-
kwargs = self._build_completion_kwargs(messages, final_config)
|
286
|
-
|
287
|
-
# Create retry wrapper for async
|
288
|
-
context = {
|
289
|
-
"model": final_config.model,
|
290
|
-
"provider": final_config.get_provider(),
|
291
|
-
"message_count": len(messages),
|
292
|
-
}
|
293
|
-
|
294
|
-
completion_func = self.error_handler.wrap_completion(
|
295
|
-
self._call_litellm_acompletion, context=context
|
296
|
-
)
|
297
|
-
|
298
|
-
# Make the async completion call
|
299
|
-
response = await completion_func(kwargs)
|
300
|
-
|
301
|
-
# Extract message objects from response
|
302
|
-
# Check if n > 1 to determine return type
|
303
|
-
n_value = final_config.n or 1
|
304
|
-
if n_value > 1:
|
305
|
-
return [
|
306
|
-
self._message_to_dict(choice.message) for choice in response.choices
|
307
|
-
]
|
308
|
-
else:
|
309
|
-
return self._message_to_dict(response.choices[0].message)
|
310
|
-
|
311
|
-
def create_completions_batch(
|
312
|
-
self, messages_list: list[list[dict[str, Any]]], **overrides: Any
|
313
|
-
) -> list[Union[dict, list[dict]]]:
|
314
|
-
"""Create multiple completions in batch.
|
315
|
-
|
316
|
-
Parameters
|
317
|
-
----------
|
318
|
-
messages_list : List[List[Dict[str, Any]]]
|
319
|
-
List of message lists to process.
|
320
|
-
**overrides : Any
|
321
|
-
Runtime parameter overrides.
|
322
|
-
|
323
|
-
Returns
|
324
|
-
-------
|
325
|
-
List[dict] | List[List[dict]]
|
326
|
-
List of completion responses. Each element is a dict when n=1 or n is None,
|
327
|
-
or a list of dicts when n>1. Response dicts contain 'content' and may contain 'reasoning_content'.
|
328
|
-
"""
|
329
|
-
results = []
|
330
|
-
for messages in messages_list:
|
331
|
-
result = self.create_completion(messages, **overrides)
|
332
|
-
results.append(result)
|
333
|
-
return results
|
334
|
-
|
335
|
-
def _build_completion_kwargs(
|
336
|
-
self, messages: list[dict[str, Any]], config: LLMConfig
|
337
|
-
) -> dict[str, Any]:
|
338
|
-
"""Build kwargs for LiteLLM completion call.
|
339
|
-
|
340
|
-
Parameters
|
341
|
-
----------
|
342
|
-
messages : List[Dict[str, Any]]
|
343
|
-
Messages in OpenAI format.
|
344
|
-
config : LLMConfig
|
345
|
-
Final configuration after merging overrides.
|
346
|
-
|
347
|
-
Returns
|
348
|
-
-------
|
349
|
-
Dict[str, Any]
|
350
|
-
Kwargs for litellm.completion().
|
351
|
-
"""
|
352
|
-
kwargs = {
|
353
|
-
"model": config.model,
|
354
|
-
"messages": messages,
|
355
|
-
}
|
356
|
-
|
357
|
-
# Add API configuration
|
358
|
-
if config.api_key:
|
359
|
-
kwargs["api_key"] = config.api_key
|
360
|
-
|
361
|
-
if config.api_base:
|
362
|
-
kwargs["api_base"] = config.api_base
|
363
|
-
|
364
|
-
# Add generation parameters
|
365
|
-
generation_kwargs = config.get_generation_kwargs()
|
366
|
-
kwargs.update(generation_kwargs)
|
367
|
-
|
368
|
-
return kwargs
|
369
|
-
|
370
|
-
def _call_litellm_completion(self, kwargs: dict[str, Any]) -> Any:
|
371
|
-
"""Call LiteLLM completion with error handling.
|
372
|
-
|
373
|
-
Parameters
|
374
|
-
----------
|
375
|
-
kwargs : Dict[str, Any]
|
376
|
-
Arguments for litellm.completion().
|
377
|
-
|
378
|
-
Returns
|
379
|
-
-------
|
380
|
-
Any
|
381
|
-
LiteLLM completion response.
|
382
|
-
"""
|
383
|
-
logger.debug(
|
384
|
-
f"Calling LiteLLM completion for model '{kwargs['model']}'",
|
385
|
-
extra={
|
386
|
-
"model": kwargs["model"],
|
387
|
-
"message_count": len(kwargs["messages"]),
|
388
|
-
"generation_params": {
|
389
|
-
k: v
|
390
|
-
for k, v in kwargs.items()
|
391
|
-
if k in ["temperature", "max_tokens", "top_p", "n"]
|
392
|
-
},
|
393
|
-
},
|
394
|
-
)
|
395
|
-
|
396
|
-
response = completion(**kwargs)
|
397
|
-
|
398
|
-
logger.debug(
|
399
|
-
f"LiteLLM completion successful for model '{kwargs['model']}'",
|
400
|
-
extra={
|
401
|
-
"model": kwargs["model"],
|
402
|
-
"choices_count": len(response.choices),
|
403
|
-
},
|
404
|
-
)
|
405
|
-
|
406
|
-
return response
|
407
|
-
|
408
|
-
async def _call_litellm_acompletion(self, kwargs: dict[str, Any]) -> Any:
|
409
|
-
"""Call LiteLLM async completion with error handling.
|
410
|
-
|
411
|
-
Parameters
|
412
|
-
----------
|
413
|
-
kwargs : Dict[str, Any]
|
414
|
-
Arguments for litellm.acompletion().
|
415
|
-
|
416
|
-
Returns
|
417
|
-
-------
|
418
|
-
Any
|
419
|
-
LiteLLM completion response.
|
420
|
-
"""
|
421
|
-
logger.debug(
|
422
|
-
f"Calling LiteLLM async completion for model '{kwargs['model']}'",
|
423
|
-
extra={
|
424
|
-
"model": kwargs["model"],
|
425
|
-
"message_count": len(kwargs["messages"]),
|
426
|
-
},
|
427
|
-
)
|
428
|
-
|
429
|
-
response = await acompletion(**kwargs)
|
430
|
-
|
431
|
-
logger.debug(
|
432
|
-
f"LiteLLM async completion successful for model '{kwargs['model']}'",
|
433
|
-
extra={
|
434
|
-
"model": kwargs["model"],
|
435
|
-
"choices_count": len(response.choices),
|
436
|
-
},
|
437
|
-
)
|
438
|
-
|
439
|
-
return response
|
440
|
-
|
441
|
-
def get_model_info(self) -> dict[str, Any]:
|
442
|
-
"""Get information about the configured model.
|
443
|
-
|
444
|
-
Returns
|
445
|
-
-------
|
446
|
-
Dict[str, Any]
|
447
|
-
Model information.
|
448
|
-
"""
|
449
|
-
return {
|
450
|
-
"model": self.config.model,
|
451
|
-
"provider": self.config.get_provider(),
|
452
|
-
"model_name": self.config.get_model_name(),
|
453
|
-
"is_local": self.config.is_local_model(),
|
454
|
-
"api_base": self.config.api_base,
|
455
|
-
"is_loaded": self._is_loaded,
|
456
|
-
}
|
457
|
-
|
458
|
-
def __enter__(self):
|
459
|
-
"""Context manager entry."""
|
460
|
-
self.load()
|
461
|
-
return self
|
462
|
-
|
463
|
-
def __exit__(self, _exc_type, _exc_val, _exc_tb):
|
464
|
-
"""Context manager exit."""
|
465
|
-
self.unload()
|
466
|
-
|
467
|
-
def __repr__(self) -> str:
|
468
|
-
"""String representation."""
|
469
|
-
return (
|
470
|
-
f"LLMClientManager(model='{self.config.model}', "
|
471
|
-
f"provider='{self.config.get_provider()}', loaded={self._is_loaded})"
|
472
|
-
)
|