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,626 @@
1
+ """Service container for ostruct CLI tool managers."""
2
+
3
+ import logging
4
+ from abc import abstractmethod
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from typing import Any, Dict, List, Optional, Protocol
8
+
9
+ from openai import AsyncOpenAI
10
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError
11
+
12
+ from .types import CLIParams
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Type alias for CLI parameters
17
+ # CLIParams imported from types module
18
+
19
+
20
+ class ServiceStatus(Enum):
21
+ """Service health status."""
22
+
23
+ HEALTHY = "healthy"
24
+ DEGRADED = "degraded"
25
+ UNHEALTHY = "unhealthy"
26
+ UNKNOWN = "unknown"
27
+
28
+
29
+ @dataclass
30
+ class ServiceHealth:
31
+ """Service health information."""
32
+
33
+ status: ServiceStatus
34
+ message: str
35
+ details: Optional[Dict[str, Any]] = None
36
+
37
+
38
+ class ServiceConfigurationBase(BaseModel):
39
+ """Base configuration for all services."""
40
+
41
+ model_config = ConfigDict(
42
+ extra="allow"
43
+ ) # Allow additional fields for service-specific config
44
+
45
+ enabled: bool = Field(
46
+ default=True, description="Whether the service is enabled"
47
+ )
48
+ timeout: Optional[float] = Field(
49
+ default=None, description="Service timeout in seconds"
50
+ )
51
+ retry_attempts: int = Field(
52
+ default=3, description="Number of retry attempts"
53
+ )
54
+
55
+
56
+ class MCPServiceConfiguration(ServiceConfigurationBase):
57
+ """Configuration for MCP service."""
58
+
59
+ servers: List[Dict[str, Any]] = Field(
60
+ default_factory=list, description="MCP server configurations"
61
+ )
62
+ connection_timeout: float = Field(
63
+ default=30.0, description="Connection timeout in seconds"
64
+ )
65
+ max_retries: int = Field(
66
+ default=3, description="Maximum retry attempts for server connections"
67
+ )
68
+
69
+ def validate_servers(self) -> List[str]:
70
+ """Validate server configurations and return any errors."""
71
+ errors = []
72
+ for i, server in enumerate(self.servers):
73
+ if not isinstance(server, dict):
74
+ errors.append(f"Server {i}: Must be a dictionary")
75
+ continue
76
+ if "name" not in server:
77
+ errors.append(f"Server {i}: Missing required 'name' field")
78
+ if "command" not in server:
79
+ errors.append(f"Server {i}: Missing required 'command' field")
80
+ return errors
81
+
82
+
83
+ class CodeInterpreterServiceConfiguration(ServiceConfigurationBase):
84
+ """Configuration for Code Interpreter service."""
85
+
86
+ max_file_size_mb: float = Field(
87
+ default=100.0, description="Maximum file size in MB"
88
+ )
89
+ allowed_file_types: List[str] = Field(
90
+ default_factory=lambda: [".py", ".txt", ".json", ".csv", ".md"],
91
+ description="Allowed file extensions",
92
+ )
93
+ cleanup_on_exit: bool = Field(
94
+ default=True, description="Clean up uploaded files on exit"
95
+ )
96
+
97
+ def validate_file_types(self) -> List[str]:
98
+ """Validate file type configurations."""
99
+ errors = []
100
+ for file_type in self.allowed_file_types:
101
+ if not isinstance(file_type, str):
102
+ errors.append(
103
+ f"File type must be string, got {type(file_type)}"
104
+ )
105
+ elif not file_type.startswith("."):
106
+ errors.append(f"File type '{file_type}' must start with '.'")
107
+ return errors
108
+
109
+
110
+ class FileSearchServiceConfiguration(ServiceConfigurationBase):
111
+ """Configuration for File Search service."""
112
+
113
+ max_files: int = Field(
114
+ default=1000, description="Maximum number of files to index"
115
+ )
116
+ chunk_size: int = Field(
117
+ default=800, description="Text chunk size for indexing"
118
+ )
119
+ overlap: int = Field(default=400, description="Text chunk overlap")
120
+ cleanup_vector_stores: bool = Field(
121
+ default=True, description="Clean up vector stores on exit"
122
+ )
123
+
124
+ def validate_chunk_settings(self) -> List[str]:
125
+ """Validate chunk size and overlap settings."""
126
+ errors = []
127
+ if self.chunk_size <= 0:
128
+ errors.append("Chunk size must be positive")
129
+ if self.overlap < 0:
130
+ errors.append("Overlap cannot be negative")
131
+ if self.overlap >= self.chunk_size:
132
+ errors.append("Overlap must be less than chunk size")
133
+ return errors
134
+
135
+
136
+ class ServiceConfigurationValidator:
137
+ """Validates service configurations."""
138
+
139
+ @staticmethod
140
+ def validate_mcp_config(config: Dict[str, Any]) -> MCPServiceConfiguration:
141
+ """Validate MCP service configuration."""
142
+ try:
143
+ mcp_config = MCPServiceConfiguration(**config)
144
+ server_errors = mcp_config.validate_servers()
145
+ if server_errors:
146
+ raise ValueError(
147
+ f"MCP server validation errors: {'; '.join(server_errors)}"
148
+ )
149
+ return mcp_config
150
+ except (ValidationError, ValueError) as e:
151
+ logger.error(f"MCP configuration validation failed: {e}")
152
+ raise
153
+
154
+ @staticmethod
155
+ def validate_code_interpreter_config(
156
+ config: Dict[str, Any],
157
+ ) -> CodeInterpreterServiceConfiguration:
158
+ """Validate Code Interpreter service configuration."""
159
+ try:
160
+ ci_config = CodeInterpreterServiceConfiguration(**config)
161
+ file_type_errors = ci_config.validate_file_types()
162
+ if file_type_errors:
163
+ raise ValueError(
164
+ f"Code Interpreter file type errors: {'; '.join(file_type_errors)}"
165
+ )
166
+ return ci_config
167
+ except (ValidationError, ValueError) as e:
168
+ logger.error(
169
+ f"Code Interpreter configuration validation failed: {e}"
170
+ )
171
+ raise
172
+
173
+ @staticmethod
174
+ def validate_file_search_config(
175
+ config: Dict[str, Any],
176
+ ) -> FileSearchServiceConfiguration:
177
+ """Validate File Search service configuration."""
178
+ try:
179
+ fs_config = FileSearchServiceConfiguration(**config)
180
+ chunk_errors = fs_config.validate_chunk_settings()
181
+ if chunk_errors:
182
+ raise ValueError(
183
+ f"File Search chunk setting errors: {'; '.join(chunk_errors)}"
184
+ )
185
+ return fs_config
186
+ except (ValidationError, ValueError) as e:
187
+ logger.error(f"File Search configuration validation failed: {e}")
188
+ raise
189
+
190
+
191
+ class ToolManagerProtocol(Protocol):
192
+ """Protocol defining the interface for tool managers."""
193
+
194
+ @abstractmethod
195
+ async def cleanup(self) -> None:
196
+ """Clean up resources used by the tool manager."""
197
+ ...
198
+
199
+ @abstractmethod
200
+ async def health_check(self) -> ServiceHealth:
201
+ """Check the health status of the tool manager."""
202
+ ...
203
+
204
+
205
+ class MCPManagerProtocol(ToolManagerProtocol):
206
+ """Protocol for MCP server managers."""
207
+
208
+ @abstractmethod
209
+ def get_tools_for_responses_api(self) -> List[dict]:
210
+ """Get tools configured for OpenAI Responses API."""
211
+ ...
212
+
213
+
214
+ class CodeInterpreterManagerProtocol(ToolManagerProtocol):
215
+ """Protocol for code interpreter managers."""
216
+
217
+ @abstractmethod
218
+ async def upload_files_for_code_interpreter(
219
+ self, files: List[str]
220
+ ) -> List[str]:
221
+ """Upload files for code interpreter access."""
222
+ ...
223
+
224
+ @abstractmethod
225
+ async def cleanup_uploaded_files(self) -> None:
226
+ """Clean up uploaded files."""
227
+ ...
228
+
229
+ @abstractmethod
230
+ def build_tool_config(self, file_ids: List[str]) -> Dict[str, Any]:
231
+ """Build Code Interpreter tool configuration."""
232
+ ...
233
+
234
+
235
+ class FileSearchManagerProtocol(ToolManagerProtocol):
236
+ """Protocol for file search managers."""
237
+
238
+ @abstractmethod
239
+ async def upload_files_to_vector_store(
240
+ self, files: List[str], vector_store_id: str
241
+ ) -> Dict[str, Any]:
242
+ """Upload files to vector store."""
243
+ ...
244
+
245
+ @abstractmethod
246
+ async def cleanup_resources(self) -> None:
247
+ """Clean up vector store resources."""
248
+ ...
249
+
250
+ @abstractmethod
251
+ def build_tool_config(self, vector_store_id: str) -> Dict[str, Any]:
252
+ """Build File Search tool configuration."""
253
+ ...
254
+
255
+
256
+ class ServiceFactoryProtocol(Protocol):
257
+ """Protocol for service factories."""
258
+
259
+ async def create_mcp_manager(
260
+ self, args: CLIParams
261
+ ) -> Optional[MCPManagerProtocol]:
262
+ """Create MCP server manager."""
263
+ ...
264
+
265
+ async def create_code_interpreter_manager(
266
+ self, args: CLIParams, client: AsyncOpenAI
267
+ ) -> Optional[CodeInterpreterManagerProtocol]:
268
+ """Create code interpreter manager."""
269
+ ...
270
+
271
+ async def create_file_search_manager(
272
+ self, args: CLIParams, client: AsyncOpenAI
273
+ ) -> Optional[FileSearchManagerProtocol]:
274
+ """Create file search manager."""
275
+ ...
276
+
277
+
278
+ class DefaultServiceFactory:
279
+ """Default factory for creating service instances."""
280
+
281
+ async def create_mcp_manager(
282
+ self, args: CLIParams
283
+ ) -> Optional[MCPManagerProtocol]:
284
+ """Create MCP server manager."""
285
+ if not args.get("mcp_servers"):
286
+ return None
287
+ from .runner import process_mcp_configuration
288
+
289
+ manager = await process_mcp_configuration(args)
290
+ # The manager implements MCPManagerProtocol interface
291
+ return manager # type: ignore[return-value]
292
+
293
+ async def create_code_interpreter_manager(
294
+ self, args: CLIParams, client: AsyncOpenAI
295
+ ) -> Optional[CodeInterpreterManagerProtocol]:
296
+ """Create code interpreter manager."""
297
+ if not args.get("code_interpreter"):
298
+ return None
299
+ from .runner import process_code_interpreter_configuration
300
+
301
+ manager_info = await process_code_interpreter_configuration(
302
+ args, client
303
+ )
304
+ return manager_info.get("manager") if manager_info else None
305
+
306
+ async def create_file_search_manager(
307
+ self, args: CLIParams, client: AsyncOpenAI
308
+ ) -> Optional[FileSearchManagerProtocol]:
309
+ """Create file search manager."""
310
+ if not args.get("file_search"):
311
+ return None
312
+ from .runner import process_file_search_configuration
313
+
314
+ manager_info = await process_file_search_configuration(args, client)
315
+ return manager_info.get("manager") if manager_info else None
316
+
317
+
318
+ class ServiceContainer:
319
+ """Service container for managing tool managers and their dependencies."""
320
+
321
+ def __init__(
322
+ self,
323
+ client: AsyncOpenAI,
324
+ args: CLIParams,
325
+ factory: Optional[ServiceFactoryProtocol] = None,
326
+ ):
327
+ """Initialize service container.
328
+
329
+ Args:
330
+ client: Configured AsyncOpenAI client
331
+ args: CLI parameters
332
+ factory: Service factory for creating managers (uses default if None)
333
+ """
334
+ self.client = client
335
+ self.args = args
336
+ self.factory = factory or DefaultServiceFactory()
337
+ self._mcp_manager: Optional[MCPManagerProtocol] = None
338
+ self._code_interpreter_manager: Optional[
339
+ CodeInterpreterManagerProtocol
340
+ ] = None
341
+ self._file_search_manager: Optional[FileSearchManagerProtocol] = None
342
+
343
+ # Validate configurations at initialization
344
+ self._validated_configs: Dict[
345
+ str, Optional[ServiceConfigurationBase]
346
+ ] = {}
347
+ self._validate_service_configurations()
348
+
349
+ async def get_mcp_manager(self) -> Optional[MCPManagerProtocol]:
350
+ """Get or create MCP server manager.
351
+
352
+ Returns:
353
+ Configured MCP manager instance or None if not needed
354
+ """
355
+ if self._mcp_manager is None and self.args.get("mcp_servers"):
356
+ self._mcp_manager = await self.factory.create_mcp_manager(
357
+ self.args
358
+ )
359
+ return self._mcp_manager
360
+
361
+ async def get_code_interpreter_manager(
362
+ self,
363
+ ) -> Optional[CodeInterpreterManagerProtocol]:
364
+ """Get or create code interpreter manager.
365
+
366
+ Returns:
367
+ Configured code interpreter manager instance or None if not needed
368
+ """
369
+ if self._code_interpreter_manager is None and self.args.get(
370
+ "code_interpreter"
371
+ ):
372
+ self._code_interpreter_manager = (
373
+ await self.factory.create_code_interpreter_manager(
374
+ self.args, self.client
375
+ )
376
+ )
377
+ return self._code_interpreter_manager
378
+
379
+ async def get_file_search_manager(
380
+ self,
381
+ ) -> Optional[FileSearchManagerProtocol]:
382
+ """Get or create file search manager.
383
+
384
+ Returns:
385
+ Configured file search manager instance or None if not needed
386
+ """
387
+ if self._file_search_manager is None and self.args.get("file_search"):
388
+ self._file_search_manager = (
389
+ await self.factory.create_file_search_manager(
390
+ self.args, self.client
391
+ )
392
+ )
393
+ return self._file_search_manager
394
+
395
+ async def cleanup(self) -> None:
396
+ """Clean up all managed services."""
397
+ cleanup_tasks = []
398
+
399
+ # Clean up MCP manager if it exists
400
+ if self._mcp_manager:
401
+ # MCPServerManager doesn't have a cleanup method, skip it
402
+ pass
403
+
404
+ # Clean up code interpreter manager
405
+ if self._code_interpreter_manager:
406
+ # Use the specific cleanup method for code interpreter
407
+ if hasattr(
408
+ self._code_interpreter_manager, "cleanup_uploaded_files"
409
+ ):
410
+ cleanup_tasks.append(
411
+ self._code_interpreter_manager.cleanup_uploaded_files()
412
+ )
413
+
414
+ # Clean up file search manager
415
+ if self._file_search_manager:
416
+ # Use the specific cleanup method for file search
417
+ if hasattr(self._file_search_manager, "cleanup_resources"):
418
+ cleanup_tasks.append(
419
+ self._file_search_manager.cleanup_resources()
420
+ )
421
+
422
+ # Execute cleanup tasks concurrently
423
+ if cleanup_tasks:
424
+ import asyncio
425
+
426
+ await asyncio.gather(*cleanup_tasks, return_exceptions=True)
427
+
428
+ def is_configured(self, service_name: str) -> bool:
429
+ """Check if a service is configured.
430
+
431
+ Args:
432
+ service_name: Name of the service to check
433
+
434
+ Returns:
435
+ True if the service is configured in args
436
+ """
437
+ service_mapping = {
438
+ "mcp": "mcp_servers",
439
+ "code_interpreter": "code_interpreter",
440
+ "file_search": "file_search",
441
+ }
442
+
443
+ arg_key = service_mapping.get(service_name)
444
+ if not arg_key:
445
+ return False
446
+
447
+ return bool(self.args.get(arg_key))
448
+
449
+ def get_service_configuration(self, service_name: str) -> Any:
450
+ """Get service configuration from args.
451
+
452
+ Args:
453
+ service_name: Name of the service
454
+
455
+ Returns:
456
+ Service configuration or None
457
+ """
458
+ service_mapping = {
459
+ "mcp": "mcp_servers",
460
+ "code_interpreter": "code_interpreter",
461
+ "file_search": "file_search",
462
+ }
463
+
464
+ arg_key = service_mapping.get(service_name)
465
+ if not arg_key:
466
+ return None
467
+
468
+ return self.args.get(arg_key)
469
+
470
+ def _validate_service_configurations(self) -> None:
471
+ """Validate all service configurations at container initialization."""
472
+ validator = ServiceConfigurationValidator()
473
+
474
+ # Validate MCP configuration if present
475
+ if self.args.get("mcp_servers"):
476
+ try:
477
+ mcp_config = {"servers": self.args["mcp_servers"]}
478
+ self._validated_configs["mcp"] = validator.validate_mcp_config(
479
+ mcp_config
480
+ )
481
+ logger.debug("MCP configuration validated successfully")
482
+ except (ValidationError, ValueError) as e:
483
+ logger.warning(f"MCP configuration validation failed: {e}")
484
+ self._validated_configs["mcp"] = None
485
+
486
+ # Validate Code Interpreter configuration if present
487
+ if self.args.get("code_interpreter"):
488
+ try:
489
+ ci_config = {"enabled": True} # Basic config, extend as needed
490
+ self._validated_configs["code_interpreter"] = (
491
+ validator.validate_code_interpreter_config(ci_config)
492
+ )
493
+ logger.debug(
494
+ "Code Interpreter configuration validated successfully"
495
+ )
496
+ except (ValidationError, ValueError) as e:
497
+ logger.warning(
498
+ f"Code Interpreter configuration validation failed: {e}"
499
+ )
500
+ self._validated_configs["code_interpreter"] = None
501
+
502
+ # Validate File Search configuration if present
503
+ if self.args.get("file_search"):
504
+ try:
505
+ fs_config = {"enabled": True} # Basic config, extend as needed
506
+ self._validated_configs["file_search"] = (
507
+ validator.validate_file_search_config(fs_config)
508
+ )
509
+ logger.debug(
510
+ "File Search configuration validated successfully"
511
+ )
512
+ except (ValidationError, ValueError) as e:
513
+ logger.warning(
514
+ f"File Search configuration validation failed: {e}"
515
+ )
516
+ self._validated_configs["file_search"] = None
517
+
518
+ def get_validated_config(
519
+ self, service_name: str
520
+ ) -> Optional[ServiceConfigurationBase]:
521
+ """Get validated configuration for a service.
522
+
523
+ Args:
524
+ service_name: Name of the service
525
+
526
+ Returns:
527
+ Validated configuration or None if validation failed
528
+ """
529
+ return self._validated_configs.get(service_name)
530
+
531
+ async def check_service_health(self, service_name: str) -> ServiceHealth:
532
+ """Check health of a specific service.
533
+
534
+ Args:
535
+ service_name: Name of the service to check
536
+
537
+ Returns:
538
+ ServiceHealth with status and details
539
+ """
540
+ if service_name == "mcp":
541
+ mcp_manager = await self.get_mcp_manager()
542
+ if mcp_manager and hasattr(mcp_manager, "health_check"):
543
+ return await mcp_manager.health_check()
544
+ elif mcp_manager:
545
+ return ServiceHealth(
546
+ status=ServiceStatus.HEALTHY,
547
+ message="MCP manager is running",
548
+ details={"has_health_check": False},
549
+ )
550
+ elif service_name == "code_interpreter":
551
+ ci_manager = await self.get_code_interpreter_manager()
552
+ if ci_manager and hasattr(ci_manager, "health_check"):
553
+ return await ci_manager.health_check()
554
+ elif ci_manager:
555
+ return ServiceHealth(
556
+ status=ServiceStatus.HEALTHY,
557
+ message="Code Interpreter manager is running",
558
+ details={"has_health_check": False},
559
+ )
560
+ elif service_name == "file_search":
561
+ fs_manager = await self.get_file_search_manager()
562
+ if fs_manager and hasattr(fs_manager, "health_check"):
563
+ return await fs_manager.health_check()
564
+ elif fs_manager:
565
+ return ServiceHealth(
566
+ status=ServiceStatus.HEALTHY,
567
+ message="File Search manager is running",
568
+ details={"has_health_check": False},
569
+ )
570
+
571
+ # Service not configured or not found
572
+ return ServiceHealth(
573
+ status=ServiceStatus.UNKNOWN,
574
+ message=f"Service '{service_name}' not configured or not found",
575
+ )
576
+
577
+ async def check_all_services_health(self) -> Dict[str, ServiceHealth]:
578
+ """Check health of all configured services.
579
+
580
+ Returns:
581
+ Dictionary mapping service names to their health status
582
+ """
583
+ health_checks = {}
584
+
585
+ # Check each configured service
586
+ for service_name in ["mcp", "code_interpreter", "file_search"]:
587
+ if self.is_configured(service_name):
588
+ health_checks[service_name] = await self.check_service_health(
589
+ service_name
590
+ )
591
+
592
+ return health_checks
593
+
594
+ def get_service_info(self) -> Dict[str, Dict[str, Any]]:
595
+ """Get information about all services.
596
+
597
+ Returns:
598
+ Dictionary with service information including configuration status
599
+ """
600
+ service_info = {}
601
+
602
+ for service_name in ["mcp", "code_interpreter", "file_search"]:
603
+ is_configured = self.is_configured(service_name)
604
+ validated_config = self.get_validated_config(service_name)
605
+
606
+ service_info[service_name] = {
607
+ "configured": is_configured,
608
+ "validated": validated_config is not None,
609
+ "config_valid": (
610
+ validated_config is not None if is_configured else None
611
+ ),
612
+ "has_manager": False, # Will be updated when managers are created
613
+ }
614
+
615
+ # Check if manager exists
616
+ if service_name == "mcp" and self._mcp_manager:
617
+ service_info[service_name]["has_manager"] = True
618
+ elif (
619
+ service_name == "code_interpreter"
620
+ and self._code_interpreter_manager
621
+ ):
622
+ service_info[service_name]["has_manager"] = True
623
+ elif service_name == "file_search" and self._file_search_manager:
624
+ service_info[service_name]["has_manager"] = True
625
+
626
+ return service_info