ibm-watsonx-orchestrate 1.12.0b1__py3-none-any.whl → 1.13.0b0__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 +2 -1
  2. ibm_watsonx_orchestrate/agent_builder/connections/types.py +34 -3
  3. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +11 -2
  4. ibm_watsonx_orchestrate/agent_builder/models/types.py +17 -1
  5. ibm_watsonx_orchestrate/agent_builder/toolkits/types.py +14 -2
  6. ibm_watsonx_orchestrate/agent_builder/tools/__init__.py +1 -1
  7. ibm_watsonx_orchestrate/agent_builder/tools/langflow_tool.py +61 -1
  8. ibm_watsonx_orchestrate/agent_builder/tools/types.py +21 -3
  9. ibm_watsonx_orchestrate/agent_builder/voice_configurations/__init__.py +1 -1
  10. ibm_watsonx_orchestrate/agent_builder/voice_configurations/types.py +11 -0
  11. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +27 -51
  12. ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +2 -2
  13. ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +54 -28
  14. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_command.py +25 -2
  15. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_controller.py +249 -14
  16. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_server_controller.py +4 -4
  17. ibm_watsonx_orchestrate/cli/commands/environment/environment_command.py +5 -1
  18. ibm_watsonx_orchestrate/cli/commands/environment/environment_controller.py +6 -3
  19. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +3 -2
  20. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +1 -1
  21. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +45 -16
  22. ibm_watsonx_orchestrate/cli/commands/models/models_command.py +2 -2
  23. ibm_watsonx_orchestrate/cli/commands/models/models_controller.py +29 -10
  24. ibm_watsonx_orchestrate/cli/commands/partners/offering/partners_offering_controller.py +21 -4
  25. ibm_watsonx_orchestrate/cli/commands/partners/offering/types.py +7 -15
  26. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +19 -17
  27. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +139 -27
  28. ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +2 -2
  29. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +79 -36
  30. ibm_watsonx_orchestrate/cli/commands/voice_configurations/voice_configurations_controller.py +23 -11
  31. ibm_watsonx_orchestrate/cli/common.py +26 -0
  32. ibm_watsonx_orchestrate/cli/config.py +33 -2
  33. ibm_watsonx_orchestrate/client/connections/connections_client.py +1 -14
  34. ibm_watsonx_orchestrate/client/copilot/cpe/copilot_cpe_client.py +34 -1
  35. ibm_watsonx_orchestrate/client/knowledge_bases/knowledge_base_client.py +6 -2
  36. ibm_watsonx_orchestrate/client/model_policies/model_policies_client.py +1 -1
  37. ibm_watsonx_orchestrate/client/models/models_client.py +1 -1
  38. ibm_watsonx_orchestrate/client/threads/threads_client.py +34 -0
  39. ibm_watsonx_orchestrate/client/utils.py +29 -7
  40. ibm_watsonx_orchestrate/docker/compose-lite.yml +2 -2
  41. ibm_watsonx_orchestrate/docker/default.env +15 -9
  42. ibm_watsonx_orchestrate/flow_builder/flows/decorators.py +2 -0
  43. ibm_watsonx_orchestrate/flow_builder/flows/flow.py +59 -9
  44. ibm_watsonx_orchestrate/flow_builder/node.py +13 -1
  45. ibm_watsonx_orchestrate/flow_builder/types.py +39 -0
  46. ibm_watsonx_orchestrate/langflow/__init__.py +0 -0
  47. ibm_watsonx_orchestrate/langflow/langflow_utils.py +195 -0
  48. ibm_watsonx_orchestrate/langflow/lfx_deps.py +84 -0
  49. ibm_watsonx_orchestrate/utils/async_helpers.py +31 -0
  50. ibm_watsonx_orchestrate/utils/docker_utils.py +1177 -33
  51. ibm_watsonx_orchestrate/utils/environment.py +165 -20
  52. ibm_watsonx_orchestrate/utils/exceptions.py +1 -1
  53. ibm_watsonx_orchestrate/utils/tokens.py +51 -0
  54. ibm_watsonx_orchestrate/utils/utils.py +63 -4
  55. {ibm_watsonx_orchestrate-1.12.0b1.dist-info → ibm_watsonx_orchestrate-1.13.0b0.dist-info}/METADATA +2 -2
  56. {ibm_watsonx_orchestrate-1.12.0b1.dist-info → ibm_watsonx_orchestrate-1.13.0b0.dist-info}/RECORD +59 -52
  57. {ibm_watsonx_orchestrate-1.12.0b1.dist-info → ibm_watsonx_orchestrate-1.13.0b0.dist-info}/WHEEL +0 -0
  58. {ibm_watsonx_orchestrate-1.12.0b1.dist-info → ibm_watsonx_orchestrate-1.13.0b0.dist-info}/entry_points.txt +0 -0
  59. {ibm_watsonx_orchestrate-1.12.0b1.dist-info → ibm_watsonx_orchestrate-1.13.0b0.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,8 @@
1
1
  import typer
2
2
  from typing_extensions import Annotated
3
3
  from pathlib import Path
4
- from ibm_watsonx_orchestrate.cli.commands.copilot.copilot_controller import prompt_tune, create_agent
4
+ from ibm_watsonx_orchestrate.cli.commands.copilot.copilot_controller import prompt_tune, create_agent, \
5
+ refine_agent_with_trajectories
5
6
  from ibm_watsonx_orchestrate.cli.commands.copilot.copilot_server_controller import start_server, stop_server
6
7
 
7
8
  copilot_app = typer.Typer(no_args_is_help=True)
@@ -62,4 +63,26 @@ def prompt_tume_command(
62
63
  samples_file=samples,
63
64
  output_file=output_file,
64
65
  dry_run_flag=dry_run_flag,
65
- )
66
+ )
67
+
68
+ @copilot_app.command(name="autotune", help="Autotune the agent's instructions by incorporating insights from chat interactions and user feedback")
69
+ def agent_refine(
70
+ agent_name: Annotated[
71
+ str,
72
+ typer.Option("--agent-name", "-n", help="The name of the agent to tune"),
73
+ ],
74
+ output_file: Annotated[
75
+ str,
76
+ typer.Option("--output-file", "-o", help="Optional output file to avoid overwriting existing agent spec"),
77
+ ] = None,
78
+ use_last_chat: Annotated[
79
+ bool,
80
+ typer.Option("--use_last_chat", "-l", help="Tuning by using the last conversation with the agent instead of prompting the user to choose chats"),
81
+ ] = False,
82
+ dry_run_flag: Annotated[
83
+ bool,
84
+ typer.Option("--dry-run",
85
+ help="Dry run will prevent the tuned content being saved and output the results to console"),
86
+ ] = False,
87
+ ):
88
+ refine_agent_with_trajectories(agent_name, output_file, use_last_chat, dry_run_flag)
@@ -2,13 +2,17 @@ import logging
2
2
  import os
3
3
  import sys
4
4
  import csv
5
+ import difflib
6
+ from datetime import datetime
5
7
 
6
8
  import rich
7
9
  from rich.console import Console
8
10
  from rich.prompt import Prompt
9
11
  from rich.progress import Progress, SpinnerColumn, TextColumn
12
+ from rich.panel import Panel
13
+ from rich.table import Table
10
14
  from requests import ConnectionError
11
- from typing import List
15
+ from typing import List, Dict
12
16
  from ibm_watsonx_orchestrate.client.base_api_client import ClientAPIException
13
17
  from ibm_watsonx_orchestrate.agent_builder.knowledge_bases.types import KnowledgeBaseSpec
14
18
  from ibm_watsonx_orchestrate.agent_builder.tools import ToolSpec, ToolPermission, ToolRequestBody, ToolResponseBody
@@ -16,6 +20,7 @@ from ibm_watsonx_orchestrate.cli.commands.agents.agents_controller import Agents
16
20
  from ibm_watsonx_orchestrate.agent_builder.agents.types import DEFAULT_LLM, BaseAgentSpec
17
21
  from ibm_watsonx_orchestrate.client.agents.agent_client import AgentClient
18
22
  from ibm_watsonx_orchestrate.client.knowledge_bases.knowledge_base_client import KnowledgeBaseClient
23
+ from ibm_watsonx_orchestrate.client.threads.threads_client import ThreadsClient
19
24
  from ibm_watsonx_orchestrate.client.tools.tool_client import ToolClient
20
25
  from ibm_watsonx_orchestrate.client.copilot.cpe.copilot_cpe_client import CPEClient
21
26
  from ibm_watsonx_orchestrate.client.utils import instantiate_client
@@ -63,6 +68,7 @@ def _get_incomplete_agent_from_name(agent_name: str) -> dict:
63
68
  spec = BaseAgentSpec(**{"name": agent_name, "description": agent_name, "kind": AgentKind.NATIVE})
64
69
  return spec.model_dump()
65
70
 
71
+
66
72
  def _get_incomplete_knowledge_base_from_name(kb_name: str) -> dict:
67
73
  spec = KnowledgeBaseSpec(**{"name": kb_name, "description": kb_name})
68
74
  return spec.model_dump()
@@ -123,6 +129,7 @@ def _get_agents_from_names(collaborators_names: List[str]) -> List[dict]:
123
129
 
124
130
  return agents
125
131
 
132
+
126
133
  def _get_knowledge_bases_from_names(kb_names: List[str]) -> List[dict]:
127
134
  if not len(kb_names):
128
135
  return []
@@ -168,6 +175,10 @@ def get_native_client(*args, **kwargs):
168
175
  return instantiate_client(AgentClient)
169
176
 
170
177
 
178
+ def get_threads_client():
179
+ return instantiate_client(ThreadsClient)
180
+
181
+
171
182
  def gather_utterances(max: int) -> list[str]:
172
183
  utterances = []
173
184
  logger.info("Please provide 3 sample utterances you expect your agent to handle:")
@@ -221,7 +232,8 @@ def pre_cpe_step(cpe_client):
221
232
  rich.print('\n🤖 Copilot: ' + response["message"])
222
233
  user_message = Prompt.ask("\n👤 You").strip()
223
234
  message_content = {"user_message": user_message}
224
- elif "description" in response and response["description"]: # after we have a description, we pass the all tools
235
+ elif "description" in response and response[
236
+ "description"]: # after we have a description, we pass the all tools
225
237
  res["description"] = response["description"]
226
238
  message_content = {"tools": tools_agents_and_knowledge_bases['tools']}
227
239
  elif "tools" in response and response[
@@ -234,11 +246,13 @@ def pre_cpe_step(cpe_client):
234
246
  res["collaborators"] = [a for a in tools_agents_and_knowledge_bases["collaborators"] if
235
247
  a["name"] in response["collaborators"]]
236
248
  message_content = {"knowledge_bases": tools_agents_and_knowledge_bases['knowledge_bases']}
237
- elif "knowledge_bases" in response and response['knowledge_bases'] is not None: # after we have knowledge bases, we pass selected=True to mark that all selection were done
249
+ elif "knowledge_bases" in response and response[
250
+ 'knowledge_bases'] is not None: # after we have knowledge bases, we pass selected=True to mark that all selection were done
238
251
  res["knowledge_bases"] = [a for a in tools_agents_and_knowledge_bases["knowledge_bases"] if
239
252
  a["name"] in response["knowledge_bases"]]
240
253
  message_content = {"selected": True}
241
- elif "agent_name" in response and response['agent_name'] is not None: # once we have a name and style, this phase has ended
254
+ elif "agent_name" in response and response[
255
+ 'agent_name'] is not None: # once we have a name and style, this phase has ended
242
256
  res["agent_name"] = response["agent_name"]
243
257
  res["agent_style"] = response["agent_style"]
244
258
  return res
@@ -351,21 +365,21 @@ def prompt_tune(agent_spec: str, output_file: str | None, samples_file: str | No
351
365
  knowledge_bases = _get_knowledge_bases_from_names(agent.knowledge_base)
352
366
  try:
353
367
  new_prompt = talk_to_cpe(cpe_client=client,
354
- samples_file=samples_file,
355
- context_data={
356
- "initial_instruction": instr,
357
- 'tools': tools,
358
- 'description': agent.description,
359
- "collaborators": collaborators,
360
- "knowledge_bases": knowledge_bases
361
- })
368
+ samples_file=samples_file,
369
+ context_data={
370
+ "initial_instruction": instr,
371
+ 'tools': tools,
372
+ 'description': agent.description,
373
+ "collaborators": collaborators,
374
+ "knowledge_bases": knowledge_bases
375
+ })
362
376
  except ConnectionError:
363
377
  logger.error(
364
378
  "Failed to connect to Copilot server. Please ensure Copilot is running via `orchestrate copilot start`")
365
379
  sys.exit(1)
366
380
  except ClientAPIException:
367
381
  logger.error(
368
- "An unexpected server error has occur with in the Copilot server. Please check the logs via `orchestrate server logs`")
382
+ "An unexpected server error has occurred with in the Copilot server. Please check the logs via `orchestrate server logs`")
369
383
  sys.exit(1)
370
384
 
371
385
  if new_prompt:
@@ -394,7 +408,7 @@ def create_agent(output_file: str, llm: str, samples_file: str | None, dry_run_f
394
408
  sys.exit(1)
395
409
  except ClientAPIException:
396
410
  logger.error(
397
- "An unexpected server error has occur with in the Copilot server. Please check the logs via `orchestrate server logs`")
411
+ "An unexpected server error has occurred with in the Copilot server. Please check the logs via `orchestrate server logs`")
398
412
  sys.exit(1)
399
413
 
400
414
  tools = res["tools"]
@@ -446,3 +460,224 @@ def create_agent(output_file: str, llm: str, samples_file: str | None, dry_run_f
446
460
  for line in message_lines:
447
461
  rich.print("║ " + line.ljust(max_length) + " ║")
448
462
  rich.print("╚" + "═" * frame_width + "╝")
463
+
464
+
465
+ def _format_thread_messages(messages:List[dict]) -> List[dict]:
466
+ """
467
+ restructure and keep only the content relevant for refining the agent before sending to the refinement process
468
+ :param messages: List of messages as returned from the threads endpoint
469
+ :param messages:
470
+ :return: List of dictionaries where each dictionary represents a message
471
+ """
472
+ new_messages = []
473
+ for m in messages:
474
+ m_dict = {'role': m['role'], 'content': m['content'][0]['text'], 'type': 'text'} # text message
475
+ if m['step_history']:
476
+ step_history = m['step_history']
477
+ for step in step_history:
478
+ step_details = step['step_details'][0]
479
+ if step_details['type'] == 'tool_calls': # tool call
480
+ for t in step_details['tool_calls']:
481
+ new_messages.append(
482
+ {'role': m['role'], 'type': 'tool_call', 'args': t['args'], 'name': t['name']})
483
+ elif step_details['type'] == 'tool_response': # tool response
484
+ new_messages.append({'role': m['role'], 'type': 'tool_response', 'content': step_details['content']})
485
+ new_messages.append(m_dict)
486
+ if m['message_state']:
487
+ new_messages.append({'feedback': m['message_state']['content']['1']['feedback']})
488
+ return new_messages
489
+
490
+
491
+ def _suggest_sorted(user_input: str, options: List[str]) -> List[str]:
492
+ # Sort by similarity score
493
+ return sorted(options, key=lambda x: difflib.SequenceMatcher(None, user_input, x).ratio(), reverse=True)
494
+
495
+
496
+ def refine_agent_with_trajectories(agent_name: str, output_file: str | None, use_last_chat: bool=False, dry_run_flag: bool = False) -> None:
497
+ """
498
+ Refines an existing agent's instructions using user selected chat trajectories and saves the updated agent configuration.
499
+
500
+ This function performs a multi-step process to enhance an agent's prompt instructions based on user interactions:
501
+
502
+ 1. **Validation**: Ensures the output file path is valid and checks if the specified agent exists. If not found,
503
+ it suggests similar agent names.
504
+ 2. **Chat Retrieval**: Fetches the 10 most recent chat threads associated with the agent. If no chats are found,
505
+ the user is prompted to initiate a conversation.
506
+ 3. **User Selection**: Displays a summary of recent chats and allows the user to select which ones to use for refinement.
507
+ 4. **Refinement**: Sends selected chat messages to the Copilot Prompt Engine (CPE) to generate refined instructions.
508
+ 5. **Update and Save**: Updates the agent's instructions and either prints the
509
+ updated agent (if `dry_run_flag` is True) or saves it to the specified output file.
510
+
511
+ Parameters:
512
+ agent_name (str): The name of the agent to refine.
513
+ output_file (str): Path to the file where the refined agent configuration will be saved.
514
+ use_last_chat(bool): If true, optimize by using the last conversation with the agent, otherwise let the use choose
515
+ dry_run_flag (bool): If True, prints the refined agent configuration without saving it to disk.
516
+
517
+ Returns:
518
+ None
519
+ """
520
+
521
+ _validate_output_file(output_file, dry_run_flag)
522
+ agents_controller = AgentsController()
523
+ agents_client = get_native_client()
524
+ threads_client = get_threads_client()
525
+ all_agents = agents_controller.get_all_agents(client=agents_client)
526
+
527
+ # Step 1 - validate agent exist. If not - list the agents sorted by their distance from the user input name
528
+ agent_id = all_agents.get(agent_name)
529
+ if agent_id is None:
530
+ if len(all_agents) == 0:
531
+ raise BadRequest("No agents in workspace\nCreate your first agent using `orchestrate copilot prompt-tune`")
532
+ else:
533
+ available_sorted_str = "\n".join(_suggest_sorted(agent_name, all_agents.keys()))
534
+ raise BadRequest(f'Agent "{agent_name}" does not exist.\n\n'
535
+ f'Available agents:\n'
536
+ f'{available_sorted_str}')
537
+
538
+ rich.print(Panel(message, title="Agent Lookup", border_style="blue"))
539
+ return
540
+
541
+ cpe_client = get_cpe_client()
542
+ # Step 2 - retrieve chats (threads)
543
+ try:
544
+ with _get_progress_spinner() as progress:
545
+ task = progress.add_task(description="Retrieve chats", total=None)
546
+ all_threads = threads_client.get_all_threads(agent_id)
547
+ if len(all_threads) == 0:
548
+ progress.remove_task(task)
549
+ progress.refresh()
550
+ raise BadRequest(
551
+ f"No chats found for agent '{agent_name}'. To use autotune, please initiate at least one conversation with the agent. You can start a chat using `orchestrate chat start`.",
552
+ )
553
+ return
554
+ last_10_threads = all_threads[:10] #TODO use batching when server allows
555
+ last_10_chats = [_format_thread_messages(chat) for chat in
556
+ threads_client.get_threads_messages([thread['id'] for thread in last_10_threads])]
557
+
558
+ progress.remove_task(task)
559
+ progress.refresh()
560
+ except ConnectionError:
561
+ logger.error(
562
+ f"Failed to retrieve threads (chats) for agent {agent_name}")
563
+ sys.exit(1)
564
+ except ClientAPIException:
565
+ logger.error(
566
+ f"An unexpected server error has occurred while retrieving threads for agent {agent_name}. Please check the logs via `orchestrate server logs`")
567
+ sys.exit(1)
568
+
569
+ # Step 3 - show chats and let the user choose
570
+ if use_last_chat:
571
+ title = "Selected chat"
572
+ else:
573
+ title = "10 Most Recent Chats"
574
+ table = Table(title=title)
575
+ table.add_column("Number", justify="right")
576
+ table.add_column("Chat Date", justify="left")
577
+ table.add_column("Title", justify="left")
578
+ table.add_column("Last User Message", justify="left")
579
+ table.add_column("Last User Feedback", justify="left")
580
+
581
+ for i, (thread, chat) in enumerate(zip(last_10_threads, last_10_chats), start=1):
582
+ all_user_messages = [msg for msg in chat if 'role' in msg and msg['role'] == 'user']
583
+
584
+ if len(all_user_messages) == 0:
585
+ last_user_message = ""
586
+ else:
587
+ last_user_message = all_user_messages[-1]['content']
588
+ all_feedbacks = [msg for msg in chat if 'feedback' in msg and 'text' in msg['feedback']]
589
+ if len(all_feedbacks) == 0:
590
+ last_feedback = ""
591
+ else:
592
+ last_feedback = f"{'👍' if all_feedbacks[-1]['feedback']['is_positive'] else '👎'} {all_feedbacks[-1]['feedback']['text']}"
593
+
594
+ table.add_row(str(i), datetime.strptime(thread['created_on'], '%Y-%m-%dT%H:%M:%S.%fZ').strftime(
595
+ '%B %d, %Y at %I:%M %p'), thread['title'], last_user_message, last_feedback)
596
+ table.add_row("", "", "")
597
+ if use_last_chat:
598
+ break
599
+
600
+ rich.print(table)
601
+
602
+ if use_last_chat:
603
+ rich.print("Tuning using the last conversation with the agent")
604
+ threads_messages = [last_10_chats[0]]
605
+ else:
606
+ threads_messages = get_user_selection(last_10_chats)
607
+
608
+ # Step 4 - run the refiner
609
+ try:
610
+ with _get_progress_spinner() as progress:
611
+ agent = agents_controller.get_agent_by_id(id=agent_id)
612
+ task = progress.add_task(description="Running Prompt Refiner", total=None)
613
+ tools_client = get_tool_client()
614
+ knowledge_base_client = get_knowledge_bases_client()
615
+ # loaded agent contains the ids of the tools/collabs/knowledge bases, convert them back to names.
616
+ agent.tools = [tools_client.get_draft_by_id(id)['name'] for id in agent.tools]
617
+ agent.knowledge_base = [knowledge_base_client.get_by_id(id)['name'] for id in agent.knowledge_base]
618
+ agent.collaborators = [agents_client.get_draft_by_id(id)['name'] for id in agent.collaborators]
619
+ tools = _get_tools_from_names(agent.tools)
620
+ collaborators = _get_agents_from_names(agent.collaborators)
621
+ knowledge_bases = _get_knowledge_bases_from_names(agent.knowledge_base)
622
+ if agent.instructions is None:
623
+ raise BadRequest("Agent must have instructions in order to use the autotune command. To build an instruction use `orchestrate copilot prompt-tune -f <path_to_agent_yaml> -o <path_to_new_agent_yaml>`")
624
+ response = cpe_client.refine_agent_with_chats(agent.instructions, tools=tools, collaborators=collaborators,
625
+ knowledge_bases=knowledge_bases, trajectories_with_feedback=threads_messages)
626
+ progress.remove_task(task)
627
+ progress.refresh()
628
+ except ConnectionError:
629
+ logger.error(
630
+ "Failed to connect to Copilot server. Please ensure Copilot is running via `orchestrate copilot start`")
631
+ sys.exit(1)
632
+ except ClientAPIException:
633
+ logger.error(
634
+ "An unexpected server error has occurred with in the Copilot server. Please check the logs via `orchestrate server logs`")
635
+ sys.exit(1)
636
+
637
+ # Step 5 - update the agent and print/save the results
638
+ agent.instructions = response['instruction']
639
+
640
+ if dry_run_flag:
641
+ rich.print(agent.model_dump(exclude_none=True))
642
+ return
643
+
644
+ if os.path.dirname(output_file):
645
+ os.makedirs(os.path.dirname(output_file), exist_ok=True)
646
+ agent.id = None # remove existing agent id before saving
647
+ AgentsController.persist_record(agent, output_file=output_file)
648
+
649
+ logger.info(f"Your agent refinement session finished successfully!")
650
+ logger.info(f"Agent YAML with the updated instruction saved in file: {os.path.abspath(output_file)}")
651
+
652
+
653
+
654
+ def get_user_selection(chats: List[List[Dict]]) -> List[List[Dict]]:
655
+ """
656
+ Prompts the user to select up to 5 chat threads by entering their indices.
657
+
658
+ Parameters:
659
+ chats (List[List[Dict]]): A list of chat threads, where each thread is a list of message dictionaries.
660
+
661
+ Returns:
662
+ List[List[Dict]]: A list of selected chat threads based on user input.
663
+ """
664
+ while True:
665
+ try:
666
+ eg_str = "1" if len(chats) < 2 else "1, 2"
667
+ input_str = input(
668
+ f"Please enter up to 5 indices of chats you'd like to select, separated by commas (e.g. {eg_str}): "
669
+ )
670
+
671
+ choices = [int(choice.strip()) for choice in input_str.split(',')]
672
+
673
+ if len(choices) > 5:
674
+ rich.print("You can select up to 5 chats only. Please try again.")
675
+ continue
676
+
677
+ if all(1 <= choice <= len(chats) for choice in choices):
678
+ selected_threads = [chats[choice - 1] for choice in choices]
679
+ return selected_threads
680
+ else:
681
+ rich.print(f"Please enter only numbers between 1 and {len(chats)}.")
682
+ except ValueError:
683
+ rich.print("Invalid input. Please enter valid integers separated by commas.")
@@ -1,9 +1,9 @@
1
1
  import logging
2
2
  import sys
3
- from pathlib import Path
4
3
  import time
4
+ from pathlib import Path
5
+
5
6
  import requests
6
- from urllib.parse import urlparse
7
7
 
8
8
  from ibm_watsonx_orchestrate.cli.config import Config
9
9
  from ibm_watsonx_orchestrate.utils.docker_utils import DockerLoginService, DockerComposeCore, DockerUtils
@@ -45,7 +45,7 @@ def run_compose_lite_cpe(user_env_file: Path) -> bool:
45
45
 
46
46
  final_env_file = env_service.write_merged_env_file(merged_env_dict)
47
47
 
48
- compose_core = DockerComposeCore(env_service)
48
+ compose_core = DockerComposeCore(env_service=env_service)
49
49
 
50
50
  result = compose_core.service_up(service_name="cpe", friendly_name="Copilot", final_env_file=final_env_file)
51
51
 
@@ -75,7 +75,7 @@ def run_compose_lite_cpe_down(is_reset: bool = False) -> None:
75
75
 
76
76
  cli_config = Config()
77
77
  env_service = EnvService(cli_config)
78
- compose_core = DockerComposeCore(env_service)
78
+ compose_core = DockerComposeCore(env_service=env_service)
79
79
 
80
80
  result = compose_core.service_down(service_name="cpe", friendly_name="Copilot", final_env_file=final_env_file, is_reset=is_reset)
81
81
 
@@ -43,9 +43,13 @@ def activate_env(
43
43
  test_package_version_override: Annotated[
44
44
  str,
45
45
  typer.Option("--test-package-version-override", help="Which prereleased package version to reference when using --registry testpypi", hidden=True),
46
+ ] = None,
47
+ skip_version_check: Annotated[
48
+ bool,
49
+ typer.Option('--skip-version-check/--enable-version-check', help='Use this flag to skip validating that adk version in use exists in pypi (for clients who mirror the ADK to a local registry and do not have local access to pypi).')
46
50
  ] = None
47
51
  ):
48
- environment_controller.activate(name=name, apikey=apikey,username=username, password=password, registry=registry, test_package_version_override=test_package_version_override)
52
+ environment_controller.activate(name=name, apikey=apikey, username=username, password=password, registry=registry, test_package_version_override=test_package_version_override, skip_version_check=skip_version_check)
49
53
 
50
54
 
51
55
  @environment_app.command(name="add")
@@ -18,8 +18,9 @@ from ibm_watsonx_orchestrate.cli.config import (
18
18
  ENV_IAM_URL_OPT,
19
19
  ENVIRONMENTS_SECTION_HEADER,
20
20
  PROTECTED_ENV_NAME,
21
- ENV_AUTH_TYPE, PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TYPE_OPT, PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT, BYPASS_SSL, VERIFY,
22
- DEFAULT_CONFIG_FILE_CONTENT
21
+ ENV_AUTH_TYPE, PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TYPE_OPT, PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT,
22
+ BYPASS_SSL, VERIFY,
23
+ DEFAULT_CONFIG_FILE_CONTENT, PYTHON_REGISTRY_SKIP_VERSION_CHECK_OPT
23
24
  )
24
25
  from ibm_watsonx_orchestrate.client.client import Client
25
26
  from ibm_watsonx_orchestrate.client.client_errors import ClientError
@@ -131,7 +132,7 @@ def _login(name: str, apikey: str = None, username: str = None, password: str =
131
132
  except ClientError as e:
132
133
  raise ClientError(e)
133
134
 
134
- def activate(name: str, apikey: str=None, username: str=None, password: str=None, registry: RegistryType=None, test_package_version_override=None) -> None:
135
+ def activate(name: str, apikey: str=None, username: str=None, password: str=None, registry: RegistryType=None, test_package_version_override=None, skip_version_check=None) -> None:
135
136
  cfg = Config()
136
137
  auth_cfg = Config(AUTH_CONFIG_FILE_FOLDER, AUTH_CONFIG_FILE)
137
138
  env_cfg = cfg.read(ENVIRONMENTS_SECTION_HEADER, name)
@@ -159,6 +160,8 @@ def activate(name: str, apikey: str=None, username: str=None, password: str=None
159
160
  elif cfg.read(PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TYPE_OPT) is None:
160
161
  cfg.write(PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TYPE_OPT, DEFAULT_CONFIG_FILE_CONTENT[PYTHON_REGISTRY_HEADER][PYTHON_REGISTRY_TYPE_OPT])
161
162
  cfg.write(PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT, test_package_version_override)
163
+ if skip_version_check is not None:
164
+ cfg.write(PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_SKIP_VERSION_CHECK_OPT, skip_version_check)
162
165
 
163
166
  logger.info(f"Environment '{name}' is now active")
164
167
  is_cpd = is_cpd_env(url)
@@ -54,7 +54,8 @@ def read_env_file(env_path: Path|str) -> dict:
54
54
  def validate_watsonx_credentials(user_env_file: str) -> bool:
55
55
  required_sets = [
56
56
  ["WATSONX_SPACE_ID", "WATSONX_APIKEY"],
57
- ["WO_INSTANCE", "WO_API_KEY"]
57
+ ["WO_INSTANCE", "WO_API_KEY"],
58
+ ["WO_INSTANCE", "WO_PASSWORD", "WO_USERNAME"]
58
59
  ]
59
60
 
60
61
  def has_valid_keys(env: dict) -> bool:
@@ -75,7 +76,7 @@ def validate_watsonx_credentials(user_env_file: str) -> bool:
75
76
  user_env = read_env_file(user_env_file)
76
77
 
77
78
  if not has_valid_keys(user_env):
78
- logger.error("Error: The environment file does not contain the required keys: either WATSONX_SPACE_ID and WATSONX_APIKEY or WO_INSTANCE and WO_API_KEY.")
79
+ logger.error("Error: The environment file does not contain the required keys: either WATSONX_SPACE_ID and WATSONX_APIKEY or WO_INSTANCE and WO_API_KEY or WO_INSTANCE and WO_USERNAME and WO_PASSWORD.")
79
80
  sys.exit(1)
80
81
 
81
82
  # Update os.environ with whichever set is present
@@ -52,7 +52,7 @@ class EvaluationsController:
52
52
 
53
53
  if "WATSONX_SPACE_ID" in os.environ and "WATSONX_APIKEY" in os.environ:
54
54
  provider = "watsonx"
55
- elif "WO_INSTANCE" in os.environ and "WO_API_KEY" in os.environ:
55
+ elif "WO_INSTANCE" in os.environ and ("WO_API_KEY" in os.environ or "WO_PASSWORD" in os.environ):
56
56
  provider = "model_proxy"
57
57
  else:
58
58
  logger.error(
@@ -6,14 +6,15 @@ import logging
6
6
  import importlib
7
7
  import inspect
8
8
  from pathlib import Path
9
- from typing import List
9
+ from typing import List, Any
10
10
 
11
11
  from ibm_watsonx_orchestrate.agent_builder.knowledge_bases.knowledge_base import KnowledgeBase
12
12
  from ibm_watsonx_orchestrate.client.knowledge_bases.knowledge_base_client import KnowledgeBaseClient
13
13
  from ibm_watsonx_orchestrate.client.base_api_client import ClientAPIException
14
14
  from ibm_watsonx_orchestrate.client.connections import get_connections_client
15
15
  from ibm_watsonx_orchestrate.client.utils import instantiate_client
16
- from ibm_watsonx_orchestrate.agent_builder.knowledge_bases.types import FileUpload
16
+ from ibm_watsonx_orchestrate.agent_builder.knowledge_bases.types import FileUpload, KnowledgeBaseListEntry
17
+ from ibm_watsonx_orchestrate.cli.common import ListFormats, rich_table_to_markdown
17
18
 
18
19
  logger = logging.getLogger(__name__)
19
20
 
@@ -198,8 +199,7 @@ class KnowledgeBaseController:
198
199
 
199
200
  logger.info(f"Knowledge base '{kb.name}' updated successfully")
200
201
 
201
-
202
- def knowledge_base_status( self, id: str, name: str) -> None:
202
+ def knowledge_base_status( self, id: str, name: str, format: ListFormats = None) -> dict | str | None:
203
203
  knowledge_base_id = self.get_id(id, name)
204
204
  response = self.get_client().status(knowledge_base_id)
205
205
 
@@ -219,13 +219,25 @@ class KnowledgeBaseController:
219
219
 
220
220
  response["id"] = kbID
221
221
 
222
+ if format == ListFormats.JSON:
223
+ return response
224
+
225
+
222
226
  [table.add_column(to_column_name(col), {}) for col in response.keys()]
223
227
  table.add_row(*[str(val) for val in response.values()])
224
228
 
229
+ if format == ListFormats.Table:
230
+ return rich_table_to_markdown(table)
231
+
225
232
  rich.print(table)
226
233
 
227
234
 
228
- def list_knowledge_bases(self, verbose: bool=False):
235
+ def list_knowledge_bases(self, verbose: bool=False, format: ListFormats=None)-> List[dict[str, Any]] | List[KnowledgeBaseListEntry] | str | None:
236
+
237
+ if verbose and format:
238
+ logger.error("For knowledge base list, `--verbose` and `--format` are mutually exclusive options")
239
+ sys.exit(1)
240
+
229
241
  response = self.get_client().get()
230
242
  knowledge_bases = [KnowledgeBase.model_validate(knowledge_base) for knowledge_base in response]
231
243
 
@@ -234,7 +246,9 @@ class KnowledgeBaseController:
234
246
  for kb in knowledge_bases:
235
247
  knowledge_base_list.append(json.loads(kb.model_dump_json(exclude_none=True)))
236
248
  rich.print(rich.json.JSON(json.dumps(knowledge_base_list, indent=4)))
249
+ return knowledge_base_list
237
250
  else:
251
+ knowledge_base_details=[]
238
252
  table = rich.table.Table(
239
253
  show_header=True,
240
254
  header_style="bold white",
@@ -251,6 +265,11 @@ class KnowledgeBaseController:
251
265
  for column in column_args:
252
266
  table.add_column(column, **column_args[column])
253
267
 
268
+ connections_client = get_connections_client()
269
+ connections = connections_client.list()
270
+
271
+ connections_dict = {conn.connection_id: conn for conn in connections}
272
+
254
273
  for kb in knowledge_bases:
255
274
  app_id = ""
256
275
 
@@ -258,18 +277,28 @@ class KnowledgeBaseController:
258
277
  and kb.conversational_search_tool.index_config is not None \
259
278
  and len(kb.conversational_search_tool.index_config) > 0 \
260
279
  and kb.conversational_search_tool.index_config[0].connection_id is not None:
261
- connections_client = get_connections_client()
262
- app_id = str(connections_client.get_draft_by_id(kb.conversational_search_tool.index_config[0].connection_id))
263
-
264
- table.add_row(
265
- kb.name,
266
- kb.description,
267
- app_id,
268
- str(kb.id)
280
+ conn = connections_dict.get(kb.conversational_search_tool.index_config[0].connection_id)
281
+ if conn:
282
+ app_id = conn.app_id
283
+
284
+ entry = KnowledgeBaseListEntry(
285
+ name=kb.name,
286
+ id=str(kb.id),
287
+ description=kb.description,
288
+ app_id=app_id
269
289
  )
270
-
271
- rich.print(table)
272
-
290
+ if format == ListFormats.JSON:
291
+ knowledge_base_details.append(entry)
292
+ else:
293
+ table.add_row(*entry.get_row_details())
294
+
295
+ match format:
296
+ case ListFormats.JSON:
297
+ return knowledge_base_details
298
+ case ListFormats.Table:
299
+ return rich_table_to_markdown(table)
300
+ case _:
301
+ rich.print(table)
273
302
 
274
303
  def remove_knowledge_base(self, id: str, name: str):
275
304
  knowledge_base_id = self.get_id(id, name)
@@ -136,11 +136,11 @@ def models_policy_import(
136
136
  def models_policy_add(
137
137
  name: Annotated[
138
138
  str,
139
- typer.Option("--name", "-n", help="The name of the model to remove"),
139
+ typer.Option("--name", "-n", help="The name of the model policy to add"),
140
140
  ],
141
141
  models: Annotated[
142
142
  List[str],
143
- typer.Option('--model', '-m', help='The name of the model to add'),
143
+ typer.Option('--model', '-m', help='A list of model names the policy should contain'),
144
144
  ],
145
145
  strategy: Annotated[
146
146
  ModelPolicyStrategyMode,