ostruct-cli 0.7.2__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.2.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.2.dist-info → ostruct_cli-0.8.0.dist-info}/WHEEL +1 -1
- ostruct_cli-0.7.2.dist-info/METADATA +0 -370
- ostruct_cli-0.7.2.dist-info/RECORD +0 -45
- {ostruct_cli-0.7.2.dist-info → ostruct_cli-0.8.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,398 @@
|
|
1
|
+
"""Enhanced progress reporting with user-centric language."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
from dataclasses import dataclass
|
5
|
+
from typing import List, Optional
|
6
|
+
|
7
|
+
import click
|
8
|
+
|
9
|
+
logger = logging.getLogger(__name__)
|
10
|
+
|
11
|
+
|
12
|
+
@dataclass
|
13
|
+
class CostBreakdown:
|
14
|
+
"""Cost breakdown for transparency."""
|
15
|
+
|
16
|
+
total: float
|
17
|
+
input_cost: float
|
18
|
+
output_cost: float
|
19
|
+
input_tokens: int
|
20
|
+
output_tokens: int
|
21
|
+
code_interpreter_cost: Optional[float] = None
|
22
|
+
file_search_cost: Optional[float] = None
|
23
|
+
model: str = "gpt-4o"
|
24
|
+
|
25
|
+
@property
|
26
|
+
def has_tool_costs(self) -> bool:
|
27
|
+
"""Check if there are additional tool costs."""
|
28
|
+
return bool(self.code_interpreter_cost or self.file_search_cost)
|
29
|
+
|
30
|
+
|
31
|
+
@dataclass
|
32
|
+
class ProcessingResult:
|
33
|
+
"""Processing result summary for user reporting."""
|
34
|
+
|
35
|
+
model: str
|
36
|
+
analysis_summary: str
|
37
|
+
search_summary: Optional[str] = None
|
38
|
+
completion_summary: str = "Processing completed successfully"
|
39
|
+
files_processed: int = 0
|
40
|
+
tools_used: Optional[List[str]] = None
|
41
|
+
|
42
|
+
def __post_init__(self) -> None:
|
43
|
+
if self.tools_used is None:
|
44
|
+
self.tools_used = []
|
45
|
+
|
46
|
+
|
47
|
+
class EnhancedProgressReporter:
|
48
|
+
"""Enhanced progress reporter with user-friendly language and transparency."""
|
49
|
+
|
50
|
+
def __init__(self, verbose: bool = False, progress_level: str = "basic"):
|
51
|
+
"""Initialize the progress reporter.
|
52
|
+
|
53
|
+
Args:
|
54
|
+
verbose: Enable verbose logging output
|
55
|
+
progress_level: Progress level (none, basic, detailed)
|
56
|
+
"""
|
57
|
+
self.verbose = verbose
|
58
|
+
self.progress_level = progress_level
|
59
|
+
self.should_report = progress_level != "none"
|
60
|
+
self.detailed = progress_level == "detailed" or verbose
|
61
|
+
|
62
|
+
def report_phase(self, phase_name: str, emoji: str = "⚙️") -> None:
|
63
|
+
"""Report the start of a major processing phase.
|
64
|
+
|
65
|
+
Args:
|
66
|
+
phase_name: Name of the processing phase
|
67
|
+
emoji: Emoji icon for the phase
|
68
|
+
"""
|
69
|
+
if self.should_report:
|
70
|
+
click.echo(f"{emoji} {phase_name}...", err=True)
|
71
|
+
|
72
|
+
def report_optimization(self, optimizations: List[str]) -> None:
|
73
|
+
"""Report template optimization results with user-friendly language.
|
74
|
+
|
75
|
+
Args:
|
76
|
+
optimizations: List of optimization transformations applied
|
77
|
+
"""
|
78
|
+
if not self.should_report or not optimizations:
|
79
|
+
return
|
80
|
+
|
81
|
+
if self.detailed:
|
82
|
+
click.echo("🔧 Template optimization:", err=True)
|
83
|
+
for optimization in optimizations:
|
84
|
+
# Convert technical language to user-friendly descriptions
|
85
|
+
user_friendly = self._humanize_optimization(optimization)
|
86
|
+
click.echo(f" → {user_friendly}", err=True)
|
87
|
+
else:
|
88
|
+
click.echo(
|
89
|
+
f"🔧 Optimized template for better AI performance ({len(optimizations)} improvements)",
|
90
|
+
err=True,
|
91
|
+
)
|
92
|
+
|
93
|
+
def report_file_routing(
|
94
|
+
self,
|
95
|
+
template_files: List[str],
|
96
|
+
container_files: List[str],
|
97
|
+
vector_files: List[str],
|
98
|
+
) -> None:
|
99
|
+
"""Report file routing decisions in user-friendly terms.
|
100
|
+
|
101
|
+
Args:
|
102
|
+
template_files: Files routed to template access
|
103
|
+
container_files: Files routed to Code Interpreter
|
104
|
+
vector_files: Files routed to File Search
|
105
|
+
"""
|
106
|
+
if not self.should_report:
|
107
|
+
return
|
108
|
+
|
109
|
+
total_files = (
|
110
|
+
len(template_files) + len(container_files) + len(vector_files)
|
111
|
+
)
|
112
|
+
if total_files == 0:
|
113
|
+
return
|
114
|
+
|
115
|
+
if self.detailed:
|
116
|
+
click.echo("📂 File routing:", err=True)
|
117
|
+
for file in template_files:
|
118
|
+
click.echo(
|
119
|
+
f" → {file}: available in template for direct access",
|
120
|
+
err=True,
|
121
|
+
)
|
122
|
+
for file in container_files:
|
123
|
+
click.echo(
|
124
|
+
f" → {file}: uploaded to Code Interpreter for analysis",
|
125
|
+
err=True,
|
126
|
+
)
|
127
|
+
for file in vector_files:
|
128
|
+
click.echo(
|
129
|
+
f" → {file}: uploaded to File Search for semantic search",
|
130
|
+
err=True,
|
131
|
+
)
|
132
|
+
else:
|
133
|
+
tools_used = []
|
134
|
+
if container_files:
|
135
|
+
tools_used.append(
|
136
|
+
f"Code Interpreter ({len(container_files)} files)"
|
137
|
+
)
|
138
|
+
if vector_files:
|
139
|
+
tools_used.append(f"File Search ({len(vector_files)} files)")
|
140
|
+
if template_files:
|
141
|
+
tools_used.append(f"Template ({len(template_files)} files)")
|
142
|
+
|
143
|
+
tools_str = ", ".join(tools_used)
|
144
|
+
click.echo(
|
145
|
+
f"📂 Routed {total_files} files to: {tools_str}", err=True
|
146
|
+
)
|
147
|
+
|
148
|
+
def report_processing_start(
|
149
|
+
self, model: str, tools_used: List[str]
|
150
|
+
) -> None:
|
151
|
+
"""Report the start of AI processing with clear context.
|
152
|
+
|
153
|
+
Args:
|
154
|
+
model: Model being used for processing
|
155
|
+
tools_used: List of tools being utilized
|
156
|
+
"""
|
157
|
+
if not self.should_report:
|
158
|
+
return
|
159
|
+
|
160
|
+
tools_str = ", ".join(tools_used) if tools_used else "template only"
|
161
|
+
click.echo(
|
162
|
+
f"🤖 Processing with {model} using {tools_str}...", err=True
|
163
|
+
)
|
164
|
+
|
165
|
+
def report_processing_results(self, result: ProcessingResult) -> None:
|
166
|
+
"""Report AI processing outcomes in a user-friendly way.
|
167
|
+
|
168
|
+
Args:
|
169
|
+
result: Processing result with summary information
|
170
|
+
"""
|
171
|
+
if not self.should_report:
|
172
|
+
return
|
173
|
+
|
174
|
+
if self.detailed:
|
175
|
+
click.echo("📦 Processing results:", err=True)
|
176
|
+
click.echo(f"├── Model: {result.model}", err=True)
|
177
|
+
if result.files_processed > 0:
|
178
|
+
click.echo(
|
179
|
+
f"├── Files processed: {result.files_processed}", err=True
|
180
|
+
)
|
181
|
+
if result.tools_used:
|
182
|
+
click.echo(
|
183
|
+
f"├── Tools used: {', '.join(result.tools_used)}", err=True
|
184
|
+
)
|
185
|
+
if result.search_summary:
|
186
|
+
click.echo(f"├── 🔍 {result.search_summary}", err=True)
|
187
|
+
click.echo(f"└── ✅ {result.completion_summary}", err=True)
|
188
|
+
else:
|
189
|
+
click.echo(f"✅ {result.completion_summary}", err=True)
|
190
|
+
|
191
|
+
def report_cost_breakdown(self, cost_info: CostBreakdown) -> None:
|
192
|
+
"""Report transparent cost information to build user trust.
|
193
|
+
|
194
|
+
Args:
|
195
|
+
cost_info: Detailed cost breakdown information
|
196
|
+
"""
|
197
|
+
if not self.should_report:
|
198
|
+
return
|
199
|
+
|
200
|
+
if self.detailed:
|
201
|
+
click.echo(
|
202
|
+
f"💰 Cost breakdown: ${cost_info.total:.4f} total", err=True
|
203
|
+
)
|
204
|
+
click.echo(
|
205
|
+
f" ├── Input tokens ({cost_info.input_tokens:,}): ${cost_info.input_cost:.4f}",
|
206
|
+
err=True,
|
207
|
+
)
|
208
|
+
click.echo(
|
209
|
+
f" ├── Output tokens ({cost_info.output_tokens:,}): ${cost_info.output_cost:.4f}",
|
210
|
+
err=True,
|
211
|
+
)
|
212
|
+
if cost_info.code_interpreter_cost:
|
213
|
+
click.echo(
|
214
|
+
f" ├── Code Interpreter: ${cost_info.code_interpreter_cost:.4f}",
|
215
|
+
err=True,
|
216
|
+
)
|
217
|
+
if cost_info.file_search_cost:
|
218
|
+
click.echo(
|
219
|
+
f" └── File Search: ${cost_info.file_search_cost:.4f}",
|
220
|
+
err=True,
|
221
|
+
)
|
222
|
+
else:
|
223
|
+
if cost_info.total > 0.01: # Only show cost if significant
|
224
|
+
if cost_info.has_tool_costs:
|
225
|
+
click.echo(
|
226
|
+
f"💰 Total cost: ${cost_info.total:.3f} (model + tools)",
|
227
|
+
err=True,
|
228
|
+
)
|
229
|
+
else:
|
230
|
+
click.echo(
|
231
|
+
f"💰 Total cost: ${cost_info.total:.3f}", err=True
|
232
|
+
)
|
233
|
+
|
234
|
+
def report_file_downloads(
|
235
|
+
self, downloaded_files: List[str], download_dir: str
|
236
|
+
) -> None:
|
237
|
+
"""Report file download operations clearly.
|
238
|
+
|
239
|
+
Args:
|
240
|
+
downloaded_files: List of downloaded file paths
|
241
|
+
download_dir: Directory where files were downloaded
|
242
|
+
"""
|
243
|
+
if not self.should_report or not downloaded_files:
|
244
|
+
return
|
245
|
+
|
246
|
+
if self.detailed:
|
247
|
+
click.echo(
|
248
|
+
f"📥 Downloaded {len(downloaded_files)} generated files to {download_dir}:",
|
249
|
+
err=True,
|
250
|
+
)
|
251
|
+
for file_path in downloaded_files:
|
252
|
+
click.echo(f" → {file_path}", err=True)
|
253
|
+
else:
|
254
|
+
click.echo(
|
255
|
+
f"📥 Downloaded {len(downloaded_files)} files to {download_dir}",
|
256
|
+
err=True,
|
257
|
+
)
|
258
|
+
|
259
|
+
def report_error(
|
260
|
+
self,
|
261
|
+
error_type: str,
|
262
|
+
error_message: str,
|
263
|
+
suggestions: Optional[List[str]] = None,
|
264
|
+
) -> None:
|
265
|
+
"""Report errors with helpful context and suggestions.
|
266
|
+
|
267
|
+
Args:
|
268
|
+
error_type: Type of error for categorization
|
269
|
+
error_message: Main error message
|
270
|
+
suggestions: Optional list of actionable suggestions
|
271
|
+
"""
|
272
|
+
if not self.should_report:
|
273
|
+
return
|
274
|
+
|
275
|
+
click.echo(f"❌ {error_type}: {error_message}", err=True)
|
276
|
+
|
277
|
+
if suggestions and self.detailed:
|
278
|
+
click.echo("💡 Suggestions:", err=True)
|
279
|
+
for suggestion in suggestions:
|
280
|
+
click.echo(f" • {suggestion}", err=True)
|
281
|
+
|
282
|
+
def report_validation_results(
|
283
|
+
self,
|
284
|
+
schema_valid: bool,
|
285
|
+
template_valid: bool,
|
286
|
+
token_count: int,
|
287
|
+
token_limit: int,
|
288
|
+
) -> None:
|
289
|
+
"""Report validation results with clear status indicators.
|
290
|
+
|
291
|
+
Args:
|
292
|
+
schema_valid: Whether schema validation passed
|
293
|
+
template_valid: Whether template validation passed
|
294
|
+
token_count: Current token count
|
295
|
+
token_limit: Token limit for the model
|
296
|
+
"""
|
297
|
+
if not self.should_report:
|
298
|
+
return
|
299
|
+
|
300
|
+
if self.detailed:
|
301
|
+
click.echo("✅ Validation results:", err=True)
|
302
|
+
click.echo(
|
303
|
+
f" ├── Schema: {'✅ Valid' if schema_valid else '❌ Invalid'}",
|
304
|
+
err=True,
|
305
|
+
)
|
306
|
+
click.echo(
|
307
|
+
f" ├── Template: {'✅ Valid' if template_valid else '❌ Invalid'}",
|
308
|
+
err=True,
|
309
|
+
)
|
310
|
+
click.echo(
|
311
|
+
f" └── Tokens: {token_count:,} / {token_limit:,} ({(token_count / token_limit) * 100:.1f}%)",
|
312
|
+
err=True,
|
313
|
+
)
|
314
|
+
else:
|
315
|
+
if schema_valid and template_valid:
|
316
|
+
usage_pct = (token_count / token_limit) * 100
|
317
|
+
if usage_pct > 80:
|
318
|
+
click.echo(
|
319
|
+
f"✅ Validation passed (⚠️ {usage_pct:.0f}% token usage)",
|
320
|
+
err=True,
|
321
|
+
)
|
322
|
+
else:
|
323
|
+
click.echo("✅ Validation passed", err=True)
|
324
|
+
|
325
|
+
def _humanize_optimization(self, technical_message: str) -> str:
|
326
|
+
"""Convert technical optimization messages to user-friendly language.
|
327
|
+
|
328
|
+
Args:
|
329
|
+
technical_message: Technical optimization message
|
330
|
+
|
331
|
+
Returns:
|
332
|
+
User-friendly description of the optimization
|
333
|
+
"""
|
334
|
+
# Convert technical messages to user-friendly descriptions
|
335
|
+
if (
|
336
|
+
"moved" in technical_message.lower()
|
337
|
+
and "appendix" in technical_message.lower()
|
338
|
+
):
|
339
|
+
file_name = technical_message.split("Moved ")[-1].split(
|
340
|
+
" to appendix"
|
341
|
+
)[0]
|
342
|
+
return f"Moved large file '{file_name}' to organized appendix"
|
343
|
+
elif "built structured appendix" in technical_message.lower():
|
344
|
+
return "Organized file content into structured appendix for better AI processing"
|
345
|
+
elif "moved directory" in technical_message.lower():
|
346
|
+
return technical_message.replace(
|
347
|
+
"Moved directory", "Organized directory"
|
348
|
+
)
|
349
|
+
else:
|
350
|
+
return technical_message
|
351
|
+
|
352
|
+
|
353
|
+
# Global progress reporter instance
|
354
|
+
_progress_reporter: Optional[EnhancedProgressReporter] = None
|
355
|
+
|
356
|
+
|
357
|
+
def get_progress_reporter() -> EnhancedProgressReporter:
|
358
|
+
"""Get the global progress reporter instance.
|
359
|
+
|
360
|
+
Returns:
|
361
|
+
Global progress reporter instance
|
362
|
+
"""
|
363
|
+
global _progress_reporter
|
364
|
+
if _progress_reporter is None:
|
365
|
+
_progress_reporter = EnhancedProgressReporter()
|
366
|
+
return _progress_reporter
|
367
|
+
|
368
|
+
|
369
|
+
def configure_progress_reporter(
|
370
|
+
verbose: bool = False, progress_level: str = "basic"
|
371
|
+
) -> None:
|
372
|
+
"""Configure the global progress reporter.
|
373
|
+
|
374
|
+
Args:
|
375
|
+
verbose: Enable verbose logging output
|
376
|
+
progress_level: Progress level (none, basic, detailed)
|
377
|
+
"""
|
378
|
+
global _progress_reporter
|
379
|
+
_progress_reporter = EnhancedProgressReporter(verbose, progress_level)
|
380
|
+
|
381
|
+
|
382
|
+
def report_phase(phase_name: str, emoji: str = "⚙️") -> None:
|
383
|
+
"""Convenience function to report a processing phase."""
|
384
|
+
get_progress_reporter().report_phase(phase_name, emoji)
|
385
|
+
|
386
|
+
|
387
|
+
def report_success(message: str) -> None:
|
388
|
+
"""Convenience function to report success."""
|
389
|
+
reporter = get_progress_reporter()
|
390
|
+
if reporter.should_report:
|
391
|
+
click.echo(f"✅ {message}", err=True)
|
392
|
+
|
393
|
+
|
394
|
+
def report_info(message: str, emoji: str = "ℹ️") -> None:
|
395
|
+
"""Convenience function to report information."""
|
396
|
+
reporter = get_progress_reporter()
|
397
|
+
if reporter.should_report:
|
398
|
+
click.echo(f"{emoji} {message}", err=True)
|
ostruct/cli/registry_updates.py
CHANGED
@@ -11,15 +11,20 @@ import time
|
|
11
11
|
from pathlib import Path
|
12
12
|
from typing import Optional, Tuple
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
# Model Registry Integration - Using external openai-model-registry library
|
15
|
+
from openai_model_registry import ModelRegistry
|
16
|
+
|
17
|
+
|
18
|
+
# For compatibility with existing code
|
19
|
+
class RegistryUpdateStatus:
|
20
|
+
UPDATE_AVAILABLE = "UPDATE_AVAILABLE"
|
21
|
+
ALREADY_CURRENT = "ALREADY_CURRENT"
|
22
|
+
|
18
23
|
|
19
24
|
logger = logging.getLogger(__name__)
|
20
25
|
|
21
26
|
# Constants
|
22
|
-
UPDATE_CHECK_ENV_VAR = "
|
27
|
+
UPDATE_CHECK_ENV_VAR = "OSTRUCT_DISABLE_REGISTRY_UPDATE_CHECKS"
|
23
28
|
UPDATE_CHECK_INTERVAL_SECONDS = (
|
24
29
|
86400 # Check for updates once per day (24 hours)
|
25
30
|
)
|
@@ -122,17 +127,17 @@ def check_for_registry_updates() -> Tuple[bool, Optional[str]]:
|
|
122
127
|
return False, None
|
123
128
|
|
124
129
|
try:
|
125
|
-
registry = ModelRegistry()
|
130
|
+
registry = ModelRegistry.get_instance()
|
126
131
|
result = registry.check_for_updates()
|
127
132
|
|
128
133
|
# Save the check time regardless of the result
|
129
134
|
_save_last_check_time()
|
130
135
|
|
131
|
-
if result.status ==
|
136
|
+
if result.status.value == "update_available":
|
132
137
|
return True, (
|
133
138
|
"A new model registry is available. "
|
134
139
|
"This may include support for new models or features. "
|
135
|
-
"
|
140
|
+
"Run 'ostruct update-registry' to update."
|
136
141
|
)
|
137
142
|
|
138
143
|
return False, None
|
@@ -154,7 +159,7 @@ def get_update_notification() -> Optional[str]:
|
|
154
159
|
try:
|
155
160
|
update_available, message = check_for_registry_updates()
|
156
161
|
if update_available and message:
|
157
|
-
return message
|
162
|
+
return str(message) # Explicit cast to ensure str type
|
158
163
|
return None
|
159
164
|
except Exception as e:
|
160
165
|
# Ensure any errors don't affect normal operation
|