nvidia-nat 1.4.0a20251102__py3-none-any.whl → 1.4.0a20251112__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.
Files changed (30) hide show
  1. nat/cli/commands/workflow/workflow_commands.py +3 -2
  2. nat/eval/dataset_handler/dataset_filter.py +34 -2
  3. nat/eval/evaluate.py +1 -1
  4. nat/eval/utils/weave_eval.py +17 -3
  5. nat/front_ends/fastapi/fastapi_front_end_config.py +7 -0
  6. nat/front_ends/fastapi/fastapi_front_end_plugin.py +13 -7
  7. nat/front_ends/fastapi/fastapi_front_end_plugin_worker.py +20 -14
  8. nat/llm/aws_bedrock_llm.py +11 -9
  9. nat/llm/azure_openai_llm.py +12 -4
  10. nat/llm/litellm_llm.py +11 -4
  11. nat/llm/nim_llm.py +11 -9
  12. nat/llm/openai_llm.py +12 -9
  13. nat/tool/code_execution/code_sandbox.py +3 -6
  14. nat/tool/code_execution/local_sandbox/Dockerfile.sandbox +19 -32
  15. nat/tool/code_execution/local_sandbox/local_sandbox_server.py +5 -0
  16. nat/tool/code_execution/local_sandbox/sandbox.requirements.txt +2 -0
  17. nat/tool/code_execution/local_sandbox/start_local_sandbox.sh +10 -4
  18. nat/tool/server_tools.py +15 -2
  19. nat/utils/__init__.py +8 -4
  20. nat/utils/io/yaml_tools.py +73 -3
  21. {nvidia_nat-1.4.0a20251102.dist-info → nvidia_nat-1.4.0a20251112.dist-info}/METADATA +3 -1
  22. {nvidia_nat-1.4.0a20251102.dist-info → nvidia_nat-1.4.0a20251112.dist-info}/RECORD +27 -30
  23. nat/data_models/temperature_mixin.py +0 -44
  24. nat/data_models/top_p_mixin.py +0 -44
  25. nat/tool/code_execution/test_code_execution_sandbox.py +0 -414
  26. {nvidia_nat-1.4.0a20251102.dist-info → nvidia_nat-1.4.0a20251112.dist-info}/WHEEL +0 -0
  27. {nvidia_nat-1.4.0a20251102.dist-info → nvidia_nat-1.4.0a20251112.dist-info}/entry_points.txt +0 -0
  28. {nvidia_nat-1.4.0a20251102.dist-info → nvidia_nat-1.4.0a20251112.dist-info}/licenses/LICENSE-3rd-party.txt +0 -0
  29. {nvidia_nat-1.4.0a20251102.dist-info → nvidia_nat-1.4.0a20251112.dist-info}/licenses/LICENSE.md +0 -0
  30. {nvidia_nat-1.4.0a20251102.dist-info → nvidia_nat-1.4.0a20251112.dist-info}/top_level.txt +0 -0
@@ -354,7 +354,8 @@ def reinstall_command(workflow_name):
354
354
 
355
355
  @click.command()
356
356
  @click.argument('workflow_name')
357
- def delete_command(workflow_name: str):
357
+ @click.option('-y', '--yes', "yes_flag", is_flag=True, default=False, help='Do not prompt for confirmation.')
358
+ def delete_command(workflow_name: str, yes_flag: bool):
358
359
  """
359
360
  Delete a NAT workflow and uninstall its package.
360
361
 
@@ -362,7 +363,7 @@ def delete_command(workflow_name: str):
362
363
  workflow_name (str): The name of the workflow to delete.
363
364
  """
364
365
  try:
365
- if not click.confirm(f"Are you sure you want to delete the workflow '{workflow_name}'?"):
366
+ if not yes_flag and not click.confirm(f"Are you sure you want to delete the workflow '{workflow_name}'?"):
366
367
  click.echo("Workflow deletion cancelled.")
367
368
  return
368
369
  editable = get_repo_root() is not None
@@ -13,6 +13,8 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
 
16
+ import fnmatch
17
+
16
18
  import pandas as pd
17
19
 
18
20
  from nat.data_models.dataset_handler import EvalFilterConfig
@@ -24,6 +26,7 @@ class DatasetFilter:
24
26
  - If a allowlist is provided, only keep rows matching the filter values.
25
27
  - If a denylist is provided, remove rows matching the filter values.
26
28
  - If the filter column does not exist in the DataFrame, the filtering is skipped for that column.
29
+ - Supports Unix shell-style wildcards (``*``, ``?``, ``[seq]``, ``[!seq]``) for string matching.
27
30
 
28
31
  This is a utility class that is dataset agnostic and can be used to filter any DataFrame based on the provided
29
32
  filter configuration.
@@ -33,6 +36,33 @@ class DatasetFilter:
33
36
 
34
37
  self.filter_config = filter_config
35
38
 
39
+ @staticmethod
40
+ def _match_wildcard_patterns(series: pd.Series, patterns: list[str | int | float]) -> pd.Series:
41
+ """
42
+ Match series values against wildcard patterns and exact values.
43
+
44
+ Args:
45
+ series (pd.Series): pandas Series to match against
46
+ patterns (list[str | int | float]): List of patterns/values
47
+
48
+ Returns:
49
+ pd.Series: Boolean Series indicating matches
50
+ """
51
+ # Convert series to string for pattern matching
52
+ str_series = series.astype(str)
53
+
54
+ # Initialize boolean mask
55
+ matches = pd.Series([False] * len(series), index=series.index)
56
+
57
+ # Check each pattern using fnmatch with list comprehension to avoid lambda capture
58
+ for pattern in patterns:
59
+ pattern_str = str(pattern)
60
+ pattern_matches = pd.Series([fnmatch.fnmatch(val, pattern_str) for val in str_series],
61
+ index=str_series.index)
62
+ matches |= pattern_matches
63
+
64
+ return matches
65
+
36
66
  def apply_filters(self, df) -> pd.DataFrame:
37
67
 
38
68
  filtered_df = df.copy()
@@ -41,12 +71,14 @@ class DatasetFilter:
41
71
  if self.filter_config.allowlist:
42
72
  for column, values in self.filter_config.allowlist.field.items():
43
73
  if column in filtered_df.columns:
44
- filtered_df = filtered_df[filtered_df[column].isin(values)]
74
+ matches = self._match_wildcard_patterns(filtered_df[column], values)
75
+ filtered_df = filtered_df[matches]
45
76
 
46
77
  # Apply denylist (remove specified rows)
47
78
  if self.filter_config.denylist:
48
79
  for column, values in self.filter_config.denylist.field.items():
49
80
  if column in filtered_df.columns:
50
- filtered_df = filtered_df[~filtered_df[column].isin(values)]
81
+ matches = self._match_wildcard_patterns(filtered_df[column], values)
82
+ filtered_df = filtered_df[~matches]
51
83
 
52
84
  return filtered_df
nat/eval/evaluate.py CHANGED
@@ -514,7 +514,7 @@ class EvaluationRun:
514
514
  # Run workflow and evaluate
515
515
  async with WorkflowEvalBuilder.from_config(config=config) as eval_workflow:
516
516
  # Initialize Weave integration
517
- self.weave_eval.initialize_logger(workflow_alias, self.eval_input, config)
517
+ self.weave_eval.initialize_logger(workflow_alias, self.eval_input, config, job_id=job_id)
518
518
 
519
519
  with self.eval_trace_context.evaluation_context():
520
520
  # Run workflow
@@ -82,7 +82,7 @@ class WeaveEvaluationIntegration:
82
82
  """Get the full dataset for Weave."""
83
83
  return [item.full_dataset_entry for item in eval_input.eval_input_items]
84
84
 
85
- def initialize_logger(self, workflow_alias: str, eval_input: EvalInput, config: Any):
85
+ def initialize_logger(self, workflow_alias: str, eval_input: EvalInput, config: Any, job_id: str | None = None):
86
86
  """Initialize the Weave evaluation logger."""
87
87
  if not self.client and not self.initialize_client():
88
88
  # lazy init the client
@@ -92,10 +92,16 @@ class WeaveEvaluationIntegration:
92
92
  weave_dataset = self._get_weave_dataset(eval_input)
93
93
  config_dict = config.model_dump(mode="json")
94
94
  config_dict["name"] = workflow_alias
95
+
96
+ # Include job_id in eval_attributes if provided
97
+ eval_attributes = {}
98
+ if job_id:
99
+ eval_attributes["job_id"] = job_id
100
+
95
101
  self.eval_logger = self.evaluation_logger_cls(model=config_dict,
96
102
  dataset=weave_dataset,
97
103
  name=workflow_alias,
98
- eval_attributes={})
104
+ eval_attributes=eval_attributes)
99
105
  self.pred_loggers = {}
100
106
 
101
107
  # Capture the current evaluation call for context propagation
@@ -136,9 +142,17 @@ class WeaveEvaluationIntegration:
136
142
  coros = []
137
143
  for eval_output_item in eval_output.eval_output_items:
138
144
  if eval_output_item.id in self.pred_loggers:
145
+ # Structure the score as a dict and include reasoning if available
146
+ score_value = {
147
+ "score": eval_output_item.score,
148
+ }
149
+
150
+ if eval_output_item.reasoning is not None:
151
+ score_value["reasoning"] = eval_output_item.reasoning
152
+
139
153
  coros.append(self.pred_loggers[eval_output_item.id].alog_score(
140
154
  scorer=evaluator_name,
141
- score=eval_output_item.score,
155
+ score=score_value,
142
156
  ))
143
157
 
144
158
  # Execute all coroutines concurrently
@@ -211,6 +211,13 @@ class FastApiFrontEndConfig(FrontEndBaseConfig, name="fastapi"):
211
211
  "Maximum number of async jobs to run concurrently, this controls the number of dask workers created. "
212
212
  "This parameter is only used when scheduler_address is `None` and a Dask local cluster is created."),
213
213
  ge=1)
214
+ dask_workers: typing.Literal["threads", "processes"] = Field(
215
+ default="processes",
216
+ description=(
217
+ "Type of Dask workers to use. Options are 'threads' for Threaded Dask workers or 'processes' for "
218
+ "Process based Dask workers. This parameter is only used when scheduler_address is `None` and a local Dask "
219
+ "cluster is created."),
220
+ )
214
221
  dask_log_level: str = Field(
215
222
  default="WARNING",
216
223
  description="Logging level for Dask.",
@@ -120,18 +120,24 @@ class FastApiFrontEndPlugin(DaskClientMixin, FrontEndBase[FastApiFrontEndConfig]
120
120
 
121
121
  from dask.distributed import LocalCluster
122
122
 
123
- self._cluster = LocalCluster(processes=True,
123
+ use_threads = self.front_end_config.dask_workers == 'threads'
124
+
125
+ # set n_workers to max_running_async_jobs + 1 to allow for one worker to handle the cleanup task
126
+ self._cluster = LocalCluster(processes=not use_threads,
124
127
  silence_logs=dask_log_level,
125
- n_workers=self.front_end_config.max_running_async_jobs,
126
- threads_per_worker=1)
128
+ protocol="tcp",
129
+ n_workers=self.front_end_config.max_running_async_jobs + 1)
127
130
 
128
131
  self._scheduler_address = self._cluster.scheduler.address
129
132
 
130
- with self.blocking_client(self._scheduler_address) as client:
131
- # Client.run submits a function to be run on each worker
132
- client.run(self._setup_worker)
133
+ if not use_threads and sys.platform != "win32":
134
+ with self.blocking_client(self._scheduler_address) as client:
135
+ # Client.run submits a function to be run on each worker
136
+ client.run(self._setup_worker)
133
137
 
134
- logger.info("Created local Dask cluster with scheduler at %s", self._scheduler_address)
138
+ logger.info("Created local Dask cluster with scheduler at %s using %s workers",
139
+ self._scheduler_address,
140
+ self.front_end_config.dask_workers)
135
141
 
136
142
  except ImportError:
137
143
  logger.warning("Dask is not installed, async execution and evaluation will not be available.")
@@ -544,7 +544,8 @@ class FastApiFrontEndPluginWorker(FastApiFrontEndPluginWorkerBase):
544
544
  GenerateStreamResponseType = workflow.streaming_output_schema
545
545
  GenerateSingleResponseType = workflow.single_output_schema
546
546
 
547
- if self._dask_available:
547
+ # Skip async generation for custom routes (those with function_name)
548
+ if self._dask_available and not hasattr(endpoint, 'function_name'):
548
549
  # Append job_id and expiry_seconds to the input schema, this effectively makes these reserved keywords
549
550
  # Consider prefixing these with "nat_" to avoid conflicts
550
551
 
@@ -562,6 +563,10 @@ class FastApiFrontEndPluginWorker(FastApiFrontEndPluginWorkerBase):
562
563
  description="Optional time (in seconds) before the job expires. "
563
564
  "Clamped between 600 (10 min) and 86400 (24h).")
564
565
 
566
+ def validate_model(self):
567
+ # Override to ensure that the parent class validator is not called
568
+ return self
569
+
565
570
  # Ensure that the input is in the body. POD types are treated as query parameters
566
571
  if (not issubclass(GenerateBodyType, BaseModel)):
567
572
  GenerateBodyType = typing.Annotated[GenerateBodyType, Body()]
@@ -760,17 +765,18 @@ class FastApiFrontEndPluginWorker(FastApiFrontEndPluginWorkerBase):
760
765
  return AsyncGenerateResponse(job_id=job.job_id, status=job.status)
761
766
 
762
767
  job_id = self._job_store.ensure_job_id(request.job_id)
763
- (_, job) = await self._job_store.submit_job(job_id=job_id,
764
- expiry_seconds=request.expiry_seconds,
765
- job_fn=run_generation,
766
- sync_timeout=request.sync_timeout,
767
- job_args=[
768
- self._scheduler_address,
769
- self._db_url,
770
- self._config_file_path,
771
- job_id,
772
- request.model_dump(mode="json")
773
- ])
768
+ (_, job) = await self._job_store.submit_job(
769
+ job_id=job_id,
770
+ expiry_seconds=request.expiry_seconds,
771
+ job_fn=run_generation,
772
+ sync_timeout=request.sync_timeout,
773
+ job_args=[
774
+ self._scheduler_address,
775
+ self._db_url,
776
+ self._config_file_path,
777
+ job_id,
778
+ request.model_dump(mode="json", exclude=["job_id", "sync_timeout", "expiry_seconds"])
779
+ ])
774
780
 
775
781
  if job is not None:
776
782
  response.status_code = 200
@@ -916,7 +922,7 @@ class FastApiFrontEndPluginWorker(FastApiFrontEndPluginWorkerBase):
916
922
  responses={500: response_500},
917
923
  )
918
924
 
919
- if self._dask_available:
925
+ if self._dask_available and not hasattr(endpoint, 'function_name'):
920
926
  app.add_api_route(
921
927
  path=f"{endpoint.path}/async",
922
928
  endpoint=post_async_generation(request_type=AsyncGenerateRequest),
@@ -930,7 +936,7 @@ class FastApiFrontEndPluginWorker(FastApiFrontEndPluginWorkerBase):
930
936
  else:
931
937
  raise ValueError(f"Unsupported method {endpoint.method}")
932
938
 
933
- if self._dask_available:
939
+ if self._dask_available and not hasattr(endpoint, 'function_name'):
934
940
  app.add_api_route(
935
941
  path=f"{endpoint.path}/async/job/{{job_id}}",
936
942
  endpoint=get_async_job_status,
@@ -25,18 +25,10 @@ from nat.data_models.optimizable import OptimizableField
25
25
  from nat.data_models.optimizable import OptimizableMixin
26
26
  from nat.data_models.optimizable import SearchSpace
27
27
  from nat.data_models.retry_mixin import RetryMixin
28
- from nat.data_models.temperature_mixin import TemperatureMixin
29
28
  from nat.data_models.thinking_mixin import ThinkingMixin
30
- from nat.data_models.top_p_mixin import TopPMixin
31
29
 
32
30
 
33
- class AWSBedrockModelConfig(LLMBaseConfig,
34
- RetryMixin,
35
- OptimizableMixin,
36
- TemperatureMixin,
37
- TopPMixin,
38
- ThinkingMixin,
39
- name="aws_bedrock"):
31
+ class AWSBedrockModelConfig(LLMBaseConfig, RetryMixin, OptimizableMixin, ThinkingMixin, name="aws_bedrock"):
40
32
  """An AWS Bedrock llm provider to be used with an LLM client."""
41
33
 
42
34
  model_config = ConfigDict(protected_namespaces=(), extra="allow")
@@ -61,6 +53,16 @@ class AWSBedrockModelConfig(LLMBaseConfig,
61
53
  default=None, description="Bedrock endpoint to use. Needed if you don't want to default to us-east-1 endpoint.")
62
54
  credentials_profile_name: str | None = Field(
63
55
  default=None, description="The name of the profile in the ~/.aws/credentials or ~/.aws/config files.")
56
+ temperature: float | None = OptimizableField(
57
+ default=None,
58
+ ge=0.0,
59
+ description="Sampling temperature to control randomness in the output.",
60
+ space=SearchSpace(high=0.9, low=0.1, step=0.2))
61
+ top_p: float | None = OptimizableField(default=None,
62
+ ge=0.0,
63
+ le=1.0,
64
+ description="Top-p for distribution sampling.",
65
+ space=SearchSpace(high=1.0, low=0.5, step=0.1))
64
66
 
65
67
 
66
68
  @register_llm_provider(config_type=AWSBedrockModelConfig)
@@ -22,17 +22,15 @@ from nat.builder.llm import LLMProviderInfo
22
22
  from nat.cli.register_workflow import register_llm_provider
23
23
  from nat.data_models.common import OptionalSecretStr
24
24
  from nat.data_models.llm import LLMBaseConfig
25
+ from nat.data_models.optimizable import OptimizableField
26
+ from nat.data_models.optimizable import SearchSpace
25
27
  from nat.data_models.retry_mixin import RetryMixin
26
- from nat.data_models.temperature_mixin import TemperatureMixin
27
28
  from nat.data_models.thinking_mixin import ThinkingMixin
28
- from nat.data_models.top_p_mixin import TopPMixin
29
29
 
30
30
 
31
31
  class AzureOpenAIModelConfig(
32
32
  LLMBaseConfig,
33
33
  RetryMixin,
34
- TemperatureMixin,
35
- TopPMixin,
36
34
  ThinkingMixin,
37
35
  name="azure_openai",
38
36
  ):
@@ -50,6 +48,16 @@ class AzureOpenAIModelConfig(
50
48
  serialization_alias="azure_deployment",
51
49
  description="The Azure OpenAI hosted model/deployment name.")
52
50
  seed: int | None = Field(default=None, description="Random seed to set for generation.")
51
+ temperature: float | None = OptimizableField(
52
+ default=None,
53
+ ge=0.0,
54
+ description="Sampling temperature to control randomness in the output.",
55
+ space=SearchSpace(high=0.9, low=0.1, step=0.2))
56
+ top_p: float | None = OptimizableField(default=None,
57
+ ge=0.0,
58
+ le=1.0,
59
+ description="Top-p for distribution sampling.",
60
+ space=SearchSpace(high=1.0, low=0.5, step=0.1))
53
61
 
54
62
 
55
63
  @register_llm_provider(config_type=AzureOpenAIModelConfig)
nat/llm/litellm_llm.py CHANGED
@@ -26,18 +26,15 @@ from nat.data_models.common import OptionalSecretStr
26
26
  from nat.data_models.llm import LLMBaseConfig
27
27
  from nat.data_models.optimizable import OptimizableField
28
28
  from nat.data_models.optimizable import OptimizableMixin
29
+ from nat.data_models.optimizable import SearchSpace
29
30
  from nat.data_models.retry_mixin import RetryMixin
30
- from nat.data_models.temperature_mixin import TemperatureMixin
31
31
  from nat.data_models.thinking_mixin import ThinkingMixin
32
- from nat.data_models.top_p_mixin import TopPMixin
33
32
 
34
33
 
35
34
  class LiteLlmModelConfig(
36
35
  LLMBaseConfig,
37
36
  OptimizableMixin,
38
37
  RetryMixin,
39
- TemperatureMixin,
40
- TopPMixin,
41
38
  ThinkingMixin,
42
39
  name="litellm",
43
40
  ):
@@ -54,6 +51,16 @@ class LiteLlmModelConfig(
54
51
  serialization_alias="model",
55
52
  description="The LiteLlm hosted model name.")
56
53
  seed: int | None = Field(default=None, description="Random seed to set for generation.")
54
+ temperature: float | None = OptimizableField(
55
+ default=None,
56
+ ge=0.0,
57
+ description="Sampling temperature to control randomness in the output.",
58
+ space=SearchSpace(high=0.9, low=0.1, step=0.2))
59
+ top_p: float | None = OptimizableField(default=None,
60
+ ge=0.0,
61
+ le=1.0,
62
+ description="Top-p for distribution sampling.",
63
+ space=SearchSpace(high=1.0, low=0.5, step=0.1))
57
64
 
58
65
 
59
66
  @register_llm_provider(config_type=LiteLlmModelConfig)
nat/llm/nim_llm.py CHANGED
@@ -27,18 +27,10 @@ from nat.data_models.optimizable import OptimizableField
27
27
  from nat.data_models.optimizable import OptimizableMixin
28
28
  from nat.data_models.optimizable import SearchSpace
29
29
  from nat.data_models.retry_mixin import RetryMixin
30
- from nat.data_models.temperature_mixin import TemperatureMixin
31
30
  from nat.data_models.thinking_mixin import ThinkingMixin
32
- from nat.data_models.top_p_mixin import TopPMixin
33
31
 
34
32
 
35
- class NIMModelConfig(LLMBaseConfig,
36
- RetryMixin,
37
- OptimizableMixin,
38
- TemperatureMixin,
39
- TopPMixin,
40
- ThinkingMixin,
41
- name="nim"):
33
+ class NIMModelConfig(LLMBaseConfig, RetryMixin, OptimizableMixin, ThinkingMixin, name="nim"):
42
34
  """An NVIDIA Inference Microservice (NIM) llm provider to be used with an LLM client."""
43
35
 
44
36
  model_config = ConfigDict(protected_namespaces=(), extra="allow")
@@ -51,6 +43,16 @@ class NIMModelConfig(LLMBaseConfig,
51
43
  max_tokens: PositiveInt = OptimizableField(default=300,
52
44
  description="Maximum number of tokens to generate.",
53
45
  space=SearchSpace(high=2176, low=128, step=512))
46
+ temperature: float | None = OptimizableField(
47
+ default=None,
48
+ ge=0.0,
49
+ description="Sampling temperature to control randomness in the output.",
50
+ space=SearchSpace(high=0.9, low=0.1, step=0.2))
51
+ top_p: float | None = OptimizableField(default=None,
52
+ ge=0.0,
53
+ le=1.0,
54
+ description="Top-p for distribution sampling.",
55
+ space=SearchSpace(high=1.0, low=0.5, step=0.1))
54
56
 
55
57
 
56
58
  @register_llm_provider(config_type=NIMModelConfig)
nat/llm/openai_llm.py CHANGED
@@ -24,19 +24,12 @@ from nat.data_models.common import OptionalSecretStr
24
24
  from nat.data_models.llm import LLMBaseConfig
25
25
  from nat.data_models.optimizable import OptimizableField
26
26
  from nat.data_models.optimizable import OptimizableMixin
27
+ from nat.data_models.optimizable import SearchSpace
27
28
  from nat.data_models.retry_mixin import RetryMixin
28
- from nat.data_models.temperature_mixin import TemperatureMixin
29
29
  from nat.data_models.thinking_mixin import ThinkingMixin
30
- from nat.data_models.top_p_mixin import TopPMixin
31
30
 
32
31
 
33
- class OpenAIModelConfig(LLMBaseConfig,
34
- RetryMixin,
35
- OptimizableMixin,
36
- TemperatureMixin,
37
- TopPMixin,
38
- ThinkingMixin,
39
- name="openai"):
32
+ class OpenAIModelConfig(LLMBaseConfig, RetryMixin, OptimizableMixin, ThinkingMixin, name="openai"):
40
33
  """An OpenAI LLM provider to be used with an LLM client."""
41
34
 
42
35
  model_config = ConfigDict(protected_namespaces=(), extra="allow")
@@ -48,6 +41,16 @@ class OpenAIModelConfig(LLMBaseConfig,
48
41
  description="The OpenAI hosted model name.")
49
42
  seed: int | None = Field(default=None, description="Random seed to set for generation.")
50
43
  max_retries: int = Field(default=10, description="The max number of retries for the request.")
44
+ temperature: float | None = OptimizableField(
45
+ default=None,
46
+ ge=0.0,
47
+ description="Sampling temperature to control randomness in the output.",
48
+ space=SearchSpace(high=0.9, low=0.1, step=0.2))
49
+ top_p: float | None = OptimizableField(default=None,
50
+ ge=0.0,
51
+ le=1.0,
52
+ description="Top-p for distribution sampling.",
53
+ space=SearchSpace(high=1.0, low=0.5, step=0.1))
51
54
 
52
55
 
53
56
  @register_llm_provider(config_type=OpenAIModelConfig)
@@ -92,7 +92,9 @@ class Sandbox(abc.ABC):
92
92
  raise ValueError(f"Language {language} not supported")
93
93
 
94
94
  generated_code = generated_code.strip().strip("`")
95
- code_to_execute = textwrap.dedent("""
95
+ # Use json.dumps to properly escape the generated_code instead of repr()
96
+ escaped_code = json.dumps(generated_code)
97
+ code_to_execute = textwrap.dedent(f"""
96
98
  import traceback
97
99
  import json
98
100
  import os
@@ -101,11 +103,6 @@ class Sandbox(abc.ABC):
101
103
  import io
102
104
  warnings.filterwarnings('ignore')
103
105
  os.environ['OPENBLAS_NUM_THREADS'] = '16'
104
- """).strip()
105
-
106
- # Use json.dumps to properly escape the generated_code instead of repr()
107
- escaped_code = json.dumps(generated_code)
108
- code_to_execute += textwrap.dedent(f"""
109
106
 
110
107
  generated_code = {escaped_code}
111
108
 
@@ -12,43 +12,26 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- # Use the base image with Python 3.10 and Flask
16
- FROM tiangolo/uwsgi-nginx-flask:python3.10
17
-
18
- # Install dependencies required for Lean 4 and other tools
19
- RUN apt-get update && \
20
- apt-get install -y curl git && \
21
- curl https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh -sSf | sh -s -- -y && \
22
- /root/.elan/bin/elan toolchain install leanprover/lean4:v4.12.0 && \
23
- /root/.elan/bin/elan default leanprover/lean4:v4.12.0 && \
24
- /root/.elan/bin/elan self update
25
-
26
- # Set environment variables to include Lean and elan/lake in the PATH
27
- ENV PATH="/root/.elan/bin:$PATH"
28
-
29
- # Create Lean project directory and initialize a new Lean project with Mathlib4
30
- RUN mkdir -p /lean4 && cd /lean4 && \
31
- /root/.elan/bin/lake new my_project && \
32
- cd my_project && \
33
- echo 'leanprover/lean4:v4.12.0' > lean-toolchain && \
34
- echo 'require mathlib from git "https://github.com/leanprover-community/mathlib4" @ "v4.12.0"' >> lakefile.lean
35
-
36
- # Download and cache Mathlib4 to avoid recompiling, then build the project
37
- RUN cd /lean4/my_project && \
38
- /root/.elan/bin/lake exe cache get && \
39
- /root/.elan/bin/lake build
40
-
41
- # Set environment variables to include Lean project path
42
- ENV LEAN_PATH="/lean4/my_project"
43
- ENV PATH="/lean4/my_project:$PATH"
15
+ # UWSGI_CHEAPER sets the number of initial uWSGI worker processes
16
+ # UWSGI_PROCESSES sets the maximum number of uWSGI worker processes
17
+ ARG UWSGI_CHEAPER=5
18
+ ARG UWSGI_PROCESSES=10
19
+
20
+ # Use the base image with Python 3.13
21
+ FROM python:3.13-slim-bookworm
22
+
23
+
24
+ RUN apt update && \
25
+ apt upgrade && \
26
+ apt install -y --no-install-recommends libexpat1 && \
27
+ apt clean && \
28
+ rm -rf /var/lib/apt/lists/*
44
29
 
45
30
  # Set up application code and install Python dependencies
46
31
  COPY sandbox.requirements.txt /app/requirements.txt
47
32
  RUN pip install --no-cache-dir -r /app/requirements.txt
48
33
  COPY local_sandbox_server.py /app/main.py
49
-
50
- # Set the working directory to /app
51
- WORKDIR /app
34
+ RUN mkdir /workspace
52
35
 
53
36
  # Set Flask app environment variables and ports
54
37
  ARG UWSGI_CHEAPER
@@ -58,3 +41,7 @@ ARG UWSGI_PROCESSES
58
41
  ENV UWSGI_PROCESSES=$UWSGI_PROCESSES
59
42
 
60
43
  ENV LISTEN_PORT=6000
44
+ EXPOSE 6000
45
+
46
+ WORKDIR /app
47
+ CMD uwsgi --http 0.0.0.0:${LISTEN_PORT} --master -p ${UWSGI_PROCESSES} --force-cwd /workspace -w main:app
@@ -194,5 +194,10 @@ def execute():
194
194
  return do_execute(request)
195
195
 
196
196
 
197
+ @app.route("/", methods=["GET"])
198
+ def status() -> tuple[dict[str, str], int]:
199
+ return ({"status": "ok"}, 200)
200
+
201
+
197
202
  if __name__ == '__main__':
198
203
  app.run(port=6000)
@@ -1,6 +1,8 @@
1
+ Flask==3.1
1
2
  numpy
2
3
  pandas
3
4
  scipy
4
5
  ipython
5
6
  plotly
6
7
  pydantic
8
+ pyuwsgi==2.0.*
@@ -19,7 +19,11 @@
19
19
 
20
20
  DOCKER_COMMAND=${DOCKER_COMMAND:-"docker"}
21
21
  SANDBOX_NAME=${1:-'local-sandbox'}
22
- NUM_THREADS=10
22
+
23
+ # UWSGI_CHEAPER sets the number of initial uWSGI worker processes
24
+ # UWSGI_PROCESSES sets the maximum number of uWSGI worker processes
25
+ UWSGI_CHEAPER=${UWSGI_CHEAPER:-5}
26
+ UWSGI_PROCESSES=${UWSGI_PROCESSES:-10}
23
27
 
24
28
  # Get the output_data directory path for mounting
25
29
  # Priority: command line argument > environment variable > default path (current directory)
@@ -37,14 +41,16 @@ fi
37
41
  # Check if the Docker image already exists
38
42
  if ! ${DOCKER_COMMAND} images ${SANDBOX_NAME} | grep -q "${SANDBOX_NAME}"; then
39
43
  echo "Docker image not found locally. Building ${SANDBOX_NAME}..."
40
- ${DOCKER_COMMAND} build --tag=${SANDBOX_NAME} --build-arg="UWSGI_PROCESSES=$((${NUM_THREADS} * 10))" --build-arg="UWSGI_CHEAPER=${NUM_THREADS}" -f Dockerfile.sandbox .
44
+ ${DOCKER_COMMAND} build --tag=${SANDBOX_NAME} \
45
+ --build-arg="UWSGI_PROCESSES=${UWSGI_PROCESSES}" \
46
+ --build-arg="UWSGI_CHEAPER=${UWSGI_CHEAPER}" \
47
+ -f Dockerfile.sandbox .
41
48
  else
42
49
  echo "Using existing Docker image: ${SANDBOX_NAME}"
43
50
  fi
44
51
 
45
52
  # Mount the output_data directory directly so files created in container appear in the local directory
46
- ${DOCKER_COMMAND} run --rm --name=local-sandbox \
53
+ ${DOCKER_COMMAND} run --rm -ti --name=local-sandbox \
47
54
  --network=host \
48
55
  -v "${OUTPUT_DATA_PATH}:/workspace" \
49
- -w /workspace \
50
56
  ${SANDBOX_NAME}
nat/tool/server_tools.py CHANGED
@@ -32,14 +32,23 @@ class RequestAttributesTool(FunctionBaseConfig, name="current_request_attributes
32
32
  @register_function(config_type=RequestAttributesTool)
33
33
  async def current_request_attributes(config: RequestAttributesTool, builder: Builder):
34
34
 
35
+ from pydantic import RootModel
36
+ from pydantic.types import JsonValue
35
37
  from starlette.datastructures import Headers
36
38
  from starlette.datastructures import QueryParams
37
39
 
38
- async def _get_request_attributes(unused: str) -> str:
40
+ class RequestBody(RootModel[JsonValue]):
41
+ """
42
+ Data model that accepts a request body of any valid JSON type.
43
+ """
44
+ root: JsonValue
45
+
46
+ async def _get_request_attributes(request_body: RequestBody) -> str:
39
47
 
40
48
  from nat.builder.context import Context
41
49
  nat_context = Context.get()
42
50
 
51
+ # Access request attributes from context
43
52
  method: str | None = nat_context.metadata.method
44
53
  url_path: str | None = nat_context.metadata.url_path
45
54
  url_scheme: str | None = nat_context.metadata.url_scheme
@@ -51,6 +60,9 @@ async def current_request_attributes(config: RequestAttributesTool, builder: Bui
51
60
  cookies: dict[str, str] | None = nat_context.metadata.cookies
52
61
  conversation_id: str | None = nat_context.conversation_id
53
62
 
63
+ # Access the request body data - can be any valid JSON type
64
+ request_body_data: JsonValue = request_body.root
65
+
54
66
  return (f"Method: {method}, "
55
67
  f"URL Path: {url_path}, "
56
68
  f"URL Scheme: {url_scheme}, "
@@ -60,7 +72,8 @@ async def current_request_attributes(config: RequestAttributesTool, builder: Bui
60
72
  f"Client Host: {client_host}, "
61
73
  f"Client Port: {client_port}, "
62
74
  f"Cookies: {cookies}, "
63
- f"Conversation Id: {conversation_id}")
75
+ f"Conversation Id: {conversation_id}, "
76
+ f"Request Body: {request_body_data}")
64
77
 
65
78
  yield FunctionInfo.from_fn(_get_request_attributes,
66
79
  description="Returns the acquired user defined request attributes.")