ostruct-cli 0.8.2__py3-none-any.whl → 0.8.3__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.
- ostruct/cli/click_options.py +111 -8
- ostruct/cli/code_interpreter.py +210 -17
- ostruct/cli/commands/run.py +56 -0
- ostruct/cli/config.py +20 -1
- ostruct/cli/errors.py +2 -30
- ostruct/cli/file_info.py +55 -20
- ostruct/cli/file_utils.py +19 -3
- ostruct/cli/json_extract.py +75 -0
- ostruct/cli/model_creation.py +1 -1
- ostruct/cli/runner.py +461 -180
- ostruct/cli/sentinel.py +29 -0
- ostruct/cli/template_optimizer.py +11 -7
- ostruct/cli/template_processor.py +243 -115
- ostruct/cli/template_rendering.py +41 -1
- ostruct/cli/template_validation.py +41 -3
- ostruct/cli/types.py +14 -1
- {ostruct_cli-0.8.2.dist-info → ostruct_cli-0.8.3.dist-info}/METADATA +88 -2
- {ostruct_cli-0.8.2.dist-info → ostruct_cli-0.8.3.dist-info}/RECORD +21 -19
- {ostruct_cli-0.8.2.dist-info → ostruct_cli-0.8.3.dist-info}/LICENSE +0 -0
- {ostruct_cli-0.8.2.dist-info → ostruct_cli-0.8.3.dist-info}/WHEEL +0 -0
- {ostruct_cli-0.8.2.dist-info → ostruct_cli-0.8.3.dist-info}/entry_points.txt +0 -0
ostruct/cli/sentinel.py
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
"""Sentinel JSON extraction utility for two-pass Code Interpreter workaround."""
|
2
|
+
|
3
|
+
import json
|
4
|
+
import re
|
5
|
+
from typing import Any, Dict, Optional
|
6
|
+
|
7
|
+
_SENT_RE = re.compile(r"===BEGIN_JSON===\s*(\{.*?})\s*===END_JSON===", re.S)
|
8
|
+
|
9
|
+
|
10
|
+
def extract_json_block(text: str) -> Optional[Dict[str, Any]]:
|
11
|
+
"""Extract JSON block from sentinel markers in text.
|
12
|
+
|
13
|
+
Args:
|
14
|
+
text: Response text that may contain sentinel-wrapped JSON
|
15
|
+
|
16
|
+
Returns:
|
17
|
+
Parsed JSON dict if found, None otherwise
|
18
|
+
"""
|
19
|
+
m = _SENT_RE.search(text)
|
20
|
+
if not m:
|
21
|
+
return None
|
22
|
+
try:
|
23
|
+
result = json.loads(m.group(1))
|
24
|
+
# Ensure we return a dict, not other JSON types
|
25
|
+
if isinstance(result, dict):
|
26
|
+
return result
|
27
|
+
return None
|
28
|
+
except json.JSONDecodeError:
|
29
|
+
return None
|
@@ -251,19 +251,15 @@ class TemplateOptimizer:
|
|
251
251
|
if match:
|
252
252
|
return match.group(1)
|
253
253
|
|
254
|
-
# Pattern 2: variable.content
|
254
|
+
# Pattern 2: variable.content - handle ALL single-file variables
|
255
255
|
match = re.search(r"(\w+)\.content", content_expr)
|
256
256
|
if match:
|
257
257
|
var_name = match.group(1)
|
258
258
|
# Skip if this is a loop variable
|
259
259
|
if var_name in loop_variables:
|
260
260
|
return None
|
261
|
-
#
|
262
|
-
|
263
|
-
keyword in var_name.lower()
|
264
|
-
for keyword in ["file", "config", "data", "script", "code"]
|
265
|
-
):
|
266
|
-
return f"${var_name}" # Use variable name as placeholder
|
261
|
+
# Return placeholder for ANY .content access to prevent directory misclassification
|
262
|
+
return f"${var_name}"
|
267
263
|
|
268
264
|
# Pattern 3: files['filename'].content
|
269
265
|
match = re.search(
|
@@ -374,6 +370,14 @@ class TemplateOptimizer:
|
|
374
370
|
if dir_var in loop_variables:
|
375
371
|
return full_match
|
376
372
|
|
373
|
+
# Skip if this is a single-file variable accessed via .content
|
374
|
+
# These should be handled by the file pattern, not directory pattern
|
375
|
+
if content_expr.endswith(".content"):
|
376
|
+
# Check if this would be handled by _extract_file_path
|
377
|
+
file_path = self._extract_file_path(content_expr, loop_variables)
|
378
|
+
if file_path:
|
379
|
+
return full_match # Let file pattern handle it
|
380
|
+
|
377
381
|
reference = f"the files and subdirectories in <dir:{dir_var}>"
|
378
382
|
self.dir_references[dir_var] = reference
|
379
383
|
self.optimization_log.append(
|
@@ -23,6 +23,7 @@ from .errors import (
|
|
23
23
|
VariableNameError,
|
24
24
|
)
|
25
25
|
from .explicit_file_processor import ProcessingResult
|
26
|
+
from .file_info import FileRoutingIntent
|
26
27
|
from .file_utils import FileInfoList, collect_files
|
27
28
|
from .path_utils import validate_path_mapping
|
28
29
|
from .security import SecurityManager
|
@@ -263,6 +264,31 @@ def process_system_prompt(
|
|
263
264
|
)
|
264
265
|
base_prompt += web_search_instructions
|
265
266
|
|
267
|
+
# Add Code Interpreter download instructions if CI is enabled and auto_download is true
|
268
|
+
code_interpreter_enabled = template_context.get(
|
269
|
+
"code_interpreter_enabled", False
|
270
|
+
)
|
271
|
+
auto_download_enabled = template_context.get("auto_download_enabled", True)
|
272
|
+
|
273
|
+
if code_interpreter_enabled and auto_download_enabled:
|
274
|
+
ci_download_instructions = (
|
275
|
+
"\n\nWhen using Code Interpreter to create files, always include markdown download links "
|
276
|
+
"in your response using this exact format: [Download filename.ext](sandbox:/mnt/data/filename.ext). "
|
277
|
+
"This enables automatic file download for the user. Include the download link immediately "
|
278
|
+
"after creating each file."
|
279
|
+
)
|
280
|
+
|
281
|
+
# Add sentinel instructions for two-pass mode
|
282
|
+
ci_config = template_context.get("code_interpreter_config", {})
|
283
|
+
if ci_config.get("download_strategy") == "two_pass_sentinel":
|
284
|
+
ci_download_instructions += (
|
285
|
+
"\n\nAfter saving your files and printing the download links, output "
|
286
|
+
"your JSON response between exactly these markers:\n"
|
287
|
+
"===BEGIN_JSON===\n{ ... }\n===END_JSON===\n"
|
288
|
+
)
|
289
|
+
|
290
|
+
base_prompt += ci_download_instructions
|
291
|
+
|
266
292
|
return base_prompt
|
267
293
|
|
268
294
|
|
@@ -779,25 +805,44 @@ async def create_template_context_from_routing(
|
|
779
805
|
"file-search", []
|
780
806
|
)
|
781
807
|
|
782
|
-
#
|
783
|
-
#
|
784
|
-
|
785
|
-
|
808
|
+
# Intent mapping is implemented directly in the file categorization logic below
|
809
|
+
# This ensures files from different CLI flags get the correct routing intent
|
810
|
+
|
811
|
+
# Process files by intent groups instead of lumping them all together
|
812
|
+
files_dict = {}
|
786
813
|
|
787
|
-
#
|
814
|
+
# Group files by their intent
|
815
|
+
files_by_intent: Dict[FileRoutingIntent, List[Tuple[str, str]]] = {
|
816
|
+
FileRoutingIntent.TEMPLATE_ONLY: [],
|
817
|
+
FileRoutingIntent.CODE_INTERPRETER: [],
|
818
|
+
FileRoutingIntent.FILE_SEARCH: [],
|
819
|
+
}
|
820
|
+
|
821
|
+
dirs_by_intent: Dict[FileRoutingIntent, List[Tuple[str, str]]] = {
|
822
|
+
FileRoutingIntent.TEMPLATE_ONLY: [],
|
823
|
+
FileRoutingIntent.CODE_INTERPRETER: [],
|
824
|
+
FileRoutingIntent.FILE_SEARCH: [],
|
825
|
+
}
|
826
|
+
|
827
|
+
# Track processed files to avoid duplicates between CLI args and routing result
|
828
|
+
seen_files: Set[str] = set()
|
829
|
+
|
830
|
+
# Categorize files by their source and assign appropriate intent
|
831
|
+
# Template files from CLI args
|
788
832
|
template_file_paths = args.get("template_files", [])
|
789
833
|
for template_file_path in template_file_paths:
|
790
834
|
if isinstance(template_file_path, (str, Path)):
|
791
|
-
# Auto-generate name for single-arg form: -ft config.yaml
|
792
835
|
file_name = _generate_template_variable_name(
|
793
836
|
str(template_file_path)
|
794
837
|
)
|
795
838
|
file_path_str = str(template_file_path)
|
796
839
|
if file_path_str not in seen_files:
|
797
|
-
|
840
|
+
files_by_intent[FileRoutingIntent.TEMPLATE_ONLY].append(
|
841
|
+
(file_name, file_path_str)
|
842
|
+
)
|
798
843
|
seen_files.add(file_path_str)
|
799
844
|
|
800
|
-
#
|
845
|
+
# Template file aliases from CLI args
|
801
846
|
template_file_aliases = args.get("template_file_aliases", [])
|
802
847
|
for name_path_tuple in template_file_aliases:
|
803
848
|
if (
|
@@ -805,51 +850,26 @@ async def create_template_context_from_routing(
|
|
805
850
|
and len(name_path_tuple) == 2
|
806
851
|
):
|
807
852
|
custom_name, file_path_raw = name_path_tuple
|
808
|
-
|
809
|
-
file_name = str(
|
810
|
-
custom_name
|
811
|
-
) # Always use custom name for aliases
|
812
|
-
|
813
|
-
if file_path not in seen_files:
|
814
|
-
files_tuples.append((file_name, file_path))
|
815
|
-
seen_files.add(file_path)
|
816
|
-
|
817
|
-
# Also process template_files from routing result (for compatibility)
|
818
|
-
for template_file_item in template_files:
|
819
|
-
if isinstance(template_file_item, (str, Path)):
|
820
|
-
file_name = _generate_template_variable_name(
|
821
|
-
str(template_file_item)
|
822
|
-
)
|
823
|
-
file_path_str = str(template_file_item)
|
853
|
+
file_path_str = str(file_path_raw)
|
824
854
|
if file_path_str not in seen_files:
|
825
|
-
|
855
|
+
files_by_intent[FileRoutingIntent.TEMPLATE_ONLY].append(
|
856
|
+
(str(custom_name), file_path_str)
|
857
|
+
)
|
826
858
|
seen_files.add(file_path_str)
|
827
|
-
elif (
|
828
|
-
isinstance(template_file_item, tuple)
|
829
|
-
and len(template_file_item) == 2
|
830
|
-
):
|
831
|
-
# Handle tuple format (name, path)
|
832
|
-
_, template_file_path = template_file_item
|
833
|
-
template_file_path_str = str(template_file_path)
|
834
|
-
file_name = _generate_template_variable_name(
|
835
|
-
template_file_path_str
|
836
|
-
)
|
837
|
-
if template_file_path_str not in seen_files:
|
838
|
-
files_tuples.append((file_name, template_file_path_str))
|
839
|
-
seen_files.add(template_file_path_str)
|
840
859
|
|
841
|
-
#
|
860
|
+
# Code interpreter files from CLI args
|
842
861
|
code_interpreter_file_paths = args.get("code_interpreter_files", [])
|
843
862
|
for ci_file_path in code_interpreter_file_paths:
|
844
863
|
if isinstance(ci_file_path, (str, Path)):
|
845
|
-
# Auto-generate name: -fc data.csv
|
846
864
|
file_name = _generate_template_variable_name(str(ci_file_path))
|
847
865
|
file_path_str = str(ci_file_path)
|
848
866
|
if file_path_str not in seen_files:
|
849
|
-
|
867
|
+
files_by_intent[FileRoutingIntent.CODE_INTERPRETER].append(
|
868
|
+
(file_name, file_path_str)
|
869
|
+
)
|
850
870
|
seen_files.add(file_path_str)
|
851
871
|
|
852
|
-
#
|
872
|
+
# Code interpreter file aliases from CLI args
|
853
873
|
code_interpreter_file_aliases = args.get(
|
854
874
|
"code_interpreter_file_aliases", []
|
855
875
|
)
|
@@ -859,44 +879,26 @@ async def create_template_context_from_routing(
|
|
859
879
|
and len(name_path_tuple) == 2
|
860
880
|
):
|
861
881
|
custom_name, file_path_raw = name_path_tuple
|
862
|
-
|
863
|
-
file_name = str(
|
864
|
-
custom_name
|
865
|
-
) # Always use custom name for aliases
|
866
|
-
|
867
|
-
if file_path not in seen_files:
|
868
|
-
files_tuples.append((file_name, file_path))
|
869
|
-
seen_files.add(file_path)
|
870
|
-
|
871
|
-
# Also process code_interpreter_files from routing result (for compatibility)
|
872
|
-
for ci_file_item in code_interpreter_files:
|
873
|
-
if isinstance(ci_file_item, (str, Path)):
|
874
|
-
file_name = _generate_template_variable_name(str(ci_file_item))
|
875
|
-
file_path_str = str(ci_file_item)
|
882
|
+
file_path_str = str(file_path_raw)
|
876
883
|
if file_path_str not in seen_files:
|
877
|
-
|
884
|
+
files_by_intent[FileRoutingIntent.CODE_INTERPRETER].append(
|
885
|
+
(str(custom_name), file_path_str)
|
886
|
+
)
|
878
887
|
seen_files.add(file_path_str)
|
879
|
-
elif isinstance(ci_file_item, tuple) and len(ci_file_item) == 2:
|
880
|
-
# Handle tuple format (name, path)
|
881
|
-
_, ci_file_path = ci_file_item
|
882
|
-
ci_file_path_str = str(ci_file_path)
|
883
|
-
file_name = _generate_template_variable_name(ci_file_path_str)
|
884
|
-
if ci_file_path_str not in seen_files:
|
885
|
-
files_tuples.append((file_name, ci_file_path_str))
|
886
|
-
seen_files.add(ci_file_path_str)
|
887
888
|
|
888
|
-
#
|
889
|
+
# File search files from CLI args
|
889
890
|
file_search_file_paths = args.get("file_search_files", [])
|
890
891
|
for fs_file_path in file_search_file_paths:
|
891
892
|
if isinstance(fs_file_path, (str, Path)):
|
892
|
-
# Auto-generate name: -fs docs.pdf
|
893
893
|
file_name = _generate_template_variable_name(str(fs_file_path))
|
894
894
|
file_path_str = str(fs_file_path)
|
895
895
|
if file_path_str not in seen_files:
|
896
|
-
|
896
|
+
files_by_intent[FileRoutingIntent.FILE_SEARCH].append(
|
897
|
+
(file_name, file_path_str)
|
898
|
+
)
|
897
899
|
seen_files.add(file_path_str)
|
898
900
|
|
899
|
-
#
|
901
|
+
# File search file aliases from CLI args
|
900
902
|
file_search_file_aliases = args.get("file_search_file_aliases", [])
|
901
903
|
for name_path_tuple in file_search_file_aliases:
|
902
904
|
if (
|
@@ -904,79 +906,131 @@ async def create_template_context_from_routing(
|
|
904
906
|
and len(name_path_tuple) == 2
|
905
907
|
):
|
906
908
|
custom_name, file_path_raw = name_path_tuple
|
907
|
-
|
908
|
-
|
909
|
-
|
910
|
-
|
909
|
+
file_path_str = str(file_path_raw)
|
910
|
+
if file_path_str not in seen_files:
|
911
|
+
files_by_intent[FileRoutingIntent.FILE_SEARCH].append(
|
912
|
+
(str(custom_name), file_path_str)
|
913
|
+
)
|
914
|
+
seen_files.add(file_path_str)
|
915
|
+
|
916
|
+
# Process files from routing result and map to their proper intents
|
917
|
+
# Only add files that haven't been processed from CLI args
|
918
|
+
for template_file_item in template_files:
|
919
|
+
if isinstance(template_file_item, (str, Path)):
|
920
|
+
file_name = _generate_template_variable_name(
|
921
|
+
str(template_file_item)
|
922
|
+
)
|
923
|
+
file_path_str = str(template_file_item)
|
924
|
+
if file_path_str not in seen_files:
|
925
|
+
files_by_intent[FileRoutingIntent.TEMPLATE_ONLY].append(
|
926
|
+
(file_name, file_path_str)
|
927
|
+
)
|
928
|
+
seen_files.add(file_path_str)
|
929
|
+
elif (
|
930
|
+
isinstance(template_file_item, tuple)
|
931
|
+
and len(template_file_item) == 2
|
932
|
+
):
|
933
|
+
_, template_file_path = template_file_item
|
934
|
+
file_path_str = str(template_file_path)
|
935
|
+
file_name = _generate_template_variable_name(file_path_str)
|
936
|
+
if file_path_str not in seen_files:
|
937
|
+
files_by_intent[FileRoutingIntent.TEMPLATE_ONLY].append(
|
938
|
+
(file_name, file_path_str)
|
939
|
+
)
|
940
|
+
seen_files.add(file_path_str)
|
911
941
|
|
912
|
-
|
913
|
-
|
914
|
-
|
942
|
+
for ci_file_item in code_interpreter_files:
|
943
|
+
if isinstance(ci_file_item, (str, Path)):
|
944
|
+
file_name = _generate_template_variable_name(str(ci_file_item))
|
945
|
+
file_path_str = str(ci_file_item)
|
946
|
+
if file_path_str not in seen_files:
|
947
|
+
files_by_intent[FileRoutingIntent.CODE_INTERPRETER].append(
|
948
|
+
(file_name, file_path_str)
|
949
|
+
)
|
950
|
+
seen_files.add(file_path_str)
|
951
|
+
elif isinstance(ci_file_item, tuple) and len(ci_file_item) == 2:
|
952
|
+
_, ci_file_path = ci_file_item
|
953
|
+
file_path_str = str(ci_file_path)
|
954
|
+
file_name = _generate_template_variable_name(file_path_str)
|
955
|
+
if file_path_str not in seen_files:
|
956
|
+
files_by_intent[FileRoutingIntent.CODE_INTERPRETER].append(
|
957
|
+
(file_name, file_path_str)
|
958
|
+
)
|
959
|
+
seen_files.add(file_path_str)
|
915
960
|
|
916
|
-
# Also process file_search_files from routing result (for compatibility)
|
917
961
|
for fs_file_item in file_search_files:
|
918
962
|
if isinstance(fs_file_item, (str, Path)):
|
919
963
|
file_name = _generate_template_variable_name(str(fs_file_item))
|
920
964
|
file_path_str = str(fs_file_item)
|
921
965
|
if file_path_str not in seen_files:
|
922
|
-
|
966
|
+
files_by_intent[FileRoutingIntent.FILE_SEARCH].append(
|
967
|
+
(file_name, file_path_str)
|
968
|
+
)
|
923
969
|
seen_files.add(file_path_str)
|
924
970
|
elif isinstance(fs_file_item, tuple) and len(fs_file_item) == 2:
|
925
|
-
# Handle tuple format (name, path)
|
926
971
|
_, fs_file_path = fs_file_item
|
927
|
-
|
928
|
-
file_name = _generate_template_variable_name(
|
929
|
-
if
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
dir_mappings = []
|
972
|
+
file_path_str = str(fs_file_path)
|
973
|
+
file_name = _generate_template_variable_name(file_path_str)
|
974
|
+
if file_path_str not in seen_files:
|
975
|
+
files_by_intent[FileRoutingIntent.FILE_SEARCH].append(
|
976
|
+
(file_name, file_path_str)
|
977
|
+
)
|
978
|
+
seen_files.add(file_path_str)
|
935
979
|
|
936
|
-
#
|
980
|
+
# Categorize directories by their intent
|
937
981
|
routing = routing_result.routing
|
938
982
|
for alias_name, dir_path in routing.template_dir_aliases:
|
939
|
-
|
983
|
+
dirs_by_intent[FileRoutingIntent.TEMPLATE_ONLY].append(
|
984
|
+
(alias_name, str(dir_path))
|
985
|
+
)
|
940
986
|
for alias_name, dir_path in routing.code_interpreter_dir_aliases:
|
941
|
-
|
987
|
+
dirs_by_intent[FileRoutingIntent.CODE_INTERPRETER].append(
|
988
|
+
(alias_name, str(dir_path))
|
989
|
+
)
|
942
990
|
for alias_name, dir_path in routing.file_search_dir_aliases:
|
943
|
-
|
991
|
+
dirs_by_intent[FileRoutingIntent.FILE_SEARCH].append(
|
992
|
+
(alias_name, str(dir_path))
|
993
|
+
)
|
944
994
|
|
945
|
-
# Auto-naming directories
|
995
|
+
# Auto-naming directories from CLI args
|
946
996
|
template_dirs = args.get("template_dirs", [])
|
947
997
|
for dir_path in template_dirs:
|
948
998
|
dir_name = _generate_template_variable_name(str(dir_path))
|
949
|
-
|
999
|
+
dirs_by_intent[FileRoutingIntent.TEMPLATE_ONLY].append(
|
1000
|
+
(dir_name, str(dir_path))
|
1001
|
+
)
|
950
1002
|
|
951
1003
|
code_interpreter_dirs = args.get("code_interpreter_dirs", [])
|
952
1004
|
for dir_path in code_interpreter_dirs:
|
953
1005
|
dir_name = _generate_template_variable_name(str(dir_path))
|
954
|
-
|
1006
|
+
dirs_by_intent[FileRoutingIntent.CODE_INTERPRETER].append(
|
1007
|
+
(dir_name, str(dir_path))
|
1008
|
+
)
|
955
1009
|
|
956
1010
|
file_search_dirs = args.get("file_search_dirs", [])
|
957
1011
|
for dir_path in file_search_dirs:
|
958
1012
|
dir_name = _generate_template_variable_name(str(dir_path))
|
959
|
-
|
1013
|
+
dirs_by_intent[FileRoutingIntent.FILE_SEARCH].append(
|
1014
|
+
(dir_name, str(dir_path))
|
1015
|
+
)
|
960
1016
|
|
961
|
-
# Process files
|
962
|
-
|
963
|
-
file_mappings
|
964
|
-
|
965
|
-
|
966
|
-
|
967
|
-
|
968
|
-
|
969
|
-
|
970
|
-
|
971
|
-
|
972
|
-
|
973
|
-
|
974
|
-
|
975
|
-
|
976
|
-
|
977
|
-
|
978
|
-
# which FileInfo.content interprets as potentially template-routed.
|
979
|
-
)
|
1017
|
+
# Process files by intent groups with appropriate routing_intent
|
1018
|
+
for intent, file_mappings in files_by_intent.items():
|
1019
|
+
if file_mappings or dirs_by_intent[intent]:
|
1020
|
+
intent_files_dict = collect_files(
|
1021
|
+
file_mappings=cast(
|
1022
|
+
List[Tuple[str, Union[str, Path]]], file_mappings
|
1023
|
+
),
|
1024
|
+
dir_mappings=cast(
|
1025
|
+
List[Tuple[str, Union[str, Path]]],
|
1026
|
+
dirs_by_intent[intent],
|
1027
|
+
),
|
1028
|
+
dir_recursive=args.get("recursive", False),
|
1029
|
+
security_manager=security_manager,
|
1030
|
+
routing_type="template", # Keep routing_type for template context accessibility
|
1031
|
+
routing_intent=intent, # Use intent for warning logic
|
1032
|
+
)
|
1033
|
+
files_dict.update(intent_files_dict)
|
980
1034
|
|
981
1035
|
# Handle legacy files and directories separately to preserve variable names
|
982
1036
|
legacy_files = args.get("files", [])
|
@@ -997,6 +1051,7 @@ async def create_template_context_from_routing(
|
|
997
1051
|
dir_recursive=args.get("recursive", False),
|
998
1052
|
security_manager=security_manager,
|
999
1053
|
routing_type="template", # Legacy flags are considered template-only
|
1054
|
+
routing_intent=FileRoutingIntent.TEMPLATE_ONLY, # Legacy files use template-only intent
|
1000
1055
|
)
|
1001
1056
|
# Merge legacy results into the main template context
|
1002
1057
|
files_dict.update(legacy_files_dict)
|
@@ -1042,8 +1097,18 @@ async def create_template_context_from_routing(
|
|
1042
1097
|
config = OstructConfig.load(config_path)
|
1043
1098
|
web_search_config = config.get_web_search_config()
|
1044
1099
|
|
1100
|
+
# Apply universal tool toggle overrides (Step 3: Config override hook)
|
1101
|
+
enabled_tools: set[str] = args.get("_enabled_tools", set()) # type: ignore[assignment]
|
1102
|
+
disabled_tools: set[str] = args.get("_disabled_tools", set()) # type: ignore[assignment]
|
1103
|
+
|
1045
1104
|
# Determine if web search should be enabled
|
1046
|
-
if
|
1105
|
+
if "web-search" in enabled_tools:
|
1106
|
+
# Universal --enable-tool web-search takes highest precedence
|
1107
|
+
web_search_enabled = True
|
1108
|
+
elif "web-search" in disabled_tools:
|
1109
|
+
# Universal --disable-tool web-search takes highest precedence
|
1110
|
+
web_search_enabled = False
|
1111
|
+
elif web_search_from_cli:
|
1047
1112
|
# Explicit --web-search flag takes precedence
|
1048
1113
|
web_search_enabled = True
|
1049
1114
|
elif no_web_search_from_cli:
|
@@ -1055,6 +1120,69 @@ async def create_template_context_from_routing(
|
|
1055
1120
|
|
1056
1121
|
context["web_search_enabled"] = web_search_enabled
|
1057
1122
|
|
1123
|
+
# Add Code Interpreter context variables
|
1124
|
+
# Check if Code Interpreter is enabled by looking for CI files or tools
|
1125
|
+
ci_enabled_by_routing = bool(
|
1126
|
+
args.get("code_interpreter_files")
|
1127
|
+
or args.get("code_interpreter_file_aliases")
|
1128
|
+
or args.get("code_interpreter_dirs")
|
1129
|
+
or args.get("code_interpreter_dir_aliases")
|
1130
|
+
or args.get("code_interpreter", False)
|
1131
|
+
)
|
1132
|
+
|
1133
|
+
# Apply universal tool toggle overrides for code-interpreter
|
1134
|
+
if "code-interpreter" in enabled_tools:
|
1135
|
+
# Universal --enable-tool takes highest precedence
|
1136
|
+
code_interpreter_enabled = True
|
1137
|
+
elif "code-interpreter" in disabled_tools:
|
1138
|
+
# Universal --disable-tool takes highest precedence
|
1139
|
+
code_interpreter_enabled = False
|
1140
|
+
else:
|
1141
|
+
# Fall back to routing-based enablement
|
1142
|
+
code_interpreter_enabled = ci_enabled_by_routing
|
1143
|
+
context["code_interpreter_enabled"] = code_interpreter_enabled
|
1144
|
+
|
1145
|
+
# Add auto_download setting from configuration
|
1146
|
+
if code_interpreter_enabled:
|
1147
|
+
ci_config = config.get_code_interpreter_config()
|
1148
|
+
context["auto_download_enabled"] = ci_config.get(
|
1149
|
+
"auto_download", True
|
1150
|
+
)
|
1151
|
+
|
1152
|
+
# Determine effective download strategy (config + feature flags)
|
1153
|
+
effective_ci_config = dict(
|
1154
|
+
ci_config
|
1155
|
+
) # Copy to avoid modifying original
|
1156
|
+
enabled_features = args.get("enabled_features", [])
|
1157
|
+
disabled_features = args.get("disabled_features", [])
|
1158
|
+
|
1159
|
+
if enabled_features or disabled_features:
|
1160
|
+
try:
|
1161
|
+
from .click_options import parse_feature_flags
|
1162
|
+
|
1163
|
+
parsed_flags = parse_feature_flags(
|
1164
|
+
tuple(enabled_features), tuple(disabled_features)
|
1165
|
+
)
|
1166
|
+
ci_hack_flag = parsed_flags.get("ci-download-hack")
|
1167
|
+
if ci_hack_flag == "on":
|
1168
|
+
effective_ci_config["download_strategy"] = (
|
1169
|
+
"two_pass_sentinel"
|
1170
|
+
)
|
1171
|
+
elif ci_hack_flag == "off":
|
1172
|
+
effective_ci_config["download_strategy"] = (
|
1173
|
+
"single_pass"
|
1174
|
+
)
|
1175
|
+
except Exception as e:
|
1176
|
+
logger.warning(
|
1177
|
+
f"Failed to parse feature flags in template processor: {e}"
|
1178
|
+
)
|
1179
|
+
|
1180
|
+
# Add the effective CI config for template processing
|
1181
|
+
context["code_interpreter_config"] = effective_ci_config
|
1182
|
+
else:
|
1183
|
+
context["auto_download_enabled"] = False
|
1184
|
+
context["code_interpreter_config"] = {}
|
1185
|
+
|
1058
1186
|
return context
|
1059
1187
|
|
1060
1188
|
except PathSecurityError:
|
@@ -56,6 +56,7 @@ Notes:
|
|
56
56
|
|
57
57
|
import logging
|
58
58
|
import os
|
59
|
+
import re
|
59
60
|
from typing import Any, Dict, List, Optional, Union
|
60
61
|
|
61
62
|
import jinja2
|
@@ -95,6 +96,45 @@ TemplateContextValue = Union[
|
|
95
96
|
]
|
96
97
|
|
97
98
|
|
99
|
+
def _extract_variable_name_from_jinja_error(error_message: str) -> str:
|
100
|
+
"""Extract the actual variable name from a Jinja2 UndefinedError message.
|
101
|
+
|
102
|
+
Handles various Jinja2 error message formats:
|
103
|
+
- "'variable_name' is undefined"
|
104
|
+
- "'object_description' has no attribute 'property_name'"
|
105
|
+
- Other formats
|
106
|
+
|
107
|
+
Args:
|
108
|
+
error_message: The string representation of the Jinja2 UndefinedError
|
109
|
+
|
110
|
+
Returns:
|
111
|
+
The extracted variable name, or a sanitized version if parsing fails
|
112
|
+
"""
|
113
|
+
# Pattern 1: Standard undefined variable: "'variable_name' is undefined"
|
114
|
+
match = re.match(r"'([^']+)' is undefined", error_message)
|
115
|
+
if match:
|
116
|
+
return match.group(1)
|
117
|
+
|
118
|
+
# Pattern 2: Attribute access on object: "'object' has no attribute 'property'"
|
119
|
+
# In this case, we want to extract just the property name, not the object description
|
120
|
+
match = re.match(r"'[^']+' has no attribute '([^']+)'", error_message)
|
121
|
+
if match:
|
122
|
+
property_name = match.group(1)
|
123
|
+
return property_name
|
124
|
+
|
125
|
+
# Pattern 3: Try to find any quoted identifier that looks like a variable name
|
126
|
+
# Look for quoted strings that contain only valid Python identifier characters
|
127
|
+
quoted_parts: List[str] = re.findall(r"'([^']+)'", error_message)
|
128
|
+
for part in quoted_parts:
|
129
|
+
# Check if this looks like a variable name (not a class name or description)
|
130
|
+
if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", part) and "." not in part:
|
131
|
+
return part
|
132
|
+
|
133
|
+
# Fallback: If we can't parse it properly, return a generic message
|
134
|
+
# This avoids exposing internal class names to users
|
135
|
+
return "unknown_variable"
|
136
|
+
|
137
|
+
|
98
138
|
def render_template(
|
99
139
|
template_str: str,
|
100
140
|
context: Dict[str, Any],
|
@@ -343,7 +383,7 @@ def render_template(
|
|
343
383
|
return result
|
344
384
|
except jinja2.UndefinedError as e:
|
345
385
|
# Extract variable name from error message
|
346
|
-
var_name = str(e)
|
386
|
+
var_name = _extract_variable_name_from_jinja_error(str(e))
|
347
387
|
error_msg = (
|
348
388
|
f"Missing required template variable: {var_name}\n"
|
349
389
|
f"Available variables: {', '.join(sorted(context.keys()))}\n"
|