ibm-watsonx-orchestrate 1.10.2__py3-none-any.whl → 1.11.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. ibm_watsonx_orchestrate/__init__.py +2 -1
  2. ibm_watsonx_orchestrate/agent_builder/agents/types.py +13 -0
  3. ibm_watsonx_orchestrate/agent_builder/connections/types.py +53 -6
  4. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +24 -9
  5. ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +10 -2
  6. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +404 -173
  7. ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +33 -4
  8. ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +62 -6
  9. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_controller.py +6 -2
  10. ibm_watsonx_orchestrate/cli/commands/environment/environment_command.py +1 -1
  11. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +174 -2
  12. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +93 -9
  13. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +0 -3
  14. ibm_watsonx_orchestrate/cli/commands/server/types.py +14 -6
  15. ibm_watsonx_orchestrate/client/base_api_client.py +31 -10
  16. ibm_watsonx_orchestrate/client/connections/connections_client.py +14 -0
  17. ibm_watsonx_orchestrate/client/service_instance.py +19 -34
  18. ibm_watsonx_orchestrate/client/utils.py +3 -1
  19. ibm_watsonx_orchestrate/docker/compose-lite.yml +6 -1
  20. ibm_watsonx_orchestrate/docker/default.env +13 -11
  21. ibm_watsonx_orchestrate/flow_builder/data_map.py +4 -1
  22. ibm_watsonx_orchestrate/flow_builder/flows/flow.py +117 -7
  23. ibm_watsonx_orchestrate/flow_builder/node.py +76 -5
  24. ibm_watsonx_orchestrate/flow_builder/types.py +344 -10
  25. {ibm_watsonx_orchestrate-1.10.2.dist-info → ibm_watsonx_orchestrate-1.11.0b1.dist-info}/METADATA +2 -2
  26. {ibm_watsonx_orchestrate-1.10.2.dist-info → ibm_watsonx_orchestrate-1.11.0b1.dist-info}/RECORD +29 -29
  27. {ibm_watsonx_orchestrate-1.10.2.dist-info → ibm_watsonx_orchestrate-1.11.0b1.dist-info}/WHEEL +0 -0
  28. {ibm_watsonx_orchestrate-1.10.2.dist-info → ibm_watsonx_orchestrate-1.11.0b1.dist-info}/entry_points.txt +0 -0
  29. {ibm_watsonx_orchestrate-1.10.2.dist-info → ibm_watsonx_orchestrate-1.11.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -1,9 +1,22 @@
1
1
  import json
2
-
2
+ from ibm_watsonx_orchestrate.utils.exceptions import BadRequest
3
3
  import requests
4
4
  from abc import ABC, abstractmethod
5
5
  from ibm_cloud_sdk_core.authenticators import MCSPAuthenticator
6
6
  from typing_extensions import List
7
+ from contextlib import contextmanager
8
+
9
+ @contextmanager
10
+ def ssl_handler():
11
+ try:
12
+ yield
13
+ except requests.exceptions.SSLError as e:
14
+ error_message = str(e)
15
+ if "self-signed certificate in certificate chain" in error_message:
16
+ reason = "[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate in certificate chain"
17
+ else:
18
+ reason = error_message
19
+ raise BadRequest(f"SSL handshake failed for request '{e.request.path_url}'. Reason: '{reason}'")
7
20
 
8
21
 
9
22
  class ClientAPIException(requests.HTTPError):
@@ -50,7 +63,8 @@ class BaseAPIClient:
50
63
 
51
64
  def _get(self, path: str, params: dict = None, data=None, return_raw=False) -> dict:
52
65
  url = f"{self.base_url}{path}"
53
- response = requests.get(url, headers=self._get_headers(), params=params, data=data, verify=self.verify)
66
+ with ssl_handler():
67
+ response = requests.get(url, headers=self._get_headers(), params=params, data=data, verify=self.verify)
54
68
  self._check_response(response)
55
69
  if not return_raw:
56
70
  return response.json()
@@ -59,13 +73,15 @@ class BaseAPIClient:
59
73
 
60
74
  def _post(self, path: str, data: dict = None, files: dict = None) -> dict:
61
75
  url = f"{self.base_url}{path}"
62
- response = requests.post(url, headers=self._get_headers(), json=data, files=files, verify=self.verify)
76
+ with ssl_handler():
77
+ response = requests.post(url, headers=self._get_headers(), json=data, files=files, verify=self.verify)
63
78
  self._check_response(response)
64
79
  return response.json() if response.text else {}
65
80
 
66
81
  def _post_nd_json(self, path: str, data: dict = None, files: dict = None) -> List[dict]:
67
82
  url = f"{self.base_url}{path}"
68
- response = requests.post(url, headers=self._get_headers(), json=data, files=files)
83
+ with ssl_handler():
84
+ response = requests.post(url, headers=self._get_headers(), json=data, files=files)
69
85
  self._check_response(response)
70
86
 
71
87
  res = []
@@ -76,33 +92,38 @@ class BaseAPIClient:
76
92
 
77
93
  def _post_form_data(self, path: str, data: dict = None, files: dict = None) -> dict:
78
94
  url = f"{self.base_url}{path}"
79
- # Use data argument instead of json so data is encoded as application/x-www-form-urlencoded
80
- response = requests.post(url, headers=self._get_headers(), data=data, files=files, verify=self.verify)
95
+ with ssl_handler():
96
+ # Use data argument instead of json so data is encoded as application/x-www-form-urlencoded
97
+ response = requests.post(url, headers=self._get_headers(), data=data, files=files, verify=self.verify)
81
98
  self._check_response(response)
82
99
  return response.json() if response.text else {}
83
100
 
84
101
  def _put(self, path: str, data: dict = None) -> dict:
85
102
 
86
103
  url = f"{self.base_url}{path}"
87
- response = requests.put(url, headers=self._get_headers(), json=data, verify=self.verify)
104
+ with ssl_handler():
105
+ response = requests.put(url, headers=self._get_headers(), json=data, verify=self.verify)
88
106
  self._check_response(response)
89
107
  return response.json() if response.text else {}
90
108
 
91
109
  def _patch(self, path: str, data: dict = None) -> dict:
92
110
  url = f"{self.base_url}{path}"
93
- response = requests.patch(url, headers=self._get_headers(), json=data, verify=self.verify)
111
+ with ssl_handler():
112
+ response = requests.patch(url, headers=self._get_headers(), json=data, verify=self.verify)
94
113
  self._check_response(response)
95
114
  return response.json() if response.text else {}
96
115
 
97
116
  def _patch_form_data(self, path: str, data: dict = None, files = None) -> dict:
98
117
  url = f"{self.base_url}{path}"
99
- response = requests.patch(url, headers=self._get_headers(), data=data, files=files, verify=self.verify)
118
+ with ssl_handler():
119
+ response = requests.patch(url, headers=self._get_headers(), data=data, files=files, verify=self.verify)
100
120
  self._check_response(response)
101
121
  return response.json() if response.text else {}
102
122
 
103
123
  def _delete(self, path: str, data=None) -> dict:
104
124
  url = f"{self.base_url}{path}"
105
- response = requests.delete(url, headers=self._get_headers(), json=data, verify=self.verify)
125
+ with ssl_handler():
126
+ response = requests.delete(url, headers=self._get_headers(), json=data, verify=self.verify)
106
127
  self._check_response(response)
107
128
  return response.json() if response.text else {}
108
129
 
@@ -177,3 +177,17 @@ class ConnectionsClient(BaseAPIClient):
177
177
  logger.warning(f"Connections not found. Returning connection ID: {conn_id}")
178
178
  return conn_id
179
179
  raise e
180
+
181
+ def get_drafts_by_ids(self, conn_ids) -> List[ListConfigsResponse]:
182
+ try:
183
+ res = self._get(f"/connections/applications?connectionIds={','.join(conn_ids)}")
184
+ import json
185
+ json.dumps(res)
186
+ return [ListConfigsResponse.model_validate(conn) for conn in res.get("applications", [])]
187
+ except ValidationError as e:
188
+ logger.error("Recieved unexpected response from server")
189
+ raise e
190
+ except ClientAPIException as e:
191
+ if e.response.status_code == 404:
192
+ return []
193
+ raise e
@@ -10,7 +10,7 @@ from ibm_cloud_sdk_core.authenticators import MCSPV2Authenticator
10
10
  from ibm_cloud_sdk_core.authenticators import IAMAuthenticator
11
11
  from ibm_cloud_sdk_core.authenticators import CloudPakForDataAuthenticator
12
12
 
13
- from ibm_watsonx_orchestrate.client.utils import check_token_validity, is_cpd_env
13
+ from ibm_watsonx_orchestrate.client.utils import check_token_validity, is_cpd_env, is_ibm_cloud_platform
14
14
  from ibm_watsonx_orchestrate.client.base_service_instance import BaseServiceInstance
15
15
  from ibm_watsonx_orchestrate.cli.commands.environment.types import EnvironmentAuthType
16
16
 
@@ -21,14 +21,6 @@ from ibm_watsonx_orchestrate.client.client_errors import (
21
21
  import logging
22
22
  logger = logging.getLogger(__name__)
23
23
 
24
- from ibm_watsonx_orchestrate.cli.config import (
25
- Config,
26
- CONTEXT_SECTION_HEADER,
27
- CONTEXT_ACTIVE_ENV_OPT,
28
- ENVIRONMENTS_SECTION_HEADER,
29
- ENV_WXO_URL_OPT
30
- )
31
-
32
24
  class ServiceInstance(BaseServiceInstance):
33
25
  """Connect, get details, and check usage of a Watson Machine Learning service instance."""
34
26
 
@@ -50,29 +42,28 @@ class ServiceInstance(BaseServiceInstance):
50
42
  return self._client.token
51
43
 
52
44
  def _create_token(self) -> str:
53
- if not self._credentials.auth_type:
54
- if ".cloud.ibm.com" in self._credentials.url:
55
- logger.warning("Using IBM IAM Auth Type. If this is incorrect please use the '--type' flag to explicitly choose one of 'mcsp', 'mcsp_v1', 'mcsp_v2' or 'cpd' ")
56
- return self._authenticate(EnvironmentAuthType.IBM_CLOUD_IAM)
57
- elif is_cpd_env(self._credentials.url):
58
- logger.warning("Using CPD Auth Type. If this is incorrect please use the '--type' flag to explicitly choose one of 'ibm_iam', 'mcsp', 'mcsp_v1' or 'mcsp_v2' ")
59
- return self._authenticate(EnvironmentAuthType.CPD)
60
- else:
61
- logger.warning("Using MCSP Auth Type. If this is incorrect please use the '--type' flag to explicitly choose one of 'ibm_iam', 'mcsp_v1', 'mcsp_v2' or 'cpd' ")
62
- try:
63
- return self._authenticate(EnvironmentAuthType.MCSP_V1)
64
- except:
65
- return self._authenticate(EnvironmentAuthType.MCSP_V2)
66
- auth_type = self._credentials.auth_type.lower()
45
+ inferred_auth_type = None
46
+ if is_ibm_cloud_platform(self._credentials.url):
47
+ inferred_auth_type = EnvironmentAuthType.IBM_CLOUD_IAM
48
+ elif is_cpd_env(self._credentials.url):
49
+ inferred_auth_type = EnvironmentAuthType.CPD
50
+ else:
51
+ inferred_auth_type = EnvironmentAuthType.MCSP
52
+
53
+ if self._credentials.auth_type:
54
+ if self._credentials.auth_type != inferred_auth_type:
55
+ logger.warning(f"Overriding the default authentication type '{inferred_auth_type}' for url '{self._credentials.url}' with '{self._credentials.auth_type.lower()}'")
56
+ auth_type = self._credentials.auth_type.lower()
57
+ else:
58
+ inferred_type_options = [t for t in EnvironmentAuthType if t != inferred_auth_type]
59
+ logger.warning(f"Using '{inferred_auth_type}' Auth Type. If this is incorrect please use the '--type' flag to explicitly choose one of {', '.join(inferred_type_options[:-1])} or {inferred_type_options[-1]}")
60
+ auth_type = inferred_auth_type
61
+
67
62
  if auth_type == "mcsp":
68
63
  try:
69
64
  return self._authenticate(EnvironmentAuthType.MCSP_V1)
70
65
  except:
71
66
  return self._authenticate(EnvironmentAuthType.MCSP_V2)
72
- elif auth_type == "mcsp_v1":
73
- return self._authenticate(EnvironmentAuthType.MCSP_V1)
74
- elif auth_type == "mcsp_v2":
75
- return self._authenticate(EnvironmentAuthType.MCSP_V2)
76
67
  else:
77
68
  return self._authenticate(auth_type)
78
69
 
@@ -100,13 +91,7 @@ class ServiceInstance(BaseServiceInstance):
100
91
  if self._credentials.iam_url is not None:
101
92
  url = self._credentials.iam_url
102
93
  else:
103
- cfg = Config()
104
- env_cfg = cfg.get(ENVIRONMENTS_SECTION_HEADER)
105
- matching_wxo_url = next(
106
- (env_config['wxo_url'] for env_config in env_cfg.values() if 'bypass_ssl' in env_config and 'verify' in env_config),
107
- None
108
- )
109
- base_url = matching_wxo_url.split("/orchestrate")[0]
94
+ base_url = self._credentials.url.split("/orchestrate")[0]
110
95
  url = f"{base_url}/icp4d-api"
111
96
 
112
97
  password = self._credentials.password if self._credentials.password is not None else None
@@ -66,7 +66,7 @@ def is_ibm_cloud_platform(url:str | None = None) -> bool:
66
66
  if url is None:
67
67
  url = get_current_env_url()
68
68
 
69
- if url.__contains__("cloud.ibm.com"):
69
+ if ".cloud.ibm.com" in url:
70
70
  return True
71
71
  return False
72
72
 
@@ -161,6 +161,8 @@ def instantiate_client(client: type[T] , url: str | None=None) -> T:
161
161
  client_instance = client(base_url=url, api_key=token, is_local=is_local_dev(url), verify=False)
162
162
  elif verify is not None:
163
163
  client_instance = client(base_url=url, api_key=token, is_local=is_local_dev(url), verify=verify)
164
+ else:
165
+ client_instance = client(base_url=url, api_key=token, is_local=is_local_dev(url))
164
166
  else:
165
167
  client_instance = client(base_url=url, api_key=token, is_local=is_local_dev(url))
166
168
 
@@ -62,6 +62,7 @@ services:
62
62
  WATSONX_API_KEY: ${WATSONX_APIKEY}
63
63
  WATSONX_URL: ${WATSONX_URL}
64
64
  WATSONX_SPACE_ID: ${WATSONX_SPACE_ID}
65
+ NODE_TLS_REJECT_UNAUTHORIZED: ${AI_GATEWAY_TLS_REJECT_UNAUTHORIZED}
65
66
 
66
67
  wxo-agent-gateway:
67
68
  image: ${AGENT_GATEWAY_REGISTRY:-us.icr.io/watson-orchestrate-private}/wxo-agent-gateway:${AGENT_GATEWAY_TAG:-latest}
@@ -132,7 +133,7 @@ services:
132
133
  DOCPROC_ENABLED: ${DOCPROC_ENABLED:-false}
133
134
  IS_OBSERVABILITY_FEATURE_ENABLED: "true"
134
135
  ALLOW_INSECURE_TLS: "true"
135
- ENABLE_EDIT_PROMPTS: "true"
136
+ ENABLE_EDIT_PROMPTS: "false"
136
137
  LANGFLOW_ENABLED: ${LANGFLOW_ENABLED:-false}
137
138
  command: 'npm start'
138
139
  ports:
@@ -327,6 +328,7 @@ services:
327
328
  ENABLE_WEBHOOKS: false
328
329
  DISABLE_JSON_LOG_CELERY: true
329
330
  WXO_DEPLOYMENT_PLATFORM: saas
331
+ CPD_VERIFY: ${CPD_VERIFY}
330
332
  CONNECTION_MANAGER_URL: http://wxo-server-connection-manager:3001
331
333
  CHANNEL_SESSION_REDIS_URL: redis://wxo-server-redis:6379/5
332
334
  WXO_MILVUS_URI: http://wxo-milvus-standalone:19530
@@ -376,6 +378,7 @@ services:
376
378
  IS_WXO_LITE: "TRUE"
377
379
  TRM_BASE_URL: http://tools-runtime-manager:8080
378
380
  AGENT_STEP_DETAILS: redis://wxo-server-redis:6379/0
381
+ RECURSION_LIMIT: ${RECURSION_LIMIT}
379
382
  AGENT_GATEWAY_URI: http://wxo-agent-gateway:8989
380
383
  JWT_SECRET: ${JWT_SECRET}
381
384
  POSTGRES_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@wxo-server-db:5432/postgres
@@ -446,6 +449,7 @@ services:
446
449
  IBM_TELEMETRY_TRACER_ENDPOINT: http://jaeger:4318/v1/traces
447
450
  USE_IBM_TELEMETRY: ${USE_IBM_TELEMETRY:-false}
448
451
  WXO_DEPLOYMENT_PLATFORM: saas
452
+ CPD_VERIFY: ${CPD_VERIFY}
449
453
  CALLBACK_HOST_URL: ${CALLBACK_HOST_URL:-http://wxo-server:4321}
450
454
  LANGFLOW_ENABLED: ${LANGFLOW_ENABLED:-false}
451
455
 
@@ -653,6 +657,7 @@ services:
653
657
  - WATSONX_URL=${WATSONX_URL}
654
658
  - PROXY_SERVER_URL=http://jaeger-proxy:9201
655
659
  - WXO_DEPLOYMENT_PLATFORM=saas
660
+ - CPD_VERIFY=${CPD_VERIFY}
656
661
  - TENANT_API_KEY=${AGENTOPS_API_KEY}
657
662
  - TENANT_CONFIG_URL=http://wxo-server:4321
658
663
  - TENANT_DEFAULT_USERNAME=${ES_USERNAME}
@@ -36,7 +36,7 @@ DB_ENCRYPTION_KEY=dummy_db_encryption_key
36
36
  BASE_URL=dummy_base_url
37
37
  SERVER_TYPE=CELERY
38
38
  SQLALCHEMY_DEBUG=false
39
-
39
+ RECURSION_LIMIT=50
40
40
  LANGFUSE_HOST=http://host.docker.internal:3010
41
41
  LANGFUSE_EMAIL=orchestrate@ibm.com
42
42
  LANGFUSE_USERNAME=orchestrate
@@ -58,10 +58,10 @@ REGISTRY_URL=
58
58
 
59
59
 
60
60
 
61
- SERVER_TAG=29-08-2025-59ef405
61
+ SERVER_TAG=05-09-2025-Hb2zH5Ggf
62
62
  SERVER_REGISTRY=
63
63
 
64
- WORKER_TAG=29-08-2025-cf32b2d
64
+ WORKER_TAG=05-09-2025-Hb2zH5Ggf
65
65
  WORKER_REGISTRY=
66
66
 
67
67
  AI_GATEWAY_TAG=20-08-2025-9ed6d40
@@ -73,9 +73,9 @@ AGENT_GATEWAY_REGISTRY=
73
73
  DB_REGISTRY=
74
74
  # If you build multiarch set all three of these to the same, we have a pr against main
75
75
  # to not have this separation, but we can merge it later
76
- DBTAG=29-07-2025-9f3661b
77
- AMDDBTAG=29-07-2025-9f3661b
78
- ARM64DBTAG=29-07-2025-9f3661b
76
+ DBTAG=04-09-2025-48c39
77
+ AMDDBTAG=04-09-2025-48c39
78
+ ARM64DBTAG=04-09-2025-48c39
79
79
 
80
80
  UI_REGISTRY=
81
81
  UITAG=29-08-2025
@@ -92,7 +92,7 @@ TR_REGISTRY=
92
92
  BUILDER_TAG=27-08-2025-7432aca
93
93
  BUILDER_REGISTRY=
94
94
 
95
- FLOW_RUNTIME_TAG=18-08-2025-v3
95
+ FLOW_RUNTIME_TAG=26-08-2025-3b7f19e
96
96
  FLOW_RUMTIME_REGISTRY=
97
97
 
98
98
 
@@ -105,7 +105,7 @@ JAEGER_PROXY_REGISTRY=
105
105
  SOCKET_HANDLER_TAG=29-05-2025
106
106
  SOCKET_HANDLER_REGISTRY=
107
107
 
108
- CPE_TAG=18-08-2025-ae1308e
108
+ CPE_TAG=29-08-2025-e612bea
109
109
  CPE_REGISTRY=
110
110
 
111
111
  VOICE_CONTROLLER_TAG=12-08-2025
@@ -118,14 +118,16 @@ LANGFLOW_IMAGE=
118
118
  WDU_TAG=2.6.1
119
119
  WDU_REGISTRY=
120
120
 
121
- DOCPROC_DPS_TAG=20250815-010747-277-173db2a
122
- DOCPROC_LLMSERVICE_TAG=20250820-153924-128-55cf4d5
121
+ DOCPROC_DPS_TAG=20250826-201302-283-24eb106
122
+ DOCPROC_LLMSERVICE_TAG=20250826-main-133-42a5320
123
123
  DOCPROC_CACHE_TAG=20250814-master-82-cf33f87
124
- DOCPROC_DPI_TAG=20250815-004755-273-e65f26b4
124
+ DOCPROC_DPI_TAG=20250828-154151-285-e4dd1cff
125
125
  DOCPROC_REGISTRY=
126
126
 
127
127
  # END -- IMAGE REGISTRIES AND TAGS
128
128
 
129
+ CPD_VERIFY=true
130
+ AI_GATEWAY_TLS_REJECT_UNAUTHORIZED=1
129
131
  TAVILY_API_KEY=dummy_tavily_api_key
130
132
  PREFERRED_MODELS=meta-llama/llama-3-2-90b-vision-instruct,meta-llama/llama-3-405b-instruct
131
133
  INCOMPATIBLE_MODELS=flan,embedding,cross-encoder,tinytimemixers
@@ -12,7 +12,10 @@ class DataMap(BaseModel):
12
12
  def to_json(self) -> dict[str, Any]:
13
13
  model_spec = {}
14
14
  if self.maps and len(self.maps) > 0:
15
- model_spec["maps"] = [assignment.model_dump() for assignment in self.maps]
15
+ model_spec["maps"] = []
16
+ for assignment in self.maps:
17
+ model_spec["maps"].append(assignment.model_dump())
18
+ return model_spec
16
19
 
17
20
  def add(self, line: Assignment) -> Self:
18
21
  self.maps.append(line)
@@ -25,7 +25,7 @@ from ibm_watsonx_orchestrate.client.tools.tool_client import ToolClient
25
25
  from ibm_watsonx_orchestrate.client.tools.tempus_client import TempusClient
26
26
  from ibm_watsonx_orchestrate.client.utils import instantiate_client
27
27
  from ..types import (
28
- DocProcKVPSchema, EndNodeSpec, Expression, ForeachPolicy, ForeachSpec, LoopSpec, BranchNodeSpec, MatchPolicy, PlainTextReadingOrder, PromptLLMParameters, PromptNodeSpec, TimerNodeSpec,
28
+ DocProcKVPSchema, Assignment, Conditions, EndNodeSpec, Expression, ForeachPolicy, ForeachSpec, LoopSpec, BranchNodeSpec, MatchPolicy, NodeIdCondition, PlainTextReadingOrder, PromptExample, PromptLLMParameters, PromptNodeSpec, TimerNodeSpec,
29
29
  StartNodeSpec, ToolSpec, JsonSchemaObject, ToolRequestBody, ToolResponseBody, UserFieldKind, UserFieldOption, UserFlowSpec, UserNodeSpec, WaitPolicy,
30
30
  DocProcSpec, TextExtractionResponse, DocProcInput, DecisionsNodeSpec, DecisionsRule, DocExtSpec, File, DocumentClassificationResponse, DocClassifierSpec, DocumentProcessingCommonInput
31
31
  )
@@ -64,7 +64,7 @@ class FlowEdge(BaseModel):
64
64
 
65
65
  class Flow(Node):
66
66
  '''Flow represents a flow that will be run by wxO Flow engine.'''
67
- output_map: DataMap | None = None
67
+ output_map: dict[str, DataMap] | None = None
68
68
  nodes: dict[str, SerializeAsAny[Node]] = {}
69
69
  edges: List[FlowEdge] = []
70
70
  schemas: dict[str, JsonSchemaObject] = {}
@@ -401,6 +401,7 @@ class Flow(Node):
401
401
  display_name: str|None=None,
402
402
  system_prompt: str | list[str] | None = None,
403
403
  user_prompt: str | list[str] | None = None,
404
+ prompt_examples: list[PromptExample] | None = None,
404
405
  llm: str | None = None,
405
406
  llm_parameters: PromptLLMParameters | None = None,
406
407
  description: str | None = None,
@@ -422,6 +423,7 @@ class Flow(Node):
422
423
  description=description,
423
424
  system_prompt=system_prompt,
424
425
  user_prompt=user_prompt,
426
+ prompt_examples=prompt_examples,
425
427
  llm=llm,
426
428
  llm_parameters=llm_parameters,
427
429
  input_schema=_get_tool_request_body(input_schema_obj),
@@ -438,7 +440,7 @@ class Flow(Node):
438
440
  node = self._add_node(node)
439
441
  return cast(PromptNode, node)
440
442
 
441
- def docclassfier(self,
443
+ def docclassifier(self,
442
444
  name: str,
443
445
  llm : str = "watsonx/meta-llama/llama-3-2-90b-vision-instruct",
444
446
  version: str = "TIP",
@@ -605,6 +607,7 @@ class Flow(Node):
605
607
  display_name: str|None=None,
606
608
  description: str | None = None,
607
609
  input_map: DataMap = None,
610
+ document_structure: bool = False,
608
611
  kvp_schemas: list[DocProcKVPSchema] = None,
609
612
  enable_hw: bool = False) -> DocProcNode:
610
613
 
@@ -629,6 +632,7 @@ class Flow(Node):
629
632
  output_schema=_get_tool_response_body(output_schema_obj),
630
633
  output_schema_object = output_schema_obj,
631
634
  task=task,
635
+ document_structure=document_structure,
632
636
  plain_text_reading_order=plain_text_reading_order,
633
637
  enable_hw=enable_hw,
634
638
  kvp_schemas=kvp_schemas
@@ -721,7 +725,7 @@ class Flow(Node):
721
725
  '''Create a single node flow with an automatic START and END node.'''
722
726
  return self.sequence(START, node, END)
723
727
 
724
- def branch(self, evaluator: Union[Callable, Expression]) -> "Branch":
728
+ def branch(self, evaluator: Union[Callable, Expression, Conditions]) -> 'Branch':
725
729
  '''Create a BRANCH node'''
726
730
  e = evaluator
727
731
  if isinstance(evaluator, Callable):
@@ -735,11 +739,19 @@ class Flow(Node):
735
739
  # e = new_script_spec
736
740
  elif isinstance(evaluator, str):
737
741
  e = Expression(expression=evaluator)
742
+ elif isinstance(evaluator, list):
743
+ e = Conditions(conditions=evaluator)
738
744
 
739
745
  spec = BranchNodeSpec(name = "branch_" + str(self._next_sequence_id()), evaluator=e)
740
746
  branch_node = Branch(spec = spec, containing_flow=self)
741
747
  return cast(Branch, self._add_node(branch_node))
742
748
 
749
+ def conditions(self) -> 'Branch':
750
+ '''Create a Branch node with empty Conditions evaluator (if-else)'''
751
+ spec = BranchNodeSpec(name = "branch_" + str(self._next_sequence_id()), evaluator=Conditions(conditions=[]))
752
+ branch_conditions_node = Branch(spec = spec, containing_flow=self)
753
+ return cast(Branch, self._add_node(branch_conditions_node))
754
+
743
755
  def wait_for(self, *args) -> "Wait":
744
756
  '''Wait for all incoming nodes to complete.'''
745
757
  raise ValueError("Not implemented yet.")
@@ -754,6 +766,77 @@ class Flow(Node):
754
766
 
755
767
  # return cast(Wait, self.node(wait_node))
756
768
 
769
+ def map_flow_output_with_variable(self, target_output_variable: str, variable: str, default_value: str = None) -> Self:
770
+ if self.output_map and "spec" in self.output_map:
771
+ maps = self.output_map["spec"].maps or []
772
+ else:
773
+ maps = []
774
+
775
+ curr_map_metadata = {
776
+ "assignmentType": "variable"
777
+ }
778
+
779
+ target_variable = "flow.output." + target_output_variable
780
+ value_expression = "flow." + variable
781
+
782
+ if default_value:
783
+ maps.append(Assignment(target_variable=target_variable, value_expression=value_expression, default_value=default_value, metadata=curr_map_metadata))
784
+ else:
785
+ maps.append(Assignment(target_variable=target_variable, value_expression=value_expression, metadata=curr_map_metadata))
786
+
787
+ flow_output_map_spec = DataMap(maps=maps)
788
+
789
+ if self.output_map and "spec" in self.output_map:
790
+ self.output_map["spec"] = flow_output_map_spec
791
+ else:
792
+ self.output_map = {"spec": flow_output_map_spec}
793
+ return self
794
+
795
+ def map_output(self, output_variable: str, expression: str, default_value: str = None) -> Self:
796
+ if self.output_map and "spec" in self.output_map:
797
+ maps = self.output_map["spec"].maps or []
798
+ else:
799
+ maps = []
800
+
801
+ curr_map_metadata = {
802
+ "assignmentType": "pyExpression"
803
+ }
804
+
805
+ target_variable = "flow.output." + output_variable
806
+ value_expression = expression
807
+
808
+ if default_value:
809
+ maps.append(Assignment(target_variable=target_variable, value_expression=value_expression, default_value=default_value, metadata=curr_map_metadata))
810
+ else:
811
+ maps.append(Assignment(target_variable=target_variable, value_expression=value_expression, metadata=curr_map_metadata))
812
+
813
+ flow_output_map_spec = DataMap(maps=maps)
814
+
815
+ if self.output_map and "spec" in self.output_map:
816
+ self.output_map["spec"] = flow_output_map_spec
817
+ else:
818
+ self.output_map = {"spec": flow_output_map_spec}
819
+ return self
820
+
821
+ def map_flow_output_with_none(self, target_output_variable: str) -> Self:
822
+ if self.output_map and "spec" in self.output_map:
823
+ maps = self.output_map["spec"].maps or []
824
+ else:
825
+ maps = []
826
+
827
+
828
+ target_variable = "flow.output." + target_output_variable
829
+
830
+ maps.append(Assignment(target_variable=target_variable, value_expression=None))
831
+
832
+ flow_output_map_spec = DataMap(maps=maps)
833
+
834
+ if self.output_map and "spec" in self.output_map:
835
+ self.output_map["spec"] = flow_output_map_spec
836
+ else:
837
+ self.output_map = {"spec": flow_output_map_spec}
838
+ return self
839
+
757
840
 
758
841
  def foreach(self, item_schema: type[BaseModel],
759
842
  input_schema: type[BaseModel] |None=None,
@@ -814,8 +897,6 @@ class Flow(Node):
814
897
  input_schema: type[BaseModel] |None=None,
815
898
  output_schema: type[BaseModel] |None=None) -> "UserFlow": # return a UserFlow object
816
899
 
817
- raise ValueError("userflow is NOT supported yet and it's interface will change.")
818
-
819
900
  output_schema_obj = _get_json_schema_obj("output", output_schema)
820
901
  input_schema_obj = _get_json_schema_obj("input", input_schema)
821
902
 
@@ -917,6 +998,11 @@ class Flow(Node):
917
998
  for key, value in self.metadata.items():
918
999
  metadata_dict[key] = value
919
1000
  flow_dict["metadata"] = metadata_dict
1001
+
1002
+ if self.output_map and "spec" in self.output_map:
1003
+ flow_dict["output_map"] = {
1004
+ "spec": self.output_map["spec"].to_json()
1005
+ }
920
1006
  return flow_dict
921
1007
 
922
1008
  def _get_node_id(self, node: Union[str, Node]) -> str:
@@ -1226,6 +1312,27 @@ class Branch(FlowControl):
1226
1312
  raise ValueError("Cannot have custom label __default__. Use default() instead.")
1227
1313
 
1228
1314
  return self._add_case(label, node)
1315
+
1316
+ def condition(self, to_node: Node, expression: str="", default: bool=False) -> Self:
1317
+ '''
1318
+ Add a condition to this branch node.
1319
+
1320
+ Parameters:
1321
+ expression (str): The expression of this condition.
1322
+ to_node (Node): The node to go to when expression is evaluated to true.
1323
+ default (bool): The condition is the default (else) case.
1324
+ '''
1325
+
1326
+ node_id = self.containing_flow._get_node_id(to_node)
1327
+ if default:
1328
+ condition = NodeIdCondition(node_id=node_id, default=default)
1329
+ else:
1330
+ condition = NodeIdCondition(expression=expression, node_id=node_id, default=default)
1331
+
1332
+ self.spec.evaluator.conditions.append(condition)
1333
+ self.containing_flow.edge(self, to_node)
1334
+
1335
+ return self
1229
1336
 
1230
1337
  def default(self, node: Node) -> Self:
1231
1338
  '''
@@ -1445,13 +1552,14 @@ class UserFlow(Flow):
1445
1552
  kind: UserFieldKind = UserFieldKind.Text,
1446
1553
  display_name: str | None = None,
1447
1554
  description: str | None = None,
1555
+ direction: str | None = None,
1448
1556
  default: Any | None = None,
1449
1557
  text: str = None, # The text used to ask question to the user, e.g. 'what is your name?'
1450
1558
  option: UserFieldOption | None = None,
1451
1559
  is_list: bool = False,
1452
1560
  min: Any | None = None,
1453
1561
  max: Any | None = None,
1454
- input_map: DataMap = None,
1562
+ input_map: DataMap | None= None,
1455
1563
  custom: dict[str, Any] = {}) -> UserNode:
1456
1564
  '''create a node in the flow'''
1457
1565
  # create a json schema object based on the single field
@@ -1483,6 +1591,8 @@ class UserFlow(Flow):
1483
1591
  description = description,
1484
1592
  default = default,
1485
1593
  text = text,
1594
+ direction = direction,
1595
+ input_map = input_map,
1486
1596
  option = option,
1487
1597
  is_list = is_list,
1488
1598
  min = min,