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.
- ibm_watsonx_orchestrate/__init__.py +1 -1
- ibm_watsonx_orchestrate/agent_builder/connections/types.py +34 -3
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +13 -2
- ibm_watsonx_orchestrate/agent_builder/models/types.py +17 -1
- ibm_watsonx_orchestrate/agent_builder/toolkits/types.py +14 -2
- ibm_watsonx_orchestrate/agent_builder/tools/__init__.py +1 -1
- ibm_watsonx_orchestrate/agent_builder/tools/types.py +21 -3
- ibm_watsonx_orchestrate/agent_builder/voice_configurations/__init__.py +1 -1
- ibm_watsonx_orchestrate/agent_builder/voice_configurations/types.py +11 -0
- ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +31 -53
- ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +2 -2
- ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +54 -28
- ibm_watsonx_orchestrate/cli/commands/copilot/copilot_command.py +36 -2
- ibm_watsonx_orchestrate/cli/commands/copilot/copilot_controller.py +270 -26
- ibm_watsonx_orchestrate/cli/commands/copilot/copilot_server_controller.py +4 -4
- ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +30 -3
- ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_environment_manager.py +158 -0
- ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_command.py +26 -0
- ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +150 -34
- ibm_watsonx_orchestrate/cli/commands/models/models_command.py +2 -2
- ibm_watsonx_orchestrate/cli/commands/models/models_controller.py +29 -10
- ibm_watsonx_orchestrate/cli/commands/server/server_command.py +50 -18
- ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +139 -27
- ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +2 -2
- ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +43 -29
- ibm_watsonx_orchestrate/cli/commands/voice_configurations/voice_configurations_controller.py +23 -11
- ibm_watsonx_orchestrate/cli/common.py +26 -0
- ibm_watsonx_orchestrate/cli/config.py +30 -1
- ibm_watsonx_orchestrate/client/agents/agent_client.py +1 -1
- ibm_watsonx_orchestrate/client/connections/connections_client.py +1 -14
- ibm_watsonx_orchestrate/client/copilot/cpe/copilot_cpe_client.py +55 -11
- ibm_watsonx_orchestrate/client/knowledge_bases/knowledge_base_client.py +6 -2
- ibm_watsonx_orchestrate/client/model_policies/model_policies_client.py +1 -1
- ibm_watsonx_orchestrate/client/models/models_client.py +1 -1
- ibm_watsonx_orchestrate/client/threads/threads_client.py +34 -0
- ibm_watsonx_orchestrate/client/tools/tempus_client.py +4 -2
- ibm_watsonx_orchestrate/client/utils.py +29 -7
- ibm_watsonx_orchestrate/docker/compose-lite.yml +3 -2
- ibm_watsonx_orchestrate/docker/default.env +15 -10
- ibm_watsonx_orchestrate/flow_builder/flows/flow.py +28 -12
- ibm_watsonx_orchestrate/flow_builder/types.py +25 -0
- ibm_watsonx_orchestrate/flow_builder/utils.py +1 -9
- ibm_watsonx_orchestrate/utils/async_helpers.py +31 -0
- ibm_watsonx_orchestrate/utils/docker_utils.py +1177 -33
- ibm_watsonx_orchestrate/utils/environment.py +165 -20
- ibm_watsonx_orchestrate/utils/exceptions.py +1 -1
- ibm_watsonx_orchestrate/utils/tokens.py +51 -0
- ibm_watsonx_orchestrate/utils/utils.py +57 -2
- {ibm_watsonx_orchestrate-1.12.2.dist-info → ibm_watsonx_orchestrate-1.13.0b1.dist-info}/METADATA +2 -2
- {ibm_watsonx_orchestrate-1.12.2.dist-info → ibm_watsonx_orchestrate-1.13.0b1.dist-info}/RECORD +53 -48
- {ibm_watsonx_orchestrate-1.12.2.dist-info → ibm_watsonx_orchestrate-1.13.0b1.dist-info}/WHEEL +0 -0
- {ibm_watsonx_orchestrate-1.12.2.dist-info → ibm_watsonx_orchestrate-1.13.0b1.dist-info}/entry_points.txt +0 -0
- {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[
|
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[
|
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[
|
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(
|
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
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
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
|
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
|
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
|
|