ostruct-cli 0.8.29__py3-none-any.whl → 1.0.1__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 +157 -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 +175 -5
- ostruct/cli/explicit_file_processor.py +0 -15
- ostruct/cli/file_info.py +97 -15
- ostruct/cli/file_list.py +43 -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/validators.py +255 -54
- {ostruct_cli-0.8.29.dist-info → ostruct_cli-1.0.1.dist-info}/METADATA +231 -128
- ostruct_cli-1.0.1.dist-info/RECORD +80 -0
- ostruct/cli/commands/quick_ref.py +0 -54
- ostruct/cli/template_optimizer.py +0 -478
- ostruct_cli-0.8.29.dist-info/RECORD +0 -71
- {ostruct_cli-0.8.29.dist-info → ostruct_cli-1.0.1.dist-info}/LICENSE +0 -0
- {ostruct_cli-0.8.29.dist-info → ostruct_cli-1.0.1.dist-info}/WHEEL +0 -0
- {ostruct_cli-0.8.29.dist-info → ostruct_cli-1.0.1.dist-info}/entry_points.txt +0 -0
@@ -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
|