ibm-watsonx-orchestrate 1.1.0__py3-none-any.whl → 1.3.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 (24) hide show
  1. ibm_watsonx_orchestrate/__init__.py +1 -1
  2. ibm_watsonx_orchestrate/agent_builder/agents/types.py +4 -1
  3. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/knowledge_base.py +16 -3
  4. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/knowledge_base_requests.py +4 -20
  5. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +4 -13
  6. ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +4 -12
  7. ibm_watsonx_orchestrate/agent_builder/tools/types.py +18 -5
  8. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +2 -0
  9. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_command.py +2 -2
  10. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +63 -38
  11. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +71 -0
  12. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +212 -0
  13. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +49 -21
  14. ibm_watsonx_orchestrate/cli/init_helper.py +43 -0
  15. ibm_watsonx_orchestrate/cli/main.py +7 -3
  16. ibm_watsonx_orchestrate/client/connections/connections_client.py +13 -13
  17. ibm_watsonx_orchestrate/client/toolkit/toolkit_client.py +81 -0
  18. ibm_watsonx_orchestrate/docker/compose-lite.yml +1 -0
  19. ibm_watsonx_orchestrate/docker/default.env +7 -7
  20. {ibm_watsonx_orchestrate-1.1.0.dist-info → ibm_watsonx_orchestrate-1.3.0.dist-info}/METADATA +3 -2
  21. {ibm_watsonx_orchestrate-1.1.0.dist-info → ibm_watsonx_orchestrate-1.3.0.dist-info}/RECORD +24 -20
  22. {ibm_watsonx_orchestrate-1.1.0.dist-info → ibm_watsonx_orchestrate-1.3.0.dist-info}/WHEEL +0 -0
  23. {ibm_watsonx_orchestrate-1.1.0.dist-info → ibm_watsonx_orchestrate-1.3.0.dist-info}/entry_points.txt +0 -0
  24. {ibm_watsonx_orchestrate-1.1.0.dist-info → ibm_watsonx_orchestrate-1.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -5,7 +5,7 @@
5
5
 
6
6
  pkg_name = "ibm-watsonx-orchestrate"
7
7
 
8
- __version__ = "1.1.0"
8
+ __version__ = "1.3.0"
9
9
 
10
10
 
11
11
 
@@ -5,6 +5,7 @@ from typing import List, Optional, Dict
5
5
  from pydantic import BaseModel, model_validator, ConfigDict
6
6
  from ibm_watsonx_orchestrate.agent_builder.tools import BaseTool
7
7
  from ibm_watsonx_orchestrate.agent_builder.knowledge_bases.types import KnowledgeBaseSpec
8
+ from ibm_watsonx_orchestrate.agent_builder.knowledge_bases.knowledge_base import KnowledgeBase
8
9
  from pydantic import Field, AliasChoices
9
10
  from typing import Annotated
10
11
 
@@ -85,6 +86,8 @@ class AgentSpec(BaseAgentSpec):
85
86
  def __init__(self, *args, **kwargs):
86
87
  if "tools" in kwargs and kwargs["tools"]:
87
88
  kwargs["tools"] = [x.__tool_spec__.name if isinstance(x, BaseTool) else x for x in kwargs["tools"]]
89
+ if "knowledge_base" in kwargs and kwargs["knowledge_base"]:
90
+ kwargs["knowledge_base"] = [x.name if isinstance(x, KnowledgeBase) else x for x in kwargs["knowledge_base"]]
88
91
  if "collaborators" in kwargs and kwargs["collaborators"]:
89
92
  kwargs["collaborators"] = [x.name if isinstance(x, BaseAgentSpec) else x for x in kwargs["collaborators"]]
90
93
  super().__init__(*args, **kwargs)
@@ -101,7 +104,7 @@ class AgentSpec(BaseAgentSpec):
101
104
 
102
105
  def validate_agent_fields(values: dict) -> dict:
103
106
  # Check for empty strings or whitespace
104
- for field in ["id", "name", "kind", "description", "collaborators", "tools"]:
107
+ for field in ["id", "name", "kind", "description", "collaborators", "tools", "knowledge_base"]:
105
108
  value = values.get(field)
106
109
  if value and not str(value).strip():
107
110
  raise ValueError(f"{field} cannot be empty or just whitespace")
@@ -1,7 +1,7 @@
1
1
  import json
2
2
  from ibm_watsonx_orchestrate.utils.utils import yaml_safe_load
3
- from .types import KnowledgeBaseSpec
4
-
3
+ from .types import KnowledgeBaseSpec, KnowledgeBaseKind
4
+ from pydantic import model_validator
5
5
 
6
6
  class KnowledgeBase(KnowledgeBaseSpec):
7
7
 
@@ -24,4 +24,17 @@ class KnowledgeBase(KnowledgeBaseSpec):
24
24
  return f"KnowledgeBase(id='{self.id}', name='{self.name}', description='{self.description}')"
25
25
 
26
26
  def __str__(self):
27
- return self.__repr__()
27
+ return self.__repr__()
28
+
29
+ # Not a model validator since we only want to validate this on import
30
+ def validate_documents_or_index_exists(self):
31
+ if self.documents and self.conversational_search_tool and self.conversational_search_tool.index_config or \
32
+ (not self.documents and (not self.conversational_search_tool or not self.conversational_search_tool.index_config)):
33
+ raise ValueError("Must provide either \"documents\" or \"conversational_search_tool.index_config\", but not both")
34
+ return self
35
+
36
+ @model_validator(mode="after")
37
+ def validate_kind(self):
38
+ if self.kind != KnowledgeBaseKind.KNOWLEDGE_BASE:
39
+ raise ValueError(f"The specified kind '{self.kind}' cannot be used to create a knowledge base")
40
+ return self
@@ -1,12 +1,12 @@
1
1
  import json
2
2
  from ibm_watsonx_orchestrate.utils.utils import yaml_safe_load
3
- from .types import CreateKnowledgeBase, PatchKnowledgeBase, KnowledgeBaseKind
3
+ from .types import KnowledgeBaseSpec, PatchKnowledgeBase, KnowledgeBaseKind
4
4
 
5
5
 
6
- class KnowledgeBaseCreateRequest(CreateKnowledgeBase):
6
+ class KnowledgeBaseCreateRequest(KnowledgeBaseSpec):
7
7
 
8
8
  @staticmethod
9
- def from_spec(file: str) -> 'CreateKnowledgeBase':
9
+ def from_spec(file: str) -> 'KnowledgeBaseSpec':
10
10
  with open(file, 'r') as f:
11
11
  if file.endswith('.yaml') or file.endswith('.yml'):
12
12
  content = yaml_safe_load(f)
@@ -15,20 +15,10 @@ class KnowledgeBaseCreateRequest(CreateKnowledgeBase):
15
15
  else:
16
16
  raise ValueError('file must end in .json, .yaml, or .yml')
17
17
 
18
- if (content.get('documents') and content.get("conversational_search_tool", {}).get("index_config")) or \
19
- (not content.get('documents') and not content.get("conversational_search_tool", {}).get("index_config")):
20
- raise ValueError("Must provide either \"documents\" or \"conversational_search_tool.index_config\", but not both")
21
-
22
18
  if not content.get("spec_version"):
23
19
  raise ValueError(f"Field 'spec_version' not provided. Please ensure provided spec conforms to a valid spec format")
24
20
 
25
- if not content.get("kind"):
26
- raise ValueError(f"Field 'kind' not provided. Should be 'knowledge_base'")
27
-
28
- if content.get("kind") != KnowledgeBaseKind.KNOWLEDGE_BASE:
29
- raise ValueError(f"Field 'kind' should be 'knowledge_base', but is set to '{content.get('kind')}'")
30
-
31
- knowledge_base = CreateKnowledgeBase.model_validate(content)
21
+ knowledge_base = KnowledgeBaseSpec.model_validate(content)
32
22
 
33
23
  return knowledge_base
34
24
 
@@ -48,12 +38,6 @@ class KnowledgeBaseUpdateRequest(PatchKnowledgeBase):
48
38
  if not content.get("spec_version"):
49
39
  raise ValueError(f"Field 'spec_version' not provided. Please ensure provided spec conforms to a valid spec format")
50
40
 
51
- if not content.get("kind"):
52
- raise ValueError(f"Field 'kind' not provided. Should be 'knowledge_base'")
53
-
54
- if content.get("kind") != KnowledgeBaseKind.KNOWLEDGE_BASE:
55
- raise ValueError(f"Field 'kind' should be 'knowledge_base', but is set to '{content.get('kind')}'")
56
-
57
41
  patch = PatchKnowledgeBase.model_validate(content)
58
42
 
59
43
  return patch
@@ -206,16 +206,6 @@ class KnowledgeBaseBuiltInVectorIndexConfig(BaseModel):
206
206
  chunk_overlap: Optional[int] = None
207
207
  limit: Optional[int] = None
208
208
 
209
- class CreateKnowledgeBase(BaseModel):
210
- """request payload schema"""
211
- name: Optional[str] = None
212
- description: Optional[str] = None
213
- documents: list[str] = None
214
- vector_index: Optional[KnowledgeBaseBuiltInVectorIndexConfig] = None
215
- conversational_search_tool: Optional[ConversationalSearchConfig] = None
216
- prioritize_built_in_index: Optional[bool] = None
217
-
218
-
219
209
  class PatchKnowledgeBase(BaseModel):
220
210
  """request payload schema"""
221
211
  description: Optional[str] = None
@@ -224,20 +214,21 @@ class PatchKnowledgeBase(BaseModel):
224
214
  prioritize_built_in_index: Optional[bool] = None
225
215
  representation: Optional[KnowledgeBaseRepresentation] = None
226
216
 
227
-
228
217
  class KnowledgeBaseSpec(BaseModel):
229
218
  """Schema for a complete knowledge-base."""
230
219
  spec_version: SpecVersion = None
231
220
  kind: KnowledgeBaseKind = KnowledgeBaseKind.KNOWLEDGE_BASE
232
221
  id: Optional[UUID] = None
233
222
  tenant_id: Optional[str] = None
234
- name: Optional[str] = None
223
+ name: str
235
224
  description: Optional[str] = None
236
225
  vector_index: Optional[KnowledgeBaseBuiltInVectorIndexConfig] = None
237
226
  conversational_search_tool: Optional[ConversationalSearchConfig] | Optional[UUID] = None
238
227
  prioritize_built_in_index: Optional[bool] = None
228
+ representation: Optional[KnowledgeBaseRepresentation] = None
239
229
  vector_index_id: Optional[UUID] = None
240
230
  created_by: Optional[str] = None
241
231
  created_on: Optional[datetime] = None
242
232
  updated_at: Optional[datetime] = None
243
-
233
+ # For import/update
234
+ documents: list[str] = None
@@ -115,17 +115,9 @@ def create_openapi_json_tool(
115
115
  raise ValueError(
116
116
  f"Path {http_path} did not have an http_method {http_method}. Available methods are {list(route.keys())}")
117
117
 
118
- operation_id = (
119
- re.sub(
120
- '_+',
121
- '_',
122
- re.sub(
123
- r'[^a-zA-Z_]',
124
- '_',
125
- route_spec.get('operationId', None)
126
- )
127
- )
128
- ) if route_spec.get('operationId', None) is not None else None
118
+ operation_id = re.sub( r'(\W|_)+', '_', route_spec.get('operationId') ) \
119
+ if route_spec.get('operationId', None) else None
120
+
129
121
  spec_name = name or operation_id
130
122
  spec_permission = permission or _action_to_perm(route_spec.get('x-ibm-operation', {}).get('action'))
131
123
  if spec_name is None:
@@ -142,7 +134,7 @@ def create_openapi_json_tool(
142
134
  description=spec_description,
143
135
  permission=spec_permission
144
136
  )
145
-
137
+
146
138
  spec.input_schema = input_schema or ToolRequestBody(
147
139
  type='object',
148
140
  properties={},
@@ -1,5 +1,5 @@
1
1
  from enum import Enum
2
- from typing import List, Any, Dict, Literal, Optional
2
+ from typing import List, Any, Dict, Literal, Optional, Union
3
3
 
4
4
  from pydantic import BaseModel, model_validator, ConfigDict, Field, AliasChoices
5
5
 
@@ -14,7 +14,7 @@ class ToolPermission(str, Enum):
14
14
  class JsonSchemaObject(BaseModel):
15
15
  model_config = ConfigDict(extra='allow')
16
16
 
17
- type: Optional[Literal['object', 'string', 'number', 'integer', 'boolean', 'array', 'null']] = None
17
+ type: Optional[Union[Literal['object', 'string', 'number', 'integer', 'boolean', 'array', 'null'], List[Literal['object', 'string', 'number', 'integer', 'boolean', 'array', 'null']]]] = None
18
18
  title: str | None = None
19
19
  description: str | None = None
20
20
  properties: Optional[Dict[str, 'JsonSchemaObject']] = None
@@ -34,13 +34,19 @@ class JsonSchemaObject(BaseModel):
34
34
  aliasName: str | None = None
35
35
  "Runtime feature where the sdk can provide the original name of a field before prefixing"
36
36
 
37
+ @model_validator(mode='after')
38
+ def normalize_type_field(self) -> 'JsonSchemaObject':
39
+ if isinstance(self.type, list):
40
+ self.type = self.type[0]
41
+ return self
42
+
37
43
 
38
44
  class ToolRequestBody(BaseModel):
39
45
  model_config = ConfigDict(extra='allow')
40
46
 
41
47
  type: Literal['object']
42
48
  properties: Dict[str, JsonSchemaObject]
43
- required: List[str]
49
+ required: Optional[List[str]] = None
44
50
 
45
51
 
46
52
  class ToolResponseBody(BaseModel):
@@ -52,7 +58,7 @@ class ToolResponseBody(BaseModel):
52
58
  items: JsonSchemaObject = None
53
59
  uniqueItems: bool = None
54
60
  anyOf: List['JsonSchemaObject'] = None
55
- required: List[str] = None
61
+ required: Optional[List[str]] = None
56
62
 
57
63
  class OpenApiSecurityScheme(BaseModel):
58
64
  type: Literal['apiKey', 'http', 'oauth2', 'openIdConnect']
@@ -128,6 +134,10 @@ class SkillToolBinding(BaseModel):
128
134
  class ClientSideToolBinding(BaseModel):
129
135
  pass
130
136
 
137
+ class McpToolBinding(BaseModel):
138
+ server_url: Optional[str] = None
139
+ source: str
140
+ connections: Dict[str, str]
131
141
 
132
142
  class ToolBinding(BaseModel):
133
143
  openapi: OpenApiToolBinding = None
@@ -135,6 +145,7 @@ class ToolBinding(BaseModel):
135
145
  wxflows: WxFlowsToolBinding = None
136
146
  skill: SkillToolBinding = None
137
147
  client_side: ClientSideToolBinding = None
148
+ mcp: McpToolBinding = None
138
149
 
139
150
  @model_validator(mode='after')
140
151
  def validate_binding_type(self) -> 'ToolBinding':
@@ -143,7 +154,8 @@ class ToolBinding(BaseModel):
143
154
  self.python is not None,
144
155
  self.wxflows is not None,
145
156
  self.skill is not None,
146
- self.client_side is not None
157
+ self.client_side is not None,
158
+ self.mcp is not None
147
159
  ]
148
160
  if sum(bindings) == 0:
149
161
  raise ValueError("One binding must be set")
@@ -159,4 +171,5 @@ class ToolSpec(BaseModel):
159
171
  input_schema: ToolRequestBody = None
160
172
  output_schema: ToolResponseBody = None
161
173
  binding: ToolBinding = None
174
+ toolkit_id: str | None = None
162
175
 
@@ -11,6 +11,7 @@ from copy import deepcopy
11
11
 
12
12
  from typing import Iterable, List
13
13
  from ibm_watsonx_orchestrate.cli.commands.tools.tools_controller import import_python_tool
14
+ from ibm_watsonx_orchestrate.cli.commands.knowledge_bases.knowledge_bases_controller import import_python_knowledge_base
14
15
 
15
16
  from ibm_watsonx_orchestrate.agent_builder.agents import (
16
17
  Agent,
@@ -33,6 +34,7 @@ logger = logging.getLogger(__name__)
33
34
  def import_python_agent(file: str) -> List[Agent | ExternalAgent | AssistantAgent]:
34
35
  # Import tools
35
36
  import_python_tool(file)
37
+ import_python_knowledge_base(file)
36
38
 
37
39
  file_path = Path(file)
38
40
  file_directory = file_path.parent
@@ -9,7 +9,7 @@ knowledge_bases_app = typer.Typer(no_args_is_help=True)
9
9
  def knowledge_base_import(
10
10
  file: Annotated[
11
11
  str,
12
- typer.Option("--file", "-f", help="YAML file with knowledge base definition"),
12
+ typer.Option("--file", "-f", help="YAML, JSON or Python file with knowledge base definition(s)"),
13
13
  ],
14
14
  app_id: Annotated[
15
15
  str, typer.Option(
@@ -25,7 +25,7 @@ def knowledge_base_import(
25
25
  def knowledge_base_patch(
26
26
  file: Annotated[
27
27
  str,
28
- typer.Option("--file", "-f", help="YAML file with knowledge base definition"),
28
+ typer.Option("--file", "-f", help="YAML or JSON file with knowledge base definition"),
29
29
  ],
30
30
  name: Annotated[
31
31
  str,
@@ -3,17 +3,44 @@ import json
3
3
  import rich
4
4
  import requests
5
5
  import logging
6
+ import importlib
7
+ import inspect
8
+ from pathlib import Path
9
+ from typing import List
6
10
 
7
- from ibm_watsonx_orchestrate.agent_builder.knowledge_bases.knowledge_base_requests import KnowledgeBaseCreateRequest, KnowledgeBaseUpdateRequest
11
+ from ibm_watsonx_orchestrate.agent_builder.knowledge_bases.knowledge_base_requests import KnowledgeBaseUpdateRequest
8
12
  from ibm_watsonx_orchestrate.agent_builder.knowledge_bases.knowledge_base import KnowledgeBase
9
13
  from ibm_watsonx_orchestrate.client.knowledge_bases.knowledge_base_client import KnowledgeBaseClient
10
14
  from ibm_watsonx_orchestrate.client.base_api_client import ClientAPIException
11
15
  from ibm_watsonx_orchestrate.client.connections import get_connections_client
12
-
13
16
  from ibm_watsonx_orchestrate.client.utils import instantiate_client
14
17
 
15
18
  logger = logging.getLogger(__name__)
16
19
 
20
+ def import_python_knowledge_base(file: str) -> List[KnowledgeBase]:
21
+ file_path = Path(file)
22
+ file_directory = file_path.parent
23
+ file_name = file_path.stem
24
+ sys.path.append(str(file_directory))
25
+ module = importlib.import_module(file_name)
26
+ del sys.path[-1]
27
+
28
+ knowledge_bases = []
29
+ for _, obj in inspect.getmembers(module):
30
+ if isinstance(obj, KnowledgeBase):
31
+ knowledge_bases.append(obj)
32
+ return knowledge_bases
33
+
34
+ def parse_file(file: str) -> List[KnowledgeBase]:
35
+ if file.endswith('.yaml') or file.endswith('.yml') or file.endswith(".json"):
36
+ knowledge_base = KnowledgeBase.from_spec(file=file)
37
+ return [knowledge_base]
38
+ elif file.endswith('.py'):
39
+ knowledge_bases = import_python_knowledge_base(file)
40
+ return knowledge_bases
41
+ else:
42
+ raise ValueError("file must end in .json, .yaml, .yml or .py")
43
+
17
44
  def to_column_name(col: str):
18
45
  return " ".join([word.capitalize() if not word[0].isupper() else word for word in col.split("_")])
19
46
 
@@ -34,45 +61,43 @@ class KnowledgeBaseController:
34
61
 
35
62
  def import_knowledge_base(self, file: str, app_id: str):
36
63
  client = self.get_client()
37
- create_request = KnowledgeBaseCreateRequest.from_spec(file=file)
38
64
 
39
- try:
40
- if create_request.documents:
41
- try:
65
+ knowledge_bases = parse_file(file=file)
66
+ for kb in knowledge_bases:
67
+ try:
68
+ kb.validate_documents_or_index_exists()
69
+ if kb.documents:
42
70
  file_dir = "/".join(file.split("/")[:-1])
43
- files = [('files', (get_file_name(file_path), open(file_path if file_path.startswith("/") else f"{file_dir}/{file_path}", 'rb'))) for file_path in create_request.documents]
44
- except Exception as e:
45
- logger.error(f"Error importing knowledge base: {str(e).replace('[Errno 2] ', '')}")
46
- sys.exit(1);
47
-
48
- payload = create_request.model_dump(exclude_none=True);
49
- payload.pop('documents');
50
-
51
- client.create_built_in(payload=payload, files=files)
52
- else:
53
- if len(create_request.conversational_search_tool.index_config) != 1:
54
- raise ValueError(f"Must provide exactly one conversational_search_tool.index_config. Provided {len(create_request.conversational_search_tool.index_config)}.")
71
+ files = [('files', (get_file_name(file_path), open(file_path if file_path.startswith("/") else (file_path if not file_dir else f"{file_dir}/{file_path}"), 'rb'))) for file_path in kb.documents]
72
+
73
+ payload = kb.model_dump(exclude_none=True);
74
+ payload.pop('documents');
75
+
76
+ client.create_built_in(payload=payload, files=files)
77
+ else:
78
+ if len(kb.conversational_search_tool.index_config) != 1:
79
+ raise ValueError(f"Must provide exactly one conversational_search_tool.index_config. Provided {len(kb.conversational_search_tool.index_config)}.")
80
+
81
+ if app_id:
82
+ connections_client = get_connections_client()
83
+ connection_id = None
84
+ if app_id is not None:
85
+ connections = connections_client.get_draft_by_app_id(app_id=app_id)
86
+ if not connections:
87
+ logger.error(f"No connection exists with the app-id '{app_id}'")
88
+ exit(1)
89
+
90
+ connection_id = connections.connection_id
91
+ kb.conversational_search_tool.index_config[0].connection_id = connection_id
92
+
93
+ client.create(payload=kb.model_dump(exclude_none=True))
55
94
 
56
- if app_id:
57
- connections_client = get_connections_client()
58
- connection_id = None
59
- if app_id is not None:
60
- connections = connections_client.get_draft_by_app_id(app_id=app_id)
61
- if not connections:
62
- logger.error(f"No connection exists with the app-id '{app_id}'")
63
- exit(1)
64
-
65
- connection_id = connections.connection_id
66
- create_request.conversational_search_tool.index_config[0].connection_id = connection_id
67
-
68
- client.create(payload=create_request.model_dump(exclude_none=True))
69
-
70
- logger.info(f"Successfully imported knowledge base '{create_request.name}'")
71
- except ClientAPIException as e:
72
- if "duplicate key value violates unique constraint" in e.response.text:
73
- logger.error(f"A knowledge base with the name '{create_request.name}' already exists. Failed to import knowledge base")
74
- else:
75
- logger.error(f"Error importing knowledge base '{create_request.name}\n' {e.response.text}")
95
+ logger.info(f"Successfully imported knowledge base '{kb.name}'")
96
+ except ClientAPIException as e:
97
+ if "duplicate key value violates unique constraint" in e.response.text:
98
+ logger.error(f"A knowledge base with the name '{kb.name}' already exists. Failed to import knowledge base")
99
+ else:
100
+ logger.error(f"Error importing knowledge base '{kb.name}\n' {e.response.text}")
76
101
 
77
102
  def get_id(
78
103
  self, id: str, name: str
@@ -0,0 +1,71 @@
1
+ import typer
2
+ from typing import List
3
+ from typing_extensions import Annotated, Optional
4
+ from ibm_watsonx_orchestrate.cli.commands.toolkit.toolkit_controller import ToolkitController, ToolkitKind
5
+
6
+ toolkits_app = typer.Typer(no_args_is_help=True)
7
+
8
+ @toolkits_app.command(name="import")
9
+ def import_toolkit(
10
+ kind: Annotated[
11
+ ToolkitKind,
12
+ typer.Option("--kind", "-k", help="Kind of toolkit, currently only MCP is supported"),
13
+ ],
14
+ name: Annotated[
15
+ str,
16
+ typer.Option("--name", "-n", help="Name of the toolkit"),
17
+ ],
18
+ description: Annotated[
19
+ str,
20
+ typer.Option("--description", help="Description of the toolkit"),
21
+ ],
22
+ package_root: Annotated[
23
+ str,
24
+ typer.Option("--package-root", "-p", help="Root directory of the MCP server package"),
25
+ ],
26
+ command: Annotated[
27
+ str,
28
+ typer.Option(
29
+ "--command",
30
+ help="Command to start the MCP server. Can be a string (e.g. 'node dist/index.js --transport stdio') "
31
+ "or a JSON-style list of arguments (e.g. '[\"node\", \"dist/index.js\", \"--transport\", \"stdio\"]'). "
32
+ "The first argument will be used as the executable, the rest as its arguments."
33
+ ),
34
+ ],
35
+ tools: Annotated[
36
+ Optional[str],
37
+ typer.Option("--tools", "-t", help="Comma-separated list of tools to import. Or you can use `*` to use all tools"),
38
+ ] = None,
39
+ app_id: Annotated[
40
+ List[str],
41
+ typer.Option(
42
+ "--app-id", "-a",
43
+ help='The app id of the connection to associate with this tool. A application connection represents the server authentication credentials needed to connect to this tool. Only type key_value is currently supported for MCP.'
44
+ )
45
+ ] = None
46
+ ):
47
+ if tools == "*":
48
+ tool_list = ["*"] # Wildcard to use all tools
49
+ elif tools:
50
+ tool_list = [tool.strip() for tool in tools.split(",")]
51
+ else:
52
+ tool_list = None
53
+
54
+ toolkit_controller = ToolkitController(
55
+ kind=kind,
56
+ name=name,
57
+ description=description,
58
+ package_root=package_root,
59
+ command=command,
60
+ )
61
+ toolkit_controller.import_toolkit(tools=tool_list, app_id=app_id)
62
+
63
+ @toolkits_app.command(name="remove")
64
+ def remove_toolkit(
65
+ name: Annotated[
66
+ str,
67
+ typer.Option("--name", "-n", help="Name of the toolkit you wish to remove"),
68
+ ],
69
+ ):
70
+ toolkit_controller = ToolkitController()
71
+ toolkit_controller.remove_toolkit(name=name)