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/params.py
ADDED
@@ -0,0 +1,217 @@
|
|
1
|
+
"""Parameter handling and validation for CLI attachment syntax."""
|
2
|
+
|
3
|
+
from typing import Any, Dict, Optional, Set, Tuple, TypedDict, Union
|
4
|
+
|
5
|
+
import click
|
6
|
+
|
7
|
+
# Target mapping with explicit aliases
|
8
|
+
TARGET_NORMALISE = {
|
9
|
+
"prompt": "prompt",
|
10
|
+
"code-interpreter": "code-interpreter",
|
11
|
+
"ci": "code-interpreter",
|
12
|
+
"file-search": "file-search",
|
13
|
+
"fs": "file-search",
|
14
|
+
}
|
15
|
+
|
16
|
+
|
17
|
+
class AttachmentSpec(TypedDict):
|
18
|
+
"""Type definition for attachment specifications."""
|
19
|
+
|
20
|
+
alias: str
|
21
|
+
path: Union[
|
22
|
+
str, Tuple[str, str]
|
23
|
+
] # str or ("@", "filelist.txt") for collect
|
24
|
+
targets: Set[str]
|
25
|
+
recursive: bool
|
26
|
+
pattern: Optional[str]
|
27
|
+
|
28
|
+
|
29
|
+
def normalise_targets(raw: str) -> Set[str]:
|
30
|
+
"""Normalize comma-separated target list with aliases.
|
31
|
+
|
32
|
+
Args:
|
33
|
+
raw: Comma-separated string of targets (e.g., "prompt,ci,fs")
|
34
|
+
|
35
|
+
Returns:
|
36
|
+
Set of normalized target names
|
37
|
+
|
38
|
+
Raises:
|
39
|
+
click.BadParameter: If any target is unknown
|
40
|
+
|
41
|
+
Examples:
|
42
|
+
>>> normalise_targets("prompt")
|
43
|
+
{"prompt"}
|
44
|
+
>>> normalise_targets("ci,fs")
|
45
|
+
{"code-interpreter", "file-search"}
|
46
|
+
>>> normalise_targets("")
|
47
|
+
{"prompt"}
|
48
|
+
"""
|
49
|
+
if not raw.strip(): # Guard against empty string edge case
|
50
|
+
return {"prompt"}
|
51
|
+
|
52
|
+
tokens = [t.strip().lower() for t in raw.split(",") if t.strip()]
|
53
|
+
if not tokens: # After stripping, no valid tokens remain
|
54
|
+
return {"prompt"}
|
55
|
+
|
56
|
+
# Normalize all tokens and check for unknown ones
|
57
|
+
normalized = set()
|
58
|
+
bad_tokens = set()
|
59
|
+
|
60
|
+
for token in tokens:
|
61
|
+
if token in TARGET_NORMALISE:
|
62
|
+
normalized.add(TARGET_NORMALISE[token])
|
63
|
+
else:
|
64
|
+
bad_tokens.add(token)
|
65
|
+
|
66
|
+
if bad_tokens:
|
67
|
+
valid_targets = ", ".join(sorted(TARGET_NORMALISE.keys()))
|
68
|
+
raise click.BadParameter(
|
69
|
+
f"Unknown target(s): {', '.join(sorted(bad_tokens))}. "
|
70
|
+
f"Valid targets: {valid_targets}"
|
71
|
+
)
|
72
|
+
|
73
|
+
return normalized or {"prompt"} # Fallback to prompt if somehow empty
|
74
|
+
|
75
|
+
|
76
|
+
def validate_attachment_alias(alias: str) -> str:
|
77
|
+
"""Validate and normalize attachment alias.
|
78
|
+
|
79
|
+
Args:
|
80
|
+
alias: The attachment alias to validate
|
81
|
+
|
82
|
+
Returns:
|
83
|
+
The validated alias
|
84
|
+
|
85
|
+
Raises:
|
86
|
+
click.BadParameter: If alias is invalid
|
87
|
+
"""
|
88
|
+
if not alias or not alias.strip():
|
89
|
+
raise click.BadParameter("Attachment alias cannot be empty")
|
90
|
+
|
91
|
+
alias = alias.strip()
|
92
|
+
|
93
|
+
# Basic validation - no whitespace, reasonable length
|
94
|
+
if " " in alias or "\t" in alias:
|
95
|
+
raise click.BadParameter("Attachment alias cannot contain whitespace")
|
96
|
+
|
97
|
+
if len(alias) > 64:
|
98
|
+
raise click.BadParameter(
|
99
|
+
"Attachment alias too long (max 64 characters)"
|
100
|
+
)
|
101
|
+
|
102
|
+
return alias
|
103
|
+
|
104
|
+
|
105
|
+
class AttachParam(click.ParamType):
|
106
|
+
"""Custom Click parameter type for parsing attachment specifications.
|
107
|
+
|
108
|
+
Supports space-form syntax: '[targets:]alias path'
|
109
|
+
|
110
|
+
Examples:
|
111
|
+
--attach data ./file.txt
|
112
|
+
--attach ci:analysis ./data.csv
|
113
|
+
--collect ci,fs:mixed @file-list.txt
|
114
|
+
"""
|
115
|
+
|
116
|
+
name = "attach-spec"
|
117
|
+
|
118
|
+
def __init__(self, multi: bool = False) -> None:
|
119
|
+
"""Initialize AttachParam.
|
120
|
+
|
121
|
+
Args:
|
122
|
+
multi: If True, supports @filelist syntax for collect operations
|
123
|
+
"""
|
124
|
+
self.multi = multi
|
125
|
+
|
126
|
+
def convert(
|
127
|
+
self,
|
128
|
+
value: Any,
|
129
|
+
param: Optional[click.Parameter],
|
130
|
+
ctx: Optional[click.Context],
|
131
|
+
) -> Dict[str, Any]:
|
132
|
+
"""Convert Click parameter value to AttachmentSpec.
|
133
|
+
|
134
|
+
Args:
|
135
|
+
value: Parameter value from Click (tuple for nargs=2)
|
136
|
+
param: Click parameter object
|
137
|
+
ctx: Click context
|
138
|
+
|
139
|
+
Returns:
|
140
|
+
Dict representing an AttachmentSpec
|
141
|
+
|
142
|
+
Raises:
|
143
|
+
click.BadParameter: If value format is invalid
|
144
|
+
"""
|
145
|
+
# Space form only (nargs=2) - Click passes tuple
|
146
|
+
if not isinstance(value, tuple) or len(value) != 2:
|
147
|
+
self._fail_with_usage_examples(
|
148
|
+
"Attachment must use space form syntax", param, ctx
|
149
|
+
)
|
150
|
+
|
151
|
+
spec, path = value
|
152
|
+
|
153
|
+
# Parse spec part: [targets:]alias
|
154
|
+
if ":" in spec:
|
155
|
+
# Check for Windows drive letter false positive (C:\path)
|
156
|
+
if len(spec) == 2 and spec[1] == ":" and spec[0].isalpha():
|
157
|
+
# This is likely a drive letter, treat as alias only
|
158
|
+
prefix, alias = "prompt", spec
|
159
|
+
else:
|
160
|
+
prefix, alias = spec.split(":", 1)
|
161
|
+
else:
|
162
|
+
prefix, alias = "prompt", spec
|
163
|
+
|
164
|
+
# Normalize targets using the existing function
|
165
|
+
try:
|
166
|
+
targets = normalise_targets(prefix)
|
167
|
+
except click.BadParameter:
|
168
|
+
# Re-raise with context about attachment parsing
|
169
|
+
self._fail_with_usage_examples(
|
170
|
+
f"Invalid target(s) in '{prefix}'. Use comma-separated valid targets",
|
171
|
+
param,
|
172
|
+
ctx,
|
173
|
+
)
|
174
|
+
|
175
|
+
# Validate alias
|
176
|
+
try:
|
177
|
+
alias = validate_attachment_alias(alias)
|
178
|
+
except click.BadParameter as e:
|
179
|
+
self._fail_with_usage_examples(str(e), param, ctx)
|
180
|
+
|
181
|
+
# Handle collect @filelist syntax
|
182
|
+
if self.multi and path.startswith("@"):
|
183
|
+
filelist_path = path[1:] # Remove @
|
184
|
+
if not filelist_path:
|
185
|
+
self._fail_with_usage_examples(
|
186
|
+
"Filelist path cannot be empty after @", param, ctx
|
187
|
+
)
|
188
|
+
path = ("@", filelist_path)
|
189
|
+
|
190
|
+
return {
|
191
|
+
"alias": alias,
|
192
|
+
"path": path,
|
193
|
+
"targets": targets,
|
194
|
+
"recursive": False, # Set by flag processing
|
195
|
+
"pattern": None, # Set by flag processing
|
196
|
+
}
|
197
|
+
|
198
|
+
def _fail_with_usage_examples(
|
199
|
+
self,
|
200
|
+
message: str,
|
201
|
+
param: Optional[click.Parameter],
|
202
|
+
ctx: Optional[click.Context],
|
203
|
+
) -> None:
|
204
|
+
"""Provide helpful usage examples in error messages."""
|
205
|
+
examples = [
|
206
|
+
"--file data ./file.txt",
|
207
|
+
"--file ci:analysis ./data.csv",
|
208
|
+
"--dir fs:docs ./documentation",
|
209
|
+
]
|
210
|
+
|
211
|
+
if self.multi:
|
212
|
+
examples.append("--collect ci,fs:mixed @file-list.txt")
|
213
|
+
|
214
|
+
full_message = f"{message}\n\nExamples:\n" + "\n".join(
|
215
|
+
f" {ex}" for ex in examples
|
216
|
+
)
|
217
|
+
self.fail(full_message, param, ctx)
|
@@ -0,0 +1,335 @@
|
|
1
|
+
"""Plan assembly for execution plans and run summaries.
|
2
|
+
|
3
|
+
This module provides the single source of truth for plan data structures
|
4
|
+
following UNIFIED GUIDELINES to prevent logic drift between JSON and human output.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import os
|
8
|
+
import time
|
9
|
+
from datetime import datetime
|
10
|
+
from pathlib import Path
|
11
|
+
from typing import Any, Dict, List, Optional
|
12
|
+
|
13
|
+
from .attachment_processor import ProcessedAttachments
|
14
|
+
|
15
|
+
|
16
|
+
class PlanAssembler:
|
17
|
+
"""Single source of truth for execution plan data structure.
|
18
|
+
|
19
|
+
This class ensures consistent plan format across all output types
|
20
|
+
(JSON, human-readable, etc.) by providing a single build method.
|
21
|
+
"""
|
22
|
+
|
23
|
+
@staticmethod
|
24
|
+
def validate_download_configuration(
|
25
|
+
enabled_tools: Optional[set[str]] = None,
|
26
|
+
ci_config: Optional[Dict[str, Any]] = None,
|
27
|
+
expected_files: Optional[List[str]] = None,
|
28
|
+
) -> Dict[str, Any]:
|
29
|
+
"""Validate download configuration for dry-run.
|
30
|
+
|
31
|
+
Args:
|
32
|
+
enabled_tools: Set of enabled tools
|
33
|
+
ci_config: Code Interpreter configuration
|
34
|
+
expected_files: List of expected output filenames (optional)
|
35
|
+
|
36
|
+
Returns:
|
37
|
+
Dictionary with validation results
|
38
|
+
"""
|
39
|
+
validation: Dict[str, Any] = {
|
40
|
+
"enabled": False,
|
41
|
+
"directory": None,
|
42
|
+
"writable": False,
|
43
|
+
"conflicts": [],
|
44
|
+
"issues": [],
|
45
|
+
}
|
46
|
+
|
47
|
+
# Check if Code Interpreter is enabled
|
48
|
+
if not enabled_tools or "code-interpreter" not in enabled_tools:
|
49
|
+
return validation
|
50
|
+
|
51
|
+
validation["enabled"] = True
|
52
|
+
|
53
|
+
# Get download directory
|
54
|
+
|
55
|
+
download_dir = "./downloads" # Default
|
56
|
+
if ci_config:
|
57
|
+
download_dir = ci_config.get("output_directory", download_dir)
|
58
|
+
|
59
|
+
validation["directory"] = download_dir
|
60
|
+
|
61
|
+
# Check directory permissions
|
62
|
+
try:
|
63
|
+
download_path = Path(download_dir)
|
64
|
+
parent_dir = download_path.parent
|
65
|
+
|
66
|
+
# Check if parent exists and is writable
|
67
|
+
if parent_dir.exists():
|
68
|
+
# Try to create a test file in parent
|
69
|
+
test_file = parent_dir / ".ostruct_write_test"
|
70
|
+
try:
|
71
|
+
test_file.touch()
|
72
|
+
test_file.unlink()
|
73
|
+
|
74
|
+
# If download dir exists, check it specifically
|
75
|
+
if download_path.exists():
|
76
|
+
if not download_path.is_dir():
|
77
|
+
validation["issues"].append(
|
78
|
+
f"Path exists but is not a directory: {download_dir}"
|
79
|
+
)
|
80
|
+
else:
|
81
|
+
# Test write in actual directory
|
82
|
+
test_file = download_path / ".ostruct_write_test"
|
83
|
+
test_file.touch()
|
84
|
+
test_file.unlink()
|
85
|
+
validation["writable"] = True
|
86
|
+
else:
|
87
|
+
# Directory doesn't exist but parent is writable
|
88
|
+
validation["writable"] = True
|
89
|
+
|
90
|
+
except Exception as e:
|
91
|
+
validation["issues"].append(
|
92
|
+
f"Cannot write to directory: {e}"
|
93
|
+
)
|
94
|
+
else:
|
95
|
+
validation["issues"].append(
|
96
|
+
f"Parent directory does not exist: {parent_dir}"
|
97
|
+
)
|
98
|
+
|
99
|
+
except Exception as e:
|
100
|
+
validation["issues"].append(f"Error checking directory: {e}")
|
101
|
+
|
102
|
+
# Check for potential conflicts if expected files provided
|
103
|
+
if expected_files and validation["writable"]:
|
104
|
+
try:
|
105
|
+
download_path = Path(download_dir)
|
106
|
+
if download_path.exists():
|
107
|
+
existing_files = {
|
108
|
+
f.name for f in download_path.iterdir() if f.is_file()
|
109
|
+
}
|
110
|
+
conflicts = [
|
111
|
+
f for f in expected_files if f in existing_files
|
112
|
+
]
|
113
|
+
validation["conflicts"] = conflicts
|
114
|
+
except Exception:
|
115
|
+
pass # Ignore errors in conflict detection
|
116
|
+
|
117
|
+
return validation
|
118
|
+
|
119
|
+
@staticmethod
|
120
|
+
def build_execution_plan(
|
121
|
+
processed_attachments: ProcessedAttachments,
|
122
|
+
template_path: str,
|
123
|
+
schema_path: str,
|
124
|
+
variables: Dict[str, Any],
|
125
|
+
security_mode: Optional[str] = None,
|
126
|
+
model: Optional[str] = None,
|
127
|
+
**kwargs: Any,
|
128
|
+
) -> Dict[str, Any]:
|
129
|
+
"""Build execution plan dict with consistent schema.
|
130
|
+
|
131
|
+
Args:
|
132
|
+
processed_attachments: Processed attachment specifications
|
133
|
+
template_path: Path to template file
|
134
|
+
schema_path: Path to schema file
|
135
|
+
variables: Template variables
|
136
|
+
security_mode: Security mode setting
|
137
|
+
model: Model to use for processing
|
138
|
+
**kwargs: Additional context (allowed_paths, cost_estimate, template_warning, etc.)
|
139
|
+
|
140
|
+
Returns:
|
141
|
+
Execution plan dictionary
|
142
|
+
"""
|
143
|
+
# Handle template warning information
|
144
|
+
template_warning = kwargs.get("template_warning")
|
145
|
+
original_template_path = kwargs.get("original_template_path")
|
146
|
+
|
147
|
+
# Use original path if available, otherwise use the provided path
|
148
|
+
display_path = original_template_path or template_path
|
149
|
+
|
150
|
+
template_info = {
|
151
|
+
"path": display_path,
|
152
|
+
"exists": (
|
153
|
+
Path(display_path).exists() if display_path != "---" else True
|
154
|
+
),
|
155
|
+
}
|
156
|
+
|
157
|
+
# Add warning information if present
|
158
|
+
if template_warning:
|
159
|
+
template_info["warning"] = template_warning
|
160
|
+
|
161
|
+
schema_info = {
|
162
|
+
"path": schema_path,
|
163
|
+
"exists": Path(schema_path).exists(),
|
164
|
+
}
|
165
|
+
|
166
|
+
# Build plan structure
|
167
|
+
plan = {
|
168
|
+
"schema_version": "1.0",
|
169
|
+
"type": "execution_plan",
|
170
|
+
"timestamp": datetime.now().isoformat(),
|
171
|
+
"template": template_info,
|
172
|
+
"schema": schema_info,
|
173
|
+
"model": model or "gpt-4o",
|
174
|
+
"variables": variables,
|
175
|
+
"security_mode": security_mode or "permissive",
|
176
|
+
"attachments": PlanAssembler._format_attachments(
|
177
|
+
processed_attachments
|
178
|
+
),
|
179
|
+
}
|
180
|
+
|
181
|
+
# Add tools section if enabled tools are provided
|
182
|
+
enabled_tools = kwargs.get("enabled_tools")
|
183
|
+
if enabled_tools:
|
184
|
+
tools_dict = {}
|
185
|
+
for tool in enabled_tools:
|
186
|
+
# Map tool names to boolean values for plan display
|
187
|
+
if tool == "code-interpreter":
|
188
|
+
tools_dict["code_interpreter"] = True
|
189
|
+
elif tool == "file-search":
|
190
|
+
tools_dict["file_search"] = True
|
191
|
+
elif tool == "web-search":
|
192
|
+
tools_dict["web_search"] = True
|
193
|
+
elif tool == "mcp":
|
194
|
+
tools_dict["mcp"] = True
|
195
|
+
elif (
|
196
|
+
tool != "template"
|
197
|
+
): # Skip template as it's not an external tool
|
198
|
+
tools_dict[tool] = True
|
199
|
+
|
200
|
+
if tools_dict:
|
201
|
+
plan["tools"] = tools_dict
|
202
|
+
|
203
|
+
# Add download validation for Code Interpreter
|
204
|
+
if enabled_tools and "code-interpreter" in enabled_tools:
|
205
|
+
# Get CI config if available
|
206
|
+
ci_config = kwargs.get("ci_config")
|
207
|
+
download_validation = (
|
208
|
+
PlanAssembler.validate_download_configuration(
|
209
|
+
enabled_tools=enabled_tools,
|
210
|
+
ci_config=ci_config,
|
211
|
+
expected_files=kwargs.get("expected_files"),
|
212
|
+
)
|
213
|
+
)
|
214
|
+
|
215
|
+
# Add to plan if there are issues or useful info
|
216
|
+
if download_validation["enabled"]:
|
217
|
+
plan["download_validation"] = download_validation
|
218
|
+
|
219
|
+
# Add optional fields
|
220
|
+
if kwargs.get("allowed_paths"):
|
221
|
+
plan["allowed_paths"] = kwargs["allowed_paths"]
|
222
|
+
if kwargs.get("cost_estimate"):
|
223
|
+
plan["cost_estimate"] = kwargs["cost_estimate"]
|
224
|
+
|
225
|
+
return plan
|
226
|
+
|
227
|
+
@staticmethod
|
228
|
+
def build_run_summary(
|
229
|
+
execution_plan: Dict[str, Any],
|
230
|
+
result: Optional[Dict[str, Any]] = None,
|
231
|
+
execution_time: Optional[float] = None,
|
232
|
+
**kwargs: Any,
|
233
|
+
) -> Dict[str, Any]:
|
234
|
+
"""Build run summary dict from execution plan and results.
|
235
|
+
|
236
|
+
Args:
|
237
|
+
execution_plan: Original execution plan
|
238
|
+
result: Execution results
|
239
|
+
execution_time: Time taken for execution
|
240
|
+
**kwargs: Additional summary data
|
241
|
+
|
242
|
+
Returns:
|
243
|
+
Dictionary with consistent run summary structure
|
244
|
+
"""
|
245
|
+
summary = {
|
246
|
+
"schema_version": "1.0",
|
247
|
+
"type": "run_summary",
|
248
|
+
"timestamp": time.time(),
|
249
|
+
"execution_time": execution_time,
|
250
|
+
"success": kwargs.get("success", True),
|
251
|
+
"original_plan": execution_plan,
|
252
|
+
}
|
253
|
+
|
254
|
+
if result:
|
255
|
+
summary["result"] = result
|
256
|
+
|
257
|
+
if "error" in kwargs:
|
258
|
+
summary["error"] = kwargs["error"]
|
259
|
+
summary["success"] = False
|
260
|
+
|
261
|
+
if "cost_breakdown" in kwargs:
|
262
|
+
summary["cost_breakdown"] = kwargs["cost_breakdown"]
|
263
|
+
|
264
|
+
return summary
|
265
|
+
|
266
|
+
@staticmethod
|
267
|
+
def _format_attachments(
|
268
|
+
processed_attachments: ProcessedAttachments,
|
269
|
+
) -> List[Dict[str, Any]]:
|
270
|
+
"""Format processed attachments for plan output.
|
271
|
+
|
272
|
+
Args:
|
273
|
+
processed_attachments: Processed attachment specifications
|
274
|
+
|
275
|
+
Returns:
|
276
|
+
List of formatted attachment dictionaries
|
277
|
+
"""
|
278
|
+
attachments = []
|
279
|
+
|
280
|
+
# Add all attachment types with consistent format
|
281
|
+
for alias, spec in processed_attachments.alias_map.items():
|
282
|
+
attachment = {
|
283
|
+
"alias": alias,
|
284
|
+
"path": spec.path,
|
285
|
+
"targets": sorted(
|
286
|
+
list(spec.targets)
|
287
|
+
), # Ensure consistent ordering
|
288
|
+
"type": "file" if not spec.recursive else "directory",
|
289
|
+
"exists": os.path.exists(spec.path),
|
290
|
+
"recursive": spec.recursive,
|
291
|
+
"pattern": spec.pattern,
|
292
|
+
}
|
293
|
+
|
294
|
+
# Add metadata about where this attachment will be processed
|
295
|
+
tool_info = []
|
296
|
+
if "prompt" in spec.targets:
|
297
|
+
tool_info.append("template")
|
298
|
+
if "code-interpreter" in spec.targets or "ci" in spec.targets:
|
299
|
+
tool_info.append("code_interpreter")
|
300
|
+
if "file-search" in spec.targets or "fs" in spec.targets:
|
301
|
+
tool_info.append("file_search")
|
302
|
+
|
303
|
+
attachment["processing"] = tool_info
|
304
|
+
attachments.append(attachment)
|
305
|
+
|
306
|
+
return attachments
|
307
|
+
|
308
|
+
@staticmethod
|
309
|
+
def validate_plan(plan: Dict[str, Any]) -> bool:
|
310
|
+
"""Validate plan structure for consistency.
|
311
|
+
|
312
|
+
Args:
|
313
|
+
plan: Plan dictionary to validate
|
314
|
+
|
315
|
+
Returns:
|
316
|
+
True if plan structure is valid
|
317
|
+
|
318
|
+
Raises:
|
319
|
+
ValueError: If plan structure is invalid
|
320
|
+
"""
|
321
|
+
required_fields = ["schema_version", "type", "timestamp"]
|
322
|
+
|
323
|
+
for field in required_fields:
|
324
|
+
if field not in plan:
|
325
|
+
raise ValueError(f"Missing required field: {field}")
|
326
|
+
|
327
|
+
if plan["type"] not in ["execution_plan", "run_summary"]:
|
328
|
+
raise ValueError(f"Invalid plan type: {plan['type']}")
|
329
|
+
|
330
|
+
if plan["schema_version"] != "1.0":
|
331
|
+
raise ValueError(
|
332
|
+
f"Unsupported schema version: {plan['schema_version']}"
|
333
|
+
)
|
334
|
+
|
335
|
+
return True
|