ostruct-cli 0.7.1__py3-none-any.whl → 0.8.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 +21 -3
- ostruct/cli/base_errors.py +1 -1
- ostruct/cli/cli.py +66 -1983
- ostruct/cli/click_options.py +460 -28
- ostruct/cli/code_interpreter.py +238 -0
- ostruct/cli/commands/__init__.py +32 -0
- ostruct/cli/commands/list_models.py +128 -0
- ostruct/cli/commands/quick_ref.py +50 -0
- ostruct/cli/commands/run.py +137 -0
- ostruct/cli/commands/update_registry.py +71 -0
- ostruct/cli/config.py +277 -0
- ostruct/cli/cost_estimation.py +134 -0
- ostruct/cli/errors.py +310 -6
- ostruct/cli/exit_codes.py +1 -0
- ostruct/cli/explicit_file_processor.py +548 -0
- ostruct/cli/field_utils.py +69 -0
- ostruct/cli/file_info.py +42 -9
- ostruct/cli/file_list.py +301 -102
- ostruct/cli/file_search.py +455 -0
- ostruct/cli/file_utils.py +47 -13
- ostruct/cli/mcp_integration.py +541 -0
- ostruct/cli/model_creation.py +150 -1
- ostruct/cli/model_validation.py +204 -0
- ostruct/cli/progress_reporting.py +398 -0
- ostruct/cli/registry_updates.py +14 -9
- ostruct/cli/runner.py +1418 -0
- ostruct/cli/schema_utils.py +113 -0
- ostruct/cli/services.py +626 -0
- ostruct/cli/template_debug.py +748 -0
- ostruct/cli/template_debug_help.py +162 -0
- ostruct/cli/template_env.py +15 -6
- ostruct/cli/template_filters.py +55 -3
- ostruct/cli/template_optimizer.py +474 -0
- ostruct/cli/template_processor.py +1080 -0
- ostruct/cli/template_rendering.py +69 -34
- ostruct/cli/token_validation.py +286 -0
- ostruct/cli/types.py +78 -0
- ostruct/cli/unattended_operation.py +269 -0
- ostruct/cli/validators.py +386 -3
- {ostruct_cli-0.7.1.dist-info → ostruct_cli-0.8.0.dist-info}/LICENSE +2 -0
- ostruct_cli-0.8.0.dist-info/METADATA +633 -0
- ostruct_cli-0.8.0.dist-info/RECORD +69 -0
- {ostruct_cli-0.7.1.dist-info → ostruct_cli-0.8.0.dist-info}/WHEEL +1 -1
- ostruct_cli-0.7.1.dist-info/METADATA +0 -369
- ostruct_cli-0.7.1.dist-info/RECORD +0 -45
- {ostruct_cli-0.7.1.dist-info → ostruct_cli-0.8.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,238 @@
|
|
1
|
+
"""Code Interpreter integration for ostruct CLI.
|
2
|
+
|
3
|
+
This module provides support for uploading files to OpenAI's Code Interpreter
|
4
|
+
and integrating code execution capabilities with the OpenAI Responses API.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import logging
|
8
|
+
import os
|
9
|
+
from pathlib import Path
|
10
|
+
from typing import Any, Dict, List
|
11
|
+
|
12
|
+
from openai import AsyncOpenAI
|
13
|
+
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
|
17
|
+
class CodeInterpreterManager:
|
18
|
+
"""Manager for Code Interpreter file uploads and tool integration."""
|
19
|
+
|
20
|
+
def __init__(self, client: AsyncOpenAI):
|
21
|
+
"""Initialize Code Interpreter manager.
|
22
|
+
|
23
|
+
Args:
|
24
|
+
client: AsyncOpenAI client instance
|
25
|
+
"""
|
26
|
+
self.client = client
|
27
|
+
self.uploaded_file_ids: List[str] = []
|
28
|
+
|
29
|
+
async def upload_files_for_code_interpreter(
|
30
|
+
self, files: List[str]
|
31
|
+
) -> List[str]:
|
32
|
+
"""Upload files for Code Interpreter (validated working pattern).
|
33
|
+
|
34
|
+
This method uploads files to OpenAI's file storage with the correct
|
35
|
+
purpose for Code Interpreter usage.
|
36
|
+
|
37
|
+
Args:
|
38
|
+
files: List of file paths to upload
|
39
|
+
|
40
|
+
Returns:
|
41
|
+
List of file IDs from successful uploads
|
42
|
+
|
43
|
+
Raises:
|
44
|
+
FileNotFoundError: If a file doesn't exist
|
45
|
+
Exception: If upload fails
|
46
|
+
"""
|
47
|
+
file_ids = []
|
48
|
+
|
49
|
+
for file_path in files:
|
50
|
+
try:
|
51
|
+
# Validate file exists
|
52
|
+
if not os.path.exists(file_path):
|
53
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
54
|
+
|
55
|
+
# Get file info
|
56
|
+
file_size = os.path.getsize(file_path)
|
57
|
+
logger.debug(
|
58
|
+
f"Uploading file: {file_path} ({file_size} bytes)"
|
59
|
+
)
|
60
|
+
|
61
|
+
# Upload with correct purpose for Code Interpreter
|
62
|
+
with open(file_path, "rb") as f:
|
63
|
+
file_obj = await self.client.files.create(
|
64
|
+
file=f,
|
65
|
+
purpose="assistants", # Validated correct purpose
|
66
|
+
)
|
67
|
+
file_ids.append(file_obj.id)
|
68
|
+
logger.debug(
|
69
|
+
f"Successfully uploaded {file_path} with ID: {file_obj.id}"
|
70
|
+
)
|
71
|
+
|
72
|
+
except Exception as e:
|
73
|
+
logger.error(f"Failed to upload file {file_path}: {e}")
|
74
|
+
# Clean up any successfully uploaded files on error
|
75
|
+
await self._cleanup_uploaded_files(file_ids)
|
76
|
+
raise
|
77
|
+
|
78
|
+
# Store for potential cleanup
|
79
|
+
self.uploaded_file_ids.extend(file_ids)
|
80
|
+
return file_ids
|
81
|
+
|
82
|
+
def build_tool_config(self, file_ids: List[str]) -> dict:
|
83
|
+
"""Build Code Interpreter tool configuration.
|
84
|
+
|
85
|
+
Creates a tool configuration compatible with the OpenAI Responses API
|
86
|
+
for Code Interpreter functionality.
|
87
|
+
|
88
|
+
Args:
|
89
|
+
file_ids: List of uploaded file IDs
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
Tool configuration dict for Responses API
|
93
|
+
"""
|
94
|
+
return {
|
95
|
+
"type": "code_interpreter",
|
96
|
+
"container": {"type": "auto", "file_ids": file_ids},
|
97
|
+
}
|
98
|
+
|
99
|
+
async def download_generated_files(
|
100
|
+
self, response_file_ids: List[str], output_dir: str = "."
|
101
|
+
) -> List[str]:
|
102
|
+
"""Download files generated by Code Interpreter.
|
103
|
+
|
104
|
+
Args:
|
105
|
+
response_file_ids: List of file IDs from Code Interpreter response
|
106
|
+
output_dir: Directory to save downloaded files
|
107
|
+
|
108
|
+
Returns:
|
109
|
+
List of local file paths where files were saved
|
110
|
+
|
111
|
+
Raises:
|
112
|
+
Exception: If download fails
|
113
|
+
"""
|
114
|
+
downloaded_paths = []
|
115
|
+
output_path = Path(output_dir)
|
116
|
+
output_path.mkdir(exist_ok=True)
|
117
|
+
|
118
|
+
for file_id in response_file_ids:
|
119
|
+
try:
|
120
|
+
# Get file info
|
121
|
+
file_info = await self.client.files.retrieve(file_id)
|
122
|
+
filename = (
|
123
|
+
file_info.filename or f"generated_file_{file_id[:8]}.dat"
|
124
|
+
)
|
125
|
+
|
126
|
+
# Download file content
|
127
|
+
file_content = await self.client.files.content(file_id)
|
128
|
+
|
129
|
+
# Save to local file
|
130
|
+
local_path = output_path / filename
|
131
|
+
with open(local_path, "wb") as f:
|
132
|
+
f.write(file_content.read())
|
133
|
+
|
134
|
+
downloaded_paths.append(str(local_path))
|
135
|
+
logger.debug(f"Downloaded generated file: {local_path}")
|
136
|
+
|
137
|
+
except Exception as e:
|
138
|
+
logger.error(f"Failed to download file {file_id}: {e}")
|
139
|
+
raise
|
140
|
+
|
141
|
+
return downloaded_paths
|
142
|
+
|
143
|
+
async def cleanup_uploaded_files(self) -> None:
|
144
|
+
"""Clean up uploaded files from OpenAI storage.
|
145
|
+
|
146
|
+
This method removes files that were uploaded during the session
|
147
|
+
to avoid accumulating files in the user's OpenAI storage.
|
148
|
+
"""
|
149
|
+
await self._cleanup_uploaded_files(self.uploaded_file_ids)
|
150
|
+
self.uploaded_file_ids.clear()
|
151
|
+
|
152
|
+
async def _cleanup_uploaded_files(self, file_ids: List[str]) -> None:
|
153
|
+
"""Internal method to clean up specific file IDs.
|
154
|
+
|
155
|
+
Args:
|
156
|
+
file_ids: List of file IDs to delete
|
157
|
+
"""
|
158
|
+
for file_id in file_ids:
|
159
|
+
try:
|
160
|
+
await self.client.files.delete(file_id)
|
161
|
+
logger.debug(f"Cleaned up uploaded file: {file_id}")
|
162
|
+
except Exception as e:
|
163
|
+
logger.warning(f"Failed to clean up file {file_id}: {e}")
|
164
|
+
|
165
|
+
def validate_files_for_upload(self, files: List[str]) -> List[str]:
|
166
|
+
"""Validate files are suitable for Code Interpreter upload.
|
167
|
+
|
168
|
+
Args:
|
169
|
+
files: List of file paths to validate
|
170
|
+
|
171
|
+
Returns:
|
172
|
+
List of validation error messages, empty if all files are valid
|
173
|
+
"""
|
174
|
+
errors = []
|
175
|
+
|
176
|
+
# Common file types supported by Code Interpreter
|
177
|
+
supported_extensions = {
|
178
|
+
".py",
|
179
|
+
".txt",
|
180
|
+
".csv",
|
181
|
+
".json",
|
182
|
+
".xlsx",
|
183
|
+
".xls",
|
184
|
+
".pdf",
|
185
|
+
".docx",
|
186
|
+
".md",
|
187
|
+
".xml",
|
188
|
+
".html",
|
189
|
+
".js",
|
190
|
+
".sql",
|
191
|
+
".log",
|
192
|
+
".yaml",
|
193
|
+
".yml",
|
194
|
+
".toml",
|
195
|
+
".ini",
|
196
|
+
}
|
197
|
+
|
198
|
+
# Size limits (approximate - OpenAI has file size limits)
|
199
|
+
max_file_size = 100 * 1024 * 1024 # 100MB
|
200
|
+
|
201
|
+
for file_path in files:
|
202
|
+
try:
|
203
|
+
if not os.path.exists(file_path):
|
204
|
+
errors.append(f"File not found: {file_path}")
|
205
|
+
continue
|
206
|
+
|
207
|
+
# Check file size
|
208
|
+
file_size = os.path.getsize(file_path)
|
209
|
+
if file_size > max_file_size:
|
210
|
+
errors.append(
|
211
|
+
f"File too large: {file_path} ({file_size / 1024 / 1024:.1f}MB > 100MB)"
|
212
|
+
)
|
213
|
+
|
214
|
+
# Check file extension
|
215
|
+
file_ext = Path(file_path).suffix.lower()
|
216
|
+
if file_ext not in supported_extensions:
|
217
|
+
logger.warning(
|
218
|
+
f"File extension {file_ext} may not be supported by Code Interpreter: {file_path}"
|
219
|
+
)
|
220
|
+
|
221
|
+
except Exception as e:
|
222
|
+
errors.append(f"Error validating file {file_path}: {e}")
|
223
|
+
|
224
|
+
return errors
|
225
|
+
|
226
|
+
def get_container_limits_info(self) -> Dict[str, Any]:
|
227
|
+
"""Get information about Code Interpreter container limits.
|
228
|
+
|
229
|
+
Returns:
|
230
|
+
Dictionary with container limit information
|
231
|
+
"""
|
232
|
+
return {
|
233
|
+
"max_runtime_minutes": 20,
|
234
|
+
"idle_timeout_minutes": 2,
|
235
|
+
"max_file_size_mb": 100,
|
236
|
+
"supported_languages": ["python"],
|
237
|
+
"note": "Container expires after 20 minutes of runtime or 2 minutes of inactivity",
|
238
|
+
}
|
@@ -0,0 +1,32 @@
|
|
1
|
+
"""Command modules for ostruct CLI."""
|
2
|
+
|
3
|
+
import click
|
4
|
+
|
5
|
+
from .list_models import list_models
|
6
|
+
from .quick_ref import quick_reference
|
7
|
+
from .run import run
|
8
|
+
from .update_registry import update_registry
|
9
|
+
|
10
|
+
|
11
|
+
def create_command_group() -> click.Group:
|
12
|
+
"""Create and configure the CLI command group with all commands."""
|
13
|
+
# Create the main CLI group
|
14
|
+
group = click.Group()
|
15
|
+
|
16
|
+
# Add all commands to the group
|
17
|
+
group.add_command(run)
|
18
|
+
group.add_command(quick_reference)
|
19
|
+
group.add_command(update_registry)
|
20
|
+
group.add_command(list_models)
|
21
|
+
|
22
|
+
return group
|
23
|
+
|
24
|
+
|
25
|
+
# Export commands for easy importing
|
26
|
+
__all__ = [
|
27
|
+
"run",
|
28
|
+
"quick_reference",
|
29
|
+
"update_registry",
|
30
|
+
"list_models",
|
31
|
+
"create_command_group",
|
32
|
+
]
|
@@ -0,0 +1,128 @@
|
|
1
|
+
"""List models command for ostruct CLI."""
|
2
|
+
|
3
|
+
import json
|
4
|
+
import sys
|
5
|
+
|
6
|
+
import click
|
7
|
+
from openai_model_registry import ModelRegistry
|
8
|
+
|
9
|
+
from ..exit_codes import ExitCode
|
10
|
+
|
11
|
+
|
12
|
+
@click.command("list-models")
|
13
|
+
@click.option(
|
14
|
+
"--format",
|
15
|
+
type=click.Choice(["table", "json", "simple"]),
|
16
|
+
default="table",
|
17
|
+
help="Output format for model list",
|
18
|
+
)
|
19
|
+
@click.option(
|
20
|
+
"--show-deprecated",
|
21
|
+
is_flag=True,
|
22
|
+
help="Include deprecated models in output",
|
23
|
+
)
|
24
|
+
def list_models(format: str = "table", show_deprecated: bool = False) -> None:
|
25
|
+
"""List available models from the registry."""
|
26
|
+
try:
|
27
|
+
registry = ModelRegistry.get_instance()
|
28
|
+
models = registry.models
|
29
|
+
|
30
|
+
# Filter models if not showing deprecated
|
31
|
+
if not show_deprecated:
|
32
|
+
# Filter out deprecated models (this depends on registry implementation)
|
33
|
+
filtered_models = []
|
34
|
+
for model_id in models:
|
35
|
+
try:
|
36
|
+
capabilities = registry.get_capabilities(model_id)
|
37
|
+
# If we can get capabilities, it's likely not deprecated
|
38
|
+
filtered_models.append(
|
39
|
+
{
|
40
|
+
"id": model_id,
|
41
|
+
"context_window": capabilities.context_window,
|
42
|
+
"max_output": capabilities.max_output_tokens,
|
43
|
+
}
|
44
|
+
)
|
45
|
+
except Exception:
|
46
|
+
# Skip models that can't be accessed (likely deprecated)
|
47
|
+
continue
|
48
|
+
models_data = filtered_models
|
49
|
+
else:
|
50
|
+
# Include all models
|
51
|
+
models_data = []
|
52
|
+
for model_id in models:
|
53
|
+
try:
|
54
|
+
capabilities = registry.get_capabilities(model_id)
|
55
|
+
models_data.append(
|
56
|
+
{
|
57
|
+
"id": model_id,
|
58
|
+
"context_window": capabilities.context_window,
|
59
|
+
"max_output": capabilities.max_output_tokens,
|
60
|
+
"status": "active",
|
61
|
+
}
|
62
|
+
)
|
63
|
+
except Exception:
|
64
|
+
models_data.append(
|
65
|
+
{
|
66
|
+
"id": model_id,
|
67
|
+
"context_window": "N/A",
|
68
|
+
"max_output": "N/A",
|
69
|
+
"status": "deprecated",
|
70
|
+
}
|
71
|
+
)
|
72
|
+
|
73
|
+
if format == "table":
|
74
|
+
# Calculate dynamic column widths based on actual data
|
75
|
+
max_id_width = (
|
76
|
+
max(len(str(model["id"])) for model in models_data)
|
77
|
+
if models_data
|
78
|
+
else 8
|
79
|
+
)
|
80
|
+
max_id_width = max(max_id_width, len("Model ID"))
|
81
|
+
|
82
|
+
max_context_width = (
|
83
|
+
15 # Keep reasonable default for context window
|
84
|
+
)
|
85
|
+
max_output_width = 12 # Keep reasonable default for max output
|
86
|
+
status_width = 10 # Keep fixed for status
|
87
|
+
|
88
|
+
# Ensure minimum widths for readability
|
89
|
+
id_width = max(max_id_width, 8)
|
90
|
+
|
91
|
+
total_width = (
|
92
|
+
id_width
|
93
|
+
+ max_context_width
|
94
|
+
+ max_output_width
|
95
|
+
+ status_width
|
96
|
+
+ 6
|
97
|
+
) # 6 for spacing
|
98
|
+
|
99
|
+
click.echo("Available Models:")
|
100
|
+
click.echo("-" * total_width)
|
101
|
+
click.echo(
|
102
|
+
f"{'Model ID':<{id_width}} {'Context Window':<{max_context_width}} {'Max Output':<{max_output_width}} {'Status':<{status_width}}"
|
103
|
+
)
|
104
|
+
click.echo("-" * total_width)
|
105
|
+
for model in models_data:
|
106
|
+
status = model.get("status", "active")
|
107
|
+
context = (
|
108
|
+
f"{model['context_window']:,}"
|
109
|
+
if isinstance(model["context_window"], int)
|
110
|
+
else model["context_window"]
|
111
|
+
)
|
112
|
+
output = (
|
113
|
+
f"{model['max_output']:,}"
|
114
|
+
if isinstance(model["max_output"], int)
|
115
|
+
else model["max_output"]
|
116
|
+
)
|
117
|
+
click.echo(
|
118
|
+
f"{model['id']:<{id_width}} {context:<{max_context_width}} {output:<{max_output_width}} {status:<{status_width}}"
|
119
|
+
)
|
120
|
+
elif format == "json":
|
121
|
+
click.echo(json.dumps(models_data, indent=2))
|
122
|
+
else: # simple
|
123
|
+
for model in models_data:
|
124
|
+
click.echo(model["id"])
|
125
|
+
|
126
|
+
except Exception as e:
|
127
|
+
click.echo(f"❌ Error listing models: {str(e)}")
|
128
|
+
sys.exit(ExitCode.API_ERROR.value)
|
@@ -0,0 +1,50 @@
|
|
1
|
+
"""Quick reference command for ostruct CLI."""
|
2
|
+
|
3
|
+
import click
|
4
|
+
|
5
|
+
|
6
|
+
@click.command("quick-ref")
|
7
|
+
def quick_reference() -> None:
|
8
|
+
"""Show quick reference for file routing and common usage patterns."""
|
9
|
+
quick_ref = """
|
10
|
+
🚀 OSTRUCT QUICK REFERENCE
|
11
|
+
|
12
|
+
📁 FILE ROUTING:
|
13
|
+
-ft FILE 📄 Template access only (config files, small data)
|
14
|
+
-fc FILE 💻 Code Interpreter upload (data files, scripts)
|
15
|
+
-fs FILE 🔍 File Search vector store (documents, manuals)
|
16
|
+
|
17
|
+
-dt DIR 📁 Template directory (config dirs, reference data)
|
18
|
+
-dc DIR 📂 Code execution directory (datasets, code repos)
|
19
|
+
-ds DIR 📁 Search directory (documentation, knowledge)
|
20
|
+
|
21
|
+
🔄 ADVANCED ROUTING:
|
22
|
+
--file-for code-interpreter data.csv Single tool, single file
|
23
|
+
--file-for file-search docs.pdf Single tool, single file
|
24
|
+
--file-for code-interpreter shared.json --file-for file-search shared.json Multi-tool routing
|
25
|
+
|
26
|
+
🏷️ VARIABLES:
|
27
|
+
-V name=value Simple string variables
|
28
|
+
-J config='{"key":"value"}' JSON structured data
|
29
|
+
|
30
|
+
🔌 TOOLS:
|
31
|
+
--mcp-server label@https://server.com/sse MCP server integration
|
32
|
+
--timeout 7200 2-hour timeout for long operations
|
33
|
+
|
34
|
+
⚡ COMMON PATTERNS:
|
35
|
+
# Basic template rendering
|
36
|
+
ostruct run template.j2 schema.json -V env=prod
|
37
|
+
|
38
|
+
# Data analysis with Code Interpreter
|
39
|
+
ostruct run analysis.j2 schema.json -fc data.csv -V task=analyze
|
40
|
+
|
41
|
+
# Document search + processing
|
42
|
+
ostruct run search.j2 schema.json -fs docs/ -ft config.yaml
|
43
|
+
|
44
|
+
# Multi-tool workflow
|
45
|
+
ostruct run workflow.j2 schema.json -fc raw_data.csv -fs knowledge/ -ft config.json
|
46
|
+
|
47
|
+
📖 Full help: ostruct run --help
|
48
|
+
📖 Documentation: https://ostruct.readthedocs.io
|
49
|
+
"""
|
50
|
+
click.echo(quick_ref)
|
@@ -0,0 +1,137 @@
|
|
1
|
+
"""Run command for ostruct CLI."""
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
import json
|
5
|
+
import logging
|
6
|
+
import sys
|
7
|
+
from typing import Any
|
8
|
+
|
9
|
+
import click
|
10
|
+
|
11
|
+
from ..click_options import all_options
|
12
|
+
from ..config import OstructConfig
|
13
|
+
from ..errors import (
|
14
|
+
CLIError,
|
15
|
+
InvalidJSONError,
|
16
|
+
SchemaFileError,
|
17
|
+
SchemaValidationError,
|
18
|
+
handle_error,
|
19
|
+
)
|
20
|
+
from ..exit_codes import ExitCode
|
21
|
+
from ..runner import run_cli_async
|
22
|
+
from ..types import CLIParams
|
23
|
+
|
24
|
+
logger = logging.getLogger(__name__)
|
25
|
+
|
26
|
+
|
27
|
+
@click.command()
|
28
|
+
@click.argument("task_template", type=click.Path(exists=True))
|
29
|
+
@click.argument("schema_file", type=click.Path(exists=True))
|
30
|
+
@all_options
|
31
|
+
@click.pass_context
|
32
|
+
def run(
|
33
|
+
ctx: click.Context,
|
34
|
+
task_template: str,
|
35
|
+
schema_file: str,
|
36
|
+
**kwargs: Any,
|
37
|
+
) -> None:
|
38
|
+
"""Run structured output generation with multi-tool integration.
|
39
|
+
|
40
|
+
\b
|
41
|
+
📁 FILE ROUTING OPTIONS:
|
42
|
+
|
43
|
+
Template Access Only:
|
44
|
+
-ft, --file-for-template FILE Files available in template only
|
45
|
+
-dt, --dir-for-template DIR Directories for template access
|
46
|
+
|
47
|
+
Code Interpreter (execution & analysis):
|
48
|
+
-fc, --file-for-code-interpreter FILE Upload files for code execution
|
49
|
+
-dc, --dir-for-code-interpreter DIR Upload directories for analysis
|
50
|
+
|
51
|
+
File Search (document retrieval):
|
52
|
+
-fs, --file-for-file-search FILE Upload files for vector search
|
53
|
+
-ds, --dir-for-search DIR Upload directories for search
|
54
|
+
|
55
|
+
Advanced Routing:
|
56
|
+
--file-for TOOL PATH Route files to specific tools
|
57
|
+
Example: --file-for code-interpreter data.json
|
58
|
+
|
59
|
+
\b
|
60
|
+
🔧 TOOL INTEGRATION:
|
61
|
+
|
62
|
+
MCP Servers:
|
63
|
+
--mcp-server [LABEL@]URL Connect to MCP server
|
64
|
+
Example: --mcp-server deepwiki@https://mcp.deepwiki.com/sse
|
65
|
+
|
66
|
+
\b
|
67
|
+
⚡ EXAMPLES:
|
68
|
+
|
69
|
+
Basic usage:
|
70
|
+
ostruct run template.j2 schema.json -V name=value
|
71
|
+
|
72
|
+
Multi-tool explicit routing:
|
73
|
+
ostruct run analysis.j2 schema.json -fc data.csv -fs docs.pdf -ft config.yaml
|
74
|
+
|
75
|
+
Legacy compatibility (still works):
|
76
|
+
ostruct run template.j2 schema.json -f config main.py -d src ./src
|
77
|
+
|
78
|
+
\b
|
79
|
+
Arguments:
|
80
|
+
TASK_TEMPLATE Path to Jinja2 template file
|
81
|
+
SCHEMA_FILE Path to JSON schema file defining output structure
|
82
|
+
"""
|
83
|
+
try:
|
84
|
+
# Convert Click parameters to typed dict
|
85
|
+
params: CLIParams = {
|
86
|
+
"task_file": task_template,
|
87
|
+
"task": None,
|
88
|
+
"schema_file": schema_file,
|
89
|
+
}
|
90
|
+
# Add all kwargs to params (type ignore for dynamic key assignment)
|
91
|
+
for k, v in kwargs.items():
|
92
|
+
params[k] = v # type: ignore[literal-required]
|
93
|
+
|
94
|
+
# Apply configuration defaults if values not explicitly provided
|
95
|
+
# Check for command-level config option first, then group-level
|
96
|
+
command_config = kwargs.get("config")
|
97
|
+
if command_config:
|
98
|
+
config = OstructConfig.load(command_config)
|
99
|
+
else:
|
100
|
+
config = ctx.obj.get("config") if ctx.obj else OstructConfig()
|
101
|
+
|
102
|
+
if params.get("model") is None:
|
103
|
+
params["model"] = config.get_model_default()
|
104
|
+
|
105
|
+
# Run the async function synchronously
|
106
|
+
loop = asyncio.new_event_loop()
|
107
|
+
asyncio.set_event_loop(loop)
|
108
|
+
try:
|
109
|
+
exit_code = loop.run_until_complete(run_cli_async(params))
|
110
|
+
sys.exit(int(exit_code))
|
111
|
+
except SchemaValidationError as e:
|
112
|
+
# Log the error with full context
|
113
|
+
logger.error("Schema validation error: %s", str(e))
|
114
|
+
if e.context:
|
115
|
+
logger.debug(
|
116
|
+
"Error context: %s", json.dumps(e.context, indent=2)
|
117
|
+
)
|
118
|
+
# Re-raise to preserve error chain and exit code
|
119
|
+
raise
|
120
|
+
except (CLIError, InvalidJSONError, SchemaFileError) as e:
|
121
|
+
handle_error(e)
|
122
|
+
sys.exit(
|
123
|
+
e.exit_code
|
124
|
+
if hasattr(e, "exit_code")
|
125
|
+
else ExitCode.INTERNAL_ERROR
|
126
|
+
)
|
127
|
+
except click.UsageError as e:
|
128
|
+
handle_error(e)
|
129
|
+
sys.exit(ExitCode.USAGE_ERROR)
|
130
|
+
except Exception as e:
|
131
|
+
handle_error(e)
|
132
|
+
sys.exit(ExitCode.INTERNAL_ERROR)
|
133
|
+
finally:
|
134
|
+
loop.close()
|
135
|
+
except KeyboardInterrupt:
|
136
|
+
logger.info("Operation cancelled by user")
|
137
|
+
raise
|
@@ -0,0 +1,71 @@
|
|
1
|
+
"""Update registry command for ostruct CLI."""
|
2
|
+
|
3
|
+
import sys
|
4
|
+
from typing import Optional
|
5
|
+
|
6
|
+
import click
|
7
|
+
from openai_model_registry import ModelRegistry
|
8
|
+
|
9
|
+
from ..exit_codes import ExitCode
|
10
|
+
|
11
|
+
|
12
|
+
@click.command("update-registry")
|
13
|
+
@click.option(
|
14
|
+
"--url",
|
15
|
+
help="URL to fetch the registry from. Defaults to official repository.",
|
16
|
+
default=None,
|
17
|
+
)
|
18
|
+
@click.option(
|
19
|
+
"--force",
|
20
|
+
is_flag=True,
|
21
|
+
help="Force update even if the registry is already up to date.",
|
22
|
+
default=False,
|
23
|
+
)
|
24
|
+
def update_registry(url: Optional[str] = None, force: bool = False) -> None:
|
25
|
+
"""Update the model registry with the latest model definitions.
|
26
|
+
|
27
|
+
This command fetches the latest model registry from the official repository
|
28
|
+
or a custom URL if provided, and updates the local registry file.
|
29
|
+
|
30
|
+
Example:
|
31
|
+
ostruct update-registry
|
32
|
+
ostruct update-registry --url https://example.com/models.yml
|
33
|
+
"""
|
34
|
+
try:
|
35
|
+
registry = ModelRegistry.get_instance()
|
36
|
+
|
37
|
+
# Show current registry config path
|
38
|
+
config_path = registry.config.registry_path
|
39
|
+
click.echo(f"Current registry file: {config_path}")
|
40
|
+
|
41
|
+
if force:
|
42
|
+
click.echo("🔄 Forcing registry update...")
|
43
|
+
refresh_result = registry.refresh_from_remote(url)
|
44
|
+
if refresh_result.success:
|
45
|
+
click.echo("✅ Registry successfully updated!")
|
46
|
+
else:
|
47
|
+
click.echo(
|
48
|
+
f"❌ Failed to update registry: {refresh_result.message}"
|
49
|
+
)
|
50
|
+
return
|
51
|
+
|
52
|
+
click.echo("🔍 Checking for registry updates...")
|
53
|
+
update_result = registry.check_for_updates()
|
54
|
+
|
55
|
+
if update_result.status.value == "update_available":
|
56
|
+
click.echo(f"📦 Update available: {update_result.message}")
|
57
|
+
click.echo("🔄 Updating registry...")
|
58
|
+
refresh_result = registry.refresh_from_remote(url)
|
59
|
+
if refresh_result.success:
|
60
|
+
click.echo("✅ Registry successfully updated!")
|
61
|
+
else:
|
62
|
+
click.echo(
|
63
|
+
f"❌ Failed to update registry: {refresh_result.message}"
|
64
|
+
)
|
65
|
+
elif update_result.status.value == "already_current":
|
66
|
+
click.echo("✅ Registry is already up to date")
|
67
|
+
else:
|
68
|
+
click.echo(f"⚠️ Registry check failed: {update_result.message}")
|
69
|
+
except Exception as e:
|
70
|
+
click.echo(f"❌ Error updating registry: {str(e)}")
|
71
|
+
sys.exit(ExitCode.API_ERROR.value)
|