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.
Files changed (46) hide show
  1. ostruct/cli/__init__.py +21 -3
  2. ostruct/cli/base_errors.py +1 -1
  3. ostruct/cli/cli.py +66 -1983
  4. ostruct/cli/click_options.py +460 -28
  5. ostruct/cli/code_interpreter.py +238 -0
  6. ostruct/cli/commands/__init__.py +32 -0
  7. ostruct/cli/commands/list_models.py +128 -0
  8. ostruct/cli/commands/quick_ref.py +50 -0
  9. ostruct/cli/commands/run.py +137 -0
  10. ostruct/cli/commands/update_registry.py +71 -0
  11. ostruct/cli/config.py +277 -0
  12. ostruct/cli/cost_estimation.py +134 -0
  13. ostruct/cli/errors.py +310 -6
  14. ostruct/cli/exit_codes.py +1 -0
  15. ostruct/cli/explicit_file_processor.py +548 -0
  16. ostruct/cli/field_utils.py +69 -0
  17. ostruct/cli/file_info.py +42 -9
  18. ostruct/cli/file_list.py +301 -102
  19. ostruct/cli/file_search.py +455 -0
  20. ostruct/cli/file_utils.py +47 -13
  21. ostruct/cli/mcp_integration.py +541 -0
  22. ostruct/cli/model_creation.py +150 -1
  23. ostruct/cli/model_validation.py +204 -0
  24. ostruct/cli/progress_reporting.py +398 -0
  25. ostruct/cli/registry_updates.py +14 -9
  26. ostruct/cli/runner.py +1418 -0
  27. ostruct/cli/schema_utils.py +113 -0
  28. ostruct/cli/services.py +626 -0
  29. ostruct/cli/template_debug.py +748 -0
  30. ostruct/cli/template_debug_help.py +162 -0
  31. ostruct/cli/template_env.py +15 -6
  32. ostruct/cli/template_filters.py +55 -3
  33. ostruct/cli/template_optimizer.py +474 -0
  34. ostruct/cli/template_processor.py +1080 -0
  35. ostruct/cli/template_rendering.py +69 -34
  36. ostruct/cli/token_validation.py +286 -0
  37. ostruct/cli/types.py +78 -0
  38. ostruct/cli/unattended_operation.py +269 -0
  39. ostruct/cli/validators.py +386 -3
  40. {ostruct_cli-0.7.1.dist-info → ostruct_cli-0.8.0.dist-info}/LICENSE +2 -0
  41. ostruct_cli-0.8.0.dist-info/METADATA +633 -0
  42. ostruct_cli-0.8.0.dist-info/RECORD +69 -0
  43. {ostruct_cli-0.7.1.dist-info → ostruct_cli-0.8.0.dist-info}/WHEEL +1 -1
  44. ostruct_cli-0.7.1.dist-info/METADATA +0 -369
  45. ostruct_cli-0.7.1.dist-info/RECORD +0 -45
  46. {ostruct_cli-0.7.1.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)
@@ -11,15 +11,20 @@ import time
11
11
  from pathlib import Path
12
12
  from typing import Optional, Tuple
13
13
 
14
- from openai_structured.model_registry import (
15
- ModelRegistry,
16
- RegistryUpdateStatus,
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 = "OSTRUCT_DISABLE_UPDATE_CHECKS"
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 == RegistryUpdateStatus.UPDATE_AVAILABLE:
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
- "The registry will be automatically updated when needed."
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