ibm-watsonx-orchestrate 1.2.0__py3-none-any.whl → 1.4.2__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 (59) hide show
  1. ibm_watsonx_orchestrate/__init__.py +1 -1
  2. ibm_watsonx_orchestrate/agent_builder/agents/types.py +6 -1
  3. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/knowledge_base.py +16 -3
  4. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/knowledge_base_requests.py +4 -20
  5. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +13 -15
  6. ibm_watsonx_orchestrate/agent_builder/toolkits/base_toolkit.py +32 -0
  7. ibm_watsonx_orchestrate/agent_builder/toolkits/types.py +42 -0
  8. ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +14 -13
  9. ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +4 -2
  10. ibm_watsonx_orchestrate/agent_builder/tools/types.py +2 -1
  11. ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +29 -0
  12. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +273 -12
  13. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_command.py +2 -2
  14. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +79 -39
  15. ibm_watsonx_orchestrate/cli/commands/models/env_file_model_provider_mapper.py +180 -0
  16. ibm_watsonx_orchestrate/cli/commands/models/models_command.py +194 -8
  17. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +117 -48
  18. ibm_watsonx_orchestrate/cli/commands/server/types.py +105 -0
  19. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +55 -7
  20. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +123 -42
  21. ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +22 -1
  22. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +209 -25
  23. ibm_watsonx_orchestrate/cli/init_helper.py +43 -0
  24. ibm_watsonx_orchestrate/cli/main.py +3 -1
  25. ibm_watsonx_orchestrate/client/agents/agent_client.py +4 -1
  26. ibm_watsonx_orchestrate/client/agents/assistant_agent_client.py +5 -1
  27. ibm_watsonx_orchestrate/client/agents/external_agent_client.py +5 -1
  28. ibm_watsonx_orchestrate/client/analytics/llm/analytics_llm_client.py +2 -6
  29. ibm_watsonx_orchestrate/client/base_api_client.py +5 -2
  30. ibm_watsonx_orchestrate/client/connections/connections_client.py +15 -21
  31. ibm_watsonx_orchestrate/client/model_policies/__init__.py +0 -0
  32. ibm_watsonx_orchestrate/client/model_policies/model_policies_client.py +47 -0
  33. ibm_watsonx_orchestrate/client/model_policies/types.py +36 -0
  34. ibm_watsonx_orchestrate/client/models/__init__.py +0 -0
  35. ibm_watsonx_orchestrate/client/models/models_client.py +46 -0
  36. ibm_watsonx_orchestrate/client/models/types.py +177 -0
  37. ibm_watsonx_orchestrate/client/toolkit/toolkit_client.py +15 -6
  38. ibm_watsonx_orchestrate/client/tools/tempus_client.py +40 -0
  39. ibm_watsonx_orchestrate/client/tools/tool_client.py +8 -0
  40. ibm_watsonx_orchestrate/docker/compose-lite.yml +68 -13
  41. ibm_watsonx_orchestrate/docker/default.env +22 -12
  42. ibm_watsonx_orchestrate/docker/tempus/common-config.yaml +1 -1
  43. ibm_watsonx_orchestrate/experimental/flow_builder/__init__.py +0 -0
  44. ibm_watsonx_orchestrate/experimental/flow_builder/flows/__init__.py +41 -0
  45. ibm_watsonx_orchestrate/experimental/flow_builder/flows/constants.py +17 -0
  46. ibm_watsonx_orchestrate/experimental/flow_builder/flows/data_map.py +91 -0
  47. ibm_watsonx_orchestrate/experimental/flow_builder/flows/decorators.py +143 -0
  48. ibm_watsonx_orchestrate/experimental/flow_builder/flows/events.py +72 -0
  49. ibm_watsonx_orchestrate/experimental/flow_builder/flows/flow.py +1288 -0
  50. ibm_watsonx_orchestrate/experimental/flow_builder/node.py +97 -0
  51. ibm_watsonx_orchestrate/experimental/flow_builder/resources/flow_status.openapi.yml +98 -0
  52. ibm_watsonx_orchestrate/experimental/flow_builder/types.py +492 -0
  53. ibm_watsonx_orchestrate/experimental/flow_builder/utils.py +113 -0
  54. ibm_watsonx_orchestrate/utils/utils.py +5 -2
  55. {ibm_watsonx_orchestrate-1.2.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/METADATA +6 -2
  56. {ibm_watsonx_orchestrate-1.2.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/RECORD +59 -36
  57. {ibm_watsonx_orchestrate-1.2.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/WHEEL +0 -0
  58. {ibm_watsonx_orchestrate-1.2.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/entry_points.txt +0 -0
  59. {ibm_watsonx_orchestrate-1.2.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/licenses/LICENSE +0 -0
@@ -21,6 +21,10 @@ class ToolClient(BaseAPIClient):
21
21
  def upload_tools_artifact(self, tool_id: str, file_path: str) -> dict:
22
22
  return self._post(f"/tools/{tool_id}/upload", files={"file": (f"{tool_id}.zip", open(file_path, "rb"), "application/zip", {"Expires": "0"})})
23
23
 
24
+ def download_tools_artifact(self, tool_id: str) -> bytes:
25
+ response = self._get(f"/tools/{tool_id}/download", return_raw=True)
26
+ return response.content
27
+
24
28
  def get_draft_by_name(self, tool_name: str) -> List[dict]:
25
29
  return self.get_drafts_by_names([tool_name])
26
30
 
@@ -39,3 +43,7 @@ class ToolClient(BaseAPIClient):
39
43
  if e.response.status_code == 404 and "not found with the given name" in e.response.text:
40
44
  return ""
41
45
  raise(e)
46
+
47
+ def get_drafts_by_ids(self, tool_ids: List[str]) -> List[dict]:
48
+ formatted_tool_ids = [f"ids={x}" for x in tool_ids]
49
+ return self._get(f"/tools?{'&'.join(formatted_tool_ids)}")
@@ -49,6 +49,17 @@ services:
49
49
  ports:
50
50
  - 3001:3001
51
51
 
52
+ ai-gateway:
53
+ image: ${AI_GATEWAY_REGISTRY:-us.icr.io/watson-orchestrate-private}/ai-gateway:${AI_GATEWAY_TAG:-latest}
54
+ platform: linux/amd64
55
+ restart: unless-stopped
56
+ ports:
57
+ - "8787:8787"
58
+ environment:
59
+ WATSONX_API_KEY: ${WATSONX_APIKEY}
60
+ WATSONX_URL: ${WATSONX_URL}
61
+ WATSONX_SPACE_ID: ${WATSONX_SPACE_ID}
62
+
52
63
  ui:
53
64
  image: ${UI_REGISTRY:-us.icr.io/watson-orchestrate-private}/wxo-chat:${UITAG:-latest}
54
65
  platform: linux/amd64
@@ -95,6 +106,7 @@ services:
95
106
  environment:
96
107
  WXO_DEPLOYMENT_TYPE: 'laptop'
97
108
  AGENT_RUNTIME_ENDPOINT: http://wxo-server:4321
109
+ TEMPUS_RUNTIME_ENDPOINT: http://wxo-tempus-runtime:9044
98
110
  command: 'npm start'
99
111
  ports:
100
112
  - '4025:4025'
@@ -210,14 +222,21 @@ services:
210
222
  volumes:
211
223
  - ./sdk:/packages
212
224
  - tools:/tools
225
+
213
226
  command: >
214
- bash -c "mkdir -p /tmp/certs &&
215
- openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /tmp/certs/key.pem -out /tmp/certs/cert.pem -subj '/CN=localhost' &&
216
- uvicorn --host 0.0.0.0 --port 4322 --ssl-keyfile /tmp/certs/key.pem --ssl-certfile /tmp/certs/cert.pem --log-config /app/config/logs/log_conf.yaml --workers 2 --log-level debug wo_archer.api.main:app &
217
- (for i in {1..20}; do curl --silent --fail -k https://127.0.0.1:4322/health/alive && echo '[INFO] HTTPS Service ready' && break || echo '[INFO] Waiting for HTTPS service...' && sleep 30; done; curl --silent --fail -k https://127.0.0.1:4322/health/alive || (echo '[ERROR] HTTPS service failed to start' && exit 1) ) &&
218
- uvicorn --host 0.0.0.0 --port 4321 --log-config /app/config/logs/log_conf.yaml --log-level debug --workers 5 wo_archer.api.main:app &
219
- (for i in {1..20}; do curl --silent --fail -k http://127.0.0.1:4321/health/alive && echo '[INFO] HTTP Service ready' && break || echo '[INFO] Waiting for HTTP service...' && sleep 30; done; curl --silent --fail -k http://127.0.0.1:4321/health/alive || (echo '[ERROR] HTTP service failed to start' && exit 1) ) &&
220
- wait"
227
+ bash -c '
228
+ export PYTHONPATH="/tmp/monkey:$PYTHONPATH"
229
+ mkdir -p /tmp/monkey &&
230
+
231
+ echo -e "from uvicorn.supervisors.multiprocess import Process\n_original_is_alive = Process.is_alive\ndef patched_is_alive(self, timeout: float = 30):\n return _original_is_alive(self, timeout=30)\nProcess.is_alive = patched_is_alive" > /tmp/monkey/sitecustomize.py &&
232
+
233
+ uvicorn --host 0.0.0.0 --port 4321 --log-config /app/config/logs/log_conf.yaml --log-level debug --workers 5 wo_archer.api.main:app &
234
+
235
+ for i in {1..40}; do curl --silent --fail http://127.0.0.1:4321/health/alive && echo "[INFO] HTTP Service ready" && break || echo "[INFO] Waiting for HTTP service..." && sleep 10; done;
236
+ curl --silent --fail http://127.0.0.1:4321/health/alive || (echo "[ERROR] HTTP service failed to start" && exit 1);
237
+
238
+ wait
239
+ '
221
240
  depends_on:
222
241
  wxo-server-redis:
223
242
  condition: service_started
@@ -294,6 +313,15 @@ services:
294
313
  MILVUS_DB_NAME: default
295
314
  MILVUS_USERNAME: root
296
315
  MILVUS_PASSWORD: Milvus
316
+ WO_API_KEY: ${WO_API_KEY}
317
+ WO_USERNAME: ${WO_USERNAME}
318
+ WO_PASSWORD: ${WO_PASSWORD}
319
+ WO_INSTANCE: ${WO_INSTANCE}
320
+ USE_SAAS_ML_TOOLS_RUNTIME: ${USE_SAAS_ML_TOOLS_RUNTIME}
321
+ AUTHORIZATION_URL: ${AUTHORIZATION_URL}
322
+ WO_AUTH_TYPE: ${WO_AUTH_TYPE}
323
+ AI_GATEWAY_BASE_URL: ${AI_GATEWAY_BASE_URL}
324
+ AI_GATEWAY_ENABLED : ${AI_GATEWAY_ENABLED}
297
325
 
298
326
  wxo-server-worker:
299
327
  image: ${WORKER_REGISTRY:-us.icr.io/watson-orchestrate-private}/wxo-server-conversation_controller:${WORKER_TAG:-latest}
@@ -311,6 +339,8 @@ services:
311
339
  - ./sdk:/packages
312
340
  - tools:/tools
313
341
  environment:
342
+ AI_GATEWAY_ENABLED : ${AI_GATEWAY_ENABLED}
343
+ AI_GATEWAY_BASE_URL: ${AI_GATEWAY_BASE_URL}
314
344
  IS_WXO_LITE: 'TRUE'
315
345
  TRM_BASE_URL: http://tools-runtime-manager:8080
316
346
  AGENT_STEP_DETAILS: redis://wxo-server-redis:6379/0
@@ -372,6 +402,13 @@ services:
372
402
  MILVUS_DB_NAME: default
373
403
  MILVUS_USERNAME: root
374
404
  MILVUS_PASSWORD: Milvus
405
+ WO_API_KEY: ${WO_API_KEY}
406
+ WO_USERNAME: ${WO_USERNAME}
407
+ WO_PASSWORD: ${WO_PASSWORD}
408
+ WO_INSTANCE: ${WO_INSTANCE}
409
+ USE_SAAS_ML_TOOLS_RUNTIME: ${USE_SAAS_ML_TOOLS_RUNTIME}
410
+ AUTHORIZATION_URL: ${AUTHORIZATION_URL}
411
+ WO_AUTH_TYPE: ${WO_AUTH_TYPE}
375
412
 
376
413
  tools-runtime-manager:
377
414
  image: ${TRM_REGISTRY:-us.icr.io/watson-orchestrate-private}/tools-runtime-manager:${TRM_TAG:-latest}
@@ -407,6 +444,12 @@ services:
407
444
  VOLUME_MOUNT_PATH: "/shared-data"
408
445
  TOOLS_RUNTIME_MANAGER_BASE_URL: ${TOOLS_RUNTIME_MANAGER_BASE_URL}
409
446
  CONNECTION_SERVICE_BASE_URL: http://wxo-server-connection-manager:3001
447
+ STORAGE_S3_BUCKET: ${STORAGE_S3_BUCKET:-wxo-server-storage-bucket}
448
+ STORAGE_S3_ENDPOINT: http://wxo-server-minio:9000
449
+ STORAGE_S3_FORCE_PATH_STYLE: 'true'
450
+ STORAGE_S3_REGION: us-east-1
451
+ AWS_ACCESS_KEY_ID: ${MINIO_ROOT_USER:-minioadmin}
452
+ AWS_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD:-watsonxorchestrate}
410
453
  extra_hosts:
411
454
  - "host.docker.internal:host-gateway"
412
455
 
@@ -432,6 +475,12 @@ services:
432
475
  DB_PASSWORD: ${DB_PASSWORD:-postgres}
433
476
  DB_NAME: ${DB_NAME:-postgres}
434
477
  DB_SSLMODE: ${DB_SSLMODE:-disable} # Disable SSL if not configured
478
+ STORAGE_S3_BUCKET: ${STORAGE_S3_BUCKET:-wxo-server-storage-bucket}
479
+ STORAGE_S3_ENDPOINT: http://wxo-server-minio:9000
480
+ STORAGE_S3_FORCE_PATH_STYLE: 'true'
481
+ STORAGE_S3_REGION: us-east-1
482
+ AWS_ACCESS_KEY_ID: ${MINIO_ROOT_USER:-minioadmin}
483
+ AWS_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD:-watsonxorchestrate}
435
484
 
436
485
  ########################
437
486
  # LANGFUSE dependencies
@@ -531,11 +580,8 @@ services:
531
580
  wxo-tempus-runtime:
532
581
  image: ${FLOW_RUMTIME_REGISTRY:-us.icr.io/watson-orchestrate-private}/wxo-tempus-runtime:${FLOW_RUNTIME_TAG:-latest}
533
582
  restart: unless-stopped
534
- platform: linux/amd64
535
- profiles: [with-tempus-runtime]
536
583
  environment:
537
584
  NODE_TLS_REJECT_UNAUTHORIZED: "0"
538
- COMMON_CONFIG_FILE: "/shared/common-config.yaml"
539
585
  SERVER_MODE: production
540
586
  SERVER_ENVIRONMENT: SDK
541
587
  SERVER_HTTP_PORT: "9044"
@@ -557,6 +603,17 @@ services:
557
603
  PG_DATABASE: ${POSTGRES_USER:-postgres}
558
604
  USER_MGMT_BASE_URL: "" # TODO
559
605
  WXO_SERVER_BASE_URL: http://wxo-server:4321
606
+ TRM_SERVER_BASE_URL: http://tools-runtime-manager:8080
607
+ CONNECTION_MANAGER_SERVER_BASE_URL: http://wxo-server-connection-manager:3001
608
+ REDIS_ENDPOINT: wxo-server-redis:6379
609
+ REDIS_USER:
610
+ REDIS_PASSWORD:
611
+ REDIS_TLS: false
612
+ LANGFUSE_ENABLED: ${LANGFUSE_ENABLED:-false}
613
+ LANGFUSE_HOST: ${LANGFUSE_HOST:-http://langfuse-web:3000}
614
+ LANGFUSE_PUBLIC_KEY: ${LANGFUSE_PUBLIC_KEY:-pk-lf-7417757e-d6df-421b-957e-683b76acb5df}
615
+ LANGFUSE_SECRET_KEY: ${LANGFUSE_PRIVATE_KEY:-sk-lf-7bc4da63-7b2b-40c0-b5eb-1e0cf64f9af2}
616
+ LOG_LEVEL: info
560
617
  healthcheck:
561
618
  test: curl -k http://localhost:9044/readiness --fail
562
619
  interval: 5s
@@ -564,8 +621,6 @@ services:
564
621
  retries: 5
565
622
  ports:
566
623
  - 9044:9044
567
- volumes:
568
- - ./tempus/common-config.yaml:/shared/common-config.yaml
569
624
  depends_on:
570
625
  - wxo-server-db
571
626
 
@@ -595,4 +650,4 @@ volumes:
595
650
  networks:
596
651
  default:
597
652
  name: wxo-server
598
-
653
+
@@ -46,38 +46,46 @@ CELERY_RESULTS_TTL="3600"
46
46
  EVENT_BROKER_TTL="-1"
47
47
 
48
48
  # START -- IMAGE REGISTRIES AND TAGS
49
- # THE VALUES FOR REGISTRY_URL AND *_REGISTRY ARE NOT SET HERE; THEY ARE EITHER PROVIDED BY THE USER OR DETERMINED AT RUNTIME BASED ON WO_DEVELOPER_EDITION_SOURCE.
49
+ # The registry URL to pull the private images from, including the name of the repository in the registry.
50
+ # e.g. cp.icr.io/cp/wxo-lite
51
+ # If the registry URL is not set here or by the user, then it will be set automatically based on the value of WO_DEVELOPER_EDITION_SOURCE
52
+ # The *_REGISTRY variables are used to set the registry URL for each component. If not set, the URL here will be used.
53
+ # See get_default_registry_env_vars_by_dev_edition_source() in src/ibm_watsonx_orchestrate/cli/commands/server/server_command.py for more details.
50
54
  REGISTRY_URL=
51
55
 
52
- SERVER_TAG=03-05-2025
56
+ SERVER_TAG=22-05-2025
53
57
  SERVER_REGISTRY=
54
58
 
55
- WORKER_TAG=03-05-2025
59
+ WORKER_TAG=22-05-2025
56
60
  WORKER_REGISTRY=
57
61
 
62
+ AI_GATEWAY_TAG=20-05-2025
63
+ AI_GATEWAY_REGISTRY=
64
+
58
65
  DB_REGISTRY=
59
66
  # If you build multiarch set all three of these to the same, we have a pr against main
60
67
  # to not have this separation, but we can merge it later
61
- DBTAG=02-05-2025
62
- AMDDBTAG=02-05-2025
63
- ARM64DBTAG=02-05-2025
68
+ DBTAG=21-05-2025
69
+ AMDDBTAG=21-05-2025
70
+ ARM64DBTAG=21-05-2025
64
71
 
65
72
  UI_REGISTRY=
66
- UITAG=01-05-2025
73
+ UITAG=23-05-2025
67
74
 
68
75
  CM_REGISTRY=
69
- CM_TAG=30-04-2025
76
+ CM_TAG=13-05-2025
70
77
 
71
- TRM_TAG=02-05-2025
78
+ TRM_TAG=23-05-2025
72
79
  TRM_REGISTRY=
73
80
 
74
- TR_TAG=02-05-2025
81
+ TR_TAG=23-05-2025
75
82
  TR_REGISTRY=
76
83
 
84
+ BUILDER_TAG=21-05-2025
77
85
  BUILDER_REGISTRY=
78
- BUILDER_TAG=02-05-2025
79
86
 
80
- FLOW_RUNTIME_TAG=31-03-2025
87
+
88
+ FLOW_RUNTIME_TAG=13-05-2025
81
89
  FLOW_RUMTIME_REGISTRY=
82
90
 
83
91
  # END -- IMAGE REGISTRIES AND TAGS
@@ -110,6 +118,8 @@ WXO_BASE_URL=http://wxo-server:4321
110
118
  RUNTIME_MANAGER_API_KEY="testapikey"
111
119
  TOOLS_RUNTIME_MANAGER_BASE_URL="http://tools-runtime-manager:8080"
112
120
  CONNECTION_SERVICE_BASE_URL="http://wxo-server-connection-manager:3001"
121
+ AI_GATEWAY_BASE_URL="http://ai-gateway:8787/v1"
122
+ AI_GATEWAY_ENABLED=True
113
123
 
114
124
  #To Prevent warnings
115
125
  VECTOR_STORE_PROVIDER=
@@ -1 +1 @@
1
- NODE_LOG_LEVEL: trace
1
+ NODE_LOG_LEVEL: audit
@@ -0,0 +1,41 @@
1
+ from .constants import START, END, RESERVED
2
+ from ..types import FlowContext, TaskData, TaskEventType
3
+ from ..node import UserNode, AgentNode, StartNode, EndNode
4
+ from .flow import Flow, CompiledFlow, FlowRun, FlowEvent, FlowEventType, FlowFactory, MatchPolicy, WaitPolicy, ForeachPolicy, Branch, Foreach, Loop
5
+ from .decorators import user, flow_spec, flow
6
+ from .data_map import Assignment, DataMapSpec
7
+
8
+
9
+ __all__ = [
10
+ "START",
11
+ "END",
12
+ "RESERVED",
13
+
14
+ "FlowContext",
15
+ "TaskData",
16
+ "TaskEventType",
17
+
18
+ "UserNode",
19
+ "AgentNode",
20
+ "StartNode",
21
+ "EndNode",
22
+ "Assignment",
23
+ "DataMapSpec",
24
+
25
+ "Flow",
26
+ "CompiledFlow",
27
+ "FlowRun",
28
+ "FlowEvent",
29
+ "FlowEventType",
30
+ "FlowFactory",
31
+ "MatchPolicy",
32
+ "WaitPolicy",
33
+ "ForeachPolicy",
34
+ "Branch",
35
+ "Foreach",
36
+ "Loop",
37
+
38
+ "user",
39
+ "flow_spec",
40
+ "flow"
41
+ ]
@@ -0,0 +1,17 @@
1
+ """
2
+ Predefined constants for Flow.
3
+ """
4
+ import sys
5
+
6
+ START = sys.intern("__start__")
7
+ END = sys.intern("__end__")
8
+
9
+ ANY_USER = sys.intern("__any_user__")
10
+ FLOW_CONTEXT = sys.intern("FlowContext")
11
+
12
+ RESERVED = {
13
+ START,
14
+ END,
15
+ FLOW_CONTEXT,
16
+ ANY_USER
17
+ }
@@ -0,0 +1,91 @@
1
+ from typing import Any, List, cast
2
+ import uuid
3
+ import json
4
+ import yaml
5
+ from pydantic import BaseModel, Field, SerializeAsAny
6
+
7
+ from ibm_watsonx_orchestrate.agent_builder.tools.types import (
8
+ ToolRequestBody, ToolResponseBody, JsonSchemaObject
9
+ )
10
+ from ..types import (
11
+ _to_json_from_input_schema, _to_json_from_output_schema, SchemaRef, Assignment
12
+ )
13
+
14
+ class DataMapSpec(BaseModel):
15
+
16
+ name: str
17
+
18
+ def __init__(self, **data: Any) -> None:
19
+ if 'id' not in data:
20
+ data['id'] = str(uuid.uuid4())
21
+ super().__init__(**data)
22
+
23
+ def to_json(self) -> dict[Any, dict]:
24
+ '''Create a JSON object representing the data'''
25
+ model_spec = {}
26
+ model_spec["name"] = self.name
27
+ return model_spec
28
+
29
+
30
+
31
+ class AssignmentDataMapSpec(DataMapSpec):
32
+
33
+ maps: List[Assignment]
34
+
35
+ def to_json(self) -> dict[str, Any]:
36
+ '''Create a JSON object representing the data'''
37
+ model_spec = super().to_json()
38
+
39
+ if self.maps:
40
+ model_spec["maps"] = [assignment.model_dump() for assignment in self.maps]
41
+
42
+ return model_spec
43
+
44
+
45
+ class DataMap(BaseModel):
46
+
47
+ spec: SerializeAsAny[DataMapSpec]
48
+
49
+ def __call__(self, **kwargs):
50
+ pass
51
+
52
+ def dump_spec(self, file: str) -> None:
53
+
54
+ dumped = self.spec.model_dump(mode='json',
55
+ exclude_unset=True, exclude_none=True, by_alias=True)
56
+ with open(file, 'w', encoding="utf-8") as f:
57
+ if file.endswith('.yaml') or file.endswith('.yml'):
58
+ yaml.dump(dumped, f)
59
+ elif file.endswith('.json'):
60
+ json.dump(dumped, f, indent=2)
61
+ else:
62
+ raise ValueError('file must end in .json, .yaml, or .yml')
63
+
64
+ def dumps_spec(self) -> str:
65
+
66
+ dumped = self.spec.model_dump(mode='json',
67
+ exclude_unset=True, exclude_none=True, by_alias=True)
68
+ return json.dumps(dumped, indent=2)
69
+
70
+ def __repr__(self):
71
+ return f"DataMap(name='{self.spec.name}', description='{self.spec.description}')"
72
+
73
+ def to_json(self) -> dict[str, Any]:
74
+
75
+ obj = self.get_spec().to_json()
76
+
77
+ return { "spec": obj }
78
+
79
+ def get_spec(self) -> DataMapSpec:
80
+
81
+ return self.spec
82
+
83
+ class AssignmentDataMap(DataMap):
84
+ def get_spec(self) -> AssignmentDataMapSpec:
85
+ return cast(AssignmentDataMapSpec, self.spec)
86
+
87
+ def to_json(self) -> dict[str, Any]:
88
+
89
+ obj = super().to_json()
90
+ return obj
91
+
@@ -0,0 +1,143 @@
1
+ """
2
+ A set of decorators to help define different Flow constructs.
3
+ """
4
+
5
+ import asyncio
6
+ from functools import wraps
7
+ import logging
8
+ import inspect
9
+ from typing import Callable, Optional, Sequence
10
+ from pydantic import BaseModel
11
+ from ..types import extract_node_spec, UserNodeSpec, FlowSpec
12
+
13
+ from .flow import FlowFactory, Flow
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class FlowWrapper:
19
+ def __init__(self, func, a_model):
20
+ self.func = func
21
+ self.a_model = a_model
22
+ wraps(func)(self) # Preserve metadata
23
+
24
+ def __call__(self, *args, **kwargs):
25
+ result = self.func(self.a_model)
26
+ if not isinstance(result, Flow):
27
+ raise ValueError("Return value must be of type Flow")
28
+ return result
29
+
30
+ def user(*args, name: str|None=None, description: str|None=None, owners: Sequence[str] = ()):
31
+ """Decorator to mark a function as a task specification."""
32
+
33
+ def decorator(func: Callable):
34
+ node_spec = extract_node_spec(func, name, description)
35
+ func.__user_spec__ = UserNodeSpec(type = "user",
36
+ name = node_spec.name,
37
+ display_name = node_spec.display_name,
38
+ description = node_spec.description,
39
+ input_schema = node_spec.input_schema,
40
+ output_schema = node_spec.output_schema,
41
+ output_schema_object = node_spec.output_schema_object,
42
+ owners=owners)
43
+
44
+ @wraps(func)
45
+ def wrapper(*args, **kwargs):
46
+ logger.error(f"User node {name} is not supported yet.")
47
+ r = func(*args, **kwargs)
48
+ return r
49
+
50
+ return wrapper
51
+
52
+ if len(args) == 1 and callable(args[0]):
53
+ return decorator(args[0])
54
+ else:
55
+ return decorator
56
+
57
+
58
+ def flow_spec(*args,
59
+ name: Optional[str]=None,
60
+ description: str|None=None,
61
+ initiators: Sequence[str] = ()):
62
+ """Decorator to mark a function as a flow specification."""
63
+
64
+ def decorator(func: Callable):
65
+ node_spec = extract_node_spec(func, name, description)
66
+ a_spec = FlowSpec(type = "flow",
67
+ name = node_spec.name,
68
+ display_name = node_spec.display_name,
69
+ description = node_spec.description,
70
+ input_schema = node_spec.input_schema,
71
+ output_schema = node_spec.output_schema,
72
+ output_schema_object = node_spec.output_schema_object,
73
+ initiators = initiators)
74
+
75
+ # we should also check a flow is async
76
+ if not asyncio.iscoroutinefunction(func):
77
+ raise ValueError("Flow must be asynchronous.")
78
+
79
+ logger.info("Generated flow spec: %s", a_spec)
80
+ func.__flow_spec__ = a_spec
81
+
82
+ @wraps(func)
83
+ def wrapper(*args, **kwargs):
84
+ logger.info("Creating flow spec: %s", name)
85
+ r = func(*args, **kwargs)
86
+ logger.info("Flow spec %s returned: %s", name, r)
87
+ return r
88
+
89
+ return wrapper
90
+
91
+ if len(args) == 1 and callable(args[0]):
92
+ return decorator(args[0])
93
+ else:
94
+ return decorator
95
+
96
+ def flow(*args,
97
+ name: Optional[str]=None,
98
+ display_name: Optional[str]=None,
99
+ description: str|None=None,
100
+ input_schema: type[BaseModel] | None = None,
101
+ output_schema: type[BaseModel] | None = None,
102
+ initiators: Sequence[str] = ()):
103
+ """Decorator to mark a function as a flow model builder."""
104
+
105
+ def decorator(func: Callable):
106
+ """
107
+ Decorator that takes a function as an argument and returns a wrapper function.
108
+ The wrapper function takes a single argument of type Flow and calls the original function with the created flow as an argument.
109
+ """
110
+
111
+ sig = inspect.signature(func)
112
+ if len(sig.parameters) != 1:
113
+ raise ValueError("Only one argument is allowed")
114
+ param = list(sig.parameters.values())[0]
115
+ if param.annotation != Flow:
116
+ raise ValueError("Argument must be of type Flow")
117
+ if sig.return_annotation != Flow:
118
+ raise ValueError("Return value must be of type Flow")
119
+
120
+ node_spec = extract_node_spec(func, name, description)
121
+ a_model = FlowFactory.create_flow(
122
+ name = node_spec.name,
123
+ display_name = display_name,
124
+ description = node_spec.description,
125
+ input_schema = input_schema,
126
+ output_schema = output_schema,
127
+ initiators = initiators)
128
+
129
+ # logger.info("Creating flow model: %s", a_model.spec.name)
130
+
131
+ # @wraps(func)
132
+ # def wrapper(*args, **kwargs):
133
+ # result = func(a_model)
134
+ # if not isinstance(result, Flow):
135
+ # raise ValueError("Return value must be of type Flow")
136
+ # return result
137
+
138
+ return FlowWrapper(func, a_model)
139
+
140
+ if len(args) == 1 and callable(args[0]):
141
+ return decorator(args[0])
142
+ else:
143
+ return decorator
@@ -0,0 +1,72 @@
1
+ import redis
2
+ import time
3
+ import json
4
+ from dotenv import load_dotenv
5
+ import os
6
+
7
+ from typing import (
8
+ AsyncIterator, Union
9
+ )
10
+
11
+ from ..types import (
12
+ FlowEventType, TaskEventType, FlowEvent, FlowContext
13
+ )
14
+
15
+ class StreamConsumer:
16
+ def __init__(self, instance_id: str):
17
+
18
+ load_dotenv()
19
+ self.redis_host = os.getenv("REDIS_HOST", "localhost")
20
+ self.redis_port = os.getenv("REDIS_PORT", 6379)
21
+ self.redis_db = os.getenv("REDIS_DB", 0)
22
+ self.redis = redis.Redis(host=self.redis_host, port=self.redis_port, db=self.redis_db)
23
+ self.instance_id = instance_id
24
+ self.stream_name = f"tempus:{self.instance_id}"
25
+ self.last_processed_id = 0
26
+
27
+
28
+ async def consume(self) -> AsyncIterator[FlowEvent]:
29
+
30
+ while True:
31
+ try:
32
+ # XREAD command: Read new messages from the stream
33
+ messages = self.redis.xread({self.stream_name: self.last_processed_id}, block=5000, count=10)
34
+
35
+ for stream, events in messages:
36
+ for event_id, event_data in events:
37
+ self.last_processed_id = event_id # Update the last read event ID
38
+ flow_event = deserialize_flow_event(event_data)
39
+ yield flow_event
40
+
41
+ except Exception as e:
42
+ print(f"Error occurred: {e}")
43
+ time.sleep(5) # Wait for 5 seconds before retrying
44
+
45
+ time.sleep(1) # Sleep for 1 second before checking for new messages
46
+
47
+ def deserialize_flow_event(byte_data: bytes) -> FlowEvent:
48
+ """Deserialize byte data into a FlowEvent object."""
49
+ # Decode the byte data
50
+ decoded_data = byte_data[b'message'].decode('utf-8')
51
+
52
+ # Parse the JSON string into a dictionary
53
+ parsed_data = json.loads(decoded_data)
54
+
55
+ # Deserialize into FlowEvent using Pydantic's parsing
56
+ flow_event = FlowEvent(
57
+ kind=get_event_type(parsed_data["kind"]),
58
+ context=FlowContext(**parsed_data["context"]) if "context" in parsed_data and parsed_data["context"] != {} else None,
59
+ error=json.loads(parsed_data["error"]) if "error" in parsed_data and parsed_data["error"] != {} and len(parsed_data["error"]) > 0 else None,
60
+ )
61
+
62
+ return flow_event
63
+
64
+ def get_event_type(selected_event_type: str) -> Union[FlowEventType, TaskEventType]:
65
+ """Selects the right event type from the corresponding enumerator"""
66
+ eventKind = selected_event_type.upper()
67
+ if eventKind in FlowEventType.__members__:
68
+ return FlowEventType(selected_event_type)
69
+ elif eventKind in TaskEventType.__members__:
70
+ return TaskEventType(selected_event_type)
71
+ else:
72
+ raise ValueError(f"Invalid event type: {eventKind}")