aiqtoolkit 1.2.0.dev0__py3-none-any.whl → 1.2.0rc1__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.
Potentially problematic release.
This version of aiqtoolkit might be problematic. Click here for more details.
- aiq/agent/base.py +170 -8
- aiq/agent/dual_node.py +1 -1
- aiq/agent/react_agent/agent.py +146 -112
- aiq/agent/react_agent/prompt.py +1 -6
- aiq/agent/react_agent/register.py +36 -35
- aiq/agent/rewoo_agent/agent.py +36 -35
- aiq/agent/rewoo_agent/register.py +2 -2
- aiq/agent/tool_calling_agent/agent.py +3 -7
- aiq/agent/tool_calling_agent/register.py +1 -1
- aiq/authentication/__init__.py +14 -0
- aiq/authentication/api_key/__init__.py +14 -0
- aiq/authentication/api_key/api_key_auth_provider.py +92 -0
- aiq/authentication/api_key/api_key_auth_provider_config.py +124 -0
- aiq/authentication/api_key/register.py +26 -0
- aiq/authentication/exceptions/__init__.py +14 -0
- aiq/authentication/exceptions/api_key_exceptions.py +38 -0
- aiq/authentication/exceptions/auth_code_grant_exceptions.py +86 -0
- aiq/authentication/exceptions/call_back_exceptions.py +38 -0
- aiq/authentication/exceptions/request_exceptions.py +54 -0
- aiq/authentication/http_basic_auth/__init__.py +0 -0
- aiq/authentication/http_basic_auth/http_basic_auth_provider.py +81 -0
- aiq/authentication/http_basic_auth/register.py +30 -0
- aiq/authentication/interfaces.py +93 -0
- aiq/authentication/oauth2/__init__.py +14 -0
- aiq/authentication/oauth2/oauth2_auth_code_flow_provider.py +107 -0
- aiq/authentication/oauth2/oauth2_auth_code_flow_provider_config.py +39 -0
- aiq/authentication/oauth2/register.py +25 -0
- aiq/authentication/register.py +21 -0
- aiq/builder/builder.py +64 -2
- aiq/builder/component_utils.py +16 -3
- aiq/builder/context.py +37 -0
- aiq/builder/eval_builder.py +43 -2
- aiq/builder/function.py +44 -12
- aiq/builder/function_base.py +1 -1
- aiq/builder/intermediate_step_manager.py +6 -8
- aiq/builder/user_interaction_manager.py +3 -0
- aiq/builder/workflow.py +23 -18
- aiq/builder/workflow_builder.py +421 -61
- aiq/cli/commands/info/list_mcp.py +103 -16
- aiq/cli/commands/sizing/__init__.py +14 -0
- aiq/cli/commands/sizing/calc.py +294 -0
- aiq/cli/commands/sizing/sizing.py +27 -0
- aiq/cli/commands/start.py +2 -1
- aiq/cli/entrypoint.py +2 -0
- aiq/cli/register_workflow.py +80 -0
- aiq/cli/type_registry.py +151 -30
- aiq/data_models/api_server.py +124 -12
- aiq/data_models/authentication.py +231 -0
- aiq/data_models/common.py +35 -7
- aiq/data_models/component.py +17 -9
- aiq/data_models/component_ref.py +33 -0
- aiq/data_models/config.py +60 -3
- aiq/data_models/dataset_handler.py +2 -1
- aiq/data_models/embedder.py +1 -0
- aiq/data_models/evaluate.py +23 -0
- aiq/data_models/function_dependencies.py +8 -0
- aiq/data_models/interactive.py +10 -1
- aiq/data_models/intermediate_step.py +38 -5
- aiq/data_models/its_strategy.py +30 -0
- aiq/data_models/llm.py +1 -0
- aiq/data_models/memory.py +1 -0
- aiq/data_models/object_store.py +44 -0
- aiq/data_models/profiler.py +1 -0
- aiq/data_models/retry_mixin.py +35 -0
- aiq/data_models/span.py +187 -0
- aiq/data_models/telemetry_exporter.py +2 -2
- aiq/embedder/nim_embedder.py +2 -1
- aiq/embedder/openai_embedder.py +2 -1
- aiq/eval/config.py +19 -1
- aiq/eval/dataset_handler/dataset_handler.py +87 -2
- aiq/eval/evaluate.py +208 -27
- aiq/eval/evaluator/base_evaluator.py +73 -0
- aiq/eval/evaluator/evaluator_model.py +1 -0
- aiq/eval/intermediate_step_adapter.py +11 -5
- aiq/eval/rag_evaluator/evaluate.py +55 -15
- aiq/eval/rag_evaluator/register.py +6 -1
- aiq/eval/remote_workflow.py +7 -2
- aiq/eval/runners/__init__.py +14 -0
- aiq/eval/runners/config.py +39 -0
- aiq/eval/runners/multi_eval_runner.py +54 -0
- aiq/eval/trajectory_evaluator/evaluate.py +22 -65
- aiq/eval/tunable_rag_evaluator/evaluate.py +150 -168
- aiq/eval/tunable_rag_evaluator/register.py +2 -0
- aiq/eval/usage_stats.py +41 -0
- aiq/eval/utils/output_uploader.py +10 -1
- aiq/eval/utils/weave_eval.py +184 -0
- aiq/experimental/__init__.py +0 -0
- aiq/experimental/decorators/__init__.py +0 -0
- aiq/experimental/decorators/experimental_warning_decorator.py +130 -0
- aiq/experimental/inference_time_scaling/__init__.py +0 -0
- aiq/experimental/inference_time_scaling/editing/__init__.py +0 -0
- aiq/experimental/inference_time_scaling/editing/iterative_plan_refinement_editor.py +147 -0
- aiq/experimental/inference_time_scaling/editing/llm_as_a_judge_editor.py +204 -0
- aiq/experimental/inference_time_scaling/editing/motivation_aware_summarization.py +107 -0
- aiq/experimental/inference_time_scaling/functions/__init__.py +0 -0
- aiq/experimental/inference_time_scaling/functions/execute_score_select_function.py +105 -0
- aiq/experimental/inference_time_scaling/functions/its_tool_orchestration_function.py +205 -0
- aiq/experimental/inference_time_scaling/functions/its_tool_wrapper_function.py +146 -0
- aiq/experimental/inference_time_scaling/functions/plan_select_execute_function.py +224 -0
- aiq/experimental/inference_time_scaling/models/__init__.py +0 -0
- aiq/experimental/inference_time_scaling/models/editor_config.py +132 -0
- aiq/experimental/inference_time_scaling/models/its_item.py +48 -0
- aiq/experimental/inference_time_scaling/models/scoring_config.py +112 -0
- aiq/experimental/inference_time_scaling/models/search_config.py +120 -0
- aiq/experimental/inference_time_scaling/models/selection_config.py +154 -0
- aiq/experimental/inference_time_scaling/models/stage_enums.py +43 -0
- aiq/experimental/inference_time_scaling/models/strategy_base.py +66 -0
- aiq/experimental/inference_time_scaling/models/tool_use_config.py +41 -0
- aiq/experimental/inference_time_scaling/register.py +36 -0
- aiq/experimental/inference_time_scaling/scoring/__init__.py +0 -0
- aiq/experimental/inference_time_scaling/scoring/llm_based_agent_scorer.py +168 -0
- aiq/experimental/inference_time_scaling/scoring/llm_based_plan_scorer.py +168 -0
- aiq/experimental/inference_time_scaling/scoring/motivation_aware_scorer.py +111 -0
- aiq/experimental/inference_time_scaling/search/__init__.py +0 -0
- aiq/experimental/inference_time_scaling/search/multi_llm_planner.py +128 -0
- aiq/experimental/inference_time_scaling/search/multi_query_retrieval_search.py +122 -0
- aiq/experimental/inference_time_scaling/search/single_shot_multi_plan_planner.py +128 -0
- aiq/experimental/inference_time_scaling/selection/__init__.py +0 -0
- aiq/experimental/inference_time_scaling/selection/best_of_n_selector.py +63 -0
- aiq/experimental/inference_time_scaling/selection/llm_based_agent_output_selector.py +131 -0
- aiq/experimental/inference_time_scaling/selection/llm_based_output_merging_selector.py +159 -0
- aiq/experimental/inference_time_scaling/selection/llm_based_plan_selector.py +128 -0
- aiq/experimental/inference_time_scaling/selection/threshold_selector.py +58 -0
- aiq/front_ends/console/authentication_flow_handler.py +233 -0
- aiq/front_ends/console/console_front_end_plugin.py +11 -2
- aiq/front_ends/fastapi/auth_flow_handlers/__init__.py +0 -0
- aiq/front_ends/fastapi/auth_flow_handlers/http_flow_handler.py +27 -0
- aiq/front_ends/fastapi/auth_flow_handlers/websocket_flow_handler.py +107 -0
- aiq/front_ends/fastapi/fastapi_front_end_config.py +93 -9
- aiq/front_ends/fastapi/fastapi_front_end_controller.py +68 -0
- aiq/front_ends/fastapi/fastapi_front_end_plugin.py +14 -1
- aiq/front_ends/fastapi/fastapi_front_end_plugin_worker.py +537 -52
- aiq/front_ends/fastapi/html_snippets/__init__.py +14 -0
- aiq/front_ends/fastapi/html_snippets/auth_code_grant_success.py +35 -0
- aiq/front_ends/fastapi/job_store.py +47 -25
- aiq/front_ends/fastapi/main.py +2 -0
- aiq/front_ends/fastapi/message_handler.py +108 -89
- aiq/front_ends/fastapi/step_adaptor.py +2 -1
- aiq/llm/aws_bedrock_llm.py +57 -0
- aiq/llm/nim_llm.py +2 -1
- aiq/llm/openai_llm.py +3 -2
- aiq/llm/register.py +1 -0
- aiq/meta/pypi.md +12 -12
- aiq/object_store/__init__.py +20 -0
- aiq/object_store/in_memory_object_store.py +74 -0
- aiq/object_store/interfaces.py +84 -0
- aiq/object_store/models.py +36 -0
- aiq/object_store/register.py +20 -0
- aiq/observability/__init__.py +14 -0
- aiq/observability/exporter/__init__.py +14 -0
- aiq/observability/exporter/base_exporter.py +449 -0
- aiq/observability/exporter/exporter.py +78 -0
- aiq/observability/exporter/file_exporter.py +33 -0
- aiq/observability/exporter/processing_exporter.py +269 -0
- aiq/observability/exporter/raw_exporter.py +52 -0
- aiq/observability/exporter/span_exporter.py +264 -0
- aiq/observability/exporter_manager.py +335 -0
- aiq/observability/mixin/__init__.py +14 -0
- aiq/observability/mixin/batch_config_mixin.py +26 -0
- aiq/observability/mixin/collector_config_mixin.py +23 -0
- aiq/observability/mixin/file_mixin.py +288 -0
- aiq/observability/mixin/file_mode.py +23 -0
- aiq/observability/mixin/resource_conflict_mixin.py +134 -0
- aiq/observability/mixin/serialize_mixin.py +61 -0
- aiq/observability/mixin/type_introspection_mixin.py +183 -0
- aiq/observability/processor/__init__.py +14 -0
- aiq/observability/processor/batching_processor.py +316 -0
- aiq/observability/processor/intermediate_step_serializer.py +28 -0
- aiq/observability/processor/processor.py +68 -0
- aiq/observability/register.py +36 -39
- aiq/observability/utils/__init__.py +14 -0
- aiq/observability/utils/dict_utils.py +236 -0
- aiq/observability/utils/time_utils.py +31 -0
- aiq/profiler/calc/__init__.py +14 -0
- aiq/profiler/calc/calc_runner.py +623 -0
- aiq/profiler/calc/calculations.py +288 -0
- aiq/profiler/calc/data_models.py +176 -0
- aiq/profiler/calc/plot.py +345 -0
- aiq/profiler/callbacks/langchain_callback_handler.py +22 -10
- aiq/profiler/data_models.py +24 -0
- aiq/profiler/inference_metrics_model.py +3 -0
- aiq/profiler/inference_optimization/bottleneck_analysis/nested_stack_analysis.py +8 -0
- aiq/profiler/inference_optimization/data_models.py +2 -2
- aiq/profiler/inference_optimization/llm_metrics.py +2 -2
- aiq/profiler/profile_runner.py +61 -21
- aiq/runtime/loader.py +9 -3
- aiq/runtime/runner.py +23 -9
- aiq/runtime/session.py +25 -7
- aiq/runtime/user_metadata.py +2 -3
- aiq/tool/chat_completion.py +74 -0
- aiq/tool/code_execution/README.md +152 -0
- aiq/tool/code_execution/code_sandbox.py +151 -72
- aiq/tool/code_execution/local_sandbox/.gitignore +1 -0
- aiq/tool/code_execution/local_sandbox/local_sandbox_server.py +139 -24
- aiq/tool/code_execution/local_sandbox/sandbox.requirements.txt +3 -1
- aiq/tool/code_execution/local_sandbox/start_local_sandbox.sh +27 -2
- aiq/tool/code_execution/register.py +7 -3
- aiq/tool/code_execution/test_code_execution_sandbox.py +414 -0
- aiq/tool/mcp/exceptions.py +142 -0
- aiq/tool/mcp/mcp_client.py +41 -6
- aiq/tool/mcp/mcp_tool.py +3 -2
- aiq/tool/register.py +1 -0
- aiq/tool/server_tools.py +6 -3
- aiq/utils/exception_handlers/automatic_retries.py +289 -0
- aiq/utils/exception_handlers/mcp.py +211 -0
- aiq/utils/io/model_processing.py +28 -0
- aiq/utils/log_utils.py +37 -0
- aiq/utils/string_utils.py +38 -0
- aiq/utils/type_converter.py +18 -2
- aiq/utils/type_utils.py +87 -0
- {aiqtoolkit-1.2.0.dev0.dist-info → aiqtoolkit-1.2.0rc1.dist-info}/METADATA +53 -21
- aiqtoolkit-1.2.0rc1.dist-info/RECORD +436 -0
- {aiqtoolkit-1.2.0.dev0.dist-info → aiqtoolkit-1.2.0rc1.dist-info}/WHEEL +1 -1
- {aiqtoolkit-1.2.0.dev0.dist-info → aiqtoolkit-1.2.0rc1.dist-info}/entry_points.txt +3 -0
- aiq/front_ends/fastapi/websocket.py +0 -148
- aiq/observability/async_otel_listener.py +0 -429
- aiqtoolkit-1.2.0.dev0.dist-info/RECORD +0 -316
- {aiqtoolkit-1.2.0.dev0.dist-info → aiqtoolkit-1.2.0rc1.dist-info}/licenses/LICENSE-3rd-party.txt +0 -0
- {aiqtoolkit-1.2.0.dev0.dist-info → aiqtoolkit-1.2.0rc1.dist-info}/licenses/LICENSE.md +0 -0
- {aiqtoolkit-1.2.0.dev0.dist-info → aiqtoolkit-1.2.0rc1.dist-info}/top_level.txt +0 -0
aiq/tool/register.py
CHANGED
aiq/tool/server_tools.py
CHANGED
|
@@ -23,8 +23,8 @@ class RequestAttributesTool(FunctionBaseConfig, name="current_request_attributes
|
|
|
23
23
|
"""
|
|
24
24
|
A simple tool that demonstrates how to retrieve user-defined request attributes from HTTP requests
|
|
25
25
|
within workflow tools. Please refer to the 'general' section of the configuration file located in the
|
|
26
|
-
'examples/
|
|
27
|
-
YAML file and associate it with a corresponding function to acquire request attributes.
|
|
26
|
+
'examples/getting_started/simple_web_query/configs/config-metadata.yml' directory to see how to define a
|
|
27
|
+
custom route using a YAML file and associate it with a corresponding function to acquire request attributes.
|
|
28
28
|
"""
|
|
29
29
|
pass
|
|
30
30
|
|
|
@@ -39,6 +39,7 @@ async def current_request_attributes(config: RequestAttributesTool, builder: Bui
|
|
|
39
39
|
|
|
40
40
|
from aiq.builder.context import AIQContext
|
|
41
41
|
aiq_context = AIQContext.get()
|
|
42
|
+
|
|
42
43
|
method: str | None = aiq_context.metadata.method
|
|
43
44
|
url_path: str | None = aiq_context.metadata.url_path
|
|
44
45
|
url_scheme: str | None = aiq_context.metadata.url_scheme
|
|
@@ -48,6 +49,7 @@ async def current_request_attributes(config: RequestAttributesTool, builder: Bui
|
|
|
48
49
|
client_host: str | None = aiq_context.metadata.client_host
|
|
49
50
|
client_port: int | None = aiq_context.metadata.client_port
|
|
50
51
|
cookies: dict[str, str] | None = aiq_context.metadata.cookies
|
|
52
|
+
conversation_id: str | None = aiq_context.conversation_id
|
|
51
53
|
|
|
52
54
|
return (f"Method: {method}, "
|
|
53
55
|
f"URL Path: {url_path}, "
|
|
@@ -57,7 +59,8 @@ async def current_request_attributes(config: RequestAttributesTool, builder: Bui
|
|
|
57
59
|
f"Path Params: {path_params}, "
|
|
58
60
|
f"Client Host: {client_host}, "
|
|
59
61
|
f"Client Port: {client_port}, "
|
|
60
|
-
f"Cookies: {cookies}"
|
|
62
|
+
f"Cookies: {cookies}, "
|
|
63
|
+
f"Conversation Id: {conversation_id}")
|
|
61
64
|
|
|
62
65
|
yield FunctionInfo.from_fn(_get_request_attributes,
|
|
63
66
|
description="Returns the acquired user defined request attriubutes.")
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
import asyncio
|
|
16
|
+
import copy
|
|
17
|
+
import functools
|
|
18
|
+
import inspect
|
|
19
|
+
import logging
|
|
20
|
+
import re
|
|
21
|
+
import time
|
|
22
|
+
import types
|
|
23
|
+
from collections.abc import Callable
|
|
24
|
+
from collections.abc import Iterable
|
|
25
|
+
from collections.abc import Sequence
|
|
26
|
+
from typing import Any
|
|
27
|
+
from typing import TypeVar
|
|
28
|
+
|
|
29
|
+
# pylint: disable=inconsistent-return-statements
|
|
30
|
+
|
|
31
|
+
T = TypeVar("T")
|
|
32
|
+
Exc = tuple[type[BaseException], ...] # exception classes
|
|
33
|
+
CodePattern = int | str | range # for retry_codes argument
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
37
|
+
# Helpers: status-code extraction & pattern matching
|
|
38
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
39
|
+
_CODE_ATTRS = ("code", "status", "status_code", "http_status")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _extract_status_code(exc: BaseException) -> int | None:
|
|
43
|
+
"""Return a numeric status code found inside *exc*, else None."""
|
|
44
|
+
for attr in _CODE_ATTRS:
|
|
45
|
+
if hasattr(exc, attr):
|
|
46
|
+
try:
|
|
47
|
+
return int(getattr(exc, attr))
|
|
48
|
+
except (TypeError, ValueError):
|
|
49
|
+
pass
|
|
50
|
+
if exc.args:
|
|
51
|
+
try:
|
|
52
|
+
return int(exc.args[0])
|
|
53
|
+
except (TypeError, ValueError):
|
|
54
|
+
pass
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _pattern_to_regex(pat: str) -> re.Pattern[str]:
|
|
59
|
+
"""
|
|
60
|
+
Convert simple wildcard pattern (“4xx”, “5*”, “40x”) to a ^regex$.
|
|
61
|
+
Rule: ‘x’ or ‘*’ ⇒ any digit.
|
|
62
|
+
"""
|
|
63
|
+
escaped = re.escape(pat)
|
|
64
|
+
return re.compile("^" + escaped.replace(r"\*", r"\d").replace("x", r"\d") + "$")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _code_matches(code: int, pat: CodePattern) -> bool:
|
|
68
|
+
if isinstance(pat, int):
|
|
69
|
+
return code == pat
|
|
70
|
+
if isinstance(pat, range):
|
|
71
|
+
return code in pat
|
|
72
|
+
return bool(_pattern_to_regex(pat).match(str(code)))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
76
|
+
# Unified retry-decision helper
|
|
77
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
78
|
+
def _want_retry(
|
|
79
|
+
exc: BaseException,
|
|
80
|
+
*,
|
|
81
|
+
code_patterns: Sequence[CodePattern] | None,
|
|
82
|
+
msg_substrings: Sequence[str] | None,
|
|
83
|
+
) -> bool:
|
|
84
|
+
"""
|
|
85
|
+
Return True if the exception satisfies *either* (when provided):
|
|
86
|
+
• code_patterns – matches status-code pattern(s)
|
|
87
|
+
• msg_substrings – contains any of the substrings (case-insensitive)
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
if not code_patterns and not msg_substrings:
|
|
91
|
+
logger.info("Retrying on exception %s without extra filters", exc)
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
# -------- status-code filter --------
|
|
95
|
+
if code_patterns is not None:
|
|
96
|
+
code = _extract_status_code(exc)
|
|
97
|
+
if any(_code_matches(code, p) for p in code_patterns):
|
|
98
|
+
logger.info("Retrying on exception %s with matched code %s", exc, code)
|
|
99
|
+
return True
|
|
100
|
+
|
|
101
|
+
# -------- message filter -----------
|
|
102
|
+
if msg_substrings is not None:
|
|
103
|
+
msg = str(exc).lower()
|
|
104
|
+
if any(s.lower() in msg for s in msg_substrings):
|
|
105
|
+
logger.info("Retrying on exception %s with matched message %s", exc, msg)
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
112
|
+
# Core decorator factory (sync / async / (a)gen)
|
|
113
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
114
|
+
def _retry_decorator(
|
|
115
|
+
*,
|
|
116
|
+
retries: int = 3,
|
|
117
|
+
base_delay: float = 0.25,
|
|
118
|
+
backoff: float = 2.0,
|
|
119
|
+
retry_on: Exc = (Exception, ),
|
|
120
|
+
retry_codes: Sequence[CodePattern] | None = None,
|
|
121
|
+
retry_on_messages: Sequence[str] | None = None,
|
|
122
|
+
deepcopy: bool = False,
|
|
123
|
+
) -> Callable[[Callable[..., T]], Callable[..., T]]:
|
|
124
|
+
"""
|
|
125
|
+
Build a decorator that retries with exponential back-off *iff*:
|
|
126
|
+
|
|
127
|
+
• the raised exception is an instance of one of `retry_on`
|
|
128
|
+
• AND `_want_retry()` returns True (i.e. matches codes/messages filters)
|
|
129
|
+
|
|
130
|
+
If both `retry_codes` and `retry_on_messages` are None, all exceptions are retried.
|
|
131
|
+
|
|
132
|
+
deepcopy:
|
|
133
|
+
If True, each retry receives deep‑copied *args and **kwargs* to avoid
|
|
134
|
+
mutating shared state between attempts.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
def decorate(fn: Callable[..., T]) -> Callable[..., T]:
|
|
138
|
+
use_deepcopy = deepcopy
|
|
139
|
+
|
|
140
|
+
async def _call_with_retry_async(*args, **kw) -> T:
|
|
141
|
+
delay = base_delay
|
|
142
|
+
for attempt in range(retries):
|
|
143
|
+
call_args = copy.deepcopy(args) if use_deepcopy else args
|
|
144
|
+
call_kwargs = copy.deepcopy(kw) if use_deepcopy else kw
|
|
145
|
+
try:
|
|
146
|
+
return await fn(*call_args, **call_kwargs)
|
|
147
|
+
except retry_on as exc:
|
|
148
|
+
if (not _want_retry(exc, code_patterns=retry_codes, msg_substrings=retry_on_messages)
|
|
149
|
+
or attempt == retries - 1):
|
|
150
|
+
raise
|
|
151
|
+
await asyncio.sleep(delay)
|
|
152
|
+
delay *= backoff
|
|
153
|
+
|
|
154
|
+
async def _agen_with_retry(*args, **kw):
|
|
155
|
+
delay = base_delay
|
|
156
|
+
for attempt in range(retries):
|
|
157
|
+
call_args = copy.deepcopy(args) if use_deepcopy else args
|
|
158
|
+
call_kwargs = copy.deepcopy(kw) if use_deepcopy else kw
|
|
159
|
+
try:
|
|
160
|
+
async for item in fn(*call_args, **call_kwargs):
|
|
161
|
+
yield item
|
|
162
|
+
return
|
|
163
|
+
except retry_on as exc:
|
|
164
|
+
if (not _want_retry(exc, code_patterns=retry_codes, msg_substrings=retry_on_messages)
|
|
165
|
+
or attempt == retries - 1):
|
|
166
|
+
raise
|
|
167
|
+
await asyncio.sleep(delay)
|
|
168
|
+
delay *= backoff
|
|
169
|
+
|
|
170
|
+
def _gen_with_retry(*args, **kw) -> Iterable[Any]:
|
|
171
|
+
delay = base_delay
|
|
172
|
+
for attempt in range(retries):
|
|
173
|
+
call_args = copy.deepcopy(args) if use_deepcopy else args
|
|
174
|
+
call_kwargs = copy.deepcopy(kw) if use_deepcopy else kw
|
|
175
|
+
try:
|
|
176
|
+
yield from fn(*call_args, **call_kwargs)
|
|
177
|
+
return
|
|
178
|
+
except retry_on as exc:
|
|
179
|
+
if (not _want_retry(exc, code_patterns=retry_codes, msg_substrings=retry_on_messages)
|
|
180
|
+
or attempt == retries - 1):
|
|
181
|
+
raise
|
|
182
|
+
time.sleep(delay)
|
|
183
|
+
delay *= backoff
|
|
184
|
+
|
|
185
|
+
def _sync_with_retry(*args, **kw) -> T:
|
|
186
|
+
delay = base_delay
|
|
187
|
+
for attempt in range(retries):
|
|
188
|
+
call_args = copy.deepcopy(args) if use_deepcopy else args
|
|
189
|
+
call_kwargs = copy.deepcopy(kw) if use_deepcopy else kw
|
|
190
|
+
try:
|
|
191
|
+
return fn(*call_args, **call_kwargs)
|
|
192
|
+
except retry_on as exc:
|
|
193
|
+
if (not _want_retry(exc, code_patterns=retry_codes, msg_substrings=retry_on_messages)
|
|
194
|
+
or attempt == retries - 1):
|
|
195
|
+
raise
|
|
196
|
+
time.sleep(delay)
|
|
197
|
+
delay *= backoff
|
|
198
|
+
|
|
199
|
+
# Decide which wrapper to return
|
|
200
|
+
if inspect.iscoroutinefunction(fn):
|
|
201
|
+
wrapper = _call_with_retry_async
|
|
202
|
+
elif inspect.isasyncgenfunction(fn):
|
|
203
|
+
wrapper = _agen_with_retry
|
|
204
|
+
elif inspect.isgeneratorfunction(fn):
|
|
205
|
+
wrapper = _gen_with_retry
|
|
206
|
+
else:
|
|
207
|
+
wrapper = _sync_with_retry
|
|
208
|
+
|
|
209
|
+
return functools.wraps(fn)(wrapper) # type: ignore[return-value]
|
|
210
|
+
|
|
211
|
+
return decorate
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
215
|
+
# Public helper : patch_with_retry
|
|
216
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
217
|
+
def patch_with_retry(
|
|
218
|
+
obj: Any,
|
|
219
|
+
*,
|
|
220
|
+
retries: int = 3,
|
|
221
|
+
base_delay: float = 0.25,
|
|
222
|
+
backoff: float = 2.0,
|
|
223
|
+
retry_on: Exc = (Exception, ),
|
|
224
|
+
retry_codes: Sequence[CodePattern] | None = None,
|
|
225
|
+
retry_on_messages: Sequence[str] | None = None,
|
|
226
|
+
deepcopy: bool = False,
|
|
227
|
+
) -> Any:
|
|
228
|
+
"""
|
|
229
|
+
Patch *obj* instance-locally so **every public method** retries on failure.
|
|
230
|
+
|
|
231
|
+
Extra filters
|
|
232
|
+
-------------
|
|
233
|
+
retry_codes
|
|
234
|
+
Same as before – ints, ranges, or wildcard strings (“4xx”, “5*”…).
|
|
235
|
+
retry_on_messages
|
|
236
|
+
List of *substring* patterns. We retry only if **any** pattern
|
|
237
|
+
appears (case-insensitive) in `str(exc)`.
|
|
238
|
+
deepcopy:
|
|
239
|
+
If True, each retry receives deep‑copied *args and **kwargs* to avoid
|
|
240
|
+
mutating shared state between attempts.
|
|
241
|
+
"""
|
|
242
|
+
deco = _retry_decorator(
|
|
243
|
+
retries=retries,
|
|
244
|
+
base_delay=base_delay,
|
|
245
|
+
backoff=backoff,
|
|
246
|
+
retry_on=retry_on,
|
|
247
|
+
retry_codes=retry_codes,
|
|
248
|
+
retry_on_messages=retry_on_messages,
|
|
249
|
+
deepcopy=deepcopy,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Choose attribute source: the *class* to avoid triggering __getattr__
|
|
253
|
+
cls = obj if inspect.isclass(obj) else type(obj)
|
|
254
|
+
cls_name = getattr(cls, "__name__", str(cls))
|
|
255
|
+
|
|
256
|
+
for name, _ in inspect.getmembers(cls, callable):
|
|
257
|
+
descriptor = inspect.getattr_static(cls, name)
|
|
258
|
+
|
|
259
|
+
# Skip dunders, privates and all descriptors we must not wrap
|
|
260
|
+
if (name.startswith("_") or isinstance(descriptor, (property, staticmethod, classmethod))):
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
original = descriptor.__func__ if isinstance(descriptor, types.MethodType) else descriptor
|
|
264
|
+
wrapped = deco(original)
|
|
265
|
+
|
|
266
|
+
try: # instance‑level first
|
|
267
|
+
if not inspect.isclass(obj):
|
|
268
|
+
object.__setattr__(obj, name, types.MethodType(wrapped, obj))
|
|
269
|
+
continue
|
|
270
|
+
except Exception as exc:
|
|
271
|
+
logger.info(
|
|
272
|
+
"Instance‑level patch failed for %s.%s (%s); "
|
|
273
|
+
"falling back to class‑level patch.",
|
|
274
|
+
cls_name,
|
|
275
|
+
name,
|
|
276
|
+
exc,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
try: # class‑level fallback
|
|
280
|
+
setattr(cls, name, wrapped)
|
|
281
|
+
except Exception as exc:
|
|
282
|
+
logger.info(
|
|
283
|
+
"Cannot patch method %s.%s with automatic retries: %s",
|
|
284
|
+
cls_name,
|
|
285
|
+
name,
|
|
286
|
+
exc,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
return obj
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import ssl
|
|
18
|
+
import sys
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
from functools import wraps
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
import httpx
|
|
24
|
+
|
|
25
|
+
from aiq.tool.mcp.exceptions import MCPAuthenticationError
|
|
26
|
+
from aiq.tool.mcp.exceptions import MCPConnectionError
|
|
27
|
+
from aiq.tool.mcp.exceptions import MCPError
|
|
28
|
+
from aiq.tool.mcp.exceptions import MCPProtocolError
|
|
29
|
+
from aiq.tool.mcp.exceptions import MCPRequestError
|
|
30
|
+
from aiq.tool.mcp.exceptions import MCPSSLError
|
|
31
|
+
from aiq.tool.mcp.exceptions import MCPTimeoutError
|
|
32
|
+
from aiq.tool.mcp.exceptions import MCPToolNotFoundError
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def format_mcp_error(error: MCPError, include_traceback: bool = False) -> None:
|
|
38
|
+
"""Format MCP errors for CLI display with structured logging and user guidance.
|
|
39
|
+
|
|
40
|
+
Logs structured error information for debugging and displays user-friendly
|
|
41
|
+
error messages with actionable suggestions to stderr.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
error (MCPError): MCPError instance containing message, url, category, suggestions, and original_exception
|
|
45
|
+
include_traceback (bool, optional): Whether to include the traceback in the error message. Defaults to False.
|
|
46
|
+
"""
|
|
47
|
+
# Log structured error information for debugging
|
|
48
|
+
logger.error("MCP operation failed: %s", error, exc_info=include_traceback)
|
|
49
|
+
|
|
50
|
+
# Display user-friendly suggestions
|
|
51
|
+
for suggestion in error.suggestions:
|
|
52
|
+
print(f" → {suggestion}", file=sys.stderr)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _extract_url(args: tuple, kwargs: dict[str, Any], url_param: str, func_name: str) -> str:
|
|
56
|
+
"""Extract URL from function arguments using clean fallback chain.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
args: Function positional arguments
|
|
60
|
+
kwargs: Function keyword arguments
|
|
61
|
+
url_param (str): Parameter name containing the URL
|
|
62
|
+
func_name (str): Function name for logging
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
str: URL string or "unknown" if extraction fails
|
|
66
|
+
"""
|
|
67
|
+
# Try keyword arguments first
|
|
68
|
+
if url_param in kwargs:
|
|
69
|
+
return kwargs[url_param]
|
|
70
|
+
|
|
71
|
+
# Try self attribute (e.g., self.url)
|
|
72
|
+
if args and hasattr(args[0], url_param):
|
|
73
|
+
return getattr(args[0], url_param)
|
|
74
|
+
|
|
75
|
+
# Try common case: url as second parameter after self
|
|
76
|
+
if len(args) > 1 and url_param == "url":
|
|
77
|
+
return args[1]
|
|
78
|
+
|
|
79
|
+
# Fallback with warning
|
|
80
|
+
logger.warning("Could not extract URL for error handling in %s", func_name)
|
|
81
|
+
return "unknown"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def extract_primary_exception(exceptions: list[Exception]) -> Exception:
|
|
85
|
+
"""Extract the most relevant exception from a group.
|
|
86
|
+
|
|
87
|
+
Prioritizes connection errors over others for better user experience.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
exceptions (list[Exception]): List of exceptions from ExceptionGroup
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Exception: Most relevant exception for user feedback
|
|
94
|
+
"""
|
|
95
|
+
# Prioritize connection errors
|
|
96
|
+
for exc in exceptions:
|
|
97
|
+
if isinstance(exc, (httpx.ConnectError, ConnectionError)):
|
|
98
|
+
return exc
|
|
99
|
+
|
|
100
|
+
# Then timeout errors
|
|
101
|
+
for exc in exceptions:
|
|
102
|
+
if isinstance(exc, httpx.TimeoutException):
|
|
103
|
+
return exc
|
|
104
|
+
|
|
105
|
+
# Then SSL errors
|
|
106
|
+
for exc in exceptions:
|
|
107
|
+
if isinstance(exc, ssl.SSLError):
|
|
108
|
+
return exc
|
|
109
|
+
|
|
110
|
+
# Fall back to first exception
|
|
111
|
+
return exceptions[0]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def convert_to_mcp_error(exception: Exception, url: str) -> MCPError:
|
|
115
|
+
"""Convert single exception to appropriate MCPError.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
exception (Exception): Single exception to convert
|
|
119
|
+
url (str): MCP server URL for context
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
MCPError: Appropriate MCPError subclass
|
|
123
|
+
"""
|
|
124
|
+
match exception:
|
|
125
|
+
case httpx.ConnectError() | ConnectionError():
|
|
126
|
+
return MCPConnectionError(url, exception)
|
|
127
|
+
case httpx.TimeoutException():
|
|
128
|
+
return MCPTimeoutError(url, exception)
|
|
129
|
+
case ssl.SSLError():
|
|
130
|
+
return MCPSSLError(url, exception)
|
|
131
|
+
case httpx.RequestError():
|
|
132
|
+
return MCPRequestError(url, exception)
|
|
133
|
+
case ValueError() if "Tool" in str(exception) and "not available" in str(exception):
|
|
134
|
+
# Extract tool name from error message if possible
|
|
135
|
+
tool_name = str(exception).split("Tool ")[1].split(" not available")[0] if "Tool " in str(
|
|
136
|
+
exception) else "unknown"
|
|
137
|
+
return MCPToolNotFoundError(tool_name, url, exception)
|
|
138
|
+
case _:
|
|
139
|
+
# Handle TaskGroup error message specifically
|
|
140
|
+
if "unhandled errors in a TaskGroup" in str(exception):
|
|
141
|
+
return MCPProtocolError(url, "Failed to connect to MCP server", exception)
|
|
142
|
+
if "unauthorized" in str(exception).lower() or "forbidden" in str(exception).lower():
|
|
143
|
+
return MCPAuthenticationError(url, exception)
|
|
144
|
+
return MCPError(f"Unexpected error: {exception}", url, original_exception=exception)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def handle_mcp_exceptions(url_param: str = "url") -> Callable[..., Any]:
|
|
148
|
+
"""Decorator that handles exceptions and converts them to MCPErrors.
|
|
149
|
+
|
|
150
|
+
This decorator wraps MCP client methods and converts low-level exceptions
|
|
151
|
+
to structured MCPError instances with helpful user guidance.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
url_param (str): Name of the parameter or attribute containing the MCP server URL
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Callable[..., Any]: Decorated function
|
|
158
|
+
|
|
159
|
+
Example:
|
|
160
|
+
.. code-block:: python
|
|
161
|
+
|
|
162
|
+
@handle_mcp_exceptions("url")
|
|
163
|
+
async def get_tools(self, url: str):
|
|
164
|
+
# Method implementation
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
@handle_mcp_exceptions("url") # Uses self.url
|
|
168
|
+
async def get_tool(self):
|
|
169
|
+
# Method implementation
|
|
170
|
+
pass
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
174
|
+
|
|
175
|
+
@wraps(func)
|
|
176
|
+
async def wrapper(*args, **kwargs):
|
|
177
|
+
try:
|
|
178
|
+
return await func(*args, **kwargs)
|
|
179
|
+
except MCPError:
|
|
180
|
+
# Re-raise MCPErrors as-is
|
|
181
|
+
raise
|
|
182
|
+
except Exception as e:
|
|
183
|
+
url = _extract_url(args, kwargs, url_param, func.__name__)
|
|
184
|
+
|
|
185
|
+
# Handle ExceptionGroup by extracting most relevant exception
|
|
186
|
+
if isinstance(e, ExceptionGroup): # noqa: F821
|
|
187
|
+
primary_exception = extract_primary_exception(list(e.exceptions))
|
|
188
|
+
mcp_error = convert_to_mcp_error(primary_exception, url)
|
|
189
|
+
else:
|
|
190
|
+
mcp_error = convert_to_mcp_error(e, url)
|
|
191
|
+
|
|
192
|
+
raise mcp_error from e
|
|
193
|
+
|
|
194
|
+
return wrapper
|
|
195
|
+
|
|
196
|
+
return decorator
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def mcp_exception_handler(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
200
|
+
"""Simplified decorator for methods that have self.url attribute.
|
|
201
|
+
|
|
202
|
+
This is a convenience decorator that assumes the URL is available as self.url.
|
|
203
|
+
Follows the same pattern as schema_exception_handler in this directory.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
func (Callable[..., Any]): The function to decorate
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Callable[..., Any]: Decorated function
|
|
210
|
+
"""
|
|
211
|
+
return handle_mcp_exceptions("url")(func)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def remove_r1_think_tags(text: str):
|
|
20
|
+
pattern = r'(<think>)?.*?</think>\s*(.*)'
|
|
21
|
+
|
|
22
|
+
# Add re.DOTALL flag to make . match newlines
|
|
23
|
+
match = re.match(pattern, text, re.DOTALL)
|
|
24
|
+
|
|
25
|
+
if match:
|
|
26
|
+
return match.group(2)
|
|
27
|
+
|
|
28
|
+
return text
|
aiq/utils/log_utils.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class LogFilter(logging.Filter):
|
|
20
|
+
"""
|
|
21
|
+
This class is used to filter log records based on a defined set of criteria.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, filter_criteria: list[str]):
|
|
25
|
+
self._filter_criteria = filter_criteria
|
|
26
|
+
super().__init__()
|
|
27
|
+
|
|
28
|
+
def filter(self, record: logging.LogRecord):
|
|
29
|
+
"""
|
|
30
|
+
Evaluates whether a log record should be emitted based on the message content.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
False if the message content contains any of the filter criteria, True otherwise.
|
|
34
|
+
"""
|
|
35
|
+
if any(match in record.getMessage() for match in self._filter_criteria):
|
|
36
|
+
return False
|
|
37
|
+
return True
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from pydantic import BaseModel
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def convert_to_str(value: Any) -> str:
|
|
22
|
+
"""
|
|
23
|
+
Convert a value to a string representation.
|
|
24
|
+
Handles various types including lists, dictionaries, and other objects.
|
|
25
|
+
"""
|
|
26
|
+
if isinstance(value, str):
|
|
27
|
+
return value
|
|
28
|
+
|
|
29
|
+
if isinstance(value, list):
|
|
30
|
+
return ", ".join(map(str, value))
|
|
31
|
+
elif isinstance(value, BaseModel):
|
|
32
|
+
return value.model_dump_json(exclude_none=True, exclude_unset=True)
|
|
33
|
+
elif isinstance(value, dict):
|
|
34
|
+
return ", ".join(f"{k}: {v}" for k, v in value.items())
|
|
35
|
+
elif hasattr(value, '__str__'):
|
|
36
|
+
return str(value)
|
|
37
|
+
else:
|
|
38
|
+
raise ValueError(f"Unsupported type for conversion to string: {type(value)}")
|