ibm-watsonx-orchestrate 1.2.0__py3-none-any.whl → 1.4.2__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 (59) hide show
  1. ibm_watsonx_orchestrate/__init__.py +1 -1
  2. ibm_watsonx_orchestrate/agent_builder/agents/types.py +6 -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 +13 -15
  6. ibm_watsonx_orchestrate/agent_builder/toolkits/base_toolkit.py +32 -0
  7. ibm_watsonx_orchestrate/agent_builder/toolkits/types.py +42 -0
  8. ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +14 -13
  9. ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +4 -2
  10. ibm_watsonx_orchestrate/agent_builder/tools/types.py +2 -1
  11. ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +29 -0
  12. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +273 -12
  13. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_command.py +2 -2
  14. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +79 -39
  15. ibm_watsonx_orchestrate/cli/commands/models/env_file_model_provider_mapper.py +180 -0
  16. ibm_watsonx_orchestrate/cli/commands/models/models_command.py +194 -8
  17. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +117 -48
  18. ibm_watsonx_orchestrate/cli/commands/server/types.py +105 -0
  19. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +55 -7
  20. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +123 -42
  21. ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +22 -1
  22. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +209 -25
  23. ibm_watsonx_orchestrate/cli/init_helper.py +43 -0
  24. ibm_watsonx_orchestrate/cli/main.py +3 -1
  25. ibm_watsonx_orchestrate/client/agents/agent_client.py +4 -1
  26. ibm_watsonx_orchestrate/client/agents/assistant_agent_client.py +5 -1
  27. ibm_watsonx_orchestrate/client/agents/external_agent_client.py +5 -1
  28. ibm_watsonx_orchestrate/client/analytics/llm/analytics_llm_client.py +2 -6
  29. ibm_watsonx_orchestrate/client/base_api_client.py +5 -2
  30. ibm_watsonx_orchestrate/client/connections/connections_client.py +15 -21
  31. ibm_watsonx_orchestrate/client/model_policies/__init__.py +0 -0
  32. ibm_watsonx_orchestrate/client/model_policies/model_policies_client.py +47 -0
  33. ibm_watsonx_orchestrate/client/model_policies/types.py +36 -0
  34. ibm_watsonx_orchestrate/client/models/__init__.py +0 -0
  35. ibm_watsonx_orchestrate/client/models/models_client.py +46 -0
  36. ibm_watsonx_orchestrate/client/models/types.py +177 -0
  37. ibm_watsonx_orchestrate/client/toolkit/toolkit_client.py +15 -6
  38. ibm_watsonx_orchestrate/client/tools/tempus_client.py +40 -0
  39. ibm_watsonx_orchestrate/client/tools/tool_client.py +8 -0
  40. ibm_watsonx_orchestrate/docker/compose-lite.yml +68 -13
  41. ibm_watsonx_orchestrate/docker/default.env +22 -12
  42. ibm_watsonx_orchestrate/docker/tempus/common-config.yaml +1 -1
  43. ibm_watsonx_orchestrate/experimental/flow_builder/__init__.py +0 -0
  44. ibm_watsonx_orchestrate/experimental/flow_builder/flows/__init__.py +41 -0
  45. ibm_watsonx_orchestrate/experimental/flow_builder/flows/constants.py +17 -0
  46. ibm_watsonx_orchestrate/experimental/flow_builder/flows/data_map.py +91 -0
  47. ibm_watsonx_orchestrate/experimental/flow_builder/flows/decorators.py +143 -0
  48. ibm_watsonx_orchestrate/experimental/flow_builder/flows/events.py +72 -0
  49. ibm_watsonx_orchestrate/experimental/flow_builder/flows/flow.py +1288 -0
  50. ibm_watsonx_orchestrate/experimental/flow_builder/node.py +97 -0
  51. ibm_watsonx_orchestrate/experimental/flow_builder/resources/flow_status.openapi.yml +98 -0
  52. ibm_watsonx_orchestrate/experimental/flow_builder/types.py +492 -0
  53. ibm_watsonx_orchestrate/experimental/flow_builder/utils.py +113 -0
  54. ibm_watsonx_orchestrate/utils/utils.py +5 -2
  55. {ibm_watsonx_orchestrate-1.2.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/METADATA +6 -2
  56. {ibm_watsonx_orchestrate-1.2.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/RECORD +59 -36
  57. {ibm_watsonx_orchestrate-1.2.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/WHEEL +0 -0
  58. {ibm_watsonx_orchestrate-1.2.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/entry_points.txt +0 -0
  59. {ibm_watsonx_orchestrate-1.2.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/licenses/LICENSE +0 -0
@@ -4,13 +4,16 @@ import rich
4
4
  import requests
5
5
  import importlib
6
6
  import inspect
7
+ import zipfile
7
8
  import sys
9
+ import io
8
10
  import logging
9
11
  from pathlib import Path
10
12
  from copy import deepcopy
11
13
 
12
14
  from typing import Iterable, List
13
- from ibm_watsonx_orchestrate.cli.commands.tools.tools_controller import import_python_tool
15
+ from ibm_watsonx_orchestrate.cli.commands.tools.tools_controller import import_python_tool, ToolsController
16
+ from ibm_watsonx_orchestrate.cli.commands.knowledge_bases.knowledge_bases_controller import import_python_knowledge_base
14
17
 
15
18
  from ibm_watsonx_orchestrate.agent_builder.agents import (
16
19
  Agent,
@@ -27,12 +30,14 @@ from ibm_watsonx_orchestrate.client.connections import get_connections_client
27
30
  from ibm_watsonx_orchestrate.client.knowledge_bases.knowledge_base_client import KnowledgeBaseClient
28
31
 
29
32
  from ibm_watsonx_orchestrate.client.utils import instantiate_client
33
+ from ibm_watsonx_orchestrate.utils.utils import check_file_in_zip
30
34
 
31
35
  logger = logging.getLogger(__name__)
32
36
 
33
37
  def import_python_agent(file: str) -> List[Agent | ExternalAgent | AssistantAgent]:
34
38
  # Import tools
35
39
  import_python_tool(file)
40
+ import_python_knowledge_base(file)
36
41
 
37
42
  file_path = Path(file)
38
43
  file_directory = file_path.parent
@@ -140,10 +145,29 @@ def get_conn_id_from_app_id(app_id: str) -> str:
140
145
  connections_client = get_connections_client()
141
146
  connection = connections_client.get_draft_by_app_id(app_id=app_id)
142
147
  if not connection:
143
- logger.error(f"No connection exits with the app-id '{app_id}'")
148
+ logger.error(f"No connection exists with the app-id '{app_id}'")
144
149
  exit(1)
145
150
  return connection.connection_id
146
151
 
152
+ def get_app_id_from_conn_id(conn_id: str) -> str:
153
+ connections_client = get_connections_client()
154
+ app_id = connections_client.get_draft_by_id(conn_id=conn_id)
155
+ if not app_id or app_id == conn_id:
156
+ logger.error(f"No connection exists with the connection id '{conn_id}'")
157
+ exit(1)
158
+ return app_id
159
+
160
+ def get_agent_details(name: str, client: AgentClient | ExternalAgentClient | AssistantAgentClient) -> dict:
161
+ agent_specs = client.get_draft_by_name(name)
162
+ if len(agent_specs) > 1:
163
+ logger.error(f"Multiple agents with the name '{name}' found. Failed to get agent")
164
+ sys.exit(1)
165
+ if len(agent_specs) == 0:
166
+ logger.error(f"No agents with the name '{name}' found. Failed to get agent")
167
+ sys.exit(1)
168
+
169
+ return agent_specs[0]
170
+
147
171
  class AgentsController:
148
172
  def __init__(self):
149
173
  self.native_client = None
@@ -225,6 +249,7 @@ class AgentsController:
225
249
  matching_external_agents = external_client.get_drafts_by_names(deref_agent.collaborators)
226
250
  matching_assistant_agents = assistant_client.get_drafts_by_names(deref_agent.collaborators)
227
251
  matching_agents = matching_native_agents + matching_external_agents + matching_assistant_agents
252
+
228
253
  name_id_lookup = {}
229
254
  for a in matching_agents:
230
255
  if a.get("name") in name_id_lookup:
@@ -243,6 +268,35 @@ class AgentsController:
243
268
 
244
269
  return deref_agent
245
270
 
271
+ def reference_collaborators(self, agent: Agent) -> Agent:
272
+ native_client = self.get_native_client()
273
+ external_client = self.get_external_client()
274
+ assistant_client = self.get_assistant_client()
275
+
276
+ ref_agent = deepcopy(agent)
277
+ matching_native_agents = native_client.get_drafts_by_ids(ref_agent.collaborators)
278
+ matching_external_agents = external_client.get_drafts_by_ids(ref_agent.collaborators)
279
+ matching_assistant_agents = assistant_client.get_drafts_by_ids(ref_agent.collaborators)
280
+ matching_agents = matching_native_agents + matching_external_agents + matching_assistant_agents
281
+
282
+ id_name_lookup = {}
283
+ for a in matching_agents:
284
+ if a.get("id") in id_name_lookup:
285
+ logger.error(f"Duplicate draft entries for collaborator '{a.get('id')}'")
286
+ sys.exit(1)
287
+ id_name_lookup[a.get("id")] = a.get("name")
288
+
289
+ ref_collaborators = []
290
+ for id in agent.collaborators:
291
+ name = id_name_lookup.get(id)
292
+ if not name:
293
+ logger.error(f"Failed to find collaborator. No agents found with the id '{id}'")
294
+ sys.exit(1)
295
+ ref_collaborators.append(name)
296
+ ref_agent.collaborators = ref_collaborators
297
+
298
+ return ref_agent
299
+
246
300
  def dereference_tools(self, agent: Agent) -> Agent:
247
301
  tool_client = self.get_tool_client()
248
302
 
@@ -252,7 +306,7 @@ class AgentsController:
252
306
  name_id_lookup = {}
253
307
  for tool in matching_tools:
254
308
  if tool.get("name") in name_id_lookup:
255
- logger.error(f"Duplicate draft entries for tol '{tool.get('name')}'")
309
+ logger.error(f"Duplicate draft entries for tool '{tool.get('name')}'")
256
310
  sys.exit(1)
257
311
  name_id_lookup[tool.get("name")] = tool.get("id")
258
312
 
@@ -267,6 +321,30 @@ class AgentsController:
267
321
 
268
322
  return deref_agent
269
323
 
324
+ def reference_tools(self, agent: Agent) -> Agent:
325
+ tool_client = self.get_tool_client()
326
+
327
+ ref_agent = deepcopy(agent)
328
+ matching_tools = tool_client.get_drafts_by_ids(ref_agent.tools)
329
+
330
+ id_name_lookup = {}
331
+ for tool in matching_tools:
332
+ if tool.get("id") in id_name_lookup:
333
+ logger.error(f"Duplicate draft entries for tool '{tool.get('id')}'")
334
+ sys.exit(1)
335
+ id_name_lookup[tool.get("id")] = tool.get("name")
336
+
337
+ ref_tools = []
338
+ for id in agent.tools:
339
+ name = id_name_lookup[id]
340
+ if not name:
341
+ logger.error(f"Failed to find tool. No tools found with the id '{id}'")
342
+ sys.exit(1)
343
+ ref_tools.append(name)
344
+ ref_agent.tools = ref_tools
345
+
346
+ return ref_agent
347
+
270
348
  def dereference_knowledge_bases(self, agent: Agent) -> Agent:
271
349
  client = self.get_knowledge_base_client()
272
350
 
@@ -291,6 +369,22 @@ class AgentsController:
291
369
 
292
370
  return deref_agent
293
371
 
372
+ def reference_knowledge_bases(self, agent: Agent) -> Agent:
373
+ client = self.get_knowledge_base_client()
374
+
375
+ ref_agent = deepcopy(agent)
376
+
377
+ ref_knowledge_bases = []
378
+ for id in agent.knowledge_base:
379
+ matching_knowledge_base = client.get_by_id(id)
380
+ name = matching_knowledge_base.get("name")
381
+ if not name:
382
+ logger.error(f"Failed to find knowledge base. No knowledge base found with the id '{id}'")
383
+ sys.exit(1)
384
+ ref_knowledge_bases.append(name)
385
+ ref_agent.knowledge_base = ref_knowledge_bases
386
+ return ref_agent
387
+
294
388
  @staticmethod
295
389
  def dereference_app_id(agent: ExternalAgent | AssistantAgent) -> ExternalAgent | AssistantAgent:
296
390
  if agent.kind == AgentKind.EXTERNAL:
@@ -299,6 +393,17 @@ class AgentsController:
299
393
  agent.config.connection_id = get_conn_id_from_app_id(agent.config.app_id)
300
394
 
301
395
  return agent
396
+
397
+ @staticmethod
398
+ def reference_app_id(agent: ExternalAgent | AssistantAgent) -> ExternalAgent | AssistantAgent:
399
+ if agent.kind == AgentKind.EXTERNAL:
400
+ agent.app_id = get_app_id_from_conn_id(agent.connection_id)
401
+ agent.connection_id = None
402
+ else:
403
+ agent.config.app_id = get_app_id_from_conn_id(agent.config.connection_id)
404
+ agent.config.connection_id = None
405
+
406
+ return agent
302
407
 
303
408
 
304
409
  def dereference_native_agent_dependencies(self, agent: Agent) -> Agent:
@@ -311,6 +416,16 @@ class AgentsController:
311
416
 
312
417
  return agent
313
418
 
419
+ def reference_native_agent_dependencies(self, agent: Agent) -> Agent:
420
+ if agent.collaborators and len(agent.collaborators):
421
+ agent = self.reference_collaborators(agent)
422
+ if agent.tools and len(agent.tools):
423
+ agent = self.reference_tools(agent)
424
+ if agent.knowledge_base and len(agent.knowledge_base):
425
+ agent = self.reference_knowledge_bases(agent)
426
+
427
+ return agent
428
+
314
429
  def dereference_external_or_assistant_agent_dependencies(self, agent: ExternalAgent | AssistantAgent) -> ExternalAgent | AssistantAgent:
315
430
  agent_dict = agent.model_dump()
316
431
 
@@ -318,13 +433,28 @@ class AgentsController:
318
433
  agent = self.dereference_app_id(agent)
319
434
 
320
435
  return agent
321
-
322
- def dereference_agent_dependencies(self, agent: Agent ) -> Agent | ExternalAgent | AssistantAgent:
436
+
437
+ def reference_external_or_assistant_agent_dependencies(self, agent: ExternalAgent | AssistantAgent) -> ExternalAgent | AssistantAgent:
438
+ agent_dict = agent.model_dump()
439
+
440
+ if agent_dict.get("connection_id") or agent.config.model_dump().get("connection_id"):
441
+ agent = self.reference_app_id(agent)
442
+
443
+ return agent
444
+
445
+ # Convert all names used in an agent to the corresponding ids
446
+ def dereference_agent_dependencies(self, agent: Agent | ExternalAgent | AssistantAgent ) -> Agent | ExternalAgent | AssistantAgent:
323
447
  if isinstance(agent, Agent):
324
448
  return self.dereference_native_agent_dependencies(agent)
325
449
  if isinstance(agent, ExternalAgent) or isinstance(agent, AssistantAgent):
326
450
  return self.dereference_external_or_assistant_agent_dependencies(agent)
327
-
451
+
452
+ # Convert all ids used in an agent to the corresponding names
453
+ def reference_agent_dependencies(self, agent: Agent | ExternalAgent | AssistantAgent ) -> Agent | ExternalAgent | AssistantAgent:
454
+ if isinstance(agent, Agent):
455
+ return self.reference_native_agent_dependencies(agent)
456
+ if isinstance(agent, ExternalAgent) or isinstance(agent, AssistantAgent):
457
+ return self.reference_external_or_assistant_agent_dependencies(agent)
328
458
 
329
459
  def publish_or_update_agents(
330
460
  self, agents: Iterable[Agent]
@@ -366,13 +496,13 @@ class AgentsController:
366
496
 
367
497
  def publish_agent(self, agent: Agent, **kwargs) -> None:
368
498
  if isinstance(agent, Agent):
369
- self.get_native_client().create(agent.model_dump())
499
+ self.get_native_client().create(agent.model_dump(exclude_none=True))
370
500
  logger.info(f"Agent '{agent.name}' imported successfully")
371
501
  if isinstance(agent, ExternalAgent):
372
- self.get_external_client().create(agent.model_dump())
502
+ self.get_external_client().create(agent.model_dump(exclude_none=True))
373
503
  logger.info(f"External Agent '{agent.name}' imported successfully")
374
504
  if isinstance(agent, AssistantAgent):
375
- self.get_assistant_client().create(agent.model_dump(by_alias=True))
505
+ self.get_assistant_client().create(agent.model_dump(exclude_none=True, by_alias=True))
376
506
  logger.info(f"Assistant Agent '{agent.name}' imported successfully")
377
507
 
378
508
  def update_agent(
@@ -380,15 +510,15 @@ class AgentsController:
380
510
  ) -> None:
381
511
  if isinstance(agent, Agent):
382
512
  logger.info(f"Existing Agent '{agent.name}' found. Updating...")
383
- self.get_native_client().update(agent_id, agent.model_dump())
513
+ self.get_native_client().update(agent_id, agent.model_dump(exclude_none=True))
384
514
  logger.info(f"Agent '{agent.name}' updated successfully")
385
515
  if isinstance(agent, ExternalAgent):
386
516
  logger.info(f"Existing External Agent '{agent.name}' found. Updating...")
387
- self.get_external_client().update(agent_id, agent.model_dump())
517
+ self.get_external_client().update(agent_id, agent.model_dump(exclude_none=True))
388
518
  logger.info(f"External Agent '{agent.name}' updated successfully")
389
519
  if isinstance(agent, AssistantAgent):
390
520
  logger.info(f"Existing Assistant Agent '{agent.name}' found. Updating...")
391
- self.get_assistant_client().update(agent_id, agent.model_dump(by_alias=True))
521
+ self.get_assistant_client().update(agent_id, agent.model_dump(exclude_none=True, by_alias=True))
392
522
  logger.info(f"Assistant Agent '{agent.name}' updated successfully")
393
523
 
394
524
  @staticmethod
@@ -657,4 +787,135 @@ class AgentsController:
657
787
  except requests.HTTPError as e:
658
788
  logger.error(e.response.text)
659
789
  exit(1)
790
+
791
+ def get_spec_file_content(self, agent: Agent | ExternalAgent | AssistantAgent):
792
+ ref_agent = self.reference_agent_dependencies(agent)
793
+ agent_spec = ref_agent.model_dump(mode='json', exclude_none=True)
794
+ return agent_spec
795
+
796
+ def get_agent(self, name: str, kind: AgentKind) -> Agent | ExternalAgent | AssistantAgent:
797
+ match kind:
798
+ case AgentKind.NATIVE:
799
+ client = self.get_native_client()
800
+ agent_details = get_agent_details(name=name, client=client)
801
+ agent = Agent.model_validate(agent_details)
802
+ case AgentKind.EXTERNAL:
803
+ client = self.get_external_client()
804
+ agent_details = get_agent_details(name=name, client=client)
805
+ agent = ExternalAgent.model_validate(agent_details)
806
+ case AgentKind.ASSISTANT:
807
+ client = self.get_assistant_client()
808
+ agent_details = get_agent_details(name=name, client=client)
809
+ agent = AssistantAgent.model_validate(agent_details)
810
+
811
+ return agent
812
+
813
+ def get_agent_by_id(self, id: str) -> Agent | ExternalAgent | AssistantAgent:
814
+ native_client = self.get_native_client()
815
+ external_client = self.get_external_client()
816
+ assistant_client = self.get_assistant_client()
817
+
818
+ native_result = native_client.get_draft_by_id(id)
819
+ external_result = external_client.get_draft_by_id(id)
820
+ assistant_result = assistant_client.get_draft_by_id(id)
821
+
822
+ if native_result:
823
+ return Agent.model_validate(native_result)
824
+ if external_result:
825
+ return ExternalAgent.model_validate(external_result)
826
+ if assistant_result:
827
+ return AssistantAgent.model_validate(assistant_result)
828
+
829
+
830
+ def export_agent(self, name: str, kind: AgentKind, output_path: str, agent_only_flag: bool=False, zip_file_out: zipfile.ZipFile | None = None) -> None:
831
+
832
+ output_file = Path(output_path)
833
+ output_file_extension = output_file.suffix
834
+ output_file_name = output_file.stem
835
+ if not agent_only_flag and output_file_extension != ".zip":
836
+ logger.error(f"Output file must end with the extension '.zip'. Provided file '{output_path}' ends with '{output_file_extension}'")
837
+ sys.exit(1)
838
+ elif agent_only_flag and (output_file_extension != ".yaml" and output_file_extension != ".yml"):
839
+ logger.error(f"Output file must end with the extension '.yaml' or '.yml'. Provided file '{output_path}' ends with '{output_file_extension}'")
840
+ sys.exit(1)
841
+
842
+ agent = self.get_agent(name, kind)
843
+ agent_spec_file_content = self.get_spec_file_content(agent)
844
+
845
+ agent_spec_file_content.pop("hidden", None)
846
+ agent_spec_file_content.pop("id", None)
847
+ agent_spec_file_content["spec_version"] = SpecVersion.V1.value
848
+
849
+ if agent_only_flag:
850
+ logger.info(f"Exported agent definition for '{name}' to '{output_path}'")
851
+ with open(output_path, 'w') as outfile:
852
+ yaml.dump(agent_spec_file_content, outfile, sort_keys=False, default_flow_style=False)
853
+ return
854
+
855
+ close_file_flag = False
856
+ if zip_file_out is None:
857
+ close_file_flag = True
858
+ zip_file_out = zipfile.ZipFile(output_path, "w")
859
+
860
+ logger.info(f"Exporting agent definition for '{name}'")
861
+
862
+ agent_spec_yaml = yaml.dump(agent_spec_file_content, sort_keys=False, default_flow_style=False)
863
+ agent_spec_yaml_bytes = agent_spec_yaml.encode("utf-8")
864
+ agent_spec_yaml_file = io.BytesIO(agent_spec_yaml_bytes)
865
+
866
+ # Skip processing an agent if its already been saved
867
+ agent_file_path = f"{output_file_name}/agents/{agent_spec_file_content.get('kind', 'unknown')}/{agent_spec_file_content.get('name')}.yaml"
868
+ if check_file_in_zip(file_path=agent_file_path, zip_file=zip_file_out):
869
+ logger.warning(f"Skipping {agent_spec_file_content.get('name')}, agent with that name already exists in the output folder")
870
+ if close_file_flag:
871
+ zip_file_out.close()
872
+ return
873
+
874
+ zip_file_out.writestr(
875
+ agent_file_path,
876
+ agent_spec_yaml_file.getvalue()
877
+ )
878
+
879
+ tools_contoller = ToolsController()
880
+ for tool_name in agent_spec_file_content.get("tools", []):
881
+
882
+ base_tool_file_path = f"{output_file_name}/tools/{tool_name}/"
883
+ if check_file_in_zip(file_path=base_tool_file_path, zip_file=zip_file_out):
884
+ continue
885
+
886
+ logger.info(f"Exporting tool '{tool_name}'")
887
+ tool_artifact_bytes = tools_contoller.download_tool(tool_name)
888
+ if not tool_artifact_bytes:
889
+ continue
890
+
891
+ with zipfile.ZipFile(io.BytesIO(tool_artifact_bytes), "r") as zip_file_in:
892
+ for item in zip_file_in.infolist():
893
+ buffer = zip_file_in.read(item.filename)
894
+ if (item.filename != 'bundle-format'):
895
+ zip_file_out.writestr(
896
+ f"{base_tool_file_path}{item.filename}",
897
+ buffer
898
+ )
899
+
900
+ for kb_name in agent_spec_file_content.get("knowledge_base", []):
901
+ logger.warning(f"Skipping {kb_name}, knowledge_bases are currently unsupported by export")
902
+
903
+ if kind == AgentKind.NATIVE:
904
+ for collaborator_id in agent.collaborators:
905
+ collaborator = self.get_agent_by_id(collaborator_id)
906
+
907
+ if not collaborator:
908
+ logger.warning(f"Skipping {collaborator_id}, no agent with id {collaborator_id} found")
909
+ continue
910
+
911
+ self.export_agent(
912
+ name=collaborator.name,
913
+ kind=collaborator.kind,
914
+ output_path=output_path,
915
+ agent_only_flag=False,
916
+ zip_file_out=zip_file_out)
917
+
918
+ if close_file_flag:
919
+ logger.info(f"Successfully wrote agents and tools to '{output_path}'")
920
+ zip_file_out.close()
660
921
 
@@ -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
 
@@ -22,6 +49,15 @@ def get_file_name(path: str):
22
49
  # return to_column_name(path.split("/")[-1].split(".")[0])
23
50
  return path.split("/")[-1]
24
51
 
52
+ def get_relative_file_path(path, dir):
53
+ if path.startswith("/"):
54
+ return path
55
+ elif path.startswith("./"):
56
+ return f"{dir}{path.removeprefix('.')}"
57
+ else:
58
+ return f"{dir}/{path}"
59
+
60
+
25
61
  class KnowledgeBaseController:
26
62
  def __init__(self):
27
63
  self.client = None
@@ -34,45 +70,46 @@ class KnowledgeBaseController:
34
70
 
35
71
  def import_knowledge_base(self, file: str, app_id: str):
36
72
  client = self.get_client()
37
- create_request = KnowledgeBaseCreateRequest.from_spec(file=file)
38
73
 
39
- try:
40
- if create_request.documents:
41
- try:
74
+ knowledge_bases = parse_file(file=file)
75
+ for kb in knowledge_bases:
76
+ try:
77
+ kb.validate_documents_or_index_exists()
78
+ if kb.documents:
42
79
  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)}.")
80
+ files = [('files', (get_file_name(file_path), open(get_relative_file_path(file_path, file_dir), 'rb'))) for file_path in kb.documents]
81
+
82
+ kb.prioritize_built_in_index = True
83
+ payload = kb.model_dump(exclude_none=True);
84
+ payload.pop('documents');
85
+
86
+ client.create_built_in(payload=payload, files=files)
87
+ else:
88
+ if len(kb.conversational_search_tool.index_config) != 1:
89
+ raise ValueError(f"Must provide exactly one conversational_search_tool.index_config. Provided {len(kb.conversational_search_tool.index_config)}.")
90
+
91
+
92
+ if app_id:
93
+ connections_client = get_connections_client()
94
+ connection_id = None
95
+ if app_id is not None:
96
+ connections = connections_client.get_draft_by_app_id(app_id=app_id)
97
+ if not connections:
98
+ logger.error(f"No connection exists with the app-id '{app_id}'")
99
+ exit(1)
100
+
101
+ connection_id = connections.connection_id
102
+ kb.conversational_search_tool.index_config[0].connection_id = connection_id
103
+
104
+ kb.prioritize_built_in_index = False
105
+ client.create(payload=kb.model_dump(exclude_none=True))
55
106
 
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}")
107
+ logger.info(f"Successfully imported knowledge base '{kb.name}'")
108
+ except ClientAPIException as e:
109
+ if "duplicate key value violates unique constraint" in e.response.text:
110
+ logger.error(f"A knowledge base with the name '{kb.name}' already exists. Failed to import knowledge base")
111
+ else:
112
+ logger.error(f"Error importing knowledge base '{kb.name}\n' {e.response.text}")
76
113
 
77
114
  def get_id(
78
115
  self, id: str, name: str
@@ -101,13 +138,16 @@ class KnowledgeBaseController:
101
138
 
102
139
  if update_request.documents:
103
140
  file_dir = "/".join(file.split("/")[:-1])
104
- 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 update_request.documents]
141
+ files = [('files', (get_file_name(file_path), open(get_relative_file_path(file_path, file_dir), 'rb'))) for file_path in update_request.documents]
105
142
 
143
+ update_request.prioritize_built_in_index = True
106
144
  payload = update_request.model_dump(exclude_none=True);
107
145
  payload.pop('documents');
108
146
 
109
147
  self.get_client().update_with_documents(knowledge_base_id, payload=payload, files=files)
110
148
  else:
149
+ if update_request.conversational_search_tool and update_request.conversational_search_tool.index_config:
150
+ update_request.prioritize_built_in_index = False
111
151
  self.get_client().update(knowledge_base_id, update_request.model_dump(exclude_none=True))
112
152
 
113
153
  logEnding = f"with ID '{id}'" if id else f"'{name}'"