ibm-watsonx-orchestrate 1.6.0b0__py3-none-any.whl → 1.6.1__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/agents/agent.py +1 -0
- ibm_watsonx_orchestrate/agent_builder/agents/types.py +5 -1
- ibm_watsonx_orchestrate/agent_builder/agents/webchat_customizations/__init__.py +2 -0
- ibm_watsonx_orchestrate/agent_builder/agents/webchat_customizations/prompts.py +34 -0
- ibm_watsonx_orchestrate/agent_builder/agents/webchat_customizations/welcome_content.py +20 -0
- ibm_watsonx_orchestrate/agent_builder/connections/__init__.py +2 -2
- ibm_watsonx_orchestrate/agent_builder/connections/connections.py +21 -7
- ibm_watsonx_orchestrate/agent_builder/connections/types.py +39 -36
- ibm_watsonx_orchestrate/agent_builder/tools/flow_tool.py +83 -0
- ibm_watsonx_orchestrate/agent_builder/tools/types.py +7 -1
- ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +56 -18
- ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_controller.py +104 -21
- ibm_watsonx_orchestrate/cli/commands/chat/chat_command.py +2 -0
- ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +26 -18
- ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +61 -61
- ibm_watsonx_orchestrate/cli/commands/environment/environment_controller.py +1 -1
- ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +118 -30
- ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +22 -9
- ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +2 -2
- ibm_watsonx_orchestrate/cli/commands/server/server_command.py +123 -5
- ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +9 -3
- ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +107 -22
- ibm_watsonx_orchestrate/client/agents/agent_client.py +74 -6
- ibm_watsonx_orchestrate/client/base_api_client.py +2 -1
- ibm_watsonx_orchestrate/client/connections/connections_client.py +18 -9
- ibm_watsonx_orchestrate/client/connections/utils.py +4 -2
- ibm_watsonx_orchestrate/client/local_service_instance.py +1 -1
- ibm_watsonx_orchestrate/client/service_instance.py +3 -3
- ibm_watsonx_orchestrate/client/tools/tempus_client.py +8 -3
- ibm_watsonx_orchestrate/client/utils.py +10 -0
- ibm_watsonx_orchestrate/docker/compose-lite.yml +228 -67
- ibm_watsonx_orchestrate/docker/default.env +32 -13
- ibm_watsonx_orchestrate/docker/proxy-config-single.yaml +12 -0
- ibm_watsonx_orchestrate/flow_builder/flows/flow.py +15 -5
- ibm_watsonx_orchestrate/flow_builder/utils.py +78 -48
- ibm_watsonx_orchestrate/run/connections.py +4 -4
- {ibm_watsonx_orchestrate-1.6.0b0.dist-info → ibm_watsonx_orchestrate-1.6.1.dist-info}/METADATA +2 -2
- {ibm_watsonx_orchestrate-1.6.0b0.dist-info → ibm_watsonx_orchestrate-1.6.1.dist-info}/RECORD +42 -37
- {ibm_watsonx_orchestrate-1.6.0b0.dist-info → ibm_watsonx_orchestrate-1.6.1.dist-info}/WHEEL +0 -0
- {ibm_watsonx_orchestrate-1.6.0b0.dist-info → ibm_watsonx_orchestrate-1.6.1.dist-info}/entry_points.txt +0 -0
- {ibm_watsonx_orchestrate-1.6.0b0.dist-info → ibm_watsonx_orchestrate-1.6.1.dist-info}/licenses/LICENSE +0 -0
@@ -5,10 +5,12 @@ import os
|
|
5
5
|
import yaml
|
6
6
|
import csv
|
7
7
|
import rich
|
8
|
-
from pathlib import Path
|
9
8
|
import sys
|
10
|
-
|
9
|
+
import shutil
|
11
10
|
|
11
|
+
from rich.panel import Panel
|
12
|
+
from pathlib import Path
|
13
|
+
from dotenv import dotenv_values
|
12
14
|
from typing import Optional
|
13
15
|
from typing_extensions import Annotated
|
14
16
|
|
@@ -46,6 +48,32 @@ def validate_watsonx_credentials(user_env_file: str) -> bool:
|
|
46
48
|
os.environ.update({key: user_env[key] for key in required_keys})
|
47
49
|
logger.info("WatsonX credentials validated successfully.")
|
48
50
|
|
51
|
+
def read_csv(data_path: str, delimiter="\t"):
|
52
|
+
data = []
|
53
|
+
with open(data_path, "r") as f:
|
54
|
+
tsv_reader = csv.reader(f, delimiter=delimiter)
|
55
|
+
for line in tsv_reader:
|
56
|
+
data.append(line)
|
57
|
+
|
58
|
+
return data
|
59
|
+
|
60
|
+
def performance_test(agent_name, data_path, output_dir = None, user_env_file = None):
|
61
|
+
test_data = read_csv(data_path)
|
62
|
+
|
63
|
+
controller = EvaluationsController()
|
64
|
+
generated_performance_tests = controller.generate_performance_test(agent_name, test_data)
|
65
|
+
|
66
|
+
generated_perf_test_dir = Path(output_dir) / "generated_performance_tests"
|
67
|
+
generated_perf_test_dir.mkdir(exist_ok=True, parents=True)
|
68
|
+
|
69
|
+
for idx, test in enumerate(generated_performance_tests):
|
70
|
+
test_name = f"validate_external_agent_evaluation_test_{idx}.json"
|
71
|
+
with open(generated_perf_test_dir / test_name, encoding="utf-8", mode="w+") as f:
|
72
|
+
json.dump(test, f, indent=4)
|
73
|
+
|
74
|
+
rich.print(f"Performance test cases saved at path '{str(generated_perf_test_dir)}'")
|
75
|
+
rich.print("[gold3]Running Performance Test")
|
76
|
+
evaluate(output_dir=output_dir, test_paths=str(generated_perf_test_dir))
|
49
77
|
|
50
78
|
@evaluation_app.command(name="evaluate", help="Evaluate an agent against a set of test cases")
|
51
79
|
def evaluate(
|
@@ -115,7 +143,7 @@ def generate(
|
|
115
143
|
stories_path: Annotated[
|
116
144
|
str,
|
117
145
|
typer.Option(
|
118
|
-
"--
|
146
|
+
"--stories-path", "-s",
|
119
147
|
help="Path to the CSV file containing user stories for test case generation. "
|
120
148
|
"The file has 'story' and 'agent' columns."
|
121
149
|
)
|
@@ -123,14 +151,14 @@ def generate(
|
|
123
151
|
tools_path: Annotated[
|
124
152
|
str,
|
125
153
|
typer.Option(
|
126
|
-
"--
|
154
|
+
"--tools-path", "-t",
|
127
155
|
help="Path to the directory containing tool definitions."
|
128
156
|
)
|
129
157
|
],
|
130
158
|
output_dir: Annotated[
|
131
159
|
Optional[str],
|
132
160
|
typer.Option(
|
133
|
-
"--
|
161
|
+
"--output-dir", "-o",
|
134
162
|
help="Directory to save the generated test cases."
|
135
163
|
)
|
136
164
|
] = None,
|
@@ -151,7 +179,7 @@ def generate(
|
|
151
179
|
def analyze(data_path: Annotated[
|
152
180
|
str,
|
153
181
|
typer.Option(
|
154
|
-
"--
|
182
|
+
"--data-path", "-d",
|
155
183
|
help="Path to the directory that has the saved results"
|
156
184
|
)
|
157
185
|
],
|
@@ -167,30 +195,31 @@ def analyze(data_path: Annotated[
|
|
167
195
|
controller = EvaluationsController()
|
168
196
|
controller.analyze(data_path=data_path)
|
169
197
|
|
170
|
-
|
171
|
-
@evaluation_app.command(name="validate_external", help="Validate an external agent against a set of inputs")
|
198
|
+
@evaluation_app.command(name="validate-external", help="Validate an external agent against a set of inputs")
|
172
199
|
def validate_external(
|
173
200
|
data_path: Annotated[
|
174
201
|
str,
|
175
202
|
typer.Option(
|
176
|
-
"--
|
177
|
-
help="Path to .
|
203
|
+
"--tsv", "-t",
|
204
|
+
help="Path to .tsv file of inputs"
|
178
205
|
)
|
179
206
|
],
|
180
|
-
|
207
|
+
external_agent_config: Annotated[
|
181
208
|
str,
|
182
209
|
typer.Option(
|
183
|
-
"--config", "-
|
184
|
-
help="Path to the external agent yaml"
|
210
|
+
"--external-agent-config", "-ext",
|
211
|
+
help="Path to the external agent yaml",
|
212
|
+
|
185
213
|
)
|
186
214
|
],
|
187
215
|
credential: Annotated[
|
188
216
|
str,
|
189
217
|
typer.Option(
|
190
218
|
"--credential", "-crd",
|
191
|
-
help="credential string"
|
219
|
+
help="credential string",
|
220
|
+
rich_help_panel="Parameters for Validation"
|
192
221
|
)
|
193
|
-
],
|
222
|
+
] = None,
|
194
223
|
output_dir: Annotated[
|
195
224
|
str,
|
196
225
|
typer.Option(
|
@@ -204,21 +233,80 @@ def validate_external(
|
|
204
233
|
"--env-file", "-e",
|
205
234
|
help="Path to a .env file that overrides default.env. Then environment variables override both."
|
206
235
|
),
|
236
|
+
] = None,
|
237
|
+
agent_name: Annotated[
|
238
|
+
str,
|
239
|
+
typer.Option(
|
240
|
+
"--agent_name", "-a",
|
241
|
+
help="Name of the native agent which has the external agent to test registered as a collaborater. See: https://developer.watson-orchestrate.ibm.com/agents/build_agent#native-agents)." \
|
242
|
+
" If this parameter is pased, validation of the external agent is not run.",
|
243
|
+
rich_help_panel="Parameters for Input Evaluation"
|
244
|
+
)
|
207
245
|
] = None
|
208
246
|
):
|
209
|
-
|
247
|
+
|
210
248
|
validate_watsonx_credentials(user_env_file)
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
249
|
+
Path(output_dir).mkdir(exist_ok=True)
|
250
|
+
shutil.copy(data_path, os.path.join(output_dir, "input_sample.tsv"))
|
251
|
+
|
252
|
+
if agent_name is not None:
|
253
|
+
eval_dir = os.path.join(output_dir, "evaluation")
|
254
|
+
if os.path.exists(eval_dir):
|
255
|
+
rich.print(f"[yellow]: found existing {eval_dir} in target directory. All content is removed.")
|
256
|
+
shutil.rmtree(os.path.join(output_dir, "evaluation"))
|
257
|
+
Path(eval_dir).mkdir(exist_ok=True)
|
258
|
+
# save external agent config even though its not used for evaluation
|
259
|
+
# it can help in later debugging customer agents
|
260
|
+
with open(os.path.join(eval_dir, "external_agent_cfg.yaml"), "w+") as f:
|
261
|
+
with open(external_agent_config, "r") as cfg:
|
262
|
+
external_agent_config = yaml.safe_load(cfg)
|
263
|
+
yaml.safe_dump(external_agent_config, f, indent=4)
|
264
|
+
|
265
|
+
rich.print(f"[gold3]Starting evaluation of inputs in '{data_path}' against '{agent_name}'[/gold3]")
|
266
|
+
performance_test(
|
267
|
+
agent_name=agent_name,
|
268
|
+
data_path=data_path,
|
269
|
+
output_dir=eval_dir,
|
270
|
+
user_env_file=user_env_file
|
271
|
+
)
|
272
|
+
|
273
|
+
else:
|
274
|
+
with open(external_agent_config, "r") as f:
|
275
|
+
external_agent_config = yaml.safe_load(f)
|
276
|
+
controller = EvaluationsController()
|
277
|
+
test_data = []
|
278
|
+
with open(data_path, "r") as f:
|
279
|
+
csv_reader = csv.reader(f, delimiter="\t")
|
280
|
+
for line in csv_reader:
|
281
|
+
test_data.append(line[0])
|
282
|
+
|
283
|
+
# save validation results in "validation_results" sub-dir
|
284
|
+
validation_folder = Path(output_dir) / "validation_results"
|
285
|
+
if os.path.exists(validation_folder):
|
286
|
+
rich.print(f"[yellow]: found existing {validation_folder} in target directory. All content is removed.")
|
287
|
+
shutil.rmtree(validation_folder)
|
288
|
+
validation_folder.mkdir(exist_ok=True, parents=True)
|
289
|
+
|
290
|
+
# validate the inputs in the provided csv file
|
291
|
+
summary = controller.external_validate(external_agent_config, test_data, credential)
|
292
|
+
with open(validation_folder / "validation_results.json", "w") as f:
|
293
|
+
json.dump(summary, f, indent=4)
|
294
|
+
|
295
|
+
# validate sample block inputs
|
296
|
+
rich.print("[gold3]Validating external agent to see if it can handle an array of messages.")
|
297
|
+
block_input_summary = controller.external_validate(external_agent_config, test_data, credential, add_context=True)
|
298
|
+
with open(validation_folder / "sample_block_validation_results.json", "w") as f:
|
299
|
+
json.dump(block_input_summary, f, indent=4)
|
300
|
+
|
301
|
+
user_validation_successful = all([item["success"] for item in summary])
|
302
|
+
block_validation_successful = all([item["success"] for item in block_input_summary])
|
303
|
+
|
304
|
+
if user_validation_successful and block_validation_successful:
|
305
|
+
msg = (
|
306
|
+
f"[green]Validation is successful. The result is saved to '{str(validation_folder)}'.[/green]\n"
|
307
|
+
"You can add the external agent as a collaborator agent. See: https://developer.watson-orchestrate.ibm.com/agents/build_agent#native-agents."
|
308
|
+
)
|
309
|
+
else:
|
310
|
+
msg = f"[dark_orange]Schema validation did not succeed. See '{str(validation_folder)}' for failures.[/dark_orange]"
|
311
|
+
|
312
|
+
rich.print(Panel(msg))
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import logging
|
2
|
-
|
2
|
+
import os.path
|
3
|
+
from typing import List, Dict, Optional, Tuple
|
3
4
|
import csv
|
4
5
|
from pathlib import Path
|
5
6
|
import rich
|
@@ -10,12 +11,13 @@ from wxo_agentic_evaluation.batch_annotate import generate_test_cases_from_stori
|
|
10
11
|
from wxo_agentic_evaluation.arg_configs import TestConfig, AuthConfig, LLMUserConfig, ChatRecordingConfig, AnalyzeConfig
|
11
12
|
from wxo_agentic_evaluation.record_chat import record_chats
|
12
13
|
from wxo_agentic_evaluation.external_agent.external_validate import ExternalAgentValidation
|
14
|
+
from wxo_agentic_evaluation.external_agent.performance_test import ExternalAgentPerformanceTest
|
13
15
|
from ibm_watsonx_orchestrate import __version__
|
14
16
|
from ibm_watsonx_orchestrate.cli.config import Config, ENV_WXO_URL_OPT, AUTH_CONFIG_FILE, AUTH_CONFIG_FILE_FOLDER, AUTH_SECTION_HEADER, AUTH_MCSP_TOKEN_OPT
|
15
17
|
from ibm_watsonx_orchestrate.utils.utils import yaml_safe_load
|
16
18
|
from ibm_watsonx_orchestrate.cli.commands.agents.agents_controller import AgentsController
|
17
19
|
from ibm_watsonx_orchestrate.agent_builder.agents import AgentKind
|
18
|
-
|
20
|
+
import uuid
|
19
21
|
|
20
22
|
logger = logging.getLogger(__name__)
|
21
23
|
|
@@ -75,9 +77,13 @@ class EvaluationsController:
|
|
75
77
|
evaluate.main(config)
|
76
78
|
|
77
79
|
def record(self, output_dir) -> None:
|
80
|
+
|
81
|
+
|
82
|
+
random_uuid = str(uuid.uuid4())
|
83
|
+
|
78
84
|
url, tenant_name, token = self._get_env_config()
|
79
85
|
config_data = {
|
80
|
-
"output_dir": Path.cwd() if output_dir is None else Path(output_dir),
|
86
|
+
"output_dir": Path(os.path.join(Path.cwd(), random_uuid)) if output_dir is None else Path(os.path.join(output_dir,random_uuid)),
|
81
87
|
"service_url": url,
|
82
88
|
"tenant_name": tenant_name,
|
83
89
|
"token": token
|
@@ -143,16 +149,23 @@ class EvaluationsController:
|
|
143
149
|
def summarize(self) -> None:
|
144
150
|
pass
|
145
151
|
|
146
|
-
def external_validate(self, config: Dict, data: List[str], credential:str):
|
152
|
+
def external_validate(self, config: Dict, data: List[str], credential:str, add_context: bool = False):
|
147
153
|
validator = ExternalAgentValidation(credential=credential,
|
148
154
|
auth_scheme=config["auth_scheme"],
|
149
155
|
service_url=config["api_url"])
|
156
|
+
|
150
157
|
summary = []
|
151
158
|
for entry in data:
|
152
|
-
results = validator.call_validation(entry)
|
153
|
-
|
154
|
-
rich.print(f"[red] No events are generated for input {entry} [/red]")
|
155
|
-
summary.append({entry: results})
|
159
|
+
results = validator.call_validation(entry, add_context)
|
160
|
+
summary.append(results)
|
156
161
|
|
157
162
|
return summary
|
158
|
-
|
163
|
+
|
164
|
+
def generate_performance_test(self, agent_name: str, test_data: List[Tuple[str, str]]):
|
165
|
+
performance_test = ExternalAgentPerformanceTest(
|
166
|
+
agent_name=agent_name,
|
167
|
+
test_data=test_data
|
168
|
+
)
|
169
|
+
generated_performance_tests = performance_test.generate_tests()
|
170
|
+
|
171
|
+
return generated_performance_tests
|
@@ -240,6 +240,27 @@ def apply_llm_api_key_defaults(env_dict: dict) -> None:
|
|
240
240
|
env_dict.setdefault("ASSISTANT_EMBEDDINGS_SPACE_ID", space_value)
|
241
241
|
env_dict.setdefault("ROUTING_LLM_SPACE_ID", space_value)
|
242
242
|
|
243
|
+
def _is_docker_container_running(container_name):
|
244
|
+
ensure_docker_installed()
|
245
|
+
command = [ "docker",
|
246
|
+
"ps",
|
247
|
+
"-f",
|
248
|
+
f"name={container_name}"
|
249
|
+
]
|
250
|
+
result = subprocess.run(command, env=os.environ, capture_output=True)
|
251
|
+
if container_name in str(result.stdout):
|
252
|
+
return True
|
253
|
+
return False
|
254
|
+
|
255
|
+
def _check_exclusive_observibility(langfuse_enabled: bool, ibm_tele_enabled: bool):
|
256
|
+
if langfuse_enabled and ibm_tele_enabled:
|
257
|
+
return False
|
258
|
+
if langfuse_enabled and _is_docker_container_running("docker-frontend-server-1"):
|
259
|
+
return False
|
260
|
+
if ibm_tele_enabled and _is_docker_container_running("docker-langfuse-web-1"):
|
261
|
+
return False
|
262
|
+
return True
|
263
|
+
|
243
264
|
def write_merged_env_file(merged_env: dict) -> Path:
|
244
265
|
tmp = tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".env")
|
245
266
|
with tmp:
|
@@ -292,7 +313,8 @@ def get_persisted_user_env() -> dict | None:
|
|
292
313
|
user_env = cfg.get(USER_ENV_CACHE_HEADER) if cfg.get(USER_ENV_CACHE_HEADER) else None
|
293
314
|
return user_env
|
294
315
|
|
295
|
-
|
316
|
+
|
317
|
+
def run_compose_lite(final_env_file: Path, experimental_with_langfuse=False, experimental_with_ibm_telemetry=False) -> None:
|
296
318
|
compose_path = get_compose_file()
|
297
319
|
compose_command = ensure_docker_compose_installed()
|
298
320
|
db_tag = read_env_file(final_env_file).get('DBTAG', None)
|
@@ -324,9 +346,15 @@ def run_compose_lite(final_env_file: Path, experimental_with_langfuse=False) ->
|
|
324
346
|
'--profile',
|
325
347
|
'langfuse'
|
326
348
|
]
|
349
|
+
elif experimental_with_ibm_telemetry:
|
350
|
+
command = compose_command + [
|
351
|
+
'--profile',
|
352
|
+
'ibm-telemetry'
|
353
|
+
]
|
327
354
|
else:
|
328
355
|
command = compose_command
|
329
356
|
|
357
|
+
|
330
358
|
command += [
|
331
359
|
"-f", str(compose_path),
|
332
360
|
"--env-file", str(final_env_file),
|
@@ -444,6 +472,9 @@ def run_compose_lite_ui(user_env_file: Path) -> bool:
|
|
444
472
|
# do nothing, as the docker login here is not mandatory
|
445
473
|
pass
|
446
474
|
|
475
|
+
# Auto-configure callback IP for async tools
|
476
|
+
merged_env_dict = auto_configure_callback_ip(merged_env_dict)
|
477
|
+
|
447
478
|
#These are to removed warning and not used in UI component
|
448
479
|
if not 'WATSONX_SPACE_ID' in merged_env_dict:
|
449
480
|
merged_env_dict['WATSONX_SPACE_ID']='X'
|
@@ -621,8 +652,80 @@ def confirm_accepts_license_agreement(accepts_by_argument: bool):
|
|
621
652
|
logger.error('The terms and conditions were not accepted, exiting.')
|
622
653
|
exit(1)
|
623
654
|
|
624
|
-
|
625
|
-
|
655
|
+
def auto_configure_callback_ip(merged_env_dict: dict) -> dict:
|
656
|
+
"""
|
657
|
+
Automatically detect and configure CALLBACK_HOST_URL if it's empty.
|
658
|
+
|
659
|
+
Args:
|
660
|
+
merged_env_dict: The merged environment dictionary
|
661
|
+
|
662
|
+
Returns:
|
663
|
+
Updated environment dictionary with CALLBACK_HOST_URL set
|
664
|
+
"""
|
665
|
+
callback_url = merged_env_dict.get('CALLBACK_HOST_URL', '').strip()
|
666
|
+
|
667
|
+
# Only auto-configure if CALLBACK_HOST_URL is empty
|
668
|
+
if not callback_url:
|
669
|
+
logger.info("Auto-detecting local IP address for async tool callbacks...")
|
670
|
+
|
671
|
+
system = platform.system()
|
672
|
+
ip = None
|
673
|
+
|
674
|
+
try:
|
675
|
+
if system in ("Linux", "Darwin"):
|
676
|
+
result = subprocess.run(["ifconfig"], capture_output=True, text=True, check=True)
|
677
|
+
lines = result.stdout.splitlines()
|
678
|
+
|
679
|
+
for line in lines:
|
680
|
+
line = line.strip()
|
681
|
+
# Unix ifconfig output format: "inet 192.168.1.100 netmask 0xffffff00 broadcast 192.168.1.255"
|
682
|
+
if line.startswith("inet ") and "127.0.0.1" not in line:
|
683
|
+
candidate_ip = line.split()[1]
|
684
|
+
# Validate IP is not loopback or link-local
|
685
|
+
if (candidate_ip and
|
686
|
+
not candidate_ip.startswith("127.") and
|
687
|
+
not candidate_ip.startswith("169.254")):
|
688
|
+
ip = candidate_ip
|
689
|
+
break
|
690
|
+
|
691
|
+
elif system == "Windows":
|
692
|
+
result = subprocess.run(["ipconfig"], capture_output=True, text=True, check=True)
|
693
|
+
lines = result.stdout.splitlines()
|
694
|
+
|
695
|
+
for line in lines:
|
696
|
+
line = line.strip()
|
697
|
+
# Windows ipconfig output format: " IPv4 Address. . . . . . . . . . . : 192.168.1.100"
|
698
|
+
if "IPv4 Address" in line and ":" in line:
|
699
|
+
candidate_ip = line.split(":")[-1].strip()
|
700
|
+
# Validate IP is not loopback or link-local
|
701
|
+
if (candidate_ip and
|
702
|
+
not candidate_ip.startswith("127.") and
|
703
|
+
not candidate_ip.startswith("169.254")):
|
704
|
+
ip = candidate_ip
|
705
|
+
break
|
706
|
+
|
707
|
+
else:
|
708
|
+
logger.warning(f"Unsupported platform: {system}")
|
709
|
+
ip = None
|
710
|
+
|
711
|
+
except Exception as e:
|
712
|
+
logger.debug(f"IP detection failed on {system}: {e}")
|
713
|
+
ip = None
|
714
|
+
|
715
|
+
if ip:
|
716
|
+
callback_url = f"http://{ip}:4321"
|
717
|
+
merged_env_dict['CALLBACK_HOST_URL'] = callback_url
|
718
|
+
logger.info(f"Auto-configured CALLBACK_HOST_URL to: {callback_url}")
|
719
|
+
else:
|
720
|
+
# Fallback for localhost
|
721
|
+
callback_url = "http://host.docker.internal:4321"
|
722
|
+
merged_env_dict['CALLBACK_HOST_URL'] = callback_url
|
723
|
+
logger.info(f"Using Docker internal URL: {callback_url}")
|
724
|
+
logger.info("For external tools, consider using ngrok or similar tunneling service.")
|
725
|
+
else:
|
726
|
+
logger.info(f"Using existing CALLBACK_HOST_URL: {callback_url}")
|
727
|
+
|
728
|
+
return merged_env_dict
|
626
729
|
|
627
730
|
@server_app.command(name="start")
|
628
731
|
def server_start(
|
@@ -636,6 +739,11 @@ def server_start(
|
|
636
739
|
'--with-langfuse', '-l',
|
637
740
|
help='Option to enable Langfuse support.'
|
638
741
|
),
|
742
|
+
experimental_with_ibm_telemetry: bool = typer.Option(
|
743
|
+
False,
|
744
|
+
'--with-ibm-telemetry', '-i',
|
745
|
+
help=''
|
746
|
+
),
|
639
747
|
persist_env_secrets: bool = typer.Option(
|
640
748
|
False,
|
641
749
|
'--persist-env-secrets', '-p',
|
@@ -675,10 +783,18 @@ def server_start(
|
|
675
783
|
|
676
784
|
merged_env_dict = apply_server_env_dict_defaults(merged_env_dict)
|
677
785
|
|
786
|
+
# Auto-configure callback IP for async tools
|
787
|
+
merged_env_dict = auto_configure_callback_ip(merged_env_dict)
|
788
|
+
if not _check_exclusive_observibility(experimental_with_langfuse, experimental_with_ibm_telemetry):
|
789
|
+
logger.error("Please select either langfuse or ibm telemetry for observability not both")
|
790
|
+
sys.exit(1)
|
791
|
+
|
678
792
|
# Add LANGFUSE_ENABLED into the merged_env_dict, for tempus to pick up.
|
679
793
|
if experimental_with_langfuse:
|
680
794
|
merged_env_dict['LANGFUSE_ENABLED'] = 'true'
|
681
|
-
|
795
|
+
|
796
|
+
if experimental_with_ibm_telemetry:
|
797
|
+
merged_env_dict['USE_IBM_TELEMETRY'] = 'true'
|
682
798
|
|
683
799
|
try:
|
684
800
|
docker_login_by_dev_edition_source(merged_env_dict, dev_edition_source)
|
@@ -690,7 +806,9 @@ def server_start(
|
|
690
806
|
|
691
807
|
|
692
808
|
final_env_file = write_merged_env_file(merged_env_dict)
|
693
|
-
run_compose_lite(final_env_file=final_env_file,
|
809
|
+
run_compose_lite(final_env_file=final_env_file,
|
810
|
+
experimental_with_langfuse=experimental_with_langfuse,
|
811
|
+
experimental_with_ibm_telemetry=experimental_with_ibm_telemetry)
|
694
812
|
|
695
813
|
run_db_migration()
|
696
814
|
|
@@ -243,9 +243,15 @@ class ToolkitController:
|
|
243
243
|
rich.print(JSON(json.dumps(tools_list, indent=4)))
|
244
244
|
else:
|
245
245
|
table = rich.table.Table(show_header=True, header_style="bold white", show_lines=True)
|
246
|
-
|
247
|
-
|
248
|
-
|
246
|
+
column_args = {
|
247
|
+
"Name": {"overflow": "fold"},
|
248
|
+
"Kind": {},
|
249
|
+
"Description": {},
|
250
|
+
"Tools": {},
|
251
|
+
"App ID": {"overflow": "fold"}
|
252
|
+
}
|
253
|
+
for column in column_args:
|
254
|
+
table.add_column(column,**column_args[column])
|
249
255
|
|
250
256
|
tools_client = instantiate_client(ToolClient)
|
251
257
|
|