alita-sdk 0.3.365__py3-none-any.whl → 0.3.462__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.

Potentially problematic release.


This version of alita-sdk might be problematic. Click here for more details.

Files changed (118) hide show
  1. alita_sdk/cli/__init__.py +10 -0
  2. alita_sdk/cli/__main__.py +17 -0
  3. alita_sdk/cli/agent_executor.py +144 -0
  4. alita_sdk/cli/agent_loader.py +197 -0
  5. alita_sdk/cli/agent_ui.py +166 -0
  6. alita_sdk/cli/agents.py +1069 -0
  7. alita_sdk/cli/callbacks.py +576 -0
  8. alita_sdk/cli/cli.py +159 -0
  9. alita_sdk/cli/config.py +153 -0
  10. alita_sdk/cli/formatting.py +182 -0
  11. alita_sdk/cli/mcp_loader.py +315 -0
  12. alita_sdk/cli/toolkit.py +330 -0
  13. alita_sdk/cli/toolkit_loader.py +55 -0
  14. alita_sdk/cli/tools/__init__.py +9 -0
  15. alita_sdk/cli/tools/filesystem.py +905 -0
  16. alita_sdk/configurations/bitbucket.py +95 -0
  17. alita_sdk/configurations/confluence.py +96 -1
  18. alita_sdk/configurations/gitlab.py +79 -0
  19. alita_sdk/configurations/jira.py +103 -0
  20. alita_sdk/configurations/testrail.py +88 -0
  21. alita_sdk/configurations/xray.py +93 -0
  22. alita_sdk/configurations/zephyr_enterprise.py +93 -0
  23. alita_sdk/configurations/zephyr_essential.py +75 -0
  24. alita_sdk/runtime/clients/artifact.py +1 -1
  25. alita_sdk/runtime/clients/client.py +47 -10
  26. alita_sdk/runtime/clients/mcp_discovery.py +342 -0
  27. alita_sdk/runtime/clients/mcp_manager.py +262 -0
  28. alita_sdk/runtime/clients/sandbox_client.py +373 -0
  29. alita_sdk/runtime/langchain/assistant.py +70 -41
  30. alita_sdk/runtime/langchain/constants.py +6 -1
  31. alita_sdk/runtime/langchain/document_loaders/AlitaDocxMammothLoader.py +315 -3
  32. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +4 -1
  33. alita_sdk/runtime/langchain/document_loaders/constants.py +73 -100
  34. alita_sdk/runtime/langchain/langraph_agent.py +164 -38
  35. alita_sdk/runtime/langchain/utils.py +43 -7
  36. alita_sdk/runtime/models/mcp_models.py +61 -0
  37. alita_sdk/runtime/toolkits/__init__.py +24 -0
  38. alita_sdk/runtime/toolkits/application.py +8 -1
  39. alita_sdk/runtime/toolkits/artifact.py +5 -6
  40. alita_sdk/runtime/toolkits/mcp.py +895 -0
  41. alita_sdk/runtime/toolkits/tools.py +140 -50
  42. alita_sdk/runtime/tools/__init__.py +7 -2
  43. alita_sdk/runtime/tools/application.py +7 -0
  44. alita_sdk/runtime/tools/function.py +94 -5
  45. alita_sdk/runtime/tools/graph.py +10 -4
  46. alita_sdk/runtime/tools/image_generation.py +104 -8
  47. alita_sdk/runtime/tools/llm.py +204 -114
  48. alita_sdk/runtime/tools/mcp_inspect_tool.py +284 -0
  49. alita_sdk/runtime/tools/mcp_remote_tool.py +166 -0
  50. alita_sdk/runtime/tools/mcp_server_tool.py +3 -1
  51. alita_sdk/runtime/tools/sandbox.py +180 -79
  52. alita_sdk/runtime/tools/vectorstore.py +22 -21
  53. alita_sdk/runtime/tools/vectorstore_base.py +79 -26
  54. alita_sdk/runtime/utils/mcp_oauth.py +164 -0
  55. alita_sdk/runtime/utils/mcp_sse_client.py +405 -0
  56. alita_sdk/runtime/utils/streamlit.py +34 -3
  57. alita_sdk/runtime/utils/toolkit_utils.py +14 -4
  58. alita_sdk/runtime/utils/utils.py +1 -0
  59. alita_sdk/tools/__init__.py +48 -31
  60. alita_sdk/tools/ado/repos/__init__.py +1 -0
  61. alita_sdk/tools/ado/test_plan/__init__.py +1 -1
  62. alita_sdk/tools/ado/wiki/__init__.py +1 -5
  63. alita_sdk/tools/ado/work_item/__init__.py +1 -5
  64. alita_sdk/tools/ado/work_item/ado_wrapper.py +17 -8
  65. alita_sdk/tools/base_indexer_toolkit.py +194 -112
  66. alita_sdk/tools/bitbucket/__init__.py +1 -0
  67. alita_sdk/tools/chunkers/sematic/proposal_chunker.py +1 -1
  68. alita_sdk/tools/code/sonar/__init__.py +1 -1
  69. alita_sdk/tools/code_indexer_toolkit.py +15 -5
  70. alita_sdk/tools/confluence/__init__.py +2 -2
  71. alita_sdk/tools/confluence/api_wrapper.py +110 -63
  72. alita_sdk/tools/confluence/loader.py +10 -0
  73. alita_sdk/tools/elitea_base.py +22 -22
  74. alita_sdk/tools/github/__init__.py +2 -2
  75. alita_sdk/tools/gitlab/__init__.py +2 -1
  76. alita_sdk/tools/gitlab/api_wrapper.py +11 -7
  77. alita_sdk/tools/gitlab_org/__init__.py +1 -2
  78. alita_sdk/tools/google_places/__init__.py +2 -1
  79. alita_sdk/tools/jira/__init__.py +1 -0
  80. alita_sdk/tools/jira/api_wrapper.py +1 -1
  81. alita_sdk/tools/memory/__init__.py +1 -1
  82. alita_sdk/tools/non_code_indexer_toolkit.py +2 -2
  83. alita_sdk/tools/openapi/__init__.py +10 -1
  84. alita_sdk/tools/pandas/__init__.py +1 -1
  85. alita_sdk/tools/postman/__init__.py +2 -1
  86. alita_sdk/tools/postman/api_wrapper.py +18 -8
  87. alita_sdk/tools/postman/postman_analysis.py +8 -1
  88. alita_sdk/tools/pptx/__init__.py +2 -2
  89. alita_sdk/tools/qtest/__init__.py +3 -3
  90. alita_sdk/tools/qtest/api_wrapper.py +1708 -76
  91. alita_sdk/tools/rally/__init__.py +1 -2
  92. alita_sdk/tools/report_portal/__init__.py +1 -0
  93. alita_sdk/tools/salesforce/__init__.py +1 -0
  94. alita_sdk/tools/servicenow/__init__.py +2 -3
  95. alita_sdk/tools/sharepoint/__init__.py +1 -0
  96. alita_sdk/tools/sharepoint/api_wrapper.py +125 -34
  97. alita_sdk/tools/sharepoint/authorization_helper.py +191 -1
  98. alita_sdk/tools/sharepoint/utils.py +8 -2
  99. alita_sdk/tools/slack/__init__.py +1 -0
  100. alita_sdk/tools/sql/__init__.py +2 -1
  101. alita_sdk/tools/sql/api_wrapper.py +71 -23
  102. alita_sdk/tools/testio/__init__.py +1 -0
  103. alita_sdk/tools/testrail/__init__.py +1 -3
  104. alita_sdk/tools/utils/__init__.py +17 -0
  105. alita_sdk/tools/utils/content_parser.py +35 -24
  106. alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +67 -21
  107. alita_sdk/tools/xray/__init__.py +2 -1
  108. alita_sdk/tools/zephyr/__init__.py +2 -1
  109. alita_sdk/tools/zephyr_enterprise/__init__.py +1 -0
  110. alita_sdk/tools/zephyr_essential/__init__.py +1 -0
  111. alita_sdk/tools/zephyr_scale/__init__.py +1 -0
  112. alita_sdk/tools/zephyr_squad/__init__.py +1 -0
  113. {alita_sdk-0.3.365.dist-info → alita_sdk-0.3.462.dist-info}/METADATA +8 -2
  114. {alita_sdk-0.3.365.dist-info → alita_sdk-0.3.462.dist-info}/RECORD +118 -93
  115. alita_sdk-0.3.462.dist-info/entry_points.txt +2 -0
  116. {alita_sdk-0.3.365.dist-info → alita_sdk-0.3.462.dist-info}/WHEEL +0 -0
  117. {alita_sdk-0.3.365.dist-info → alita_sdk-0.3.462.dist-info}/licenses/LICENSE +0 -0
  118. {alita_sdk-0.3.365.dist-info → alita_sdk-0.3.462.dist-info}/top_level.txt +0 -0
alita_sdk/cli/cli.py ADDED
@@ -0,0 +1,159 @@
1
+ """
2
+ Main CLI application for Alita SDK.
3
+
4
+ Provides command-line interface for testing agents and toolkits,
5
+ using the same .env authentication as SDK tests and Streamlit interface.
6
+ """
7
+
8
+ # Suppress warnings FIRST before any other imports
9
+ import warnings
10
+ warnings.filterwarnings('ignore', category=DeprecationWarning)
11
+ warnings.filterwarnings('ignore', category=UserWarning)
12
+ warnings.filterwarnings('ignore', message='Unverified HTTPS request')
13
+
14
+ import click
15
+ import logging
16
+ import sys
17
+ from typing import Optional
18
+
19
+ from .config import get_config
20
+ from .formatting import get_formatter
21
+
22
+ # Configure logging
23
+ logging.basicConfig(
24
+ level=logging.WARNING,
25
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
26
+ )
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ @click.group()
31
+ @click.option('--env-file', default='.env', help='Path to .env file')
32
+ @click.option('--debug', is_flag=True, help='Enable debug logging')
33
+ @click.option('--output', type=click.Choice(['text', 'json']), default='text',
34
+ help='Output format')
35
+ @click.pass_context
36
+ def cli(ctx, env_file: str, debug: bool, output: str):
37
+ """
38
+ Alita SDK CLI - Test agents and toolkits from the command line.
39
+
40
+ Credentials are loaded from .env file with variables:
41
+ - DEPLOYMENT_URL: Alita deployment URL
42
+ - PROJECT_ID: Project ID
43
+ - API_KEY: API authentication key
44
+
45
+ Example .env file:
46
+
47
+ DEPLOYMENT_URL=https://api.elitea.ai
48
+ PROJECT_ID=123
49
+ API_KEY=your_api_key_here
50
+ """
51
+ ctx.ensure_object(dict)
52
+
53
+ # Enable debug logging if requested
54
+ if debug:
55
+ logging.getLogger('alita_sdk').setLevel(logging.DEBUG)
56
+ logger.setLevel(logging.DEBUG)
57
+ logger.debug("Debug logging enabled")
58
+
59
+ # Load configuration
60
+ config = get_config(env_file=env_file)
61
+ ctx.obj['config'] = config
62
+ ctx.obj['formatter'] = get_formatter(output)
63
+ ctx.obj['debug'] = debug
64
+
65
+ # Check if configuration is valid (but don't fail yet - some commands don't need it)
66
+ if not config.is_configured():
67
+ missing = config.get_missing_config()
68
+ ctx.obj['config_error'] = f"Missing required configuration: {', '.join(missing)}"
69
+ logger.debug(f"Configuration incomplete: {missing}")
70
+ else:
71
+ ctx.obj['config_error'] = None
72
+ logger.debug(f"Configuration loaded from {env_file}")
73
+
74
+
75
+ def get_client(ctx):
76
+ """
77
+ Get configured AlitaClient from context.
78
+
79
+ Raises click.ClickException if configuration is invalid.
80
+ """
81
+ if ctx.obj.get('config_error'):
82
+ raise click.ClickException(
83
+ f"{ctx.obj['config_error']}\n\n"
84
+ "Please ensure your .env file contains:\n"
85
+ " DEPLOYMENT_URL=https://api.elitea.ai\n"
86
+ " PROJECT_ID=123\n"
87
+ " API_KEY=your_api_key_here"
88
+ )
89
+
90
+ # Import here to avoid loading SDK if not needed
91
+ from alita_sdk.runtime.clients.client import AlitaClient
92
+
93
+ config = ctx.obj['config']
94
+
95
+ try:
96
+ client = AlitaClient(
97
+ base_url=config.deployment_url,
98
+ project_id=config.project_id,
99
+ auth_token=config.api_key
100
+ )
101
+ logger.debug(f"AlitaClient initialized for project {config.project_id}")
102
+ return client
103
+ except Exception as e:
104
+ raise click.ClickException(f"Failed to initialize AlitaClient: {str(e)}")
105
+
106
+
107
+ @cli.command()
108
+ @click.pass_context
109
+ def config(ctx):
110
+ """Show current configuration (credentials masked)."""
111
+ config_obj = ctx.obj['config']
112
+ formatter = ctx.obj['formatter']
113
+
114
+ if formatter.__class__.__name__ == 'JSONFormatter':
115
+ click.echo(formatter._dump(config_obj.to_dict()))
116
+ else:
117
+ click.echo("\nCurrent configuration:\n")
118
+ for key, value in config_obj.to_dict().items():
119
+ click.echo(f" {key}: {value}")
120
+
121
+ if not config_obj.is_configured():
122
+ missing = config_obj.get_missing_config()
123
+ click.echo(f"\n⚠ Missing: {', '.join(missing)}")
124
+ else:
125
+ click.echo("\n✓ Configuration is complete")
126
+
127
+
128
+ # Import subcommands
129
+ from . import toolkit
130
+ from . import agents
131
+
132
+ # Register subcommands
133
+ cli.add_command(toolkit.toolkit)
134
+ cli.add_command(agents.agent)
135
+
136
+ # Add top-level 'chat' command as alias to 'agent chat'
137
+ cli.add_command(agents.agent_chat, name='chat')
138
+
139
+
140
+ def main():
141
+ """Entry point for CLI."""
142
+ # Suppress warnings at entry point
143
+ warnings.filterwarnings('ignore', category=DeprecationWarning)
144
+ warnings.filterwarnings('ignore', category=UserWarning)
145
+ warnings.filterwarnings('ignore', message='Unverified HTTPS request')
146
+
147
+ try:
148
+ cli()
149
+ except KeyboardInterrupt:
150
+ click.echo("\n\nInterrupted by user", err=True)
151
+ sys.exit(130)
152
+ except Exception as e:
153
+ logger.exception("Unexpected error")
154
+ click.echo(f"\nError: {str(e)}", err=True)
155
+ sys.exit(1)
156
+
157
+
158
+ if __name__ == '__main__':
159
+ main()
@@ -0,0 +1,153 @@
1
+ """
2
+ Configuration management for Alita CLI.
3
+
4
+ Loads credentials and settings from .env files using the same pattern
5
+ as the SDK tests and Streamlit interface.
6
+ """
7
+
8
+ import os
9
+ import re
10
+ from typing import Optional, Dict, Any
11
+ from dotenv import load_dotenv
12
+ import logging
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class CLIConfig:
18
+ """Configuration manager for Alita CLI."""
19
+
20
+ def __init__(self, env_file: Optional[str] = None):
21
+ """
22
+ Initialize CLI configuration.
23
+
24
+ Args:
25
+ env_file: Path to .env file. If None, uses default (.env in current directory)
26
+ """
27
+ self.env_file = env_file or '.env'
28
+ self._load_env()
29
+
30
+ def _load_env(self):
31
+ """Load environment variables from .env file."""
32
+ if os.path.exists(self.env_file):
33
+ load_dotenv(self.env_file)
34
+ logger.debug(f"Loaded environment from {self.env_file}")
35
+ else:
36
+ logger.debug(f"No .env file found at {self.env_file}, using system environment")
37
+
38
+ @property
39
+ def deployment_url(self) -> Optional[str]:
40
+ """Get deployment URL from environment."""
41
+ return os.getenv('DEPLOYMENT_URL')
42
+
43
+ @property
44
+ def project_id(self) -> Optional[int]:
45
+ """Get project ID from environment."""
46
+ try:
47
+ value = os.getenv('PROJECT_ID')
48
+ return int(value) if value else None
49
+ except (TypeError, ValueError):
50
+ return None
51
+
52
+ @property
53
+ def api_key(self) -> Optional[str]:
54
+ """Get API key from environment."""
55
+ return os.getenv('API_KEY')
56
+
57
+ @property
58
+ def alita_dir(self) -> str:
59
+ """Get Alita directory from environment (defaults to .alita)."""
60
+ return os.getenv('ALITA_DIR', '.alita')
61
+
62
+ @property
63
+ def agents_dir(self) -> str:
64
+ """Get agents directory (derived from ALITA_DIR)."""
65
+ alita_agents = os.path.join(self.alita_dir, 'agents')
66
+ # Fallback to .github/agents if .alita/agents doesn't exist
67
+ if self.alita_dir == '.alita' and not os.path.exists(alita_agents):
68
+ if os.path.exists('.github/agents'):
69
+ return '.github/agents'
70
+ return alita_agents
71
+
72
+ @property
73
+ def tools_dir(self) -> str:
74
+ """Get tools directory (derived from ALITA_DIR)."""
75
+ return os.path.join(self.alita_dir, 'tools')
76
+
77
+ @property
78
+ def mcp_config_path(self) -> str:
79
+ """Get MCP configuration path (derived from ALITA_DIR)."""
80
+ alita_mcp = os.path.join(self.alita_dir, 'mcp.json')
81
+ # Fallback to mcp.json in current directory
82
+ if not os.path.exists(alita_mcp) and os.path.exists('mcp.json'):
83
+ return 'mcp.json'
84
+ return alita_mcp
85
+
86
+ def is_configured(self) -> bool:
87
+ """Check if all required configuration is present."""
88
+ return all([
89
+ self.deployment_url,
90
+ self.project_id is not None,
91
+ self.api_key
92
+ ])
93
+
94
+ def get_missing_config(self) -> list[str]:
95
+ """Get list of missing configuration items."""
96
+ missing = []
97
+ if not self.deployment_url:
98
+ missing.append('DEPLOYMENT_URL')
99
+ if self.project_id is None:
100
+ missing.append('PROJECT_ID')
101
+ if not self.api_key:
102
+ missing.append('API_KEY')
103
+ return missing
104
+
105
+ def to_dict(self) -> Dict[str, Any]:
106
+ """Convert configuration to dictionary."""
107
+ return {
108
+ 'deployment_url': self.deployment_url,
109
+ 'project_id': self.project_id,
110
+ 'api_key': '***' if self.api_key else None # Masked for security
111
+ }
112
+
113
+
114
+ def get_config(env_file: Optional[str] = None) -> CLIConfig:
115
+ """
116
+ Get CLI configuration instance.
117
+
118
+ Args:
119
+ env_file: Optional path to .env file
120
+
121
+ Returns:
122
+ CLIConfig instance
123
+ """
124
+ return CLIConfig(env_file=env_file)
125
+
126
+
127
+ def substitute_env_vars(text: str) -> str:
128
+ """
129
+ Substitute environment variables in text.
130
+
131
+ Supports both ${VAR} and $VAR syntax.
132
+
133
+ Args:
134
+ text: Text containing environment variable references
135
+
136
+ Returns:
137
+ Text with environment variables substituted
138
+ """
139
+ # Replace ${VAR} syntax
140
+ def replace_braced(match):
141
+ var_name = match.group(1)
142
+ return os.getenv(var_name, match.group(0))
143
+
144
+ text = re.sub(r'\$\{([^}]+)\}', replace_braced, text)
145
+
146
+ # Replace $VAR syntax (word boundaries)
147
+ def replace_simple(match):
148
+ var_name = match.group(1)
149
+ return os.getenv(var_name, match.group(0))
150
+
151
+ text = re.sub(r'\$([A-Za-z_][A-Za-z0-9_]*)', replace_simple, text)
152
+
153
+ return text
@@ -0,0 +1,182 @@
1
+ """
2
+ Output formatting utilities for Alita CLI.
3
+
4
+ Provides text and JSON formatters for displaying toolkit test results,
5
+ agent responses, and other CLI outputs.
6
+ """
7
+
8
+ import json
9
+ from typing import Any, Dict, List, Optional
10
+ from datetime import datetime
11
+
12
+
13
+ class OutputFormatter:
14
+ """Base class for output formatters."""
15
+
16
+ def format_toolkit_result(self, result: Dict[str, Any]) -> str:
17
+ """Format toolkit test result."""
18
+ raise NotImplementedError
19
+
20
+ def format_error(self, error: str) -> str:
21
+ """Format error message."""
22
+ raise NotImplementedError
23
+
24
+ def format_toolkit_list(self, toolkits: List[Dict[str, Any]]) -> str:
25
+ """Format list of available toolkits."""
26
+ raise NotImplementedError
27
+
28
+
29
+ class TextFormatter(OutputFormatter):
30
+ """Human-readable text formatter."""
31
+
32
+ def format_toolkit_result(self, result: Dict[str, Any]) -> str:
33
+ """Format toolkit test result as text."""
34
+ if not result.get('success', False):
35
+ return self.format_error(result.get('error', 'Unknown error'))
36
+
37
+ lines = [
38
+ "\n✓ Tool executed successfully\n",
39
+ f"Tool: {result.get('tool_name', 'unknown')}",
40
+ f"Toolkit: {result.get('toolkit_config', {}).get('type', 'unknown')}",
41
+ f"LLM Model: {result.get('llm_model', 'N/A')}",
42
+ f"Execution time: {result.get('execution_time_seconds', 0):.3f}s",
43
+ "",
44
+ "Result:",
45
+ ]
46
+
47
+ # Format result based on type
48
+ tool_result = result.get('result')
49
+ if isinstance(tool_result, str):
50
+ lines.append(f" {tool_result}")
51
+ elif isinstance(tool_result, dict):
52
+ for key, value in tool_result.items():
53
+ lines.append(f" {key}: {value}")
54
+ else:
55
+ lines.append(f" {str(tool_result)}")
56
+
57
+ # Add events if present
58
+ events = result.get('events_dispatched', [])
59
+ if events:
60
+ lines.extend([
61
+ "",
62
+ f"Events dispatched: {len(events)}"
63
+ ])
64
+ for event in events[:5]: # Limit to first 5 events
65
+ event_data = event.get('data', {})
66
+ message = event_data.get('message', str(event_data))
67
+ lines.append(f" - {event.get('name', 'event')}: {message}")
68
+
69
+ if len(events) > 5:
70
+ lines.append(f" ... and {len(events) - 5} more events")
71
+
72
+ return "\n".join(lines)
73
+
74
+ def format_error(self, error: str) -> str:
75
+ """Format error message as text."""
76
+ return f"\n✗ Error: {error}\n"
77
+
78
+ def format_toolkit_list(self, toolkits: List[Dict[str, Any]]) -> str:
79
+ """Format list of available toolkits as text."""
80
+ lines = ["\nAvailable toolkits:\n"]
81
+
82
+ for toolkit in sorted(toolkits, key=lambda x: x.get('name', '')):
83
+ name = toolkit.get('name', 'unknown')
84
+ class_name = toolkit.get('class_name', '')
85
+ lines.append(f" - {name}" + (f" ({class_name})" if class_name else ""))
86
+
87
+ lines.append(f"\nTotal: {len(toolkits)} toolkits")
88
+ return "\n".join(lines)
89
+
90
+ def format_toolkit_schema(self, toolkit_name: str, schema: Dict[str, Any]) -> str:
91
+ """Format toolkit schema as text."""
92
+ lines = [
93
+ f"\n{toolkit_name.title()} Toolkit Configuration Schema:\n",
94
+ ]
95
+
96
+ properties = schema.get('properties', {})
97
+ required = schema.get('required', [])
98
+
99
+ for field_name, field_schema in properties.items():
100
+ field_type = field_schema.get('type', 'any')
101
+ description = field_schema.get('description', '')
102
+ is_required = field_name in required
103
+ default = field_schema.get('default')
104
+
105
+ req_text = "required" if is_required else "optional"
106
+ lines.append(f" - {field_name} ({req_text}): {description}")
107
+ lines.append(f" Type: {field_type}")
108
+
109
+ if default is not None:
110
+ lines.append(f" Default: {default}")
111
+
112
+ # Show enum values if present
113
+ if 'enum' in field_schema:
114
+ lines.append(f" Options: {', '.join(map(str, field_schema['enum']))}")
115
+
116
+ # Handle nested objects
117
+ if field_type == 'object' and 'properties' in field_schema:
118
+ lines.append(f" Fields:")
119
+ for nested_name, nested_schema in field_schema['properties'].items():
120
+ nested_desc = nested_schema.get('description', '')
121
+ lines.append(f" - {nested_name}: {nested_desc}")
122
+
123
+ lines.append("")
124
+
125
+ return "\n".join(lines)
126
+
127
+
128
+ class JSONFormatter(OutputFormatter):
129
+ """JSON formatter for scripting and automation."""
130
+
131
+ def __init__(self, pretty: bool = True):
132
+ """
133
+ Initialize JSON formatter.
134
+
135
+ Args:
136
+ pretty: If True, format JSON with indentation
137
+ """
138
+ self.pretty = pretty
139
+
140
+ def _dump(self, data: Any) -> str:
141
+ """Dump data as JSON."""
142
+ if self.pretty:
143
+ return json.dumps(data, indent=2, default=str)
144
+ return json.dumps(data, default=str)
145
+
146
+ def format_toolkit_result(self, result: Dict[str, Any]) -> str:
147
+ """Format toolkit test result as JSON."""
148
+ return self._dump(result)
149
+
150
+ def format_error(self, error: str) -> str:
151
+ """Format error message as JSON."""
152
+ return self._dump({'success': False, 'error': error})
153
+
154
+ def format_toolkit_list(self, toolkits: List[Dict[str, Any]]) -> str:
155
+ """Format list of available toolkits as JSON."""
156
+ return self._dump({
157
+ 'toolkits': toolkits,
158
+ 'total': len(toolkits)
159
+ })
160
+
161
+ def format_toolkit_schema(self, toolkit_name: str, schema: Dict[str, Any]) -> str:
162
+ """Format toolkit schema as JSON."""
163
+ return self._dump({
164
+ 'toolkit': toolkit_name,
165
+ 'schema': schema
166
+ })
167
+
168
+
169
+ def get_formatter(output_format: str = 'text', pretty: bool = True) -> OutputFormatter:
170
+ """
171
+ Get output formatter by name.
172
+
173
+ Args:
174
+ output_format: Format type ('text' or 'json')
175
+ pretty: For JSON formatter, whether to pretty-print
176
+
177
+ Returns:
178
+ OutputFormatter instance
179
+ """
180
+ if output_format == 'json':
181
+ return JSONFormatter(pretty=pretty)
182
+ return TextFormatter()