ibm-watsonx-orchestrate 1.12.2__py3-none-any.whl → 1.13.0b1__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 (53) hide show
  1. ibm_watsonx_orchestrate/__init__.py +1 -1
  2. ibm_watsonx_orchestrate/agent_builder/connections/types.py +34 -3
  3. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +13 -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/types.py +21 -3
  8. ibm_watsonx_orchestrate/agent_builder/voice_configurations/__init__.py +1 -1
  9. ibm_watsonx_orchestrate/agent_builder/voice_configurations/types.py +11 -0
  10. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +31 -53
  11. ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +2 -2
  12. ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +54 -28
  13. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_command.py +36 -2
  14. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_controller.py +270 -26
  15. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_server_controller.py +4 -4
  16. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +30 -3
  17. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_environment_manager.py +158 -0
  18. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_command.py +26 -0
  19. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +150 -34
  20. ibm_watsonx_orchestrate/cli/commands/models/models_command.py +2 -2
  21. ibm_watsonx_orchestrate/cli/commands/models/models_controller.py +29 -10
  22. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +50 -18
  23. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +139 -27
  24. ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +2 -2
  25. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +43 -29
  26. ibm_watsonx_orchestrate/cli/commands/voice_configurations/voice_configurations_controller.py +23 -11
  27. ibm_watsonx_orchestrate/cli/common.py +26 -0
  28. ibm_watsonx_orchestrate/cli/config.py +30 -1
  29. ibm_watsonx_orchestrate/client/agents/agent_client.py +1 -1
  30. ibm_watsonx_orchestrate/client/connections/connections_client.py +1 -14
  31. ibm_watsonx_orchestrate/client/copilot/cpe/copilot_cpe_client.py +55 -11
  32. ibm_watsonx_orchestrate/client/knowledge_bases/knowledge_base_client.py +6 -2
  33. ibm_watsonx_orchestrate/client/model_policies/model_policies_client.py +1 -1
  34. ibm_watsonx_orchestrate/client/models/models_client.py +1 -1
  35. ibm_watsonx_orchestrate/client/threads/threads_client.py +34 -0
  36. ibm_watsonx_orchestrate/client/tools/tempus_client.py +4 -2
  37. ibm_watsonx_orchestrate/client/utils.py +29 -7
  38. ibm_watsonx_orchestrate/docker/compose-lite.yml +3 -2
  39. ibm_watsonx_orchestrate/docker/default.env +15 -10
  40. ibm_watsonx_orchestrate/flow_builder/flows/flow.py +28 -12
  41. ibm_watsonx_orchestrate/flow_builder/types.py +25 -0
  42. ibm_watsonx_orchestrate/flow_builder/utils.py +1 -9
  43. ibm_watsonx_orchestrate/utils/async_helpers.py +31 -0
  44. ibm_watsonx_orchestrate/utils/docker_utils.py +1177 -33
  45. ibm_watsonx_orchestrate/utils/environment.py +165 -20
  46. ibm_watsonx_orchestrate/utils/exceptions.py +1 -1
  47. ibm_watsonx_orchestrate/utils/tokens.py +51 -0
  48. ibm_watsonx_orchestrate/utils/utils.py +57 -2
  49. {ibm_watsonx_orchestrate-1.12.2.dist-info → ibm_watsonx_orchestrate-1.13.0b1.dist-info}/METADATA +2 -2
  50. {ibm_watsonx_orchestrate-1.12.2.dist-info → ibm_watsonx_orchestrate-1.13.0b1.dist-info}/RECORD +53 -48
  51. {ibm_watsonx_orchestrate-1.12.2.dist-info → ibm_watsonx_orchestrate-1.13.0b1.dist-info}/WHEEL +0 -0
  52. {ibm_watsonx_orchestrate-1.12.2.dist-info → ibm_watsonx_orchestrate-1.13.0b1.dist-info}/entry_points.txt +0 -0
  53. {ibm_watsonx_orchestrate-1.12.2.dist-info → ibm_watsonx_orchestrate-1.13.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -2,13 +2,18 @@ import logging
2
2
  import os
3
3
  import sys
4
4
  import csv
5
+ import difflib
6
+ import re
7
+ from datetime import datetime
5
8
 
6
9
  import rich
7
10
  from rich.console import Console
8
11
  from rich.prompt import Prompt
9
12
  from rich.progress import Progress, SpinnerColumn, TextColumn
13
+ from rich.panel import Panel
14
+ from rich.table import Table
10
15
  from requests import ConnectionError
11
- from typing import List
16
+ from typing import List, Dict
12
17
  from ibm_watsonx_orchestrate.client.base_api_client import ClientAPIException
13
18
  from ibm_watsonx_orchestrate.agent_builder.knowledge_bases.types import KnowledgeBaseSpec
14
19
  from ibm_watsonx_orchestrate.agent_builder.tools import ToolSpec, ToolPermission, ToolRequestBody, ToolResponseBody
@@ -16,6 +21,7 @@ from ibm_watsonx_orchestrate.cli.commands.agents.agents_controller import Agents
16
21
  from ibm_watsonx_orchestrate.agent_builder.agents.types import DEFAULT_LLM, BaseAgentSpec
17
22
  from ibm_watsonx_orchestrate.client.agents.agent_client import AgentClient
18
23
  from ibm_watsonx_orchestrate.client.knowledge_bases.knowledge_base_client import KnowledgeBaseClient
24
+ from ibm_watsonx_orchestrate.client.threads.threads_client import ThreadsClient
19
25
  from ibm_watsonx_orchestrate.client.tools.tool_client import ToolClient
20
26
  from ibm_watsonx_orchestrate.client.copilot.cpe.copilot_cpe_client import CPEClient
21
27
  from ibm_watsonx_orchestrate.client.utils import instantiate_client
@@ -63,6 +69,7 @@ def _get_incomplete_agent_from_name(agent_name: str) -> dict:
63
69
  spec = BaseAgentSpec(**{"name": agent_name, "description": agent_name, "kind": AgentKind.NATIVE})
64
70
  return spec.model_dump()
65
71
 
72
+
66
73
  def _get_incomplete_knowledge_base_from_name(kb_name: str) -> dict:
67
74
  spec = KnowledgeBaseSpec(**{"name": kb_name, "description": kb_name})
68
75
  return spec.model_dump()
@@ -123,6 +130,7 @@ def _get_agents_from_names(collaborators_names: List[str]) -> List[dict]:
123
130
 
124
131
  return agents
125
132
 
133
+
126
134
  def _get_knowledge_bases_from_names(kb_names: List[str]) -> List[dict]:
127
135
  if not len(kb_names):
128
136
  return []
@@ -168,6 +176,10 @@ def get_native_client(*args, **kwargs):
168
176
  return instantiate_client(AgentClient)
169
177
 
170
178
 
179
+ def get_threads_client():
180
+ return instantiate_client(ThreadsClient)
181
+
182
+
171
183
  def gather_utterances(max: int) -> list[str]:
172
184
  utterances = []
173
185
  logger.info("Please provide 3 sample utterances you expect your agent to handle:")
@@ -207,12 +219,12 @@ def get_deployed_tools_agents_and_knowledge_bases():
207
219
  return {"tools": all_tools, "collaborators": all_agents, "knowledge_bases": all_knowledge_bases}
208
220
 
209
221
 
210
- def pre_cpe_step(cpe_client):
222
+ def pre_cpe_step(cpe_client, chat_llm):
211
223
  tools_agents_and_knowledge_bases = get_deployed_tools_agents_and_knowledge_bases()
212
224
  user_message = ""
213
225
  with _get_progress_spinner() as progress:
214
226
  task = progress.add_task(description="Initializing Prompt Engine", total=None)
215
- response = cpe_client.submit_pre_cpe_chat(user_message=user_message)
227
+ response = cpe_client.submit_pre_cpe_chat(chat_llm=chat_llm, user_message=user_message)
216
228
  progress.remove_task(task)
217
229
 
218
230
  res = {}
@@ -221,7 +233,8 @@ def pre_cpe_step(cpe_client):
221
233
  rich.print('\n🤖 Copilot: ' + response["message"])
222
234
  user_message = Prompt.ask("\n👤 You").strip()
223
235
  message_content = {"user_message": user_message}
224
- elif "description" in response and response["description"]: # after we have a description, we pass the all tools
236
+ elif "description" in response and response[
237
+ "description"]: # after we have a description, we pass the all tools
225
238
  res["description"] = response["description"]
226
239
  message_content = {"tools": tools_agents_and_knowledge_bases['tools']}
227
240
  elif "tools" in response and response[
@@ -234,17 +247,19 @@ def pre_cpe_step(cpe_client):
234
247
  res["collaborators"] = [a for a in tools_agents_and_knowledge_bases["collaborators"] if
235
248
  a["name"] in response["collaborators"]]
236
249
  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
250
+ elif "knowledge_bases" in response and response[
251
+ 'knowledge_bases'] is not None: # after we have knowledge bases, we pass selected=True to mark that all selection were done
238
252
  res["knowledge_bases"] = [a for a in tools_agents_and_knowledge_bases["knowledge_bases"] if
239
253
  a["name"] in response["knowledge_bases"]]
240
254
  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
255
+ elif "agent_name" in response and response[
256
+ 'agent_name'] is not None: # once we have a name and style, this phase has ended
242
257
  res["agent_name"] = response["agent_name"]
243
258
  res["agent_style"] = response["agent_style"]
244
259
  return res
245
260
  with _get_progress_spinner() as progress:
246
261
  task = progress.add_task(description="Thinking...", total=None)
247
- response = cpe_client.submit_pre_cpe_chat(**message_content)
262
+ response = cpe_client.submit_pre_cpe_chat(chat_llm=chat_llm,**message_content)
248
263
  progress.remove_task(task)
249
264
 
250
265
 
@@ -300,7 +315,7 @@ def gather_examples(samples_file=None):
300
315
  return examples
301
316
 
302
317
 
303
- def talk_to_cpe(cpe_client, samples_file=None, context_data=None):
318
+ def talk_to_cpe(cpe_client, chat_llm, samples_file=None, context_data=None):
304
319
  context_data = context_data or {}
305
320
  examples = gather_examples(samples_file)
306
321
  # upload or gather input examples
@@ -308,7 +323,7 @@ def talk_to_cpe(cpe_client, samples_file=None, context_data=None):
308
323
  response = None
309
324
  with _get_progress_spinner() as progress:
310
325
  task = progress.add_task(description="Thinking...", total=None)
311
- response = cpe_client.init_with_context(context_data=context_data)
326
+ response = cpe_client.init_with_context(chat_llm=chat_llm, context_data=context_data)
312
327
  progress.remove_task(task)
313
328
  accepted_prompt = None
314
329
  while accepted_prompt is None:
@@ -320,13 +335,13 @@ def talk_to_cpe(cpe_client, samples_file=None, context_data=None):
320
335
  message = Prompt.ask("\n👤 You").strip()
321
336
  with _get_progress_spinner() as progress:
322
337
  task = progress.add_task(description="Thinking...", total=None)
323
- response = cpe_client.invoke(prompt=message)
338
+ response = cpe_client.invoke(chat_llm=chat_llm, prompt=message)
324
339
  progress.remove_task(task)
325
340
 
326
341
  return accepted_prompt
327
342
 
328
343
 
329
- def prompt_tune(agent_spec: str, output_file: str | None, samples_file: str | None, dry_run_flag: bool) -> None:
344
+ def prompt_tune(agent_spec: str, chat_llm: str | None, output_file: str | None, samples_file: str | None, dry_run_flag: bool) -> None:
330
345
  agent = AgentsController.import_agent(file=agent_spec, app_id=None)[0]
331
346
  agent_kind = agent.kind
332
347
 
@@ -339,6 +354,7 @@ def prompt_tune(agent_spec: str, output_file: str | None, samples_file: str | No
339
354
  output_file = agent_spec
340
355
 
341
356
  _validate_output_file(output_file, dry_run_flag)
357
+ _validate_chat_llm(chat_llm)
342
358
 
343
359
  client = get_cpe_client()
344
360
 
@@ -351,21 +367,22 @@ def prompt_tune(agent_spec: str, output_file: str | None, samples_file: str | No
351
367
  knowledge_bases = _get_knowledge_bases_from_names(agent.knowledge_base)
352
368
  try:
353
369
  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
- })
370
+ chat_llm=chat_llm,
371
+ samples_file=samples_file,
372
+ context_data={
373
+ "initial_instruction": instr,
374
+ 'tools': tools,
375
+ 'description': agent.description,
376
+ "collaborators": collaborators,
377
+ "knowledge_bases": knowledge_bases
378
+ })
362
379
  except ConnectionError:
363
380
  logger.error(
364
381
  "Failed to connect to Copilot server. Please ensure Copilot is running via `orchestrate copilot start`")
365
382
  sys.exit(1)
366
383
  except ClientAPIException:
367
384
  logger.error(
368
- "An unexpected server error has occur with in the Copilot server. Please check the logs via `orchestrate server logs`")
385
+ "An unexpected server error has occurred with in the Copilot server. Please check the logs via `orchestrate server logs`")
369
386
  sys.exit(1)
370
387
 
371
388
  if new_prompt:
@@ -373,28 +390,34 @@ def prompt_tune(agent_spec: str, output_file: str | None, samples_file: str | No
373
390
  agent.instructions = new_prompt
374
391
 
375
392
  if dry_run_flag:
376
- rich.print(agent.model_dump(exclude_none=True))
393
+ rich.print(agent.model_dump(exclude_none=True, mode="json"))
377
394
  else:
378
395
  if os.path.dirname(output_file):
379
396
  os.makedirs(os.path.dirname(output_file), exist_ok=True)
380
397
  AgentsController.persist_record(agent, output_file=output_file)
381
398
 
399
+ def _validate_chat_llm(chat_llm):
400
+ if chat_llm:
401
+ formatted_chat_llm = re.sub(r'[^a-zA-Z0-9/]', '-', chat_llm)
402
+ if "llama-3-3-70b-instruct" not in formatted_chat_llm:
403
+ raise BadRequest(f"Unsupported chat model for copilot {chat_llm}. Copilot supports only llama-3-3-70b-instruct at this point.")
382
404
 
383
- def create_agent(output_file: str, llm: str, samples_file: str | None, dry_run_flag: bool = False) -> None:
405
+ def create_agent(output_file: str, llm: str, chat_llm: str | None, samples_file: str | None, dry_run_flag: bool = False) -> None:
384
406
  _validate_output_file(output_file, dry_run_flag)
407
+ _validate_chat_llm(chat_llm)
385
408
  # 1. prepare the clients
386
409
  cpe_client = get_cpe_client()
387
410
 
388
411
  # 2. Pre-CPE stage:
389
412
  try:
390
- res = pre_cpe_step(cpe_client)
413
+ res = pre_cpe_step(cpe_client, chat_llm=chat_llm)
391
414
  except ConnectionError:
392
415
  logger.error(
393
416
  "Failed to connect to Copilot server. Please ensure Copilot is running via `orchestrate copilot start`")
394
417
  sys.exit(1)
395
418
  except ClientAPIException:
396
419
  logger.error(
397
- "An unexpected server error has occur with in the Copilot server. Please check the logs via `orchestrate server logs`")
420
+ "An unexpected server error has occurred with in the Copilot server. Please check the logs via `orchestrate server logs`")
398
421
  sys.exit(1)
399
422
 
400
423
  tools = res["tools"]
@@ -405,7 +428,7 @@ def create_agent(output_file: str, llm: str, samples_file: str | None, dry_run_f
405
428
  agent_style = res["agent_style"]
406
429
 
407
430
  # 4. discuss the instructions
408
- instructions = talk_to_cpe(cpe_client, samples_file,
431
+ instructions = talk_to_cpe(cpe_client, chat_llm, samples_file,
409
432
  {'description': description, 'tools': tools, 'collaborators': collaborators,
410
433
  'knowledge_bases': knowledge_bases})
411
434
 
@@ -424,7 +447,7 @@ def create_agent(output_file: str, llm: str, samples_file: str | None, dry_run_f
424
447
  agent.spec_version = SpecVersion.V1
425
448
 
426
449
  if dry_run_flag:
427
- rich.print(agent.model_dump(exclude_none=True))
450
+ rich.print(agent.model_dump(exclude_none=True, mode="json"))
428
451
  return
429
452
 
430
453
  if os.path.dirname(output_file):
@@ -446,3 +469,224 @@ def create_agent(output_file: str, llm: str, samples_file: str | None, dry_run_f
446
469
  for line in message_lines:
447
470
  rich.print("║ " + line.ljust(max_length) + " ║")
448
471
  rich.print("╚" + "═" * frame_width + "╝")
472
+
473
+
474
+ def _format_thread_messages(messages:List[dict]) -> List[dict]:
475
+ """
476
+ restructure and keep only the content relevant for refining the agent before sending to the refinement process
477
+ :param messages: List of messages as returned from the threads endpoint
478
+ :param messages:
479
+ :return: List of dictionaries where each dictionary represents a message
480
+ """
481
+ new_messages = []
482
+ for m in messages:
483
+ m_dict = {'role': m['role'], 'content': m['content'][0]['text'], 'type': 'text'} # text message
484
+ if m['step_history']:
485
+ step_history = m['step_history']
486
+ for step in step_history:
487
+ step_details = step['step_details'][0]
488
+ if step_details['type'] == 'tool_calls': # tool call
489
+ for t in step_details['tool_calls']:
490
+ new_messages.append(
491
+ {'role': m['role'], 'type': 'tool_call', 'args': t['args'], 'name': t['name']})
492
+ elif step_details['type'] == 'tool_response': # tool response
493
+ new_messages.append({'role': m['role'], 'type': 'tool_response', 'content': step_details['content']})
494
+ new_messages.append(m_dict)
495
+ if m['message_state']:
496
+ new_messages.append({'feedback': m['message_state']['content']['1']['feedback']})
497
+ return new_messages
498
+
499
+
500
+ def _suggest_sorted(user_input: str, options: List[str]) -> List[str]:
501
+ # Sort by similarity score
502
+ return sorted(options, key=lambda x: difflib.SequenceMatcher(None, user_input, x).ratio(), reverse=True)
503
+
504
+
505
+ def refine_agent_with_trajectories(agent_name: str, chat_llm: str | None, output_file: str | None,
506
+ use_last_chat: bool=False, dry_run_flag: bool = False) -> None:
507
+ """
508
+ Refines an existing agent's instructions using user selected chat trajectories and saves the updated agent configuration.
509
+
510
+ This function performs a multi-step process to enhance an agent's prompt instructions based on user interactions:
511
+
512
+ 1. **Validation**: Ensures the output file path is valid and checks if the specified agent exists. If not found,
513
+ it suggests similar agent names.
514
+ 2. **Chat Retrieval**: Fetches the 10 most recent chat threads associated with the agent. If no chats are found,
515
+ the user is prompted to initiate a conversation.
516
+ 3. **User Selection**: Displays a summary of recent chats and allows the user to select which ones to use for refinement.
517
+ 4. **Refinement**: Sends selected chat messages to the Copilot Prompt Engine (CPE) to generate refined instructions.
518
+ 5. **Update and Save**: Updates the agent's instructions and either prints the
519
+ updated agent (if `dry_run_flag` is True) or saves it to the specified output file.
520
+
521
+ Parameters:
522
+ agent_name (str): The name of the agent to refine.
523
+ chat_llm (str): The name of the model used by the refiner. If None, default model (llama-3-3-70b) is used.
524
+ output_file (str): Path to the file where the refined agent configuration will be saved.
525
+ use_last_chat(bool): If true, optimize by using the last conversation with the agent, otherwise let the use choose
526
+ dry_run_flag (bool): If True, prints the refined agent configuration without saving it to disk.
527
+
528
+ Returns:
529
+ None
530
+ """
531
+
532
+ _validate_output_file(output_file, dry_run_flag)
533
+ _validate_chat_llm(chat_llm)
534
+ agents_controller = AgentsController()
535
+ agents_client = get_native_client()
536
+ threads_client = get_threads_client()
537
+ all_agents = agents_controller.get_all_agents(client=agents_client)
538
+
539
+ # Step 1 - validate agent exist. If not - list the agents sorted by their distance from the user input name
540
+ agent_id = all_agents.get(agent_name)
541
+ if agent_id is None:
542
+ if len(all_agents) == 0:
543
+ raise BadRequest("No agents in workspace\nCreate your first agent using `orchestrate copilot prompt-tune`")
544
+ else:
545
+ available_sorted_str = "\n".join(_suggest_sorted(agent_name, all_agents.keys()))
546
+ raise BadRequest(f'Agent "{agent_name}" does not exist.\n\n'
547
+ f'Available agents:\n'
548
+ f'{available_sorted_str}')
549
+
550
+ cpe_client = get_cpe_client()
551
+ # Step 2 - retrieve chats (threads)
552
+ try:
553
+ with _get_progress_spinner() as progress:
554
+ task = progress.add_task(description="Retrieve chats", total=None)
555
+ all_threads = threads_client.get_all_threads(agent_id)
556
+ if len(all_threads) == 0:
557
+ progress.remove_task(task)
558
+ progress.refresh()
559
+ raise BadRequest(
560
+ 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`.",
561
+ )
562
+ last_10_threads = all_threads[:10] #TODO use batching when server allows
563
+ last_10_chats = [_format_thread_messages(chat) for chat in
564
+ threads_client.get_threads_messages([thread['id'] for thread in last_10_threads])]
565
+
566
+ progress.remove_task(task)
567
+ progress.refresh()
568
+ except ConnectionError:
569
+ logger.error(
570
+ f"Failed to retrieve threads (chats) for agent {agent_name}")
571
+ sys.exit(1)
572
+ except ClientAPIException:
573
+ logger.error(
574
+ f"An unexpected server error has occurred while retrieving threads for agent {agent_name}. Please check the logs via `orchestrate server logs`")
575
+ sys.exit(1)
576
+
577
+ # Step 3 - show chats and let the user choose
578
+ if use_last_chat:
579
+ title = "Selected chat"
580
+ else:
581
+ title = "10 Most Recent Chats"
582
+ table = Table(title=title)
583
+ table.add_column("Number", justify="right")
584
+ table.add_column("Chat Date", justify="left")
585
+ table.add_column("Title", justify="left")
586
+ table.add_column("Last User Message", justify="left")
587
+ table.add_column("Last User Feedback", justify="left")
588
+
589
+ for i, (thread, chat) in enumerate(zip(last_10_threads, last_10_chats), start=1):
590
+ all_user_messages = [msg for msg in chat if 'role' in msg and msg['role'] == 'user']
591
+
592
+ if len(all_user_messages) == 0:
593
+ last_user_message = ""
594
+ else:
595
+ last_user_message = all_user_messages[-1]['content']
596
+ all_feedbacks = [msg for msg in chat if 'feedback' in msg and 'text' in msg['feedback']]
597
+ if len(all_feedbacks) == 0:
598
+ last_feedback = ""
599
+ else:
600
+ last_feedback = f"{'👍' if all_feedbacks[-1]['feedback']['is_positive'] else '👎'} {all_feedbacks[-1]['feedback']['text']}"
601
+
602
+ table.add_row(str(i), datetime.strptime(thread['created_on'], '%Y-%m-%dT%H:%M:%S.%fZ').strftime(
603
+ '%B %d, %Y at %I:%M %p'), thread['title'], last_user_message, last_feedback)
604
+ table.add_row("", "", "")
605
+ if use_last_chat:
606
+ break
607
+
608
+ rich.print(table)
609
+
610
+ if use_last_chat:
611
+ rich.print("Tuning using the last conversation with the agent")
612
+ threads_messages = [last_10_chats[0]]
613
+ else:
614
+ threads_messages = get_user_selection(last_10_chats)
615
+
616
+ # Step 4 - run the refiner
617
+ try:
618
+ with _get_progress_spinner() as progress:
619
+ agent = agents_controller.get_agent_by_id(id=agent_id)
620
+ task = progress.add_task(description="Running Prompt Refiner", total=None)
621
+ tools_client = get_tool_client()
622
+ knowledge_base_client = get_knowledge_bases_client()
623
+ # loaded agent contains the ids of the tools/collabs/knowledge bases, convert them back to names.
624
+ agent.tools = [tools_client.get_draft_by_id(id)['name'] for id in agent.tools]
625
+ agent.knowledge_base = [knowledge_base_client.get_by_id(id)['name'] for id in agent.knowledge_base]
626
+ agent.collaborators = [agents_client.get_draft_by_id(id)['name'] for id in agent.collaborators]
627
+ tools = _get_tools_from_names(agent.tools)
628
+ collaborators = _get_agents_from_names(agent.collaborators)
629
+ knowledge_bases = _get_knowledge_bases_from_names(agent.knowledge_base)
630
+ if agent.instructions is None:
631
+ 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>`")
632
+ response = cpe_client.refine_agent_with_chats(instruction=agent.instructions, chat_llm=chat_llm, tools=tools,
633
+ collaborators=collaborators, knowledge_bases=knowledge_bases,
634
+ trajectories_with_feedback=threads_messages)
635
+ progress.remove_task(task)
636
+ progress.refresh()
637
+ except ConnectionError:
638
+ logger.error(
639
+ "Failed to connect to Copilot server. Please ensure Copilot is running via `orchestrate copilot start`")
640
+ sys.exit(1)
641
+ except ClientAPIException:
642
+ logger.error(
643
+ "An unexpected server error has occurred with in the Copilot server. Please check the logs via `orchestrate server logs`")
644
+ sys.exit(1)
645
+
646
+ # Step 5 - update the agent and print/save the results
647
+ agent.instructions = response['instruction']
648
+
649
+ if dry_run_flag:
650
+ rich.print(agent.model_dump(exclude_none=True, mode="json"))
651
+ return
652
+
653
+ if os.path.dirname(output_file):
654
+ os.makedirs(os.path.dirname(output_file), exist_ok=True)
655
+ agent.id = None # remove existing agent id before saving
656
+ AgentsController.persist_record(agent, output_file=output_file)
657
+
658
+ logger.info(f"Your agent refinement session finished successfully!")
659
+ logger.info(f"Agent YAML with the updated instruction saved in file: {os.path.abspath(output_file)}")
660
+
661
+
662
+
663
+ def get_user_selection(chats: List[List[Dict]]) -> List[List[Dict]]:
664
+ """
665
+ Prompts the user to select up to 5 chat threads by entering their indices.
666
+
667
+ Parameters:
668
+ chats (List[List[Dict]]): A list of chat threads, where each thread is a list of message dictionaries.
669
+
670
+ Returns:
671
+ List[List[Dict]]: A list of selected chat threads based on user input.
672
+ """
673
+ while True:
674
+ try:
675
+ eg_str = "1" if len(chats) < 2 else "1, 2"
676
+ input_str = input(
677
+ f"Please enter up to 5 indices of chats you'd like to select, separated by commas (e.g. {eg_str}): "
678
+ )
679
+
680
+ choices = [int(choice.strip()) for choice in input_str.split(',')]
681
+
682
+ if len(choices) > 5:
683
+ rich.print("You can select up to 5 chats only. Please try again.")
684
+ continue
685
+
686
+ if all(1 <= choice <= len(chats) for choice in choices):
687
+ selected_threads = [chats[choice - 1] for choice in choices]
688
+ return selected_threads
689
+ else:
690
+ rich.print(f"Please enter only numbers between 1 and {len(chats)}.")
691
+ except ValueError:
692
+ 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
 
@@ -17,10 +17,13 @@ from typing_extensions import Annotated
17
17
 
18
18
  from ibm_watsonx_orchestrate import __version__
19
19
  from ibm_watsonx_orchestrate.cli.commands.evaluations.evaluations_controller import EvaluationsController, EvaluateMode
20
+ from ibm_watsonx_orchestrate.cli.commands.evaluations.evaluations_environment_manager import run_environment_manager
20
21
  from ibm_watsonx_orchestrate.cli.commands.agents.agents_controller import AgentsController
21
22
 
22
23
  logger = logging.getLogger(__name__)
23
24
 
25
+ HIDE_ENVIRONMENT_MGR_PANEL = os.environ.get("HIDE_ENVIRONMENT_MGR_PANEL", "true").lower() == "true"
26
+
24
27
  evaluation_app = typer.Typer(no_args_is_help=True)
25
28
 
26
29
  def _native_agent_template():
@@ -142,14 +145,38 @@ def evaluate(
142
145
  "--env-file", "-e",
143
146
  help="Path to a .env file that overrides default.env. Then environment variables override both."
144
147
  ),
145
- ] = None
148
+ ] = None,
149
+ env_manager_path: Annotated[
150
+ Optional[str],
151
+ typer.Option(
152
+ "--env-manager-path",
153
+ help="""
154
+ Path to YAML configuration file containing environment settings.\n
155
+ See `./examples/evaluations/environment_manager` on how to create the environment manager file.
156
+ Note: When using this feature, you must pass the `output_dir`.
157
+ """,
158
+ rich_help_panel="Environment Manager",
159
+ hidden=HIDE_ENVIRONMENT_MGR_PANEL
160
+ )
161
+ ] = None,
146
162
  ):
163
+ validate_watsonx_credentials(user_env_file)
164
+
165
+ if env_manager_path:
166
+ if output_dir:
167
+ return run_environment_manager(
168
+ environment_manager_path=env_manager_path,
169
+ output_dir=output_dir,
170
+ )
171
+ else:
172
+ logger.error("Error: `--env_manager_path`, `--output_dir` must be provided to use the environment manager feature.")
173
+ sys.exit(1)
174
+
147
175
  if not config_file:
148
176
  if not test_paths or not output_dir:
149
177
  logger.error("Error: Both --test-paths and --output-dir must be provided when not using a config file")
150
178
  exit(1)
151
-
152
- validate_watsonx_credentials(user_env_file)
179
+
153
180
  controller = EvaluationsController()
154
181
  controller.evaluate(config_file=config_file, test_paths=test_paths, output_dir=output_dir)
155
182