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.
Files changed (50) hide show
  1. ostruct/cli/__init__.py +3 -15
  2. ostruct/cli/attachment_processor.py +455 -0
  3. ostruct/cli/attachment_template_bridge.py +973 -0
  4. ostruct/cli/cli.py +187 -33
  5. ostruct/cli/click_options.py +775 -692
  6. ostruct/cli/code_interpreter.py +195 -12
  7. ostruct/cli/commands/__init__.py +0 -3
  8. ostruct/cli/commands/run.py +289 -62
  9. ostruct/cli/config.py +23 -22
  10. ostruct/cli/constants.py +89 -0
  11. ostruct/cli/errors.py +191 -6
  12. ostruct/cli/explicit_file_processor.py +0 -15
  13. ostruct/cli/file_info.py +118 -14
  14. ostruct/cli/file_list.py +82 -1
  15. ostruct/cli/file_search.py +68 -2
  16. ostruct/cli/help_json.py +235 -0
  17. ostruct/cli/mcp_integration.py +13 -16
  18. ostruct/cli/params.py +217 -0
  19. ostruct/cli/plan_assembly.py +335 -0
  20. ostruct/cli/plan_printing.py +385 -0
  21. ostruct/cli/progress_reporting.py +8 -56
  22. ostruct/cli/quick_ref_help.py +128 -0
  23. ostruct/cli/rich_config.py +299 -0
  24. ostruct/cli/runner.py +397 -190
  25. ostruct/cli/security/__init__.py +2 -0
  26. ostruct/cli/security/allowed_checker.py +41 -0
  27. ostruct/cli/security/normalization.py +13 -9
  28. ostruct/cli/security/security_manager.py +558 -17
  29. ostruct/cli/security/types.py +15 -0
  30. ostruct/cli/template_debug.py +283 -261
  31. ostruct/cli/template_debug_help.py +233 -142
  32. ostruct/cli/template_env.py +46 -5
  33. ostruct/cli/template_filters.py +415 -8
  34. ostruct/cli/template_processor.py +240 -619
  35. ostruct/cli/template_rendering.py +49 -73
  36. ostruct/cli/template_validation.py +2 -1
  37. ostruct/cli/token_validation.py +35 -15
  38. ostruct/cli/types.py +15 -19
  39. ostruct/cli/unicode_compat.py +283 -0
  40. ostruct/cli/upload_manager.py +448 -0
  41. ostruct/cli/utils.py +30 -0
  42. ostruct/cli/validators.py +272 -54
  43. {ostruct_cli-0.8.8.dist-info → ostruct_cli-1.0.0.dist-info}/METADATA +292 -126
  44. ostruct_cli-1.0.0.dist-info/RECORD +80 -0
  45. ostruct/cli/commands/quick_ref.py +0 -54
  46. ostruct/cli/template_optimizer.py +0 -478
  47. ostruct_cli-0.8.8.dist-info/RECORD +0 -71
  48. {ostruct_cli-0.8.8.dist-info → ostruct_cli-1.0.0.dist-info}/LICENSE +0 -0
  49. {ostruct_cli-0.8.8.dist-info → ostruct_cli-1.0.0.dist-info}/WHEEL +0 -0
  50. {ostruct_cli-0.8.8.dist-info → ostruct_cli-1.0.0.dist-info}/entry_points.txt +0 -0
@@ -65,12 +65,7 @@ from jinja2 import Environment
65
65
  from .errors import TaskTemplateVariableError, TemplateValidationError
66
66
  from .file_utils import FileInfo
67
67
  from .progress import ProgressContext
68
- from .progress_reporting import get_progress_reporter
69
68
  from .template_env import create_jinja_env
70
- from .template_optimizer import (
71
- is_optimization_beneficial,
72
- optimize_template_for_llm,
73
- )
74
69
  from .template_schema import DotDict, StdinProxy
75
70
 
76
71
  __all__ = [
@@ -156,7 +151,7 @@ def render_template(
156
151
  TaskTemplateVariableError: If template variables are undefined
157
152
  TemplateValidationError: If template rendering fails for other reasons
158
153
  """
159
- from .progress import ( # Import here to avoid circular dependency
154
+ from .progress import (
160
155
  ProgressContext,
161
156
  )
162
157
 
@@ -169,7 +164,7 @@ def render_template(
169
164
  progress.update(1) # Update progress for setup
170
165
 
171
166
  if env is None:
172
- env = create_jinja_env(loader=jinja2.FileSystemLoader("."))
167
+ env, _ = create_jinja_env(loader=jinja2.FileSystemLoader("."))
173
168
 
174
169
  logger.debug("=== Raw Input ===")
175
170
  logger.debug(
@@ -304,67 +299,6 @@ def render_template(
304
299
  " %s: %s (%r)", key, type(value).__name__, value
305
300
  )
306
301
 
307
- # Apply template optimization for better LLM performance
308
- try:
309
- # Get template source - use template_str for string templates or template.source for file templates
310
- if hasattr(template, "source") and template.source:
311
- original_template_source = template.source
312
- else:
313
- original_template_source = template_str
314
-
315
- if (
316
- original_template_source
317
- and is_optimization_beneficial(
318
- original_template_source
319
- )
320
- ):
321
- logger.debug("=== Template Optimization ===")
322
- optimization_result = optimize_template_for_llm(
323
- original_template_source
324
- )
325
-
326
- if optimization_result.has_optimizations:
327
- # Report optimization to user
328
- progress_reporter = get_progress_reporter()
329
- progress_reporter.report_optimization(
330
- optimization_result.transformations
331
- )
332
-
333
- logger.info(
334
- "Template optimized for LLM performance:"
335
- )
336
- for (
337
- transformation
338
- ) in optimization_result.transformations:
339
- logger.info(f" • {transformation}")
340
- logger.info(
341
- f" • Optimization time: {optimization_result.optimization_time_ms:.1f}ms"
342
- )
343
-
344
- # Create new template from optimized content
345
- template = env.from_string(
346
- optimization_result.optimized_template
347
- )
348
- # Re-add globals to new template
349
- template.globals["template_name"] = getattr(
350
- template, "name", "<string>"
351
- )
352
- template.globals["template_path"] = getattr(
353
- template, "filename", None
354
- )
355
- else:
356
- logger.debug("No beneficial optimizations found")
357
- else:
358
- logger.debug(
359
- "Template optimization not beneficial - skipping"
360
- )
361
- except Exception as e:
362
- # If optimization fails, continue with original template
363
- logger.warning(
364
- f"Template optimization failed, using original: {e}"
365
- )
366
- # template remains unchanged
367
-
368
302
  result = template.render(**wrapped_context)
369
303
  if not isinstance(result, str):
370
304
  raise TemplateValidationError(
@@ -391,11 +325,53 @@ def render_template(
391
325
  f" -V {var_name}='value'"
392
326
  )
393
327
  raise TaskTemplateVariableError(error_msg) from e
394
- except (jinja2.TemplateError, Exception) as e:
395
- logger.error("Template rendering failed: %s", str(e))
396
- raise TemplateValidationError(
397
- f"Template rendering failed: {str(e)}"
398
- ) from e
328
+ except TypeError as e:
329
+ error_str = str(e)
330
+ # Handle iteration errors with user-friendly messages
331
+ if "object is not iterable" in error_str:
332
+ # Extract variable name from iteration context
333
+ # Look for patterns like "'ClassName' object is not iterable"
334
+ # and provide helpful guidance
335
+ user_friendly_msg = (
336
+ "Template iteration error: A variable used in a loop ({% for ... %}) "
337
+ "is not iterable. This usually means:\n"
338
+ "1. The variable is not a file object, list, or other iterable type\n"
339
+ "2. Check that your template variable names match the expected data\n"
340
+ "3. Verify the variable contains the expected data type\n"
341
+ f"Available variables: {', '.join(sorted(context.keys()))}"
342
+ )
343
+ logger.error("Template iteration error: %s", error_str)
344
+ raise TemplateValidationError(user_friendly_msg) from e
345
+ else:
346
+ # Other TypeError - preserve original behavior
347
+ logger.error("Template rendering failed: %s", str(e))
348
+ raise TemplateValidationError(
349
+ f"Template rendering failed: {str(e)}"
350
+ ) from e
351
+ except Exception as e:
352
+ # Import here to avoid circular imports
353
+ from .template_filters import (
354
+ TemplateStructureError,
355
+ format_tses_error,
356
+ )
357
+
358
+ # Handle TemplateStructureError with helpful formatting
359
+ if isinstance(e, TemplateStructureError):
360
+ formatted_error = format_tses_error(e)
361
+ logger.error(
362
+ "Template structure error: %s", formatted_error
363
+ )
364
+ raise TemplateValidationError(formatted_error) from e
365
+ elif isinstance(e, jinja2.TemplateError):
366
+ logger.error("Template rendering failed: %s", str(e))
367
+ raise TemplateValidationError(
368
+ f"Template rendering failed: {str(e)}"
369
+ ) from e
370
+ else:
371
+ logger.error("Template rendering failed: %s", str(e))
372
+ raise TemplateValidationError(
373
+ f"Template rendering failed: {str(e)}"
374
+ ) from e
399
375
 
400
376
  except ValueError as e:
401
377
  # Re-raise with original context
@@ -218,7 +218,8 @@ def validate_template_placeholders(
218
218
 
219
219
  try:
220
220
  # 1) Create Jinja2 environment with meta extension and safe undefined
221
- env = create_jinja_env(validation_mode=True)
221
+ env_tuple = create_jinja_env(validation_mode=True)
222
+ env = env_tuple[0]
222
223
 
223
224
  # Register custom filters with None-safe wrappers
224
225
  env.filters.update(
@@ -184,19 +184,36 @@ class TokenLimitValidator:
184
184
 
185
185
  for file_path, tokens in oversized_files:
186
186
  file_name = Path(file_path).name
187
-
188
- if self._is_data_file(file_path):
189
- error_msg += f" 📊 Data file: ostruct -fc {file_name} <template> <schema>\n"
190
- error_msg += f" (Moves {file_name} to Code Interpreter for data processing)\n\n"
191
- elif self._is_document_file(file_path):
192
- error_msg += f" 📄 Document: ostruct -fs {file_name} <template> <schema>\n"
193
- error_msg += f" (Moves {file_name} to File Search for semantic retrieval)\n\n"
194
- elif self._is_code_file(file_path):
195
- error_msg += f" 💻 Code file: ostruct -fc {file_name} <template> <schema>\n"
196
- error_msg += f" (Moves {file_name} to Code Interpreter for analysis)\n\n"
187
+ file_extension = Path(file_path).suffix.lower()
188
+
189
+ if file_extension in [
190
+ ".csv",
191
+ ".json",
192
+ ".xml",
193
+ ".yaml",
194
+ ".yml",
195
+ ]:
196
+ error_msg += f" 📊 Data file: ostruct --file ci:data {file_name} <template> <schema>\n"
197
+ elif file_extension in [
198
+ ".pdf",
199
+ ".txt",
200
+ ".md",
201
+ ".doc",
202
+ ".docx",
203
+ ]:
204
+ error_msg += f" 📄 Document: ostruct --file fs:docs {file_name} <template> <schema>\n"
205
+ elif file_extension in [
206
+ ".py",
207
+ ".js",
208
+ ".ts",
209
+ ".java",
210
+ ".cpp",
211
+ ".c",
212
+ ]:
213
+ error_msg += f" 💻 Code file: ostruct --file ci:code {file_name} <template> <schema>\n"
197
214
  else:
198
- error_msg += f" 📁 Large file: ostruct -fc {file_name} OR -fs {file_name} <template> <schema>\n"
199
- error_msg += " (Choose based on usage: -fc for processing, -fs for retrieval)\n\n"
215
+ error_msg += f" 📁 Large file: ostruct --file ci:data {file_name} OR --file fs:docs {file_name} <template> <schema>\n"
216
+ error_msg += " (Choose based on usage: ci for processing, fs for retrieval)\n\n"
200
217
 
201
218
  error_msg += (
202
219
  f" Size: {tokens:,} tokens ({file_path})\n\n"
@@ -256,11 +273,14 @@ class TokenLimitValidator:
256
273
  def _get_recommended_flags(self, file_path: str) -> List[str]:
257
274
  """Get recommended CLI flags for file routing."""
258
275
  if self._is_data_file(file_path) or self._is_code_file(file_path):
259
- return ["-fc", "--file-for code-interpreter"]
276
+ return ["--file ci:data", "--file ci:code"]
260
277
  elif self._is_document_file(file_path):
261
- return ["-fs", "--file-for file-search"]
278
+ return ["--file fs:docs", "--file fs:knowledge"]
262
279
  else:
263
- return ["-fc", "-fs"] # Both options for unknown files
280
+ return [
281
+ "--file ci:data",
282
+ "--file fs:docs",
283
+ ] # Both options for unknown files
264
284
 
265
285
 
266
286
  def validate_token_limits(
ostruct/cli/types.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """Type definitions for ostruct CLI."""
2
2
 
3
3
  from pathlib import Path
4
- from typing import List, Optional, Tuple, TypedDict, Union
4
+ from typing import Any, Dict, List, Optional, Tuple, TypedDict, Union
5
5
 
6
6
  # Import FileRoutingResult from validators
7
7
  FileRoutingResult = List[Tuple[Optional[str], Union[str, Path]]]
@@ -32,7 +32,6 @@ class CLIParams(TypedDict, total=False):
32
32
  timeout: float
33
33
  output_file: Optional[str]
34
34
  dry_run: bool
35
- no_progress: bool
36
35
  api_key: Optional[str]
37
36
  verbose: bool
38
37
  show_model_schema: bool
@@ -43,24 +42,30 @@ class CLIParams(TypedDict, total=False):
43
42
  frequency_penalty: Optional[float]
44
43
  presence_penalty: Optional[float]
45
44
  reasoning_effort: Optional[str]
46
- progress_level: str
45
+ progress: str
47
46
  task_file: Optional[str]
48
47
  task: Optional[str]
49
48
  schema_file: str
50
49
  mcp_servers: List[str]
50
+
51
+ # New attachment system (T3.0)
52
+ attaches: List[Dict[str, Any]] # --attach specifications
53
+ dirs: List[Dict[str, Any]] # --dir specifications
54
+ collects: List[Dict[str, Any]] # --collect specifications
51
55
  mcp_allowed_tools: List[str]
52
56
  mcp_require_approval: str
53
57
  mcp_headers: Optional[str]
54
58
  code_interpreter_files: FileRoutingResult # Fixed: was List[str]
55
59
  code_interpreter_dirs: List[str]
56
- code_interpreter_download_dir: str
57
- code_interpreter_cleanup: bool
60
+ ci_download_dir: str
61
+ ci_duplicate_outputs: Optional[str]
62
+ ci_cleanup: bool
58
63
  file_search_files: FileRoutingResult # Fixed: was List[str]
59
64
  file_search_dirs: List[str]
60
- file_search_vector_store_name: str
61
- file_search_cleanup: bool
62
- file_search_retry_count: int
63
- file_search_timeout: float
65
+ fs_store_name: str
66
+ fs_cleanup: bool
67
+ fs_retries: int
68
+ fs_timeout: float
64
69
  template_files: FileRoutingResult # Fixed: was List[str]
65
70
  template_dirs: List[str]
66
71
  template_file_aliases: List[
@@ -75,17 +80,8 @@ class CLIParams(TypedDict, total=False):
75
80
  tool_files: List[
76
81
  Tuple[str, str]
77
82
  ] # List of (tool, path) tuples from --file-for
78
- web_search: bool
79
83
  debug: bool
80
- show_templates: bool
81
- debug_templates: bool
82
- show_context: bool
83
- show_context_detailed: bool
84
- show_pre_optimization: bool
85
- show_optimization_diff: bool
86
- no_optimization: bool
87
- show_optimization_steps: bool
88
- optimization_step_detail: str
84
+
89
85
  help_debug: bool
90
86
  enabled_features: List[str] # List of feature names to enable
91
87
  disabled_features: List[str] # List of feature names to disable
@@ -0,0 +1,283 @@
1
+ """Windows Unicode compatibility utilities.
2
+
3
+ This module provides smart emoji handling for Windows terminals:
4
+ - Modern terminals (Windows Terminal, PowerShell, Win11 Console) get full emoji
5
+ - Legacy terminals (cmd.exe with cp1252) get clean text without emoji
6
+ - Detection is automatic and graceful with user overrides available
7
+
8
+ Environment Variables:
9
+ - OSTRUCT_UNICODE=auto: Auto-detect terminal capabilities (default)
10
+ - OSTRUCT_UNICODE=1/true/yes: Force emoji display regardless of detection
11
+ - OSTRUCT_UNICODE=0/false/no: Force plain text regardless of detection
12
+ - OSTRUCT_UNICODE=debug: Show detection details and use auto-detection
13
+ """
14
+
15
+ import os
16
+ import sys
17
+ from typing import Any, Optional
18
+
19
+
20
+ def _get_unicode_setting() -> str:
21
+ """Get the Unicode setting from environment variable."""
22
+ return os.environ.get("OSTRUCT_UNICODE", "auto").lower()
23
+
24
+
25
+ def _is_debug_mode() -> bool:
26
+ """Check if Unicode debugging is enabled."""
27
+ return _get_unicode_setting() == "debug"
28
+
29
+
30
+ def _debug_log(message: str) -> None:
31
+ """Log debug message if unicode debugging is enabled."""
32
+ if _is_debug_mode():
33
+ print(f"[UNICODE DEBUG] {message}", file=sys.stderr)
34
+
35
+
36
+ def _detect_modern_windows_terminal() -> bool:
37
+ """Detect if we're running in a modern Windows terminal that supports Unicode.
38
+
39
+ Returns:
40
+ True if running in Windows Terminal, PowerShell, or Win11 Console Host
41
+ False if running in legacy cmd.exe or other limited terminals
42
+ """
43
+ # Check for explicit user overrides first
44
+ unicode_setting = _get_unicode_setting()
45
+
46
+ if unicode_setting in ("1", "true", "yes", "on"):
47
+ _debug_log("Unicode forced ON via OSTRUCT_UNICODE")
48
+ return True
49
+
50
+ if unicode_setting in ("0", "false", "no", "off"):
51
+ _debug_log("Unicode forced OFF via OSTRUCT_UNICODE")
52
+ return False
53
+
54
+ # For "auto" and "debug", proceed with detection
55
+
56
+ if not sys.platform.startswith("win"):
57
+ # Non-Windows systems generally support Unicode well
58
+ _debug_log(f"Non-Windows platform ({sys.platform}): Unicode enabled")
59
+ return True
60
+
61
+ _debug_log("Windows platform detected, checking terminal capabilities...")
62
+
63
+ # Check for Windows Terminal
64
+ if os.environ.get("WT_SESSION"):
65
+ _debug_log("Windows Terminal detected via WT_SESSION")
66
+ return True
67
+
68
+ # Check for PowerShell (both Windows PowerShell and PowerShell Core)
69
+ if os.environ.get("PSModulePath"):
70
+ _debug_log("PowerShell detected via PSModulePath")
71
+ return True
72
+
73
+ # Check for modern console host (Windows 11+)
74
+ # The TERMINAL_EMULATOR environment variable is set by modern terminals
75
+ if os.environ.get("TERMINAL_EMULATOR"):
76
+ _debug_log(
77
+ f"Modern terminal detected via TERMINAL_EMULATOR: {os.environ.get('TERMINAL_EMULATOR')}"
78
+ )
79
+ return True
80
+
81
+ # Check for VS Code integrated terminal
82
+ if os.environ.get("VSCODE_INJECTION"):
83
+ _debug_log("VS Code integrated terminal detected")
84
+ return True
85
+
86
+ # Check for GitHub Codespaces
87
+ if os.environ.get("CODESPACES"):
88
+ _debug_log("GitHub Codespaces detected")
89
+ return True
90
+
91
+ # Check console code page - UTF-8 indicates modern setup
92
+ try:
93
+ import locale
94
+
95
+ encoding = locale.getpreferredencoding()
96
+ if encoding.lower() in ("utf-8", "utf8"):
97
+ _debug_log(f"UTF-8 locale detected: {encoding}")
98
+ return True
99
+ else:
100
+ _debug_log(f"Non-UTF-8 locale: {encoding}")
101
+ except Exception as e:
102
+ _debug_log(f"Locale detection failed: {e}")
103
+
104
+ # Check if stdout encoding supports Unicode
105
+ try:
106
+ stdout_encoding = getattr(sys.stdout, "encoding", None)
107
+ if stdout_encoding and stdout_encoding.lower() in ("utf-8", "utf8"):
108
+ _debug_log(f"UTF-8 stdout encoding: {stdout_encoding}")
109
+ return True
110
+ else:
111
+ _debug_log(f"Non-UTF-8 stdout encoding: {stdout_encoding}")
112
+ except Exception as e:
113
+ _debug_log(f"Stdout encoding detection failed: {e}")
114
+
115
+ # Check Windows version (Windows 10 1903+ has better Unicode support)
116
+ try:
117
+ import platform
118
+
119
+ version = platform.version()
120
+ # Windows 10 build 18362 (1903) and later have better Unicode support
121
+ if version and "10.0." in version:
122
+ build = version.split(".")[-1]
123
+ if build.isdigit() and int(build) >= 18362:
124
+ _debug_log(f"Modern Windows version detected: {version}")
125
+ return True
126
+ _debug_log(f"Windows version: {version}")
127
+ except Exception as e:
128
+ _debug_log(f"Windows version detection failed: {e}")
129
+
130
+ # Default to False for safety on Windows
131
+ _debug_log(
132
+ "No modern terminal indicators found, defaulting to legacy mode"
133
+ )
134
+ return False
135
+
136
+
137
+ def safe_emoji(emoji: str, text_without_emoji: Optional[str] = None) -> str:
138
+ """Return emoji on Unicode-capable terminals, clean text on legacy terminals.
139
+
140
+ Args:
141
+ emoji: The emoji character to display
142
+ text_without_emoji: Optional clean text version. If None, emoji is simply omitted.
143
+
144
+ Returns:
145
+ Emoji on modern terminals, clean text on legacy terminals
146
+
147
+ Examples:
148
+ safe_emoji("🚀", "START") -> "🚀" or "START"
149
+ safe_emoji("🔍") -> "🔍" or ""
150
+ """
151
+ if _detect_modern_windows_terminal():
152
+ # Modern terminal - try to use emoji
153
+ try:
154
+ # Test if we can encode the emoji
155
+ emoji.encode(sys.stdout.encoding or "utf-8")
156
+ return emoji
157
+ except (UnicodeEncodeError, LookupError, AttributeError):
158
+ # Fallback even on modern terminals if encoding fails
159
+ return text_without_emoji or ""
160
+ else:
161
+ # Legacy terminal - use clean text
162
+ return text_without_emoji or ""
163
+
164
+
165
+ def safe_format(format_string: str, *args: Any, **kwargs: Any) -> str:
166
+ """Format string with emoji safety.
167
+
168
+ Processes format strings containing emoji through safe_emoji() automatically.
169
+ Optimized to skip processing when no emoji are present.
170
+
171
+ Args:
172
+ format_string: Format string that may contain emoji
173
+ *args, **kwargs: Format arguments
174
+
175
+ Returns:
176
+ Formatted string with emoji handled appropriately for the terminal
177
+ """
178
+ # Quick check: if no emoji characters are present, skip processing entirely
179
+ # This optimizes the common case of plain text messages
180
+ # Check for the specific emoji we handle rather than broad Unicode ranges
181
+ emoji_chars = {
182
+ "🚀",
183
+ "🔍",
184
+ "⚙️",
185
+ "ℹ️",
186
+ "📖",
187
+ "💻",
188
+ "📄",
189
+ "🌐",
190
+ "🕐",
191
+ "📋",
192
+ "🤖",
193
+ "🔒",
194
+ "🛠️",
195
+ "📎",
196
+ "📥",
197
+ "📊",
198
+ "💰",
199
+ "✅",
200
+ "❌",
201
+ "⚠️",
202
+ "⏱️",
203
+ }
204
+ if not any(emoji in format_string for emoji in emoji_chars):
205
+ return format_string.format(*args, **kwargs)
206
+
207
+ # Common emoji replacements for CLI output
208
+ emoji_map = {
209
+ "🚀": "", # Just omit, the text context is clear
210
+ "🔍": "", # Just omit, "Plan" or "Execution Plan" is clear
211
+ "⚙️": "", # Just omit for progress reporting
212
+ "ℹ️": "", # Just omit for info messages
213
+ "📖": "", # Just omit for help references
214
+ "💻": "", # Just omit for Code Interpreter
215
+ "📄": "", # Just omit for file references
216
+ "🌐": "", # Just omit for web search
217
+ "🕐": "", # Just omit for timestamp
218
+ "📋": "", # Just omit for schema
219
+ "🤖": "", # Just omit for model
220
+ "🔒": "", # Just omit for security
221
+ "🛠️": "", # Just omit for tools
222
+ "📎": "", # Just omit for attachments
223
+ "📥": "", # Just omit for downloads
224
+ "📊": "", # Just omit for variables
225
+ "💰": "", # Just omit for cost
226
+ "✅": "[OK]", # Show status indicator
227
+ "❌": "[ERROR]", # Show status indicator
228
+ "⚠️": "[WARNING]", # Show status indicator
229
+ "⏱️": "", # Just omit for timing
230
+ }
231
+
232
+ # Apply emoji safety to the format string
233
+ safe_format_string = format_string
234
+ for emoji, fallback in emoji_map.items():
235
+ if emoji in format_string: # Only process if emoji is actually present
236
+ safe_format_string = safe_format_string.replace(
237
+ emoji, safe_emoji(emoji, fallback)
238
+ )
239
+
240
+ # Format with the processed string
241
+ return safe_format_string.format(*args, **kwargs)
242
+
243
+
244
+ def safe_print(message: str, **print_kwargs: Any) -> None:
245
+ """Print message with emoji safety.
246
+
247
+ Convenience function for safe printing with automatic emoji handling.
248
+
249
+ Args:
250
+ message: Message to print (may contain emoji)
251
+ **print_kwargs: Additional arguments passed to print()
252
+ """
253
+ # Fast path for messages without emoji
254
+ emoji_chars = {
255
+ "🚀",
256
+ "🔍",
257
+ "⚙️",
258
+ "ℹ️",
259
+ "📖",
260
+ "💻",
261
+ "📄",
262
+ "🌐",
263
+ "🕐",
264
+ "📋",
265
+ "🤖",
266
+ "🔒",
267
+ "🛠️",
268
+ "📎",
269
+ "📥",
270
+ "📊",
271
+ "💰",
272
+ "✅",
273
+ "❌",
274
+ "⚠️",
275
+ "⏱️",
276
+ }
277
+ if not any(emoji in message for emoji in emoji_chars):
278
+ print(message, **print_kwargs)
279
+ return
280
+
281
+ # Process emoji for compatibility
282
+ safe_message = safe_format(message)
283
+ print(safe_message, **print_kwargs)