ibm-watsonx-orchestrate 1.12.0b0__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 (66) hide show
  1. ibm_watsonx_orchestrate/__init__.py +2 -1
  2. ibm_watsonx_orchestrate/agent_builder/agents/types.py +5 -5
  3. ibm_watsonx_orchestrate/agent_builder/connections/types.py +34 -3
  4. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +11 -2
  5. ibm_watsonx_orchestrate/agent_builder/models/types.py +18 -1
  6. ibm_watsonx_orchestrate/agent_builder/toolkits/base_toolkit.py +1 -1
  7. ibm_watsonx_orchestrate/agent_builder/toolkits/types.py +14 -2
  8. ibm_watsonx_orchestrate/agent_builder/tools/__init__.py +1 -1
  9. ibm_watsonx_orchestrate/agent_builder/tools/base_tool.py +1 -1
  10. ibm_watsonx_orchestrate/agent_builder/tools/langflow_tool.py +61 -1
  11. ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +6 -0
  12. ibm_watsonx_orchestrate/agent_builder/tools/types.py +21 -3
  13. ibm_watsonx_orchestrate/agent_builder/voice_configurations/__init__.py +1 -1
  14. ibm_watsonx_orchestrate/agent_builder/voice_configurations/types.py +11 -0
  15. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +29 -53
  16. ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +2 -2
  17. ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +56 -30
  18. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_command.py +25 -2
  19. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_controller.py +249 -14
  20. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_server_controller.py +4 -4
  21. ibm_watsonx_orchestrate/cli/commands/environment/environment_command.py +5 -1
  22. ibm_watsonx_orchestrate/cli/commands/environment/environment_controller.py +6 -3
  23. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +3 -2
  24. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +1 -1
  25. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +45 -16
  26. ibm_watsonx_orchestrate/cli/commands/models/model_provider_mapper.py +23 -4
  27. ibm_watsonx_orchestrate/cli/commands/models/models_command.py +2 -2
  28. ibm_watsonx_orchestrate/cli/commands/models/models_controller.py +29 -10
  29. ibm_watsonx_orchestrate/cli/commands/partners/offering/partners_offering_controller.py +21 -4
  30. ibm_watsonx_orchestrate/cli/commands/partners/offering/types.py +7 -15
  31. ibm_watsonx_orchestrate/cli/commands/partners/partners_command.py +1 -1
  32. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +30 -20
  33. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +2 -2
  34. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +139 -27
  35. ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +2 -2
  36. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +79 -36
  37. ibm_watsonx_orchestrate/cli/commands/voice_configurations/voice_configurations_controller.py +23 -11
  38. ibm_watsonx_orchestrate/cli/common.py +26 -0
  39. ibm_watsonx_orchestrate/cli/config.py +33 -2
  40. ibm_watsonx_orchestrate/client/connections/connections_client.py +1 -14
  41. ibm_watsonx_orchestrate/client/copilot/cpe/copilot_cpe_client.py +34 -1
  42. ibm_watsonx_orchestrate/client/knowledge_bases/knowledge_base_client.py +6 -2
  43. ibm_watsonx_orchestrate/client/model_policies/model_policies_client.py +1 -1
  44. ibm_watsonx_orchestrate/client/models/models_client.py +1 -1
  45. ibm_watsonx_orchestrate/client/threads/threads_client.py +34 -0
  46. ibm_watsonx_orchestrate/client/utils.py +29 -7
  47. ibm_watsonx_orchestrate/docker/compose-lite.yml +58 -8
  48. ibm_watsonx_orchestrate/docker/default.env +26 -17
  49. ibm_watsonx_orchestrate/flow_builder/flows/decorators.py +10 -2
  50. ibm_watsonx_orchestrate/flow_builder/flows/flow.py +90 -16
  51. ibm_watsonx_orchestrate/flow_builder/node.py +14 -2
  52. ibm_watsonx_orchestrate/flow_builder/types.py +57 -3
  53. ibm_watsonx_orchestrate/langflow/__init__.py +0 -0
  54. ibm_watsonx_orchestrate/langflow/langflow_utils.py +195 -0
  55. ibm_watsonx_orchestrate/langflow/lfx_deps.py +84 -0
  56. ibm_watsonx_orchestrate/utils/async_helpers.py +31 -0
  57. ibm_watsonx_orchestrate/utils/docker_utils.py +1177 -33
  58. ibm_watsonx_orchestrate/utils/environment.py +165 -20
  59. ibm_watsonx_orchestrate/utils/exceptions.py +1 -1
  60. ibm_watsonx_orchestrate/utils/tokens.py +51 -0
  61. ibm_watsonx_orchestrate/utils/utils.py +63 -4
  62. {ibm_watsonx_orchestrate-1.12.0b0.dist-info → ibm_watsonx_orchestrate-1.13.0b0.dist-info}/METADATA +2 -2
  63. {ibm_watsonx_orchestrate-1.12.0b0.dist-info → ibm_watsonx_orchestrate-1.13.0b0.dist-info}/RECORD +66 -59
  64. {ibm_watsonx_orchestrate-1.12.0b0.dist-info → ibm_watsonx_orchestrate-1.13.0b0.dist-info}/WHEEL +0 -0
  65. {ibm_watsonx_orchestrate-1.12.0b0.dist-info → ibm_watsonx_orchestrate-1.13.0b0.dist-info}/entry_points.txt +0 -0
  66. {ibm_watsonx_orchestrate-1.12.0b0.dist-info → ibm_watsonx_orchestrate-1.13.0b0.dist-info}/licenses/LICENSE +0 -0
@@ -9,8 +9,9 @@ import yaml
9
9
  import sys
10
10
  import typer
11
11
 
12
- from typing import List
12
+ from typing import List, Optional, Any
13
13
  from ibm_watsonx_orchestrate.agent_builder.agents.types import SpecVersion
14
+
14
15
  from ibm_watsonx_orchestrate.client.utils import is_local_dev
15
16
  from ibm_watsonx_orchestrate.agent_builder.connections.types import (
16
17
  ConnectionEnvironment,
@@ -34,11 +35,13 @@ from ibm_watsonx_orchestrate.agent_builder.connections.types import (
34
35
  OAUTH_CONNECTION_TYPES,
35
36
  ConnectionCredentialsEntryLocation,
36
37
  ConnectionCredentialsEntry,
37
- ConnectionCredentialsCustomFields
38
-
38
+ ConnectionCredentialsCustomFields,
39
+ ConnectionsListEntry,
40
+ ConnectionsListResponse
39
41
  )
40
42
 
41
43
  from ibm_watsonx_orchestrate.client.connections import get_connections_client, get_connection_type
44
+ from ibm_watsonx_orchestrate.cli.common import ListFormats, rich_table_to_markdown
42
45
 
43
46
  logger = logging.getLogger(__name__)
44
47
 
@@ -470,7 +473,11 @@ def remove_connection(app_id: str) -> None:
470
473
  logger.error(response_text)
471
474
  exit(1)
472
475
 
473
- def list_connections(environment: ConnectionEnvironment | None, verbose: bool = False) -> None:
476
+ def list_connections(environment: ConnectionEnvironment | None = None, verbose: bool = False, format: Optional[ListFormats] = None) -> List[dict[str, Any]]| ConnectionsListResponse | None:
477
+ if verbose and format:
478
+ logger.error("For connections list, `--verbose` and `--format` are mutually exclusive options")
479
+ sys.exit(1)
480
+
474
481
  client = get_connections_client()
475
482
  connections = client.list()
476
483
  is_local = is_local_dev()
@@ -483,7 +490,12 @@ def list_connections(environment: ConnectionEnvironment | None, verbose: bool =
483
490
  connections_list.append(json.loads(conn.model_dump_json()))
484
491
 
485
492
  rich.print_json(json.dumps(connections_list, indent=4))
493
+ return connections_list
486
494
  else:
495
+ non_configured_connection_details = []
496
+ draft_connection_details = []
497
+ live_connection_details = []
498
+
487
499
  non_configured_table = rich.table.Table(show_header=True, header_style="bold white", show_lines=True, title="*Non-Configured")
488
500
  draft_table = rich.table.Table(show_header=True, header_style="bold white", show_lines=True, title="Draft")
489
501
  live_table = rich.table.Table(show_header=True, header_style="bold white", show_lines=True, title="Live")
@@ -501,41 +513,55 @@ def list_connections(environment: ConnectionEnvironment | None, verbose: bool =
501
513
 
502
514
  for conn in connections:
503
515
  if conn.environment is None:
504
- non_configured_table.add_row(
505
- conn.app_id,
506
- "n/a",
507
- "n/a",
508
- "❌"
516
+ entry = ConnectionsListEntry(
517
+ app_id=conn.app_id,
509
518
  )
519
+
520
+ non_configured_table.add_row(*entry.get_row_details())
521
+ non_configured_connection_details.append(entry.model_dump())
510
522
  continue
511
523
 
512
524
  try:
513
525
  connection_type = get_connection_type(security_scheme=conn.security_scheme, auth_type=conn.auth_type)
514
526
  except:
515
527
  connection_type = conn.auth_type
528
+
529
+ entry = ConnectionsListEntry(
530
+ app_id=conn.app_id,
531
+ auth_type = connection_type,
532
+ type=conn.preference,
533
+ credentials_set=conn.credentials_entered
534
+ )
516
535
 
517
536
  if conn.environment == ConnectionEnvironment.DRAFT:
518
- draft_table.add_row(
519
- conn.app_id,
520
- connection_type,
521
- conn.preference,
522
- "✅" if conn.credentials_entered else "❌"
523
- )
537
+ draft_table.add_row(*entry.get_row_details())
538
+ draft_connection_details.append(entry.model_dump())
524
539
  elif conn.environment == ConnectionEnvironment.LIVE and not is_local:
525
- live_table.add_row(
526
- conn.app_id,
527
- connection_type,
528
- conn.preference,
529
- "✅" if conn.credentials_entered else "❌"
540
+ live_table.add_row(*entry.get_row_details())
541
+ live_connection_details.append(entry.model_dump())
542
+
543
+ match format:
544
+ case ListFormats.JSON:
545
+ return ConnectionsListResponse(
546
+ non_configured=non_configured_connection_details,
547
+ draft=draft_connection_details,
548
+ live=live_connection_details
549
+ )
550
+ case ListFormats.Table:
551
+ return ConnectionsListResponse(
552
+ non_configured=rich_table_to_markdown(non_configured_table),
553
+ draft=rich_table_to_markdown(draft_table),
554
+ live=rich_table_to_markdown(live_table)
530
555
  )
531
- if environment is None and len(non_configured_table.rows):
532
- rich.print(non_configured_table)
533
- if environment == ConnectionEnvironment.DRAFT or (environment == None and len(draft_table.rows)):
534
- rich.print(draft_table)
535
- if environment == ConnectionEnvironment.LIVE or (environment == None and len(live_table.rows)):
536
- rich.print(live_table)
537
- if environment == None and not len(draft_table.rows) and not len(live_table.rows) and not len(non_configured_table.rows):
538
- logger.info("No connections found. You can create connections using `orchestrate connections add`")
556
+ case _:
557
+ if environment is None and len(non_configured_table.rows):
558
+ rich.print(non_configured_table)
559
+ if environment == ConnectionEnvironment.DRAFT or (environment == None and len(draft_table.rows)):
560
+ rich.print(draft_table)
561
+ if environment == ConnectionEnvironment.LIVE or (environment == None and len(live_table.rows)):
562
+ rich.print(live_table)
563
+ if environment == None and not len(draft_table.rows) and not len(live_table.rows) and not len(non_configured_table.rows):
564
+ logger.info("No connections found. You can create connections using `orchestrate connections add`")
539
565
 
540
566
  def import_connection(file: str) -> None:
541
567
  _parse_file(file=file)
@@ -575,7 +601,7 @@ def export_connection(output_file: str, app_id: str | None = None, connection_id
575
601
  case '.zip':
576
602
  zip_file = zipfile.ZipFile(output_path, "w")
577
603
 
578
- connection_yaml = yaml.dump(combined_connections, sort_keys=False, default_flow_style=False)
604
+ connection_yaml = yaml.dump(combined_connections, sort_keys=False, default_flow_style=False, allow_unicode=True)
579
605
  connection_yaml_bytes = connection_yaml.encode("utf-8")
580
606
  connection_yaml_file = io.BytesIO(connection_yaml_bytes)
581
607
 
@@ -588,7 +614,7 @@ def export_connection(output_file: str, app_id: str | None = None, connection_id
588
614
  case '.yaml' | '.yml':
589
615
  with open(output_path,'w') as yaml_file:
590
616
  yaml_file.write(
591
- yaml.dump(combined_connections, sort_keys=False, default_flow_style=False)
617
+ yaml.dump(combined_connections, sort_keys=False, default_flow_style=False, allow_unicode=True)
592
618
  )
593
619
 
594
620
  logger.info(f"Successfully exported connection file for {app_id}")
@@ -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(