nvidia-nat 1.3.0.dev2__py3-none-any.whl → 1.3.0rc2__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.
- aiq/__init__.py +2 -2
- nat/agent/base.py +24 -15
- nat/agent/dual_node.py +9 -4
- nat/agent/prompt_optimizer/prompt.py +68 -0
- nat/agent/prompt_optimizer/register.py +149 -0
- nat/agent/react_agent/agent.py +79 -47
- nat/agent/react_agent/register.py +50 -22
- nat/agent/reasoning_agent/reasoning_agent.py +11 -9
- nat/agent/register.py +1 -1
- nat/agent/rewoo_agent/agent.py +326 -148
- nat/agent/rewoo_agent/prompt.py +19 -22
- nat/agent/rewoo_agent/register.py +54 -27
- nat/agent/tool_calling_agent/agent.py +84 -28
- nat/agent/tool_calling_agent/register.py +51 -28
- nat/authentication/api_key/api_key_auth_provider.py +2 -2
- nat/authentication/credential_validator/bearer_token_validator.py +557 -0
- nat/authentication/http_basic_auth/http_basic_auth_provider.py +1 -1
- nat/authentication/interfaces.py +5 -2
- nat/authentication/oauth2/oauth2_auth_code_flow_provider.py +69 -36
- nat/authentication/oauth2/oauth2_resource_server_config.py +124 -0
- nat/authentication/register.py +0 -1
- nat/builder/builder.py +56 -24
- nat/builder/component_utils.py +9 -5
- nat/builder/context.py +68 -17
- nat/builder/eval_builder.py +16 -11
- nat/builder/framework_enum.py +1 -0
- nat/builder/front_end.py +1 -1
- nat/builder/function.py +378 -8
- nat/builder/function_base.py +3 -3
- nat/builder/function_info.py +6 -8
- nat/builder/user_interaction_manager.py +2 -2
- nat/builder/workflow.py +13 -1
- nat/builder/workflow_builder.py +281 -76
- nat/cli/cli_utils/config_override.py +2 -2
- nat/cli/commands/evaluate.py +1 -1
- nat/cli/commands/info/info.py +16 -6
- nat/cli/commands/info/list_channels.py +1 -1
- nat/cli/commands/info/list_components.py +7 -8
- nat/cli/commands/mcp/__init__.py +14 -0
- nat/cli/commands/mcp/mcp.py +986 -0
- nat/cli/commands/object_store/__init__.py +14 -0
- nat/cli/commands/object_store/object_store.py +227 -0
- nat/cli/commands/optimize.py +90 -0
- nat/cli/commands/registry/publish.py +2 -2
- nat/cli/commands/registry/pull.py +2 -2
- nat/cli/commands/registry/remove.py +2 -2
- nat/cli/commands/registry/search.py +15 -17
- nat/cli/commands/start.py +16 -5
- nat/cli/commands/uninstall.py +1 -1
- nat/cli/commands/workflow/templates/config.yml.j2 +14 -13
- nat/cli/commands/workflow/templates/pyproject.toml.j2 +4 -1
- nat/cli/commands/workflow/templates/register.py.j2 +2 -3
- nat/cli/commands/workflow/templates/workflow.py.j2 +35 -21
- nat/cli/commands/workflow/workflow_commands.py +62 -22
- nat/cli/entrypoint.py +8 -10
- nat/cli/main.py +3 -0
- nat/cli/register_workflow.py +38 -4
- nat/cli/type_registry.py +75 -6
- nat/control_flow/__init__.py +0 -0
- nat/control_flow/register.py +20 -0
- nat/control_flow/router_agent/__init__.py +0 -0
- nat/control_flow/router_agent/agent.py +329 -0
- nat/control_flow/router_agent/prompt.py +48 -0
- nat/control_flow/router_agent/register.py +91 -0
- nat/control_flow/sequential_executor.py +166 -0
- nat/data_models/agent.py +34 -0
- nat/data_models/api_server.py +74 -66
- nat/data_models/authentication.py +23 -9
- nat/data_models/common.py +1 -1
- nat/data_models/component.py +2 -0
- nat/data_models/component_ref.py +11 -0
- nat/data_models/config.py +41 -17
- nat/data_models/dataset_handler.py +1 -1
- nat/data_models/discovery_metadata.py +4 -4
- nat/data_models/evaluate.py +4 -1
- nat/data_models/function.py +34 -0
- nat/data_models/function_dependencies.py +14 -6
- nat/data_models/gated_field_mixin.py +242 -0
- nat/data_models/intermediate_step.py +3 -3
- nat/data_models/optimizable.py +119 -0
- nat/data_models/optimizer.py +149 -0
- nat/data_models/span.py +41 -3
- nat/data_models/swe_bench_model.py +1 -1
- nat/data_models/temperature_mixin.py +44 -0
- nat/data_models/thinking_mixin.py +86 -0
- nat/data_models/top_p_mixin.py +44 -0
- nat/embedder/nim_embedder.py +1 -1
- nat/embedder/openai_embedder.py +1 -1
- nat/embedder/register.py +0 -1
- nat/eval/config.py +3 -1
- nat/eval/dataset_handler/dataset_handler.py +71 -7
- nat/eval/evaluate.py +86 -31
- nat/eval/evaluator/base_evaluator.py +1 -1
- nat/eval/evaluator/evaluator_model.py +13 -0
- nat/eval/intermediate_step_adapter.py +1 -1
- nat/eval/rag_evaluator/evaluate.py +2 -2
- nat/eval/rag_evaluator/register.py +3 -3
- nat/eval/register.py +4 -1
- nat/eval/remote_workflow.py +3 -3
- nat/eval/runtime_evaluator/__init__.py +14 -0
- nat/eval/runtime_evaluator/evaluate.py +123 -0
- nat/eval/runtime_evaluator/register.py +100 -0
- nat/eval/swe_bench_evaluator/evaluate.py +6 -6
- nat/eval/trajectory_evaluator/evaluate.py +1 -1
- nat/eval/trajectory_evaluator/register.py +1 -1
- nat/eval/tunable_rag_evaluator/evaluate.py +4 -7
- nat/eval/utils/eval_trace_ctx.py +89 -0
- nat/eval/utils/weave_eval.py +18 -9
- nat/experimental/decorators/experimental_warning_decorator.py +27 -7
- nat/experimental/test_time_compute/functions/plan_select_execute_function.py +7 -3
- nat/experimental/test_time_compute/functions/ttc_tool_orchestration_function.py +3 -3
- nat/experimental/test_time_compute/functions/ttc_tool_wrapper_function.py +1 -1
- nat/experimental/test_time_compute/models/strategy_base.py +5 -4
- nat/experimental/test_time_compute/register.py +0 -1
- nat/experimental/test_time_compute/selection/llm_based_output_merging_selector.py +1 -3
- nat/front_ends/console/authentication_flow_handler.py +82 -30
- nat/front_ends/console/console_front_end_plugin.py +8 -5
- nat/front_ends/fastapi/auth_flow_handlers/websocket_flow_handler.py +52 -17
- nat/front_ends/fastapi/dask_client_mixin.py +65 -0
- nat/front_ends/fastapi/fastapi_front_end_config.py +36 -5
- nat/front_ends/fastapi/fastapi_front_end_controller.py +4 -4
- nat/front_ends/fastapi/fastapi_front_end_plugin.py +135 -4
- nat/front_ends/fastapi/fastapi_front_end_plugin_worker.py +452 -282
- nat/front_ends/fastapi/job_store.py +518 -99
- nat/front_ends/fastapi/main.py +11 -19
- nat/front_ends/fastapi/message_handler.py +13 -14
- nat/front_ends/fastapi/message_validator.py +19 -19
- nat/front_ends/fastapi/response_helpers.py +4 -4
- nat/front_ends/fastapi/step_adaptor.py +2 -2
- nat/front_ends/fastapi/utils.py +57 -0
- nat/front_ends/mcp/introspection_token_verifier.py +73 -0
- nat/front_ends/mcp/mcp_front_end_config.py +10 -1
- nat/front_ends/mcp/mcp_front_end_plugin.py +45 -13
- nat/front_ends/mcp/mcp_front_end_plugin_worker.py +116 -8
- nat/front_ends/mcp/tool_converter.py +44 -14
- nat/front_ends/register.py +0 -1
- nat/front_ends/simple_base/simple_front_end_plugin_base.py +3 -1
- nat/llm/aws_bedrock_llm.py +24 -12
- nat/llm/azure_openai_llm.py +13 -6
- nat/llm/litellm_llm.py +69 -0
- nat/llm/nim_llm.py +20 -8
- nat/llm/openai_llm.py +14 -6
- nat/llm/register.py +4 -1
- nat/llm/utils/env_config_value.py +2 -3
- nat/llm/utils/thinking.py +215 -0
- nat/meta/pypi.md +9 -9
- nat/object_store/register.py +0 -1
- nat/observability/exporter/base_exporter.py +3 -3
- nat/observability/exporter/file_exporter.py +1 -1
- nat/observability/exporter/processing_exporter.py +309 -81
- nat/observability/exporter/span_exporter.py +35 -15
- nat/observability/exporter_manager.py +7 -7
- nat/observability/mixin/file_mixin.py +7 -7
- nat/observability/mixin/redaction_config_mixin.py +42 -0
- nat/observability/mixin/tagging_config_mixin.py +62 -0
- nat/observability/mixin/type_introspection_mixin.py +420 -107
- nat/observability/processor/batching_processor.py +5 -7
- nat/observability/processor/falsy_batch_filter_processor.py +55 -0
- nat/observability/processor/processor.py +3 -0
- nat/observability/processor/processor_factory.py +70 -0
- nat/observability/processor/redaction/__init__.py +24 -0
- nat/observability/processor/redaction/contextual_redaction_processor.py +125 -0
- nat/observability/processor/redaction/contextual_span_redaction_processor.py +66 -0
- nat/observability/processor/redaction/redaction_processor.py +177 -0
- nat/observability/processor/redaction/span_header_redaction_processor.py +92 -0
- nat/observability/processor/span_tagging_processor.py +68 -0
- nat/observability/register.py +6 -4
- nat/profiler/calc/calc_runner.py +3 -4
- nat/profiler/callbacks/agno_callback_handler.py +1 -1
- nat/profiler/callbacks/langchain_callback_handler.py +6 -6
- nat/profiler/callbacks/llama_index_callback_handler.py +3 -3
- nat/profiler/callbacks/semantic_kernel_callback_handler.py +3 -3
- nat/profiler/data_frame_row.py +1 -1
- nat/profiler/decorators/framework_wrapper.py +62 -13
- nat/profiler/decorators/function_tracking.py +160 -3
- nat/profiler/forecasting/models/forecasting_base_model.py +3 -1
- nat/profiler/forecasting/models/linear_model.py +1 -1
- nat/profiler/forecasting/models/random_forest_regressor.py +1 -1
- nat/profiler/inference_optimization/bottleneck_analysis/nested_stack_analysis.py +1 -1
- nat/profiler/inference_optimization/bottleneck_analysis/simple_stack_analysis.py +1 -1
- nat/profiler/inference_optimization/data_models.py +3 -3
- nat/profiler/inference_optimization/experimental/prefix_span_analysis.py +8 -9
- nat/profiler/inference_optimization/token_uniqueness.py +1 -1
- nat/profiler/parameter_optimization/__init__.py +0 -0
- nat/profiler/parameter_optimization/optimizable_utils.py +93 -0
- nat/profiler/parameter_optimization/optimizer_runtime.py +67 -0
- nat/profiler/parameter_optimization/parameter_optimizer.py +153 -0
- nat/profiler/parameter_optimization/parameter_selection.py +107 -0
- nat/profiler/parameter_optimization/pareto_visualizer.py +380 -0
- nat/profiler/parameter_optimization/prompt_optimizer.py +384 -0
- nat/profiler/parameter_optimization/update_helpers.py +66 -0
- nat/profiler/profile_runner.py +14 -9
- nat/profiler/utils.py +4 -2
- nat/registry_handlers/local/local_handler.py +2 -2
- nat/registry_handlers/package_utils.py +1 -2
- nat/registry_handlers/pypi/pypi_handler.py +23 -26
- nat/registry_handlers/register.py +3 -4
- nat/registry_handlers/rest/rest_handler.py +12 -13
- nat/retriever/milvus/retriever.py +2 -2
- nat/retriever/nemo_retriever/retriever.py +1 -1
- nat/retriever/register.py +0 -1
- nat/runtime/loader.py +2 -2
- nat/runtime/runner.py +106 -8
- nat/runtime/session.py +69 -8
- nat/settings/global_settings.py +16 -5
- nat/tool/chat_completion.py +5 -2
- nat/tool/code_execution/local_sandbox/local_sandbox_server.py +3 -3
- nat/tool/datetime_tools.py +49 -9
- nat/tool/document_search.py +2 -2
- nat/tool/github_tools.py +450 -0
- nat/tool/memory_tools/get_memory_tool.py +1 -1
- nat/tool/nvidia_rag.py +1 -1
- nat/tool/register.py +2 -9
- nat/tool/retriever.py +3 -2
- nat/utils/callable_utils.py +70 -0
- nat/utils/data_models/schema_validator.py +3 -3
- nat/utils/decorators.py +210 -0
- nat/utils/exception_handlers/automatic_retries.py +104 -51
- nat/utils/exception_handlers/schemas.py +1 -1
- nat/utils/io/yaml_tools.py +2 -2
- nat/utils/log_levels.py +25 -0
- nat/utils/reactive/base/observable_base.py +2 -2
- nat/utils/reactive/base/observer_base.py +1 -1
- nat/utils/reactive/observable.py +2 -2
- nat/utils/reactive/observer.py +4 -4
- nat/utils/reactive/subscription.py +1 -1
- nat/utils/settings/global_settings.py +6 -8
- nat/utils/type_converter.py +4 -3
- nat/utils/type_utils.py +9 -5
- {nvidia_nat-1.3.0.dev2.dist-info → nvidia_nat-1.3.0rc2.dist-info}/METADATA +42 -18
- {nvidia_nat-1.3.0.dev2.dist-info → nvidia_nat-1.3.0rc2.dist-info}/RECORD +238 -196
- {nvidia_nat-1.3.0.dev2.dist-info → nvidia_nat-1.3.0rc2.dist-info}/entry_points.txt +1 -0
- nat/cli/commands/info/list_mcp.py +0 -304
- nat/tool/github_tools/create_github_commit.py +0 -133
- nat/tool/github_tools/create_github_issue.py +0 -87
- nat/tool/github_tools/create_github_pr.py +0 -106
- nat/tool/github_tools/get_github_file.py +0 -106
- nat/tool/github_tools/get_github_issue.py +0 -166
- nat/tool/github_tools/get_github_pr.py +0 -256
- nat/tool/github_tools/update_github_issue.py +0 -100
- nat/tool/mcp/exceptions.py +0 -142
- nat/tool/mcp/mcp_client.py +0 -255
- nat/tool/mcp/mcp_tool.py +0 -96
- nat/utils/exception_handlers/mcp.py +0 -211
- /nat/{tool/github_tools → agent/prompt_optimizer}/__init__.py +0 -0
- /nat/{tool/mcp → authentication/credential_validator}/__init__.py +0 -0
- {nvidia_nat-1.3.0.dev2.dist-info → nvidia_nat-1.3.0rc2.dist-info}/WHEEL +0 -0
- {nvidia_nat-1.3.0.dev2.dist-info → nvidia_nat-1.3.0rc2.dist-info}/licenses/LICENSE-3rd-party.txt +0 -0
- {nvidia_nat-1.3.0.dev2.dist-info → nvidia_nat-1.3.0rc2.dist-info}/licenses/LICENSE.md +0 -0
- {nvidia_nat-1.3.0.dev2.dist-info → nvidia_nat-1.3.0rc2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
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.
|
|
@@ -0,0 +1,227 @@
|
|
|
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 asyncio
|
|
17
|
+
import importlib
|
|
18
|
+
import logging
|
|
19
|
+
import mimetypes
|
|
20
|
+
import time
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
import click
|
|
24
|
+
|
|
25
|
+
from nat.builder.workflow_builder import WorkflowBuilder
|
|
26
|
+
from nat.data_models.object_store import ObjectStoreBaseConfig
|
|
27
|
+
from nat.object_store.interfaces import ObjectStore
|
|
28
|
+
from nat.object_store.models import ObjectStoreItem
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
STORE_CONFIGS = {
|
|
33
|
+
"s3": {
|
|
34
|
+
"module": "nat.plugins.s3.object_store", "config_class": "S3ObjectStoreClientConfig"
|
|
35
|
+
},
|
|
36
|
+
"mysql": {
|
|
37
|
+
"module": "nat.plugins.mysql.object_store", "config_class": "MySQLObjectStoreClientConfig"
|
|
38
|
+
},
|
|
39
|
+
"redis": {
|
|
40
|
+
"module": "nat.plugins.redis.object_store", "config_class": "RedisObjectStoreClientConfig"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_object_store_config(**kwargs) -> ObjectStoreBaseConfig:
|
|
46
|
+
"""Process common object store arguments and return the config class"""
|
|
47
|
+
store_type = kwargs.pop("store_type")
|
|
48
|
+
config = STORE_CONFIGS[store_type]
|
|
49
|
+
module = importlib.import_module(config["module"])
|
|
50
|
+
config_class = getattr(module, config["config_class"])
|
|
51
|
+
return config_class(**kwargs)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def upload_file(object_store: ObjectStore, file_path: Path, key: str):
|
|
55
|
+
"""
|
|
56
|
+
Upload a single file to object store.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
object_store: The object store instance to use.
|
|
60
|
+
file_path: The path to the file to upload.
|
|
61
|
+
key: The key to upload the file to.
|
|
62
|
+
"""
|
|
63
|
+
try:
|
|
64
|
+
data = await asyncio.to_thread(file_path.read_bytes)
|
|
65
|
+
|
|
66
|
+
item = ObjectStoreItem(data=data,
|
|
67
|
+
content_type=mimetypes.guess_type(str(file_path))[0],
|
|
68
|
+
metadata={
|
|
69
|
+
"original_filename": file_path.name,
|
|
70
|
+
"file_size": str(len(data)),
|
|
71
|
+
"file_extension": file_path.suffix,
|
|
72
|
+
"upload_timestamp": str(int(time.time()))
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
# Upload using upsert to allow overwriting
|
|
76
|
+
await object_store.upsert_object(key, item)
|
|
77
|
+
click.echo(f"✅ Uploaded: {file_path.name} -> {key}")
|
|
78
|
+
|
|
79
|
+
except Exception as e:
|
|
80
|
+
raise RuntimeError(f"Failed to upload {file_path.name}:\n{e}") from e
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def object_store_command_decorator(async_func):
|
|
84
|
+
"""
|
|
85
|
+
Decorator that handles the common object store command pattern.
|
|
86
|
+
|
|
87
|
+
The decorated function should take (store: ObjectStore, kwargs) as parameters
|
|
88
|
+
and return an exit code (0 for success).
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
@click.pass_context
|
|
92
|
+
def wrapper(ctx: click.Context, **kwargs):
|
|
93
|
+
config = ctx.obj["store_config"]
|
|
94
|
+
|
|
95
|
+
async def work():
|
|
96
|
+
async with WorkflowBuilder() as builder:
|
|
97
|
+
await builder.add_object_store(name="store", config=config)
|
|
98
|
+
store = await builder.get_object_store_client("store")
|
|
99
|
+
return await async_func(store, **kwargs)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
exit_code = asyncio.run(work())
|
|
103
|
+
except Exception as e:
|
|
104
|
+
raise click.ClickException(f"Command failed: {e}") from e
|
|
105
|
+
if exit_code != 0:
|
|
106
|
+
raise click.ClickException(f"Command failed with exit code {exit_code}")
|
|
107
|
+
return exit_code
|
|
108
|
+
|
|
109
|
+
return wrapper
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@click.command(name="upload", help="Upload a directory to an object store.")
|
|
113
|
+
@click.argument("local_dir",
|
|
114
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path),
|
|
115
|
+
required=True)
|
|
116
|
+
@click.help_option("--help", "-h")
|
|
117
|
+
@object_store_command_decorator
|
|
118
|
+
async def upload_command(store: ObjectStore, local_dir: Path, **_kwargs):
|
|
119
|
+
"""
|
|
120
|
+
Upload a directory to an object store.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
local_dir: The local directory to upload.
|
|
124
|
+
store: The object store to use.
|
|
125
|
+
_kwargs: Additional keyword arguments.
|
|
126
|
+
"""
|
|
127
|
+
try:
|
|
128
|
+
click.echo(f"📁 Processing directory: {local_dir}")
|
|
129
|
+
file_count = 0
|
|
130
|
+
|
|
131
|
+
# Process each file recursively
|
|
132
|
+
for file_path in local_dir.rglob("*"):
|
|
133
|
+
if file_path.is_file():
|
|
134
|
+
key = file_path.relative_to(local_dir).as_posix()
|
|
135
|
+
await upload_file(store, file_path, key)
|
|
136
|
+
file_count += 1
|
|
137
|
+
|
|
138
|
+
click.echo(f"✅ Directory uploaded successfully! {file_count} files uploaded.")
|
|
139
|
+
return 0
|
|
140
|
+
|
|
141
|
+
except Exception as e:
|
|
142
|
+
raise click.ClickException(f"❌ Failed to upload directory {local_dir}:\n {e}") from e
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@click.command(name="delete", help="Delete files from an object store.")
|
|
146
|
+
@click.argument("keys", type=str, required=True, nargs=-1)
|
|
147
|
+
@click.help_option("--help", "-h")
|
|
148
|
+
@object_store_command_decorator
|
|
149
|
+
async def delete_command(store: ObjectStore, keys: list[str], **_kwargs):
|
|
150
|
+
"""
|
|
151
|
+
Delete files from an object store.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
store: The object store to use.
|
|
155
|
+
keys: The keys to delete.
|
|
156
|
+
_kwargs: Additional keyword arguments.
|
|
157
|
+
"""
|
|
158
|
+
deleted_count = 0
|
|
159
|
+
failed_count = 0
|
|
160
|
+
for key in keys:
|
|
161
|
+
try:
|
|
162
|
+
await store.delete_object(key)
|
|
163
|
+
click.echo(f"✅ Deleted: {key}")
|
|
164
|
+
deleted_count += 1
|
|
165
|
+
except Exception as e:
|
|
166
|
+
click.echo(f"❌ Failed to delete {key}: {e}")
|
|
167
|
+
failed_count += 1
|
|
168
|
+
|
|
169
|
+
click.echo(f"✅ Deletion completed! {deleted_count} keys deleted. {failed_count} keys failed to delete.")
|
|
170
|
+
return 0 if failed_count == 0 else 1
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@click.group(name="object-store", invoke_without_command=False, help="Manage object store operations.")
|
|
174
|
+
def object_store_command(**_kwargs):
|
|
175
|
+
"""Manage object store operations including uploading files and directories."""
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def register_object_store_commands():
|
|
180
|
+
|
|
181
|
+
@click.group(name="s3", invoke_without_command=False, help="S3 object store operations.")
|
|
182
|
+
@click.argument("bucket_name", type=str, required=True)
|
|
183
|
+
@click.option("--endpoint-url", type=str, help="S3 endpoint URL")
|
|
184
|
+
@click.option("--access-key", type=str, help="S3 access key")
|
|
185
|
+
@click.option("--secret-key", type=str, help="S3 secret key")
|
|
186
|
+
@click.option("--region", type=str, help="S3 region")
|
|
187
|
+
@click.pass_context
|
|
188
|
+
def s3(ctx: click.Context, **kwargs):
|
|
189
|
+
ctx.ensure_object(dict)
|
|
190
|
+
ctx.obj["store_config"] = get_object_store_config(store_type="s3", **kwargs)
|
|
191
|
+
|
|
192
|
+
@click.group(name="mysql", invoke_without_command=False, help="MySQL object store operations.")
|
|
193
|
+
@click.argument("bucket_name", type=str, required=True)
|
|
194
|
+
@click.option("--host", type=str, help="MySQL host")
|
|
195
|
+
@click.option("--port", type=int, help="MySQL port")
|
|
196
|
+
@click.option("--db", type=str, help="MySQL database name")
|
|
197
|
+
@click.option("--username", type=str, help="MySQL username")
|
|
198
|
+
@click.option("--password", type=str, help="MySQL password")
|
|
199
|
+
@click.pass_context
|
|
200
|
+
def mysql(ctx: click.Context, **kwargs):
|
|
201
|
+
ctx.ensure_object(dict)
|
|
202
|
+
ctx.obj["store_config"] = get_object_store_config(store_type="mysql", **kwargs)
|
|
203
|
+
|
|
204
|
+
@click.group(name="redis", invoke_without_command=False, help="Redis object store operations.")
|
|
205
|
+
@click.argument("bucket_name", type=str, required=True)
|
|
206
|
+
@click.option("--host", type=str, help="Redis host")
|
|
207
|
+
@click.option("--port", type=int, help="Redis port")
|
|
208
|
+
@click.option("--db", type=int, help="Redis db")
|
|
209
|
+
@click.pass_context
|
|
210
|
+
def redis(ctx: click.Context, **kwargs):
|
|
211
|
+
ctx.ensure_object(dict)
|
|
212
|
+
ctx.obj["store_config"] = get_object_store_config(store_type="redis", **kwargs)
|
|
213
|
+
|
|
214
|
+
commands = {"s3": s3, "mysql": mysql, "redis": redis}
|
|
215
|
+
|
|
216
|
+
for store_type, config in STORE_CONFIGS.items():
|
|
217
|
+
try:
|
|
218
|
+
importlib.import_module(config["module"])
|
|
219
|
+
command = commands[store_type]
|
|
220
|
+
object_store_command.add_command(command, name=store_type)
|
|
221
|
+
command.add_command(upload_command, name="upload")
|
|
222
|
+
command.add_command(delete_command, name="delete")
|
|
223
|
+
except ImportError:
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
register_object_store_commands()
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-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 asyncio
|
|
17
|
+
import logging
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import click
|
|
21
|
+
|
|
22
|
+
from nat.data_models.optimizer import OptimizerRunConfig
|
|
23
|
+
from nat.profiler.parameter_optimization.optimizer_runtime import optimize_config
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@click.group(name=__name__, invoke_without_command=True, help="Optimize a workflow with the specified dataset.")
|
|
29
|
+
@click.option(
|
|
30
|
+
"--config_file",
|
|
31
|
+
type=click.Path(exists=True, file_okay=True, dir_okay=False, path_type=Path),
|
|
32
|
+
required=True,
|
|
33
|
+
help="A JSON/YAML file that sets the parameters for the workflow and evaluation.",
|
|
34
|
+
)
|
|
35
|
+
@click.option(
|
|
36
|
+
"--dataset",
|
|
37
|
+
type=click.Path(exists=True, file_okay=True, dir_okay=False, path_type=Path),
|
|
38
|
+
required=False,
|
|
39
|
+
help="A json file with questions and ground truth answers. This will override the dataset path in the config file.",
|
|
40
|
+
)
|
|
41
|
+
@click.option(
|
|
42
|
+
"--result_json_path",
|
|
43
|
+
type=str,
|
|
44
|
+
default="$",
|
|
45
|
+
help=("A JSON path to extract the result from the workflow. Use this when the workflow returns "
|
|
46
|
+
"multiple objects or a dictionary. For example, '$.output' will extract the 'output' field "
|
|
47
|
+
"from the result."),
|
|
48
|
+
)
|
|
49
|
+
@click.option(
|
|
50
|
+
"--endpoint",
|
|
51
|
+
type=str,
|
|
52
|
+
default=None,
|
|
53
|
+
help="Use endpoint for running the workflow. Example: http://localhost:8000/generate",
|
|
54
|
+
)
|
|
55
|
+
@click.option(
|
|
56
|
+
"--endpoint_timeout",
|
|
57
|
+
type=int,
|
|
58
|
+
default=300,
|
|
59
|
+
help="HTTP response timeout in seconds. Only relevant if endpoint is specified.",
|
|
60
|
+
)
|
|
61
|
+
@click.pass_context
|
|
62
|
+
def optimizer_command(ctx, **kwargs) -> None:
|
|
63
|
+
""" Optimize workflow with the specified dataset"""
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def run_optimizer(config: OptimizerRunConfig):
|
|
68
|
+
await optimize_config(config)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@optimizer_command.result_callback(replace=True)
|
|
72
|
+
def run_optimizer_callback(
|
|
73
|
+
processors, # pylint: disable=unused-argument
|
|
74
|
+
*,
|
|
75
|
+
config_file: Path,
|
|
76
|
+
dataset: Path,
|
|
77
|
+
result_json_path: str,
|
|
78
|
+
endpoint: str,
|
|
79
|
+
endpoint_timeout: int,
|
|
80
|
+
):
|
|
81
|
+
"""Run the optimizer with the provided config file and dataset."""
|
|
82
|
+
config = OptimizerRunConfig(
|
|
83
|
+
config_file=config_file,
|
|
84
|
+
dataset=dataset,
|
|
85
|
+
result_json_path=result_json_path,
|
|
86
|
+
endpoint=endpoint,
|
|
87
|
+
endpoint_timeout=endpoint_timeout,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
asyncio.run(run_optimizer(config))
|
|
@@ -40,7 +40,7 @@ async def publish_artifact(registry_handler_config: RegistryHandlerBaseConfig, p
|
|
|
40
40
|
try:
|
|
41
41
|
artifact = build_artifact(package_root=package_root)
|
|
42
42
|
except Exception as e:
|
|
43
|
-
logger.exception("Error building artifact: %s", e
|
|
43
|
+
logger.exception("Error building artifact: %s", e)
|
|
44
44
|
return
|
|
45
45
|
await stack.enter_async_context(registry_handler.publish(artifact=artifact))
|
|
46
46
|
|
|
@@ -82,7 +82,7 @@ def publish(channel: str, config_file: str, package_root: str) -> None:
|
|
|
82
82
|
logger.error("Publish channel '%s' has not been configured.", channel)
|
|
83
83
|
return
|
|
84
84
|
except Exception as e:
|
|
85
|
-
logger.exception("Error loading user settings: %s", e
|
|
85
|
+
logger.exception("Error loading user settings: %s", e)
|
|
86
86
|
return
|
|
87
87
|
|
|
88
88
|
asyncio.run(publish_artifact(registry_handler_config=publish_channel_config, package_root=package_root))
|
|
@@ -66,7 +66,7 @@ async def pull_artifact(registry_handler_config: RegistryHandlerBaseConfig, pack
|
|
|
66
66
|
validated_packages = PullRequestPackages(packages=package_list)
|
|
67
67
|
|
|
68
68
|
except Exception as e:
|
|
69
|
-
logger.exception("Error processing package names: %s", e
|
|
69
|
+
logger.exception("Error processing package names: %s", e)
|
|
70
70
|
return
|
|
71
71
|
|
|
72
72
|
await stack.enter_async_context(registry_handler.pull(packages=validated_packages))
|
|
@@ -112,7 +112,7 @@ def pull(channel: str, config_file: str, packages: str) -> None:
|
|
|
112
112
|
logger.error("Pull channel '%s' has not been configured.", channel)
|
|
113
113
|
return
|
|
114
114
|
except Exception as e:
|
|
115
|
-
logger.exception("Error loading user settings: %s", e
|
|
115
|
+
logger.exception("Error loading user settings: %s", e)
|
|
116
116
|
return
|
|
117
117
|
|
|
118
118
|
asyncio.run(pull_artifact(pull_channel_config, packages))
|
|
@@ -41,7 +41,7 @@ async def remove_artifact(registry_handler_config: RegistryHandlerBaseConfig, pa
|
|
|
41
41
|
try:
|
|
42
42
|
package_name_list = PackageNameVersionList(**{"packages": packages})
|
|
43
43
|
except Exception as e:
|
|
44
|
-
logger.exception("Invalid package format: '%s'", e
|
|
44
|
+
logger.exception("Invalid package format: '%s'", e)
|
|
45
45
|
|
|
46
46
|
await stack.enter_async_context(registry_handler.remove(packages=package_name_list))
|
|
47
47
|
|
|
@@ -102,7 +102,7 @@ def remove(channel: str, config_file: str, packages: str) -> None:
|
|
|
102
102
|
logger.error("Remove channel '%s' has not been configured.", channel)
|
|
103
103
|
return
|
|
104
104
|
except Exception as e:
|
|
105
|
-
logger.exception("Error loading user settings: %s", e
|
|
105
|
+
logger.exception("Error loading user settings: %s", e)
|
|
106
106
|
return
|
|
107
107
|
|
|
108
108
|
asyncio.run(remove_artifact(registry_handler_config=remove_channel_config, packages=packages_versions))
|
|
@@ -29,14 +29,13 @@ from nat.utils.data_models.schema_validator import validate_yaml
|
|
|
29
29
|
logger = logging.getLogger(__name__)
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
async def search_artifacts(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
n_results: int = 10) -> None:
|
|
32
|
+
async def search_artifacts(registry_handler_config: RegistryHandlerBaseConfig,
|
|
33
|
+
query: str,
|
|
34
|
+
search_fields: list[SearchFields],
|
|
35
|
+
visualize: bool,
|
|
36
|
+
component_types: list[ComponentEnum],
|
|
37
|
+
save_path: str | None = None,
|
|
38
|
+
n_results: int = 10) -> None:
|
|
40
39
|
|
|
41
40
|
from nat.cli.type_registry import GlobalTypeRegistry
|
|
42
41
|
from nat.registry_handlers.schemas.search import SearchQuery
|
|
@@ -116,14 +115,13 @@ async def search_artifacts( # pylint: disable=R0917
|
|
|
116
115
|
required=False,
|
|
117
116
|
help=("The component types to include in search."),
|
|
118
117
|
)
|
|
119
|
-
def search(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
output_path: str) -> None:
|
|
118
|
+
def search(config_file: str,
|
|
119
|
+
channel: str,
|
|
120
|
+
fields: list[str],
|
|
121
|
+
query: str,
|
|
122
|
+
component_types: list[ComponentEnum],
|
|
123
|
+
n_results: int,
|
|
124
|
+
output_path: str) -> None:
|
|
127
125
|
"""
|
|
128
126
|
Search for NAT artifacts with the specified configuration.
|
|
129
127
|
"""
|
|
@@ -142,7 +140,7 @@ def search( # pylint: disable=R0917
|
|
|
142
140
|
logger.error("Search channel '%s' has not been configured.", channel)
|
|
143
141
|
return
|
|
144
142
|
except Exception as e:
|
|
145
|
-
logger.exception("Error loading user settings: %s", e
|
|
143
|
+
logger.exception("Error loading user settings: %s", e)
|
|
146
144
|
return
|
|
147
145
|
|
|
148
146
|
asyncio.run(
|
nat/cli/commands/start.py
CHANGED
|
@@ -35,7 +35,6 @@ logger = logging.getLogger(__name__)
|
|
|
35
35
|
|
|
36
36
|
class StartCommandGroup(click.Group):
|
|
37
37
|
|
|
38
|
-
# pylint: disable=too-many-positional-arguments
|
|
39
38
|
def __init__(
|
|
40
39
|
self,
|
|
41
40
|
name: str | None = None,
|
|
@@ -103,12 +102,24 @@ class StartCommandGroup(click.Group):
|
|
|
103
102
|
raise ValueError(f"Invalid field '{name}'.Unions are only supported for optional parameters.")
|
|
104
103
|
|
|
105
104
|
# Handle the types
|
|
106
|
-
|
|
105
|
+
# Literal[...] -> map to click.Choice([...])
|
|
106
|
+
if (decomposed_type.origin is typing.Literal):
|
|
107
|
+
# typing.get_args returns the literal values; ensure they are strings for Click
|
|
108
|
+
literal_values = [str(v) for v in decomposed_type.args]
|
|
109
|
+
param_type = click.Choice(literal_values)
|
|
110
|
+
|
|
111
|
+
elif (issubclass(decomposed_type.root, Path)):
|
|
107
112
|
param_type = click.Path(exists=True, file_okay=True, dir_okay=False, path_type=Path)
|
|
108
113
|
|
|
109
|
-
elif (issubclass(decomposed_type.root,
|
|
114
|
+
elif (issubclass(decomposed_type.root, list | tuple | set)):
|
|
110
115
|
if (len(decomposed_type.args) == 1):
|
|
111
|
-
|
|
116
|
+
inner = DecomposedType(decomposed_type.args[0])
|
|
117
|
+
# Support containers of Literal values -> multiple Choice
|
|
118
|
+
if (inner.origin is typing.Literal):
|
|
119
|
+
literal_values = [str(v) for v in inner.args]
|
|
120
|
+
param_type = click.Choice(literal_values)
|
|
121
|
+
else:
|
|
122
|
+
param_type = inner.root
|
|
112
123
|
else:
|
|
113
124
|
param_type = None
|
|
114
125
|
|
|
@@ -225,7 +236,7 @@ class StartCommandGroup(click.Group):
|
|
|
225
236
|
return asyncio.run(run_plugin())
|
|
226
237
|
|
|
227
238
|
except Exception as e:
|
|
228
|
-
logger.error("Failed to initialize workflow"
|
|
239
|
+
logger.error("Failed to initialize workflow")
|
|
229
240
|
raise click.ClickException(str(e)) from e
|
|
230
241
|
|
|
231
242
|
def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:
|
nat/cli/commands/uninstall.py
CHANGED
|
@@ -44,7 +44,7 @@ async def uninstall_packages(packages: list[dict[str, str]]) -> None:
|
|
|
44
44
|
try:
|
|
45
45
|
package_name_list = PackageNameVersionList(**{"packages": packages})
|
|
46
46
|
except Exception as e:
|
|
47
|
-
logger.exception("Error validating package format: %s", e
|
|
47
|
+
logger.exception("Error validating package format: %s", e)
|
|
48
48
|
return
|
|
49
49
|
|
|
50
50
|
async with AsyncExitStack() as stack:
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
functions:
|
|
2
|
+
current_datetime:
|
|
3
|
+
_type: current_datetime
|
|
4
|
+
{{python_safe_workflow_name}}:
|
|
5
|
+
_type: {{python_safe_workflow_name}}
|
|
6
|
+
prefix: "Hello:"
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
llms:
|
|
9
|
+
nim_llm:
|
|
10
|
+
_type: nim
|
|
11
|
+
model_name: meta/llama-3.1-70b-instruct
|
|
12
|
+
temperature: 0.0
|
|
13
13
|
|
|
14
14
|
workflow:
|
|
15
|
-
_type:
|
|
16
|
-
|
|
15
|
+
_type: react_agent
|
|
16
|
+
llm_name: nim_llm
|
|
17
|
+
tool_names: [current_datetime, {{python_safe_workflow_name}}]
|
|
@@ -3,6 +3,9 @@ build-backend = "setuptools.build_meta"
|
|
|
3
3
|
{% if editable %}requires = ["setuptools >= 64", "setuptools-scm>=8"]
|
|
4
4
|
|
|
5
5
|
[tool.setuptools_scm]
|
|
6
|
+
# NAT uses the --first-parent flag to avoid tags from previous releases which have been merged into the develop branch
|
|
7
|
+
# from causing an unexpected version change. This can be safely removed if developing outside of the NAT repository.
|
|
8
|
+
git_describe_command = "git describe --long --first-parent"
|
|
6
9
|
root = "{{ rel_path_to_repo_root}}"{% else %}requires = ["setuptools >= 64"]{% endif %}
|
|
7
10
|
|
|
8
11
|
[project]
|
|
@@ -11,7 +14,7 @@ name = "{{ package_name }}"
|
|
|
11
14
|
dependencies = [
|
|
12
15
|
"{{ nat_dependency }}",
|
|
13
16
|
]
|
|
14
|
-
requires-python = ">=3.11,<3.
|
|
17
|
+
requires-python = ">=3.11,<3.14"
|
|
15
18
|
description = "Custom NeMo Agent Toolkit Workflow"
|
|
16
19
|
classifiers = ["Programming Language :: Python"]
|
|
17
20
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
# pylint: disable=unused-import
|
|
2
1
|
# flake8: noqa
|
|
3
2
|
|
|
4
|
-
# Import
|
|
5
|
-
from {{package_name}} import {{
|
|
3
|
+
# Import the generated workflow function to trigger registration
|
|
4
|
+
from .{{package_name}} import {{ python_safe_workflow_name }}_function
|
|
@@ -3,6 +3,7 @@ import logging
|
|
|
3
3
|
from pydantic import Field
|
|
4
4
|
|
|
5
5
|
from nat.builder.builder import Builder
|
|
6
|
+
from nat.builder.framework_enum import LLMFrameworkEnum
|
|
6
7
|
from nat.builder.function_info import FunctionInfo
|
|
7
8
|
from nat.cli.register_workflow import register_function
|
|
8
9
|
from nat.data_models.function import FunctionBaseConfig
|
|
@@ -12,25 +13,38 @@ logger = logging.getLogger(__name__)
|
|
|
12
13
|
|
|
13
14
|
class {{ workflow_class_name }}(FunctionBaseConfig, name="{{ workflow_name }}"):
|
|
14
15
|
"""
|
|
15
|
-
{{workflow_description}}
|
|
16
|
+
{{ workflow_description }}
|
|
16
17
|
"""
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
18
|
+
prefix: str = Field(default="Echo:", description="Prefix to add before the echoed text.")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@register_function(config_type={{ workflow_class_name }}, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN])
|
|
22
|
+
async def {{ python_safe_workflow_name }}_function(config: {{ workflow_class_name }}, builder: Builder):
|
|
23
|
+
"""
|
|
24
|
+
Registers a function (addressable via `{{ workflow_name }}` in the configuration).
|
|
25
|
+
This registration ensures a static mapping of the function type, `{{ workflow_name }}`, to the `{{ workflow_class_name }}` configuration object.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
config ({{ workflow_class_name }}): The configuration for the function.
|
|
29
|
+
builder (Builder): The builder object.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
FunctionInfo: The function info object for the function.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
# Define the function that will be registered.
|
|
36
|
+
async def _echo(text: str) -> str:
|
|
37
|
+
"""
|
|
38
|
+
Takes a text input and echoes back with a pre-defined prefix.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
text (str): The text to echo back.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
str: The text with the prefix.
|
|
45
|
+
"""
|
|
46
|
+
return f"{config.prefix} {text}"
|
|
47
|
+
|
|
48
|
+
# The callable is wrapped in a FunctionInfo object.
|
|
49
|
+
# The description parameter is used to describe the function.
|
|
50
|
+
yield FunctionInfo.from_fn(_echo, description=_echo.__doc__)
|