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.
- ibm_watsonx_orchestrate/__init__.py +2 -1
- ibm_watsonx_orchestrate/agent_builder/agents/types.py +5 -5
- ibm_watsonx_orchestrate/agent_builder/connections/types.py +34 -3
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +11 -2
- ibm_watsonx_orchestrate/agent_builder/models/types.py +18 -1
- ibm_watsonx_orchestrate/agent_builder/toolkits/base_toolkit.py +1 -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/base_tool.py +1 -1
- ibm_watsonx_orchestrate/agent_builder/tools/langflow_tool.py +61 -1
- ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +6 -0
- 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 +29 -53
- ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +2 -2
- ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +56 -30
- ibm_watsonx_orchestrate/cli/commands/copilot/copilot_command.py +25 -2
- ibm_watsonx_orchestrate/cli/commands/copilot/copilot_controller.py +249 -14
- ibm_watsonx_orchestrate/cli/commands/copilot/copilot_server_controller.py +4 -4
- ibm_watsonx_orchestrate/cli/commands/environment/environment_command.py +5 -1
- ibm_watsonx_orchestrate/cli/commands/environment/environment_controller.py +6 -3
- ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +3 -2
- ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +1 -1
- ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +45 -16
- ibm_watsonx_orchestrate/cli/commands/models/model_provider_mapper.py +23 -4
- 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/partners/offering/partners_offering_controller.py +21 -4
- ibm_watsonx_orchestrate/cli/commands/partners/offering/types.py +7 -15
- ibm_watsonx_orchestrate/cli/commands/partners/partners_command.py +1 -1
- ibm_watsonx_orchestrate/cli/commands/server/server_command.py +30 -20
- ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +2 -2
- 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 +79 -36
- 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 +33 -2
- ibm_watsonx_orchestrate/client/connections/connections_client.py +1 -14
- ibm_watsonx_orchestrate/client/copilot/cpe/copilot_cpe_client.py +34 -1
- 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/utils.py +29 -7
- ibm_watsonx_orchestrate/docker/compose-lite.yml +58 -8
- ibm_watsonx_orchestrate/docker/default.env +26 -17
- ibm_watsonx_orchestrate/flow_builder/flows/decorators.py +10 -2
- ibm_watsonx_orchestrate/flow_builder/flows/flow.py +90 -16
- ibm_watsonx_orchestrate/flow_builder/node.py +14 -2
- ibm_watsonx_orchestrate/flow_builder/types.py +57 -3
- ibm_watsonx_orchestrate/langflow/__init__.py +0 -0
- ibm_watsonx_orchestrate/langflow/langflow_utils.py +195 -0
- ibm_watsonx_orchestrate/langflow/lfx_deps.py +84 -0
- 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 +63 -4
- {ibm_watsonx_orchestrate-1.12.0b0.dist-info → ibm_watsonx_orchestrate-1.13.0b0.dist-info}/METADATA +2 -2
- {ibm_watsonx_orchestrate-1.12.0b0.dist-info → ibm_watsonx_orchestrate-1.13.0b0.dist-info}/RECORD +66 -59
- {ibm_watsonx_orchestrate-1.12.0b0.dist-info → ibm_watsonx_orchestrate-1.13.0b0.dist-info}/WHEEL +0 -0
- {ibm_watsonx_orchestrate-1.12.0b0.dist-info → ibm_watsonx_orchestrate-1.13.0b0.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
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
|
-
|
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
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
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
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
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[
|
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[
|
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[
|
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
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
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
|
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
|
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,
|
22
|
-
|
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(
|