ostruct-cli 0.8.8__py3-none-any.whl → 1.0.0__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/__init__.py +3 -15
- ostruct/cli/attachment_processor.py +455 -0
- ostruct/cli/attachment_template_bridge.py +973 -0
- ostruct/cli/cli.py +187 -33
- ostruct/cli/click_options.py +775 -692
- ostruct/cli/code_interpreter.py +195 -12
- ostruct/cli/commands/__init__.py +0 -3
- ostruct/cli/commands/run.py +289 -62
- ostruct/cli/config.py +23 -22
- ostruct/cli/constants.py +89 -0
- ostruct/cli/errors.py +191 -6
- ostruct/cli/explicit_file_processor.py +0 -15
- ostruct/cli/file_info.py +118 -14
- ostruct/cli/file_list.py +82 -1
- ostruct/cli/file_search.py +68 -2
- ostruct/cli/help_json.py +235 -0
- ostruct/cli/mcp_integration.py +13 -16
- ostruct/cli/params.py +217 -0
- ostruct/cli/plan_assembly.py +335 -0
- ostruct/cli/plan_printing.py +385 -0
- ostruct/cli/progress_reporting.py +8 -56
- ostruct/cli/quick_ref_help.py +128 -0
- ostruct/cli/rich_config.py +299 -0
- ostruct/cli/runner.py +397 -190
- ostruct/cli/security/__init__.py +2 -0
- ostruct/cli/security/allowed_checker.py +41 -0
- ostruct/cli/security/normalization.py +13 -9
- ostruct/cli/security/security_manager.py +558 -17
- ostruct/cli/security/types.py +15 -0
- ostruct/cli/template_debug.py +283 -261
- ostruct/cli/template_debug_help.py +233 -142
- ostruct/cli/template_env.py +46 -5
- ostruct/cli/template_filters.py +415 -8
- ostruct/cli/template_processor.py +240 -619
- ostruct/cli/template_rendering.py +49 -73
- ostruct/cli/template_validation.py +2 -1
- ostruct/cli/token_validation.py +35 -15
- ostruct/cli/types.py +15 -19
- ostruct/cli/unicode_compat.py +283 -0
- ostruct/cli/upload_manager.py +448 -0
- ostruct/cli/utils.py +30 -0
- ostruct/cli/validators.py +272 -54
- {ostruct_cli-0.8.8.dist-info → ostruct_cli-1.0.0.dist-info}/METADATA +292 -126
- ostruct_cli-1.0.0.dist-info/RECORD +80 -0
- ostruct/cli/commands/quick_ref.py +0 -54
- ostruct/cli/template_optimizer.py +0 -478
- ostruct_cli-0.8.8.dist-info/RECORD +0 -71
- {ostruct_cli-0.8.8.dist-info → ostruct_cli-1.0.0.dist-info}/LICENSE +0 -0
- {ostruct_cli-0.8.8.dist-info → ostruct_cli-1.0.0.dist-info}/WHEEL +0 -0
- {ostruct_cli-0.8.8.dist-info → ostruct_cli-1.0.0.dist-info}/entry_points.txt +0 -0
ostruct/cli/runner.py
CHANGED
@@ -4,8 +4,10 @@ import copy
|
|
4
4
|
import json
|
5
5
|
import logging
|
6
6
|
import os
|
7
|
+
import re
|
7
8
|
from pathlib import Path
|
8
9
|
from typing import Any, Dict, List, Optional, Type, Union
|
10
|
+
from urllib.parse import urlparse
|
9
11
|
|
10
12
|
from openai import AsyncOpenAI, OpenAIError
|
11
13
|
from openai_model_registry import ModelRegistry
|
@@ -14,11 +16,7 @@ from pydantic import BaseModel
|
|
14
16
|
from .code_interpreter import CodeInterpreterManager
|
15
17
|
from .config import OstructConfig
|
16
18
|
from .cost_estimation import calculate_cost_estimate, format_cost_breakdown
|
17
|
-
from .errors import
|
18
|
-
APIErrorMapper,
|
19
|
-
CLIError,
|
20
|
-
SchemaValidationError,
|
21
|
-
)
|
19
|
+
from .errors import APIErrorMapper, CLIError, SchemaValidationError
|
22
20
|
from .exit_codes import ExitCode
|
23
21
|
from .explicit_file_processor import ProcessingResult
|
24
22
|
from .file_search import FileSearchManager
|
@@ -257,8 +255,15 @@ async def process_code_interpreter_configuration(
|
|
257
255
|
config = OstructConfig.load(config_path)
|
258
256
|
ci_config = config.get_code_interpreter_config()
|
259
257
|
|
258
|
+
# Apply CLI parameter overrides to config
|
259
|
+
effective_ci_config = dict(ci_config) # Make a copy
|
260
|
+
|
261
|
+
# Override duplicate_outputs if CLI flag is provided
|
262
|
+
if args.get("ci_duplicate_outputs") is not None:
|
263
|
+
effective_ci_config["duplicate_outputs"] = args["ci_duplicate_outputs"]
|
264
|
+
|
260
265
|
# Create Code Interpreter manager
|
261
|
-
manager = CodeInterpreterManager(client,
|
266
|
+
manager = CodeInterpreterManager(client, effective_ci_config)
|
262
267
|
|
263
268
|
# Validate files before upload
|
264
269
|
validation_errors = manager.validate_files_for_upload(files_to_upload)
|
@@ -372,11 +377,9 @@ async def process_file_search_configuration(
|
|
372
377
|
|
373
378
|
try:
|
374
379
|
# Get configuration parameters
|
375
|
-
vector_store_name = args.get(
|
376
|
-
|
377
|
-
)
|
378
|
-
retry_count = args.get("file_search_retry_count", 3)
|
379
|
-
timeout = args.get("file_search_timeout", 60.0)
|
380
|
+
vector_store_name = args.get("fs_store_name", "ostruct_search")
|
381
|
+
retry_count = args.get("fs_retries", 3)
|
382
|
+
timeout = args.get("fs_timeout", 60.0)
|
380
383
|
|
381
384
|
# Create vector store with retry logic
|
382
385
|
logger.debug(
|
@@ -560,8 +563,44 @@ async def create_structured_output(
|
|
560
563
|
data, markdown_text = split_json_and_text(content)
|
561
564
|
except ValueError:
|
562
565
|
# Fallback to original parsing for non-fenced JSON
|
563
|
-
|
564
|
-
|
566
|
+
try:
|
567
|
+
data = json.loads(content.strip())
|
568
|
+
markdown_text = ""
|
569
|
+
except json.JSONDecodeError as json_error:
|
570
|
+
# DEFENSIVE PARSING: Handle Code Interpreter + Structured Outputs compatibility issue
|
571
|
+
# OpenAI's Code Interpreter can append commentary after valid JSON when using strict schemas,
|
572
|
+
# causing json.loads() to fail. This is a known intermittent issue documented in OpenAI forums.
|
573
|
+
# We extract the JSON portion and warn the user that the workaround was applied.
|
574
|
+
if tools and any(
|
575
|
+
tool.get("type") == "code_interpreter"
|
576
|
+
for tool in tools
|
577
|
+
):
|
578
|
+
logger.debug(
|
579
|
+
"Code Interpreter detected with JSON parsing failure, attempting defensive parsing"
|
580
|
+
)
|
581
|
+
|
582
|
+
# Try to extract JSON from the beginning of the response
|
583
|
+
json_match = re.search(
|
584
|
+
r"\{.*?\}", content.strip(), re.DOTALL
|
585
|
+
)
|
586
|
+
if json_match:
|
587
|
+
try:
|
588
|
+
data = json.loads(json_match.group(0))
|
589
|
+
markdown_text = ""
|
590
|
+
logger.warning(
|
591
|
+
"Code Interpreter added extra content after JSON. "
|
592
|
+
"Extracted JSON successfully using defensive parsing. "
|
593
|
+
"This is a known intermittent issue with OpenAI's API."
|
594
|
+
)
|
595
|
+
except json.JSONDecodeError:
|
596
|
+
# Even defensive parsing failed, re-raise original error
|
597
|
+
raise json_error
|
598
|
+
else:
|
599
|
+
# No JSON pattern found, re-raise original error
|
600
|
+
raise json_error
|
601
|
+
else:
|
602
|
+
# Not using Code Interpreter, re-raise original error
|
603
|
+
raise json_error
|
565
604
|
|
566
605
|
validated = output_schema.model_validate(data)
|
567
606
|
|
@@ -587,6 +626,10 @@ async def create_structured_output(
|
|
587
626
|
logger.error(f"OpenAI API error mapped: {mapped_error}")
|
588
627
|
raise mapped_error
|
589
628
|
|
629
|
+
# Re-raise already-mapped CLIError types (like SchemaValidationError)
|
630
|
+
if isinstance(e, CLIError):
|
631
|
+
raise
|
632
|
+
|
590
633
|
# Handle special schema array error with detailed guidance
|
591
634
|
if "Invalid schema for response_format" in str(
|
592
635
|
e
|
@@ -709,10 +752,12 @@ async def _execute_two_pass_sentinel(
|
|
709
752
|
if code_interpreter_info and code_interpreter_info.get("manager"):
|
710
753
|
cm = code_interpreter_info["manager"]
|
711
754
|
# Use output directory from config, fallback to args, then default
|
755
|
+
from .constants import DefaultPaths
|
756
|
+
|
712
757
|
download_dir = (
|
713
758
|
ci_config.get("output_directory")
|
714
|
-
or args.get("
|
715
|
-
or
|
759
|
+
or args.get("ci_download_dir")
|
760
|
+
or DefaultPaths.CODE_INTERPRETER_OUTPUT_DIR
|
716
761
|
)
|
717
762
|
logger.debug(f"Downloading files to: {download_dir}")
|
718
763
|
downloaded_files = await cm.download_generated_files(
|
@@ -760,8 +805,34 @@ async def _execute_two_pass_sentinel(
|
|
760
805
|
data_final, markdown_text = split_json_and_text(content)
|
761
806
|
except ValueError:
|
762
807
|
# Fallback to original parsing for non-fenced JSON
|
763
|
-
|
764
|
-
|
808
|
+
try:
|
809
|
+
data_final = json.loads(content.strip())
|
810
|
+
markdown_text = ""
|
811
|
+
except json.JSONDecodeError as json_error:
|
812
|
+
# DEFENSIVE PARSING: Handle Code Interpreter + Structured Outputs compatibility issue
|
813
|
+
# In two-pass mode, the second pass should be clean, but apply defensive parsing
|
814
|
+
# as a safety net in case the issue still occurs.
|
815
|
+
logger.debug(
|
816
|
+
"JSON parsing failure in two-pass mode, attempting defensive parsing"
|
817
|
+
)
|
818
|
+
|
819
|
+
# Try to extract JSON from the beginning of the response
|
820
|
+
json_match = re.search(r"\{.*?\}", content.strip(), re.DOTALL)
|
821
|
+
if json_match:
|
822
|
+
try:
|
823
|
+
data_final = json.loads(json_match.group(0))
|
824
|
+
markdown_text = ""
|
825
|
+
logger.warning(
|
826
|
+
"Two-pass mode: Extra content found after JSON in structured response. "
|
827
|
+
"Extracted JSON successfully using defensive parsing. "
|
828
|
+
"This should not happen in pass 2 - please report this issue."
|
829
|
+
)
|
830
|
+
except json.JSONDecodeError:
|
831
|
+
# Even defensive parsing failed, re-raise original error
|
832
|
+
raise json_error
|
833
|
+
else:
|
834
|
+
# No JSON pattern found, re-raise original error
|
835
|
+
raise json_error
|
765
836
|
|
766
837
|
validated = output_model.model_validate(data_final)
|
767
838
|
|
@@ -917,6 +988,42 @@ async def execute_model(
|
|
917
988
|
enabled_tools: set[str] = args.get("_enabled_tools", set()) # type: ignore[assignment]
|
918
989
|
disabled_tools: set[str] = args.get("_disabled_tools", set()) # type: ignore[assignment]
|
919
990
|
|
991
|
+
# Initialize shared upload manager for multi-tool file sharing (T3.2)
|
992
|
+
shared_upload_manager = None
|
993
|
+
|
994
|
+
# Check if we have new attachment system with multi-tool attachments
|
995
|
+
from .attachment_processor import (
|
996
|
+
_extract_attachments_from_args,
|
997
|
+
_has_new_attachment_syntax,
|
998
|
+
)
|
999
|
+
|
1000
|
+
if _has_new_attachment_syntax(args):
|
1001
|
+
logger.debug(
|
1002
|
+
"Initializing shared upload manager for new attachment system"
|
1003
|
+
)
|
1004
|
+
from .attachment_processor import AttachmentProcessor
|
1005
|
+
from .upload_manager import SharedUploadManager
|
1006
|
+
from .validators import validate_security_manager
|
1007
|
+
|
1008
|
+
shared_upload_manager = SharedUploadManager(client)
|
1009
|
+
|
1010
|
+
# Get security manager for file validation
|
1011
|
+
security_manager = validate_security_manager(
|
1012
|
+
base_dir=args.get("base_dir"),
|
1013
|
+
allowed_dirs=args.get("allowed_dirs"),
|
1014
|
+
allowed_dir_file=args.get("allowed_dir_file"),
|
1015
|
+
)
|
1016
|
+
|
1017
|
+
# Process and register attachments
|
1018
|
+
processor = AttachmentProcessor(security_manager)
|
1019
|
+
attachments = _extract_attachments_from_args(args)
|
1020
|
+
processed_attachments = processor.process_attachments(attachments)
|
1021
|
+
|
1022
|
+
# Register all attachments with the shared manager
|
1023
|
+
shared_upload_manager.register_attachments(processed_attachments)
|
1024
|
+
|
1025
|
+
logger.debug("Registered attachments with shared upload manager")
|
1026
|
+
|
920
1027
|
# Process MCP configuration if provided
|
921
1028
|
# Apply universal tool toggle overrides for mcp
|
922
1029
|
mcp_enabled_by_config = services.is_configured("mcp")
|
@@ -973,51 +1080,86 @@ async def execute_model(
|
|
973
1080
|
# Fall back to routing-based enablement
|
974
1081
|
ci_should_enable = bool(ci_enabled_by_routing)
|
975
1082
|
|
976
|
-
if ci_should_enable
|
977
|
-
|
978
|
-
|
979
|
-
|
980
|
-
if
|
981
|
-
#
|
982
|
-
|
983
|
-
|
984
|
-
ci_args["code_interpreter_dirs"] = (
|
985
|
-
[]
|
986
|
-
) # Files already expanded from dirs
|
987
|
-
ci_args["code_interpreter"] = (
|
988
|
-
True # Enable for service container
|
1083
|
+
if ci_should_enable:
|
1084
|
+
code_interpreter_manager = None
|
1085
|
+
file_ids = []
|
1086
|
+
|
1087
|
+
if shared_upload_manager:
|
1088
|
+
# Use shared upload manager for new attachment system
|
1089
|
+
logger.debug(
|
1090
|
+
"Using shared upload manager for Code Interpreter"
|
989
1091
|
)
|
1092
|
+
from .code_interpreter import CodeInterpreterManager
|
990
1093
|
|
991
|
-
|
992
|
-
|
993
|
-
code_interpreter_manager = (
|
994
|
-
await ci_services.get_code_interpreter_manager()
|
1094
|
+
code_interpreter_manager = CodeInterpreterManager(
|
1095
|
+
client, upload_manager=shared_upload_manager
|
995
1096
|
)
|
996
|
-
|
997
|
-
|
998
|
-
|
999
|
-
|
1000
|
-
|
1001
|
-
|
1002
|
-
|
1003
|
-
|
1004
|
-
|
1005
|
-
|
1006
|
-
|
1007
|
-
|
1008
|
-
|
1009
|
-
|
1010
|
-
|
1097
|
+
|
1098
|
+
# Get file IDs from shared manager
|
1099
|
+
file_ids = (
|
1100
|
+
await code_interpreter_manager.get_files_from_shared_manager()
|
1101
|
+
)
|
1102
|
+
logger.debug(
|
1103
|
+
f"Got {len(file_ids)} file IDs from shared manager for CI"
|
1104
|
+
)
|
1105
|
+
|
1106
|
+
elif routing_result_typed:
|
1107
|
+
# Legacy routing system - process individual files
|
1108
|
+
code_interpreter_files = (
|
1109
|
+
routing_result_typed.validated_files.get(
|
1110
|
+
"code-interpreter", []
|
1111
|
+
)
|
1112
|
+
)
|
1113
|
+
if code_interpreter_files:
|
1114
|
+
# Override args with routed files for Code Interpreter processing
|
1115
|
+
ci_args = dict(args)
|
1116
|
+
ci_args["code_interpreter_files"] = code_interpreter_files
|
1117
|
+
ci_args["code_interpreter_dirs"] = (
|
1118
|
+
[]
|
1119
|
+
) # Files already expanded from dirs
|
1120
|
+
ci_args["code_interpreter"] = (
|
1121
|
+
True # Enable for service container
|
1122
|
+
)
|
1123
|
+
|
1124
|
+
# Create temporary service container with updated args
|
1125
|
+
ci_services = ServiceContainer(client, ci_args) # type: ignore[arg-type]
|
1126
|
+
code_interpreter_manager_protocol = (
|
1127
|
+
await ci_services.get_code_interpreter_manager()
|
1128
|
+
)
|
1129
|
+
# Cast to the concrete type we know we're getting
|
1130
|
+
ci_manager: Optional[CodeInterpreterManager] = None
|
1131
|
+
if code_interpreter_manager_protocol:
|
1132
|
+
from .code_interpreter import CodeInterpreterManager
|
1133
|
+
|
1134
|
+
ci_manager = code_interpreter_manager_protocol # type: ignore[assignment]
|
1135
|
+
if ci_manager:
|
1136
|
+
# Get the uploaded file IDs from the manager
|
1137
|
+
if (
|
1138
|
+
hasattr(ci_manager, "uploaded_file_ids")
|
1139
|
+
and ci_manager.uploaded_file_ids
|
1140
|
+
):
|
1141
|
+
file_ids = ci_manager.uploaded_file_ids
|
1142
|
+
else:
|
1143
|
+
logger.warning(
|
1144
|
+
"Code Interpreter manager has no uploaded file IDs"
|
1011
1145
|
)
|
1012
|
-
|
1013
|
-
|
1014
|
-
|
1015
|
-
|
1016
|
-
|
1017
|
-
|
1018
|
-
|
1019
|
-
|
1020
|
-
|
1146
|
+
|
1147
|
+
# Build tool configuration if we have a manager and files
|
1148
|
+
if code_interpreter_manager and file_ids:
|
1149
|
+
# Cast to concrete CodeInterpreterManager to access build_tool_config
|
1150
|
+
concrete_ci_manager = code_interpreter_manager
|
1151
|
+
if hasattr(concrete_ci_manager, "build_tool_config"):
|
1152
|
+
ci_tool_config = concrete_ci_manager.build_tool_config(
|
1153
|
+
file_ids
|
1154
|
+
)
|
1155
|
+
logger.debug(
|
1156
|
+
f"Code Interpreter tool config: {ci_tool_config}"
|
1157
|
+
)
|
1158
|
+
code_interpreter_info = {
|
1159
|
+
"manager": code_interpreter_manager,
|
1160
|
+
"tool_config": ci_tool_config,
|
1161
|
+
}
|
1162
|
+
tools.append(ci_tool_config)
|
1021
1163
|
|
1022
1164
|
# Process File Search configuration if enabled
|
1023
1165
|
# Apply universal tool toggle overrides for file-search
|
@@ -1042,61 +1184,86 @@ async def execute_model(
|
|
1042
1184
|
# Fall back to routing-based enablement
|
1043
1185
|
fs_should_enable = bool(fs_enabled_by_routing)
|
1044
1186
|
|
1045
|
-
if fs_should_enable
|
1046
|
-
|
1047
|
-
|
1048
|
-
|
1049
|
-
if
|
1050
|
-
#
|
1051
|
-
|
1052
|
-
|
1053
|
-
|
1054
|
-
|
1055
|
-
|
1056
|
-
fs_args["file_search"] = True # Enable for service container
|
1057
|
-
|
1058
|
-
# Create temporary service container with updated args
|
1059
|
-
fs_services = ServiceContainer(client, fs_args) # type: ignore[arg-type]
|
1060
|
-
file_search_manager = (
|
1061
|
-
await fs_services.get_file_search_manager()
|
1187
|
+
if fs_should_enable:
|
1188
|
+
file_search_manager = None
|
1189
|
+
vector_store_id = None
|
1190
|
+
|
1191
|
+
if shared_upload_manager:
|
1192
|
+
# Use shared upload manager for new attachment system
|
1193
|
+
logger.debug("Using shared upload manager for File Search")
|
1194
|
+
from .file_search import FileSearchManager
|
1195
|
+
|
1196
|
+
file_search_manager = FileSearchManager(
|
1197
|
+
client, upload_manager=shared_upload_manager
|
1062
1198
|
)
|
1063
|
-
|
1064
|
-
|
1065
|
-
|
1066
|
-
|
1067
|
-
|
1068
|
-
|
1069
|
-
|
1070
|
-
|
1071
|
-
|
1072
|
-
|
1073
|
-
|
1074
|
-
|
1075
|
-
|
1076
|
-
|
1077
|
-
|
1078
|
-
|
1079
|
-
|
1080
|
-
|
1081
|
-
|
1082
|
-
|
1199
|
+
|
1200
|
+
# Create vector store with files from shared manager
|
1201
|
+
vector_store_id = await file_search_manager.create_vector_store_from_shared_manager(
|
1202
|
+
"ostruct_vector_store"
|
1203
|
+
)
|
1204
|
+
logger.debug(
|
1205
|
+
f"Created vector store {vector_store_id} from shared manager"
|
1206
|
+
)
|
1207
|
+
|
1208
|
+
elif routing_result_typed:
|
1209
|
+
# Legacy routing system - process individual files
|
1210
|
+
file_search_files = routing_result_typed.validated_files.get(
|
1211
|
+
"file-search", []
|
1212
|
+
)
|
1213
|
+
if file_search_files:
|
1214
|
+
# Override args with routed files for File Search processing
|
1215
|
+
fs_args = dict(args)
|
1216
|
+
fs_args["file_search_files"] = file_search_files
|
1217
|
+
fs_args["file_search_dirs"] = (
|
1218
|
+
[]
|
1219
|
+
) # Files already expanded from dirs
|
1220
|
+
fs_args["file_search"] = (
|
1221
|
+
True # Enable for service container
|
1222
|
+
)
|
1223
|
+
|
1224
|
+
# Create temporary service container with updated args
|
1225
|
+
fs_services = ServiceContainer(client, fs_args) # type: ignore[arg-type]
|
1226
|
+
file_search_manager_protocol = (
|
1227
|
+
await fs_services.get_file_search_manager()
|
1228
|
+
)
|
1229
|
+
# Cast to the concrete type we know we're getting
|
1230
|
+
fs_manager: Optional[FileSearchManager] = None
|
1231
|
+
if file_search_manager_protocol:
|
1232
|
+
from .file_search import FileSearchManager
|
1233
|
+
|
1234
|
+
fs_manager = file_search_manager_protocol # type: ignore[assignment]
|
1235
|
+
if fs_manager:
|
1236
|
+
# Get the vector store ID from the manager's created vector stores
|
1237
|
+
# The most recent one should be the one we need
|
1238
|
+
if (
|
1239
|
+
hasattr(fs_manager, "created_vector_stores")
|
1240
|
+
and fs_manager.created_vector_stores
|
1241
|
+
):
|
1242
|
+
vector_store_id = fs_manager.created_vector_stores[
|
1243
|
+
-1
|
1244
|
+
]
|
1245
|
+
else:
|
1246
|
+
logger.warning(
|
1247
|
+
"File Search manager has no created vector stores"
|
1083
1248
|
)
|
1084
|
-
file_search_info = {
|
1085
|
-
"manager": file_search_manager,
|
1086
|
-
"tool_config": fs_tool_config,
|
1087
|
-
}
|
1088
|
-
tools.append(fs_tool_config)
|
1089
|
-
else:
|
1090
|
-
logger.warning(
|
1091
|
-
"File Search manager has no created vector stores"
|
1092
|
-
)
|
1093
1249
|
|
1094
|
-
|
1095
|
-
|
1096
|
-
|
1097
|
-
|
1250
|
+
# Build tool configuration if we have a manager and vector store
|
1251
|
+
if file_search_manager and vector_store_id:
|
1252
|
+
# Cast to concrete FileSearchManager to access build_tool_config
|
1253
|
+
concrete_fs_manager = file_search_manager
|
1254
|
+
if hasattr(concrete_fs_manager, "build_tool_config"):
|
1255
|
+
fs_tool_config = concrete_fs_manager.build_tool_config(
|
1256
|
+
vector_store_id
|
1257
|
+
)
|
1258
|
+
logger.debug(f"File Search tool config: {fs_tool_config}")
|
1259
|
+
file_search_info = {
|
1260
|
+
"manager": file_search_manager,
|
1261
|
+
"tool_config": fs_tool_config,
|
1262
|
+
}
|
1263
|
+
tools.append(fs_tool_config)
|
1098
1264
|
|
1099
|
-
#
|
1265
|
+
# Process Web Search configuration if enabled
|
1266
|
+
# Apply universal tool toggle overrides for web-search
|
1100
1267
|
from typing import cast
|
1101
1268
|
|
1102
1269
|
config_path = cast(Union[str, Path, None], args.get("config"))
|
@@ -1113,12 +1280,6 @@ async def execute_model(
|
|
1113
1280
|
# Universal --disable-tool web-search takes highest precedence
|
1114
1281
|
web_search_enabled = False
|
1115
1282
|
logger.debug("Web search disabled via --disable-tool")
|
1116
|
-
elif web_search_from_cli:
|
1117
|
-
# Explicit --web-search flag takes precedence
|
1118
|
-
web_search_enabled = True
|
1119
|
-
elif no_web_search_from_cli:
|
1120
|
-
# Explicit --no-web-search flag disables
|
1121
|
-
web_search_enabled = False
|
1122
1283
|
else:
|
1123
1284
|
# Use config default
|
1124
1285
|
web_search_enabled = web_search_config.enable_by_default
|
@@ -1138,7 +1299,8 @@ async def execute_model(
|
|
1138
1299
|
|
1139
1300
|
# Check for Azure OpenAI endpoint guard-rail
|
1140
1301
|
api_base = os.getenv("OPENAI_API_BASE", "")
|
1141
|
-
|
1302
|
+
hostname = urlparse(api_base).hostname or ""
|
1303
|
+
if hostname.endswith("azure.com"):
|
1142
1304
|
logger.warning(
|
1143
1305
|
"Web search is not currently supported or may be unreliable with Azure OpenAI endpoints and has been disabled."
|
1144
1306
|
)
|
@@ -1147,40 +1309,37 @@ async def execute_model(
|
|
1147
1309
|
"type": "web_search_preview"
|
1148
1310
|
}
|
1149
1311
|
|
1150
|
-
#
|
1151
|
-
|
1152
|
-
|
1153
|
-
|
1312
|
+
# Get user location from CLI args or config
|
1313
|
+
ws_country = args.get("ws_country")
|
1314
|
+
ws_city = args.get("ws_city")
|
1315
|
+
ws_region = args.get("ws_region")
|
1154
1316
|
|
1155
|
-
#
|
1317
|
+
# Use config defaults if CLI args not provided
|
1156
1318
|
if (
|
1157
|
-
not any([
|
1319
|
+
not any([ws_country, ws_city, ws_region])
|
1158
1320
|
and web_search_config.user_location
|
1159
1321
|
):
|
1160
|
-
|
1161
|
-
|
1162
|
-
|
1322
|
+
ws_country = web_search_config.user_location.country
|
1323
|
+
ws_city = web_search_config.user_location.city
|
1324
|
+
ws_region = web_search_config.user_location.region
|
1163
1325
|
|
1164
|
-
if
|
1326
|
+
if ws_country or ws_city or ws_region:
|
1165
1327
|
user_location: Dict[str, Any] = {"type": "approximate"}
|
1166
|
-
if
|
1167
|
-
user_location["country"] =
|
1168
|
-
if
|
1169
|
-
user_location["city"] =
|
1170
|
-
if
|
1171
|
-
user_location["region"] =
|
1172
|
-
|
1328
|
+
if ws_country:
|
1329
|
+
user_location["country"] = ws_country
|
1330
|
+
if ws_city:
|
1331
|
+
user_location["city"] = ws_city
|
1332
|
+
if ws_region:
|
1333
|
+
user_location["region"] = ws_region
|
1173
1334
|
web_tool_config["user_location"] = user_location
|
1174
1335
|
|
1175
|
-
# Add
|
1176
|
-
|
1177
|
-
args.get("
|
1336
|
+
# Add ws_context_size if provided via CLI or config
|
1337
|
+
ws_context_size = (
|
1338
|
+
args.get("ws_context_size")
|
1178
1339
|
or web_search_config.search_context_size
|
1179
1340
|
)
|
1180
|
-
if
|
1181
|
-
web_tool_config["search_context_size"] =
|
1182
|
-
search_context_size
|
1183
|
-
)
|
1341
|
+
if ws_context_size:
|
1342
|
+
web_tool_config["search_context_size"] = ws_context_size
|
1184
1343
|
|
1185
1344
|
tools.append(web_tool_config)
|
1186
1345
|
logger.debug(f"Web Search tool config: {web_tool_config}")
|
@@ -1286,8 +1445,11 @@ async def execute_model(
|
|
1286
1445
|
api_response = getattr(last_response, "_api_response")
|
1287
1446
|
# Responses API has 'output' attribute, not 'messages'
|
1288
1447
|
if hasattr(api_response, "output"):
|
1448
|
+
from .constants import DefaultPaths
|
1449
|
+
|
1289
1450
|
download_dir = args.get(
|
1290
|
-
"
|
1451
|
+
"ci_download_dir",
|
1452
|
+
DefaultPaths.CODE_INTERPRETER_OUTPUT_DIR,
|
1291
1453
|
)
|
1292
1454
|
manager = code_interpreter_info["manager"]
|
1293
1455
|
|
@@ -1366,14 +1528,15 @@ async def execute_model(
|
|
1366
1528
|
) as e:
|
1367
1529
|
logger.error("API error: %s", str(e))
|
1368
1530
|
raise CLIError(str(e), exit_code=ExitCode.API_ERROR)
|
1531
|
+
except CLIError:
|
1532
|
+
# Re-raise CLIError types (like SchemaValidationError) without wrapping
|
1533
|
+
raise
|
1369
1534
|
except Exception as e:
|
1370
1535
|
logger.exception("Unexpected error during execution")
|
1371
1536
|
raise CLIError(str(e), exit_code=ExitCode.UNKNOWN_ERROR)
|
1372
1537
|
finally:
|
1373
1538
|
# Clean up Code Interpreter files if requested
|
1374
|
-
if code_interpreter_info and args.get(
|
1375
|
-
"code_interpreter_cleanup", True
|
1376
|
-
):
|
1539
|
+
if code_interpreter_info and args.get("ci_cleanup", True):
|
1377
1540
|
try:
|
1378
1541
|
manager = code_interpreter_info["manager"]
|
1379
1542
|
# Type ignore since we know this is a CodeInterpreterManager
|
@@ -1385,7 +1548,7 @@ async def execute_model(
|
|
1385
1548
|
)
|
1386
1549
|
|
1387
1550
|
# Clean up File Search resources if requested
|
1388
|
-
if file_search_info and args.get("
|
1551
|
+
if file_search_info and args.get("fs_cleanup", True):
|
1389
1552
|
try:
|
1390
1553
|
manager = file_search_info["manager"]
|
1391
1554
|
# Type ignore since we know this is a FileSearchManager
|
@@ -1424,21 +1587,15 @@ async def run_cli_async(args: CLIParams) -> ExitCode:
|
|
1424
1587
|
|
1425
1588
|
configure_debug_logging(
|
1426
1589
|
verbose=bool(args.get("verbose", False)),
|
1427
|
-
debug=bool(args.get("debug", False))
|
1428
|
-
or bool(args.get("debug_templates", False)),
|
1590
|
+
debug=bool(args.get("debug", False)),
|
1429
1591
|
)
|
1430
1592
|
|
1431
|
-
# 0a.
|
1432
|
-
if args.get("help_debug", False):
|
1433
|
-
from .template_debug_help import show_template_debug_help
|
1434
|
-
|
1435
|
-
show_template_debug_help()
|
1436
|
-
return ExitCode.SUCCESS
|
1593
|
+
# 0a. Help Debug Request is now handled by callback in click_options.py
|
1437
1594
|
|
1438
1595
|
# 0. Configure Progress Reporting
|
1439
1596
|
configure_progress_reporter(
|
1440
1597
|
verbose=args.get("verbose", False),
|
1441
|
-
|
1598
|
+
progress=args.get("progress", "basic"),
|
1442
1599
|
)
|
1443
1600
|
progress_reporter = get_progress_reporter()
|
1444
1601
|
|
@@ -1527,20 +1684,6 @@ async def run_cli_async(args: CLIParams) -> ExitCode:
|
|
1527
1684
|
token_limit=128000, # Default fallback
|
1528
1685
|
)
|
1529
1686
|
|
1530
|
-
# 3a. Web Search Compatibility Validation
|
1531
|
-
if args.get("web_search", False) and not args.get(
|
1532
|
-
"no_web_search", False
|
1533
|
-
):
|
1534
|
-
from .model_validation import validate_web_search_compatibility
|
1535
|
-
|
1536
|
-
compatibility_warning = validate_web_search_compatibility(
|
1537
|
-
args["model"], True
|
1538
|
-
)
|
1539
|
-
if compatibility_warning:
|
1540
|
-
logger.warning(compatibility_warning)
|
1541
|
-
# For production usage, consider making this an error instead of warning
|
1542
|
-
# raise CLIError(compatibility_warning, exit_code=ExitCode.VALIDATION_ERROR)
|
1543
|
-
|
1544
1687
|
# 4. Dry Run Output Phase - Moved after all validations
|
1545
1688
|
if args.get("dry_run", False):
|
1546
1689
|
report_success(
|
@@ -1576,16 +1719,16 @@ async def run_cli_async(args: CLIParams) -> ExitCode:
|
|
1576
1719
|
show_template_content(
|
1577
1720
|
system_prompt=system_prompt,
|
1578
1721
|
user_prompt=user_prompt,
|
1579
|
-
|
1580
|
-
debug=bool(args.get("debug", False))
|
1581
|
-
or bool(args.get("debug_templates", False)),
|
1722
|
+
debug=bool(args.get("debug", False)),
|
1582
1723
|
)
|
1583
1724
|
|
1584
1725
|
# Legacy verbose support for backward compatibility
|
1726
|
+
from .template_debug import TDCap, is_capacity_active
|
1727
|
+
|
1585
1728
|
if (
|
1586
1729
|
args.get("verbose", False)
|
1587
1730
|
and not args.get("debug", False)
|
1588
|
-
and not
|
1731
|
+
and not is_capacity_active(TDCap.POST_EXPAND)
|
1589
1732
|
):
|
1590
1733
|
logger.info("\nSystem Prompt:")
|
1591
1734
|
logger.info("-" * 40)
|
@@ -1677,23 +1820,87 @@ class OstructRunner:
|
|
1677
1820
|
Returns:
|
1678
1821
|
Dictionary containing configuration information
|
1679
1822
|
"""
|
1823
|
+
# Check for new attachment system
|
1824
|
+
has_new_attachments = self._has_new_attachment_syntax()
|
1825
|
+
|
1826
|
+
if has_new_attachments:
|
1827
|
+
attachment_summary = self._get_attachment_summary()
|
1828
|
+
return {
|
1829
|
+
"model": self.args.get("model"),
|
1830
|
+
"dry_run": self.args.get("dry_run", False),
|
1831
|
+
"verbose": self.args.get("verbose", False),
|
1832
|
+
"mcp_servers": len(self.args.get("mcp_servers", [])),
|
1833
|
+
"attachment_system": "new",
|
1834
|
+
"attachments": attachment_summary,
|
1835
|
+
"template_source": (
|
1836
|
+
"file" if self.args.get("task_file") else "string"
|
1837
|
+
),
|
1838
|
+
"schema_source": (
|
1839
|
+
"file" if self.args.get("schema_file") else "inline"
|
1840
|
+
),
|
1841
|
+
}
|
1842
|
+
else:
|
1843
|
+
# Legacy configuration summary
|
1844
|
+
return {
|
1845
|
+
"model": self.args.get("model"),
|
1846
|
+
"dry_run": self.args.get("dry_run", False),
|
1847
|
+
"verbose": self.args.get("verbose", False),
|
1848
|
+
"mcp_servers": len(self.args.get("mcp_servers", [])),
|
1849
|
+
"attachment_system": "legacy",
|
1850
|
+
"code_interpreter_enabled": bool(
|
1851
|
+
self.args.get("code_interpreter_files")
|
1852
|
+
or self.args.get("code_interpreter_dirs")
|
1853
|
+
),
|
1854
|
+
"file_search_enabled": bool(
|
1855
|
+
self.args.get("file_search_files")
|
1856
|
+
or self.args.get("file_search_dirs")
|
1857
|
+
),
|
1858
|
+
"template_source": (
|
1859
|
+
"file" if self.args.get("task_file") else "string"
|
1860
|
+
),
|
1861
|
+
"schema_source": (
|
1862
|
+
"file" if self.args.get("schema_file") else "inline"
|
1863
|
+
),
|
1864
|
+
}
|
1865
|
+
|
1866
|
+
def _has_new_attachment_syntax(self) -> bool:
|
1867
|
+
"""Check if CLI args contain new attachment syntax.
|
1868
|
+
|
1869
|
+
Returns:
|
1870
|
+
True if new attachment syntax is present
|
1871
|
+
"""
|
1872
|
+
new_syntax_keys = ["attaches", "dirs", "collects"]
|
1873
|
+
return any(self.args.get(key) for key in new_syntax_keys)
|
1874
|
+
|
1875
|
+
def _get_attachment_summary(self) -> Dict[str, Any]:
|
1876
|
+
"""Get summary of new-style attachments.
|
1877
|
+
|
1878
|
+
Returns:
|
1879
|
+
Dictionary containing attachment summary
|
1880
|
+
"""
|
1881
|
+
total_attachments = 0
|
1882
|
+
targets_used = set()
|
1883
|
+
|
1884
|
+
for key in ["attaches", "dirs", "collects"]:
|
1885
|
+
attachments = self.args.get(key, [])
|
1886
|
+
if isinstance(attachments, list):
|
1887
|
+
total_attachments += len(attachments)
|
1888
|
+
|
1889
|
+
for attachment in attachments:
|
1890
|
+
if isinstance(attachment, dict):
|
1891
|
+
targets = attachment.get("targets", [])
|
1892
|
+
if isinstance(targets, list):
|
1893
|
+
targets_used.update(targets)
|
1894
|
+
|
1895
|
+
# Helper function to safely get list length
|
1896
|
+
def safe_len(key: str) -> int:
|
1897
|
+
value = self.args.get(key, [])
|
1898
|
+
return len(value) if isinstance(value, list) else 0
|
1899
|
+
|
1680
1900
|
return {
|
1681
|
-
"
|
1682
|
-
"
|
1683
|
-
"
|
1684
|
-
"
|
1685
|
-
"
|
1686
|
-
self.args.get("code_interpreter_files")
|
1687
|
-
or self.args.get("code_interpreter_dirs")
|
1688
|
-
),
|
1689
|
-
"file_search_enabled": bool(
|
1690
|
-
self.args.get("file_search_files")
|
1691
|
-
or self.args.get("file_search_dirs")
|
1692
|
-
),
|
1693
|
-
"template_source": (
|
1694
|
-
"file" if self.args.get("task_file") else "string"
|
1695
|
-
),
|
1696
|
-
"schema_source": (
|
1697
|
-
"file" if self.args.get("schema_file") else "inline"
|
1698
|
-
),
|
1901
|
+
"total_attachments": total_attachments,
|
1902
|
+
"targets_used": list(targets_used),
|
1903
|
+
"attach_count": safe_len("attaches"),
|
1904
|
+
"dir_count": safe_len("dirs"),
|
1905
|
+
"collect_count": safe_len("collects"),
|
1699
1906
|
}
|