agentex-sdk 0.1.0a6__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.
- agentex/__init__.py +103 -0
- agentex/_base_client.py +1992 -0
- agentex/_client.py +506 -0
- agentex/_compat.py +219 -0
- agentex/_constants.py +14 -0
- agentex/_exceptions.py +108 -0
- agentex/_files.py +123 -0
- agentex/_models.py +829 -0
- agentex/_qs.py +150 -0
- agentex/_resource.py +43 -0
- agentex/_response.py +830 -0
- agentex/_streaming.py +333 -0
- agentex/_types.py +219 -0
- agentex/_utils/__init__.py +57 -0
- agentex/_utils/_logs.py +25 -0
- agentex/_utils/_proxy.py +65 -0
- agentex/_utils/_reflection.py +42 -0
- agentex/_utils/_resources_proxy.py +24 -0
- agentex/_utils/_streams.py +12 -0
- agentex/_utils/_sync.py +86 -0
- agentex/_utils/_transform.py +447 -0
- agentex/_utils/_typing.py +151 -0
- agentex/_utils/_utils.py +422 -0
- agentex/_version.py +4 -0
- agentex/lib/.keep +4 -0
- agentex/lib/__init__.py +0 -0
- agentex/lib/adk/__init__.py +41 -0
- agentex/lib/adk/_modules/__init__.py +0 -0
- agentex/lib/adk/_modules/acp.py +247 -0
- agentex/lib/adk/_modules/agent_task_tracker.py +176 -0
- agentex/lib/adk/_modules/agents.py +77 -0
- agentex/lib/adk/_modules/events.py +141 -0
- agentex/lib/adk/_modules/messages.py +285 -0
- agentex/lib/adk/_modules/state.py +291 -0
- agentex/lib/adk/_modules/streaming.py +75 -0
- agentex/lib/adk/_modules/tasks.py +124 -0
- agentex/lib/adk/_modules/tracing.py +194 -0
- agentex/lib/adk/providers/__init__.py +9 -0
- agentex/lib/adk/providers/_modules/__init__.py +0 -0
- agentex/lib/adk/providers/_modules/litellm.py +232 -0
- agentex/lib/adk/providers/_modules/openai.py +416 -0
- agentex/lib/adk/providers/_modules/sgp.py +85 -0
- agentex/lib/adk/utils/__init__.py +5 -0
- agentex/lib/adk/utils/_modules/__init__.py +0 -0
- agentex/lib/adk/utils/_modules/templating.py +94 -0
- agentex/lib/cli/__init__.py +0 -0
- agentex/lib/cli/commands/__init__.py +0 -0
- agentex/lib/cli/commands/agents.py +328 -0
- agentex/lib/cli/commands/init.py +227 -0
- agentex/lib/cli/commands/main.py +33 -0
- agentex/lib/cli/commands/secrets.py +169 -0
- agentex/lib/cli/commands/tasks.py +118 -0
- agentex/lib/cli/commands/uv.py +133 -0
- agentex/lib/cli/handlers/__init__.py +0 -0
- agentex/lib/cli/handlers/agent_handlers.py +160 -0
- agentex/lib/cli/handlers/cleanup_handlers.py +186 -0
- agentex/lib/cli/handlers/deploy_handlers.py +351 -0
- agentex/lib/cli/handlers/run_handlers.py +452 -0
- agentex/lib/cli/handlers/secret_handlers.py +670 -0
- agentex/lib/cli/templates/default/.dockerignore.j2 +43 -0
- agentex/lib/cli/templates/default/Dockerfile-uv.j2 +42 -0
- agentex/lib/cli/templates/default/Dockerfile.j2 +42 -0
- agentex/lib/cli/templates/default/README.md.j2 +193 -0
- agentex/lib/cli/templates/default/deploy/example.yaml.j2 +55 -0
- agentex/lib/cli/templates/default/manifest.yaml.j2 +116 -0
- agentex/lib/cli/templates/default/project/acp.py.j2 +29 -0
- agentex/lib/cli/templates/default/pyproject.toml.j2 +33 -0
- agentex/lib/cli/templates/default/requirements.txt.j2 +5 -0
- agentex/lib/cli/templates/deploy/Screenshot 2025-03-19 at 10.36.57/342/200/257AM.png +0 -0
- agentex/lib/cli/templates/deploy/example.yaml.j2 +55 -0
- agentex/lib/cli/templates/sync/.dockerignore.j2 +43 -0
- agentex/lib/cli/templates/sync/Dockerfile-uv.j2 +42 -0
- agentex/lib/cli/templates/sync/Dockerfile.j2 +42 -0
- agentex/lib/cli/templates/sync/README.md.j2 +293 -0
- agentex/lib/cli/templates/sync/deploy/example.yaml.j2 +55 -0
- agentex/lib/cli/templates/sync/manifest.yaml.j2 +116 -0
- agentex/lib/cli/templates/sync/project/acp.py.j2 +26 -0
- agentex/lib/cli/templates/sync/pyproject.toml.j2 +33 -0
- agentex/lib/cli/templates/sync/requirements.txt.j2 +5 -0
- agentex/lib/cli/templates/temporal/.dockerignore.j2 +43 -0
- agentex/lib/cli/templates/temporal/Dockerfile-uv.j2 +48 -0
- agentex/lib/cli/templates/temporal/Dockerfile.j2 +48 -0
- agentex/lib/cli/templates/temporal/README.md.j2 +316 -0
- agentex/lib/cli/templates/temporal/deploy/example.yaml.j2 +55 -0
- agentex/lib/cli/templates/temporal/manifest.yaml.j2 +137 -0
- agentex/lib/cli/templates/temporal/project/acp.py.j2 +30 -0
- agentex/lib/cli/templates/temporal/project/run_worker.py.j2 +33 -0
- agentex/lib/cli/templates/temporal/project/workflow.py.j2 +66 -0
- agentex/lib/cli/templates/temporal/pyproject.toml.j2 +34 -0
- agentex/lib/cli/templates/temporal/requirements.txt.j2 +5 -0
- agentex/lib/cli/utils/cli_utils.py +14 -0
- agentex/lib/cli/utils/credential_utils.py +103 -0
- agentex/lib/cli/utils/exceptions.py +6 -0
- agentex/lib/cli/utils/kubectl_utils.py +135 -0
- agentex/lib/cli/utils/kubernetes_secrets_utils.py +185 -0
- agentex/lib/core/__init__.py +0 -0
- agentex/lib/core/adapters/__init__.py +0 -0
- agentex/lib/core/adapters/llm/__init__.py +1 -0
- agentex/lib/core/adapters/llm/adapter_litellm.py +46 -0
- agentex/lib/core/adapters/llm/adapter_sgp.py +55 -0
- agentex/lib/core/adapters/llm/port.py +24 -0
- agentex/lib/core/adapters/streams/adapter_redis.py +128 -0
- agentex/lib/core/adapters/streams/port.py +50 -0
- agentex/lib/core/clients/__init__.py +1 -0
- agentex/lib/core/clients/temporal/__init__.py +0 -0
- agentex/lib/core/clients/temporal/temporal_client.py +181 -0
- agentex/lib/core/clients/temporal/types.py +47 -0
- agentex/lib/core/clients/temporal/utils.py +56 -0
- agentex/lib/core/services/__init__.py +0 -0
- agentex/lib/core/services/adk/__init__.py +0 -0
- agentex/lib/core/services/adk/acp/__init__.py +0 -0
- agentex/lib/core/services/adk/acp/acp.py +210 -0
- agentex/lib/core/services/adk/agent_task_tracker.py +85 -0
- agentex/lib/core/services/adk/agents.py +43 -0
- agentex/lib/core/services/adk/events.py +61 -0
- agentex/lib/core/services/adk/messages.py +164 -0
- agentex/lib/core/services/adk/providers/__init__.py +0 -0
- agentex/lib/core/services/adk/providers/litellm.py +256 -0
- agentex/lib/core/services/adk/providers/openai.py +723 -0
- agentex/lib/core/services/adk/providers/sgp.py +99 -0
- agentex/lib/core/services/adk/state.py +120 -0
- agentex/lib/core/services/adk/streaming.py +262 -0
- agentex/lib/core/services/adk/tasks.py +69 -0
- agentex/lib/core/services/adk/tracing.py +36 -0
- agentex/lib/core/services/adk/utils/__init__.py +0 -0
- agentex/lib/core/services/adk/utils/templating.py +58 -0
- agentex/lib/core/temporal/__init__.py +0 -0
- agentex/lib/core/temporal/activities/__init__.py +207 -0
- agentex/lib/core/temporal/activities/activity_helpers.py +37 -0
- agentex/lib/core/temporal/activities/adk/__init__.py +0 -0
- agentex/lib/core/temporal/activities/adk/acp/__init__.py +0 -0
- agentex/lib/core/temporal/activities/adk/acp/acp_activities.py +86 -0
- agentex/lib/core/temporal/activities/adk/agent_task_tracker_activities.py +76 -0
- agentex/lib/core/temporal/activities/adk/agents_activities.py +35 -0
- agentex/lib/core/temporal/activities/adk/events_activities.py +50 -0
- agentex/lib/core/temporal/activities/adk/messages_activities.py +94 -0
- agentex/lib/core/temporal/activities/adk/providers/__init__.py +0 -0
- agentex/lib/core/temporal/activities/adk/providers/litellm_activities.py +71 -0
- agentex/lib/core/temporal/activities/adk/providers/openai_activities.py +210 -0
- agentex/lib/core/temporal/activities/adk/providers/sgp_activities.py +42 -0
- agentex/lib/core/temporal/activities/adk/state_activities.py +85 -0
- agentex/lib/core/temporal/activities/adk/streaming_activities.py +33 -0
- agentex/lib/core/temporal/activities/adk/tasks_activities.py +48 -0
- agentex/lib/core/temporal/activities/adk/tracing_activities.py +55 -0
- agentex/lib/core/temporal/activities/adk/utils/__init__.py +0 -0
- agentex/lib/core/temporal/activities/adk/utils/templating_activities.py +41 -0
- agentex/lib/core/temporal/services/__init__.py +0 -0
- agentex/lib/core/temporal/services/temporal_task_service.py +69 -0
- agentex/lib/core/temporal/types/__init__.py +0 -0
- agentex/lib/core/temporal/types/workflow.py +5 -0
- agentex/lib/core/temporal/workers/__init__.py +0 -0
- agentex/lib/core/temporal/workers/worker.py +162 -0
- agentex/lib/core/temporal/workflows/workflow.py +26 -0
- agentex/lib/core/tracing/__init__.py +5 -0
- agentex/lib/core/tracing/processors/agentex_tracing_processor.py +117 -0
- agentex/lib/core/tracing/processors/sgp_tracing_processor.py +119 -0
- agentex/lib/core/tracing/processors/tracing_processor_interface.py +40 -0
- agentex/lib/core/tracing/trace.py +311 -0
- agentex/lib/core/tracing/tracer.py +70 -0
- agentex/lib/core/tracing/tracing_processor_manager.py +62 -0
- agentex/lib/environment_variables.py +87 -0
- agentex/lib/py.typed +0 -0
- agentex/lib/sdk/__init__.py +0 -0
- agentex/lib/sdk/config/__init__.py +0 -0
- agentex/lib/sdk/config/agent_config.py +61 -0
- agentex/lib/sdk/config/agent_manifest.py +219 -0
- agentex/lib/sdk/config/build_config.py +35 -0
- agentex/lib/sdk/config/deployment_config.py +117 -0
- agentex/lib/sdk/config/local_development_config.py +56 -0
- agentex/lib/sdk/config/project_config.py +103 -0
- agentex/lib/sdk/fastacp/__init__.py +3 -0
- agentex/lib/sdk/fastacp/base/base_acp_server.py +406 -0
- agentex/lib/sdk/fastacp/fastacp.py +74 -0
- agentex/lib/sdk/fastacp/impl/agentic_base_acp.py +72 -0
- agentex/lib/sdk/fastacp/impl/sync_acp.py +109 -0
- agentex/lib/sdk/fastacp/impl/temporal_acp.py +97 -0
- agentex/lib/sdk/fastacp/tests/README.md +297 -0
- agentex/lib/sdk/fastacp/tests/conftest.py +307 -0
- agentex/lib/sdk/fastacp/tests/pytest.ini +10 -0
- agentex/lib/sdk/fastacp/tests/run_tests.py +227 -0
- agentex/lib/sdk/fastacp/tests/test_base_acp_server.py +450 -0
- agentex/lib/sdk/fastacp/tests/test_fastacp_factory.py +344 -0
- agentex/lib/sdk/fastacp/tests/test_integration.py +477 -0
- agentex/lib/sdk/state_machine/__init__.py +6 -0
- agentex/lib/sdk/state_machine/noop_workflow.py +21 -0
- agentex/lib/sdk/state_machine/state.py +10 -0
- agentex/lib/sdk/state_machine/state_machine.py +189 -0
- agentex/lib/sdk/state_machine/state_workflow.py +16 -0
- agentex/lib/sdk/utils/__init__.py +0 -0
- agentex/lib/sdk/utils/messages.py +223 -0
- agentex/lib/types/__init__.py +0 -0
- agentex/lib/types/acp.py +94 -0
- agentex/lib/types/agent_configs.py +79 -0
- agentex/lib/types/agent_results.py +29 -0
- agentex/lib/types/credentials.py +34 -0
- agentex/lib/types/fastacp.py +61 -0
- agentex/lib/types/files.py +13 -0
- agentex/lib/types/json_rpc.py +49 -0
- agentex/lib/types/llm_messages.py +354 -0
- agentex/lib/types/task_message_updates.py +171 -0
- agentex/lib/types/tracing.py +34 -0
- agentex/lib/utils/__init__.py +0 -0
- agentex/lib/utils/completions.py +131 -0
- agentex/lib/utils/console.py +14 -0
- agentex/lib/utils/io.py +29 -0
- agentex/lib/utils/iterables.py +14 -0
- agentex/lib/utils/json_schema.py +23 -0
- agentex/lib/utils/logging.py +31 -0
- agentex/lib/utils/mcp.py +17 -0
- agentex/lib/utils/model_utils.py +46 -0
- agentex/lib/utils/parsing.py +15 -0
- agentex/lib/utils/regex.py +6 -0
- agentex/lib/utils/temporal.py +13 -0
- agentex/py.typed +0 -0
- agentex/resources/__init__.py +103 -0
- agentex/resources/agents.py +707 -0
- agentex/resources/events.py +294 -0
- agentex/resources/messages/__init__.py +33 -0
- agentex/resources/messages/batch.py +271 -0
- agentex/resources/messages/messages.py +492 -0
- agentex/resources/spans.py +557 -0
- agentex/resources/states.py +544 -0
- agentex/resources/tasks.py +615 -0
- agentex/resources/tracker.py +384 -0
- agentex/types/__init__.py +56 -0
- agentex/types/acp_type.py +7 -0
- agentex/types/agent.py +29 -0
- agentex/types/agent_list_params.py +13 -0
- agentex/types/agent_list_response.py +10 -0
- agentex/types/agent_rpc_by_name_params.py +21 -0
- agentex/types/agent_rpc_params.py +51 -0
- agentex/types/agent_rpc_params1.py +21 -0
- agentex/types/agent_rpc_response.py +20 -0
- agentex/types/agent_rpc_result.py +90 -0
- agentex/types/agent_task_tracker.py +34 -0
- agentex/types/data_content.py +30 -0
- agentex/types/data_content_param.py +31 -0
- agentex/types/data_delta.py +14 -0
- agentex/types/event.py +29 -0
- agentex/types/event_list_params.py +22 -0
- agentex/types/event_list_response.py +10 -0
- agentex/types/message_author.py +7 -0
- agentex/types/message_create_params.py +18 -0
- agentex/types/message_list_params.py +14 -0
- agentex/types/message_list_response.py +10 -0
- agentex/types/message_style.py +7 -0
- agentex/types/message_update_params.py +18 -0
- agentex/types/messages/__init__.py +8 -0
- agentex/types/messages/batch_create_params.py +16 -0
- agentex/types/messages/batch_create_response.py +10 -0
- agentex/types/messages/batch_update_params.py +16 -0
- agentex/types/messages/batch_update_response.py +10 -0
- agentex/types/shared/__init__.py +3 -0
- agentex/types/shared/task_message_update.py +83 -0
- agentex/types/span.py +36 -0
- agentex/types/span_create_params.py +40 -0
- agentex/types/span_list_params.py +12 -0
- agentex/types/span_list_response.py +10 -0
- agentex/types/span_update_params.py +37 -0
- agentex/types/state.py +25 -0
- agentex/types/state_create_params.py +16 -0
- agentex/types/state_list_params.py +16 -0
- agentex/types/state_list_response.py +10 -0
- agentex/types/state_update_params.py +16 -0
- agentex/types/task.py +23 -0
- agentex/types/task_delete_by_name_response.py +8 -0
- agentex/types/task_delete_response.py +8 -0
- agentex/types/task_list_response.py +10 -0
- agentex/types/task_message.py +33 -0
- agentex/types/task_message_content.py +16 -0
- agentex/types/task_message_content_param.py +17 -0
- agentex/types/task_message_delta.py +16 -0
- agentex/types/text_content.py +53 -0
- agentex/types/text_content_param.py +54 -0
- agentex/types/text_delta.py +14 -0
- agentex/types/tool_request_content.py +36 -0
- agentex/types/tool_request_content_param.py +37 -0
- agentex/types/tool_request_delta.py +18 -0
- agentex/types/tool_response_content.py +36 -0
- agentex/types/tool_response_content_param.py +36 -0
- agentex/types/tool_response_delta.py +18 -0
- agentex/types/tracker_list_params.py +16 -0
- agentex/types/tracker_list_response.py +10 -0
- agentex/types/tracker_update_params.py +19 -0
- agentex_sdk-0.1.0a6.dist-info/METADATA +426 -0
- agentex_sdk-0.1.0a6.dist-info/RECORD +289 -0
- agentex_sdk-0.1.0a6.dist-info/WHEEL +4 -0
- agentex_sdk-0.1.0a6.dist-info/entry_points.txt +2 -0
- agentex_sdk-0.1.0a6.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,189 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from typing import Any, Generic, TypeVar
|
3
|
+
|
4
|
+
from agentex.lib import adk
|
5
|
+
from agentex.lib.sdk.state_machine.state import State
|
6
|
+
from agentex.lib.sdk.state_machine.state_workflow import StateWorkflow
|
7
|
+
from agentex.lib.utils.model_utils import BaseModel
|
8
|
+
|
9
|
+
T = TypeVar("T", bound=BaseModel)
|
10
|
+
|
11
|
+
|
12
|
+
class StateMachine(ABC, Generic[T]):
|
13
|
+
def __init__(
|
14
|
+
self,
|
15
|
+
initial_state: str,
|
16
|
+
states: list[State],
|
17
|
+
task_id: str | None = None,
|
18
|
+
state_machine_data: T | None = None,
|
19
|
+
trace_transitions: bool = False,
|
20
|
+
):
|
21
|
+
self._task_id = task_id
|
22
|
+
self._state_map: dict[str, State] = {state.name: state for state in states}
|
23
|
+
self.state_machine_data = state_machine_data
|
24
|
+
self._initial_state = initial_state
|
25
|
+
self._trace_transitions = trace_transitions
|
26
|
+
|
27
|
+
# Validate that initial state exists
|
28
|
+
if initial_state not in self._state_map:
|
29
|
+
raise ValueError(f"Initial state '{initial_state}' not found in states")
|
30
|
+
self._current_state = self._state_map[initial_state]
|
31
|
+
|
32
|
+
def set_task_id(self, task_id: str):
|
33
|
+
self._task_id = task_id
|
34
|
+
|
35
|
+
def get_current_state(self) -> str:
|
36
|
+
return self._current_state.name
|
37
|
+
|
38
|
+
def get_current_workflow(self) -> StateWorkflow:
|
39
|
+
"""
|
40
|
+
Get the workflow of the current state.
|
41
|
+
|
42
|
+
Returns:
|
43
|
+
The workflow of the current state
|
44
|
+
|
45
|
+
Raises:
|
46
|
+
ValueError: If the current state is not found in the state map
|
47
|
+
"""
|
48
|
+
current_state = self._state_map.get(self.get_current_state())
|
49
|
+
if not current_state:
|
50
|
+
raise ValueError(f"State {self.get_current_state()} not found")
|
51
|
+
return current_state.workflow
|
52
|
+
|
53
|
+
async def transition(self, target_state_name: str):
|
54
|
+
if not self._state_map.get(target_state_name):
|
55
|
+
raise ValueError(f"State {target_state_name} not found")
|
56
|
+
self._current_state = self._state_map[target_state_name]
|
57
|
+
|
58
|
+
def get_state_machine_data(self) -> T:
|
59
|
+
return self.state_machine_data
|
60
|
+
|
61
|
+
@abstractmethod
|
62
|
+
async def terminal_condition(self) -> bool:
|
63
|
+
pass
|
64
|
+
|
65
|
+
# Overwrite this if you want to add more logic to the state machine
|
66
|
+
async def run(self):
|
67
|
+
while not await self.terminal_condition():
|
68
|
+
await self.step()
|
69
|
+
|
70
|
+
async def step(self) -> str:
|
71
|
+
current_state_name = self.get_current_state()
|
72
|
+
current_state = self._state_map.get(current_state_name)
|
73
|
+
|
74
|
+
if self._trace_transitions:
|
75
|
+
if self._task_id is None:
|
76
|
+
raise ValueError(
|
77
|
+
"Task ID is must be set before tracing can be enabled"
|
78
|
+
)
|
79
|
+
span = await adk.tracing.start_span(
|
80
|
+
trace_id=self._task_id,
|
81
|
+
name="state_transition",
|
82
|
+
input=self.state_machine_data.model_dump(),
|
83
|
+
data={"input_state": current_state_name},
|
84
|
+
)
|
85
|
+
|
86
|
+
next_state_name = await current_state.workflow.execute(
|
87
|
+
state_machine=self, state_machine_data=self.state_machine_data
|
88
|
+
)
|
89
|
+
|
90
|
+
if self._trace_transitions:
|
91
|
+
if self._task_id is None:
|
92
|
+
raise ValueError(
|
93
|
+
"Task ID is must be set before tracing can be enabled"
|
94
|
+
)
|
95
|
+
span.output = self.state_machine_data.model_dump()
|
96
|
+
span.data["output_state"] = next_state_name
|
97
|
+
await adk.tracing.end_span(trace_id=self._task_id, span=span)
|
98
|
+
|
99
|
+
await self.transition(next_state_name)
|
100
|
+
|
101
|
+
return next_state_name
|
102
|
+
|
103
|
+
async def reset_to_initial_state(self):
|
104
|
+
"""
|
105
|
+
Reset the state machine to its initial state.
|
106
|
+
"""
|
107
|
+
if self._trace_transitions:
|
108
|
+
if self._task_id is None:
|
109
|
+
raise ValueError(
|
110
|
+
"Task ID is must be set before tracing can be enabled"
|
111
|
+
)
|
112
|
+
span = await adk.tracing.start_span(
|
113
|
+
trace_id=self._task_id,
|
114
|
+
name="state_transition_reset",
|
115
|
+
input={"input_state": self.get_current_state()},
|
116
|
+
)
|
117
|
+
|
118
|
+
await self.transition(self._initial_state)
|
119
|
+
|
120
|
+
if self._trace_transitions:
|
121
|
+
span.output = {"output_state": self._initial_state}
|
122
|
+
await adk.tracing.end_span(trace_id=self._task_id, span=span)
|
123
|
+
|
124
|
+
def dump(self) -> dict[str, Any]:
|
125
|
+
"""
|
126
|
+
Save the current state of the state machine to a serializable dictionary.
|
127
|
+
This includes the current state, task_id, state machine data, and initial state.
|
128
|
+
|
129
|
+
Returns:
|
130
|
+
Dict[str, Any]: A dictionary containing the serialized state machine state
|
131
|
+
"""
|
132
|
+
return {
|
133
|
+
"task_id": self._task_id,
|
134
|
+
"current_state": self.get_current_state(),
|
135
|
+
"initial_state": self._initial_state,
|
136
|
+
"state_machine_data": self.state_machine_data.model_dump(mode="json")
|
137
|
+
if self.state_machine_data
|
138
|
+
else None,
|
139
|
+
"trace_transitions": self._trace_transitions,
|
140
|
+
}
|
141
|
+
|
142
|
+
@classmethod
|
143
|
+
async def load(cls, data: dict[str, Any], states: list[State]) -> "StateMachine[T]":
|
144
|
+
"""
|
145
|
+
Load a state machine from a previously saved dictionary.
|
146
|
+
|
147
|
+
Args:
|
148
|
+
data: The dictionary containing the saved state machine state
|
149
|
+
states: List of all possible states
|
150
|
+
|
151
|
+
Returns:
|
152
|
+
StateMachine: A new state machine instance restored to the saved state
|
153
|
+
|
154
|
+
Raises:
|
155
|
+
ValueError: If the data is invalid or missing required fields
|
156
|
+
"""
|
157
|
+
try:
|
158
|
+
task_id = data.get("task_id")
|
159
|
+
current_state_name = data.get("current_state")
|
160
|
+
initial_state = data.get("initial_state")
|
161
|
+
state_machine_data_dict = data.get("state_machine_data")
|
162
|
+
trace_transitions = data.get("trace_transitions")
|
163
|
+
|
164
|
+
if initial_state is None:
|
165
|
+
raise ValueError("Initial state not found in saved data")
|
166
|
+
|
167
|
+
# Reconstruct the state machine data into its Pydantic model
|
168
|
+
state_machine_data = None
|
169
|
+
if state_machine_data_dict is not None:
|
170
|
+
# Get the actual model type from the class's type parameters
|
171
|
+
model_type = cls.__orig_bases__[0].__args__[0]
|
172
|
+
state_machine_data = model_type.model_validate(state_machine_data_dict)
|
173
|
+
|
174
|
+
# Create a new instance
|
175
|
+
instance = cls(
|
176
|
+
initial_state=initial_state,
|
177
|
+
states=states,
|
178
|
+
task_id=task_id,
|
179
|
+
state_machine_data=state_machine_data,
|
180
|
+
trace_transitions=trace_transitions,
|
181
|
+
)
|
182
|
+
|
183
|
+
# If there's a saved state, transition to it
|
184
|
+
if current_state_name:
|
185
|
+
await instance.transition(target_state_name=current_state_name)
|
186
|
+
|
187
|
+
return instance
|
188
|
+
except Exception as e:
|
189
|
+
raise ValueError(f"Failed to restore state machine: {str(e)}") from e
|
@@ -0,0 +1,16 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from typing import TYPE_CHECKING
|
3
|
+
|
4
|
+
from pydantic import BaseModel
|
5
|
+
|
6
|
+
# Import StateMachine only for type checking to avoid circular imports
|
7
|
+
if TYPE_CHECKING:
|
8
|
+
from agentex.lib.sdk.state_machine import StateMachine
|
9
|
+
|
10
|
+
|
11
|
+
class StateWorkflow(ABC):
|
12
|
+
@abstractmethod
|
13
|
+
async def execute(
|
14
|
+
self, state_machine: "StateMachine", state_machine_data: BaseModel | None = None
|
15
|
+
) -> str:
|
16
|
+
pass
|
File without changes
|
@@ -0,0 +1,223 @@
|
|
1
|
+
import json
|
2
|
+
from abc import ABC, abstractmethod
|
3
|
+
from typing import Any, Literal, override
|
4
|
+
|
5
|
+
from agentex.lib.types.llm_messages import (
|
6
|
+
AssistantMessage,
|
7
|
+
Message,
|
8
|
+
ToolCall,
|
9
|
+
ToolCallRequest,
|
10
|
+
ToolMessage,
|
11
|
+
UserMessage,
|
12
|
+
)
|
13
|
+
from agentex.types.data_content import DataContent
|
14
|
+
from agentex.types.task_message import TaskMessage
|
15
|
+
from agentex.types.text_content import TextContent
|
16
|
+
from agentex.types.tool_request_content import ToolRequestContent
|
17
|
+
from agentex.types.tool_response_content import ToolResponseContent
|
18
|
+
|
19
|
+
|
20
|
+
class TaskMessageConverter(ABC):
|
21
|
+
"""
|
22
|
+
Abstract base class for converting a specific type of TaskMessage to an LLM Message.
|
23
|
+
|
24
|
+
Each converter should be responsible for one content type.
|
25
|
+
"""
|
26
|
+
|
27
|
+
@abstractmethod
|
28
|
+
def convert(self, task_message: TaskMessage) -> Message:
|
29
|
+
"""
|
30
|
+
Convert a TaskMessage to an LLM Message.
|
31
|
+
|
32
|
+
Args:
|
33
|
+
task_message: The TaskMessage to convert
|
34
|
+
|
35
|
+
Returns:
|
36
|
+
A Message (Pydantic model)
|
37
|
+
"""
|
38
|
+
pass
|
39
|
+
|
40
|
+
|
41
|
+
class DefaultTextContentConverter(TaskMessageConverter):
|
42
|
+
"""Converter for TEXT content type."""
|
43
|
+
|
44
|
+
@override
|
45
|
+
def convert(self, task_message: TaskMessage) -> Message:
|
46
|
+
"""Convert TEXT content to UserMessage or AssistantMessage based on author."""
|
47
|
+
if not isinstance(task_message.content, TextContent):
|
48
|
+
raise ValueError(f"Expected TextContent, got {type(task_message.content)}")
|
49
|
+
content = task_message.content
|
50
|
+
if content.author == "user":
|
51
|
+
return UserMessage(content=content.content)
|
52
|
+
else: # AGENT or custom author
|
53
|
+
return AssistantMessage(content=content.content)
|
54
|
+
|
55
|
+
|
56
|
+
class DefaultToolRequestConverter(TaskMessageConverter):
|
57
|
+
"""Converter for TOOL_REQUEST content type."""
|
58
|
+
|
59
|
+
@override
|
60
|
+
def convert(self, task_message: TaskMessage) -> Message:
|
61
|
+
"""Convert TOOL_REQUEST content to AssistantMessage with tool_calls."""
|
62
|
+
if not isinstance(task_message.content, ToolRequestContent):
|
63
|
+
raise ValueError(f"Expected ToolRequestContent, got {type(task_message.content)}")
|
64
|
+
|
65
|
+
content = task_message.content
|
66
|
+
|
67
|
+
# Ensure arguments are properly JSON serialized
|
68
|
+
arguments_str = json.dumps(content.arguments)
|
69
|
+
|
70
|
+
tool_call = ToolCallRequest(
|
71
|
+
id=content.tool_call_id,
|
72
|
+
function=ToolCall(name=content.name, arguments=arguments_str),
|
73
|
+
)
|
74
|
+
return AssistantMessage(content=None, tool_calls=[tool_call])
|
75
|
+
|
76
|
+
|
77
|
+
class DefaultToolResponseConverter(TaskMessageConverter):
|
78
|
+
"""Converter for TOOL_RESPONSE content type."""
|
79
|
+
|
80
|
+
@override
|
81
|
+
def convert(self, task_message: TaskMessage) -> Message:
|
82
|
+
"""Convert TOOL_RESPONSE content to ToolMessage."""
|
83
|
+
if not isinstance(task_message.content, ToolResponseContent):
|
84
|
+
raise ValueError(f"Expected ToolResponseContent, got {type(task_message.content)}")
|
85
|
+
|
86
|
+
content = task_message.content
|
87
|
+
return ToolMessage(
|
88
|
+
content=str(content.content),
|
89
|
+
tool_call_id=content.tool_call_id,
|
90
|
+
name=content.name,
|
91
|
+
)
|
92
|
+
|
93
|
+
|
94
|
+
class DefaultDataContentConverter(TaskMessageConverter):
|
95
|
+
"""Converter for DATA content type."""
|
96
|
+
|
97
|
+
@override
|
98
|
+
def convert(self, task_message: TaskMessage) -> Message:
|
99
|
+
"""Convert DATA content to UserMessage or AssistantMessage based on author."""
|
100
|
+
if not isinstance(task_message.content, DataContent):
|
101
|
+
raise ValueError(f"Expected DataContent, got {type(task_message.content)}")
|
102
|
+
|
103
|
+
content = task_message.content
|
104
|
+
content_str = str(content.data)
|
105
|
+
if content.author == "user":
|
106
|
+
return UserMessage(content=content_str)
|
107
|
+
else: # AGENT or custom author
|
108
|
+
return AssistantMessage(content=content_str)
|
109
|
+
|
110
|
+
|
111
|
+
class DefaultUnknownContentConverter(TaskMessageConverter):
|
112
|
+
"""Converter for unknown content types."""
|
113
|
+
|
114
|
+
@override
|
115
|
+
def convert(self, task_message: TaskMessage) -> Message:
|
116
|
+
"""Convert unknown content types to AssistantMessage with fallback text."""
|
117
|
+
|
118
|
+
content = task_message.content
|
119
|
+
fallback_content = f"Unknown message type: {content.type}"
|
120
|
+
return AssistantMessage(content=fallback_content)
|
121
|
+
|
122
|
+
|
123
|
+
def convert_task_message_to_llm_messages(
|
124
|
+
task_message: TaskMessage,
|
125
|
+
output_mode: Literal["pydantic", "dict"] = "pydantic",
|
126
|
+
text_converter: TaskMessageConverter | None = None,
|
127
|
+
tool_request_converter: TaskMessageConverter | None = None,
|
128
|
+
tool_response_converter: TaskMessageConverter | None = None,
|
129
|
+
data_converter: TaskMessageConverter | None = None,
|
130
|
+
unknown_converter: TaskMessageConverter | None = None,
|
131
|
+
) -> Message | dict[str, Any]:
|
132
|
+
"""
|
133
|
+
Convert a TaskMessage to an LLM Message format.
|
134
|
+
|
135
|
+
Args:
|
136
|
+
task_message: The TaskMessage to convert
|
137
|
+
output_mode: Whether to return a Pydantic model or dict
|
138
|
+
text_converter: Optional converter for TEXT content. Uses DefaultTextContentConverter if None.
|
139
|
+
tool_request_converter: Optional converter for TOOL_REQUEST content. Uses DefaultToolRequestConverter if None.
|
140
|
+
tool_response_converter: Optional converter for TOOL_RESPONSE content. Uses DefaultToolResponseConverter if None.
|
141
|
+
data_converter: Optional converter for DATA content. Uses DefaultDataContentConverter if None.
|
142
|
+
unknown_converter: Optional converter for unknown content. Uses DefaultUnknownContentConverter if None.
|
143
|
+
|
144
|
+
Returns:
|
145
|
+
Either a Message (Pydantic model) or dict representation
|
146
|
+
"""
|
147
|
+
content = task_message.content
|
148
|
+
|
149
|
+
# Get the appropriate converter for this content type
|
150
|
+
if content.type == "text":
|
151
|
+
converter = (
|
152
|
+
text_converter
|
153
|
+
if text_converter is not None
|
154
|
+
else DefaultTextContentConverter()
|
155
|
+
)
|
156
|
+
elif content.type == "tool_request":
|
157
|
+
converter = (
|
158
|
+
tool_request_converter
|
159
|
+
if tool_request_converter is not None
|
160
|
+
else DefaultToolRequestConverter()
|
161
|
+
)
|
162
|
+
elif content.type == "tool_response":
|
163
|
+
converter = (
|
164
|
+
tool_response_converter
|
165
|
+
if tool_response_converter is not None
|
166
|
+
else DefaultToolResponseConverter()
|
167
|
+
)
|
168
|
+
elif content.type == "data":
|
169
|
+
converter = (
|
170
|
+
data_converter
|
171
|
+
if data_converter is not None
|
172
|
+
else DefaultDataContentConverter()
|
173
|
+
)
|
174
|
+
else:
|
175
|
+
converter = (
|
176
|
+
unknown_converter
|
177
|
+
if unknown_converter is not None
|
178
|
+
else DefaultUnknownContentConverter()
|
179
|
+
)
|
180
|
+
|
181
|
+
message = converter.convert(task_message)
|
182
|
+
|
183
|
+
if output_mode == "dict":
|
184
|
+
return message.model_dump()
|
185
|
+
return message
|
186
|
+
|
187
|
+
|
188
|
+
def convert_task_messages_to_llm_messages(
|
189
|
+
task_messages: list[TaskMessage],
|
190
|
+
output_mode: Literal["pydantic", "dict"] = "pydantic",
|
191
|
+
text_converter: TaskMessageConverter | None = None,
|
192
|
+
tool_request_converter: TaskMessageConverter | None = None,
|
193
|
+
tool_response_converter: TaskMessageConverter | None = None,
|
194
|
+
data_converter: TaskMessageConverter | None = None,
|
195
|
+
unknown_converter: TaskMessageConverter | None = None,
|
196
|
+
) -> list[Message | dict[str, Any]]:
|
197
|
+
"""
|
198
|
+
Convert a list of TaskMessages to LLM Message format.
|
199
|
+
|
200
|
+
Args:
|
201
|
+
task_messages: List of TaskMessages to convert
|
202
|
+
output_mode: Whether to return Pydantic models or dicts
|
203
|
+
text_converter: Optional converter for TEXT content. Uses DefaultTextContentConverter if None.
|
204
|
+
tool_request_converter: Optional converter for TOOL_REQUEST content. Uses DefaultToolRequestConverter if None.
|
205
|
+
tool_response_converter: Optional converter for TOOL_RESPONSE content. Uses DefaultToolResponseConverter if None.
|
206
|
+
data_converter: Optional converter for DATA content. Uses DefaultDataContentConverter if None.
|
207
|
+
unknown_converter: Optional converter for unknown content. Uses DefaultUnknownContentConverter if None.
|
208
|
+
|
209
|
+
Returns:
|
210
|
+
List of either Messages (Pydantic models) or dicts
|
211
|
+
"""
|
212
|
+
return [
|
213
|
+
convert_task_message_to_llm_messages(
|
214
|
+
task_message,
|
215
|
+
output_mode,
|
216
|
+
text_converter,
|
217
|
+
tool_request_converter,
|
218
|
+
tool_response_converter,
|
219
|
+
data_converter,
|
220
|
+
unknown_converter,
|
221
|
+
)
|
222
|
+
for task_message in task_messages
|
223
|
+
]
|
File without changes
|
agentex/lib/types/acp.py
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
from enum import Enum
|
2
|
+
from typing import Any
|
3
|
+
|
4
|
+
from pydantic import BaseModel, Field
|
5
|
+
|
6
|
+
from agentex.types.agent import Agent
|
7
|
+
from agentex.types.event import Event
|
8
|
+
from agentex.types.task_message_content import TaskMessageContent
|
9
|
+
from agentex.types.task import Task
|
10
|
+
|
11
|
+
|
12
|
+
class RPCMethod(str, Enum):
|
13
|
+
"""Available JSON-RPC methods for agent communication."""
|
14
|
+
|
15
|
+
EVENT_SEND = "event/send"
|
16
|
+
MESSAGE_SEND = "message/send"
|
17
|
+
TASK_CANCEL = "task/cancel"
|
18
|
+
TASK_CREATE = "task/create"
|
19
|
+
|
20
|
+
|
21
|
+
class CreateTaskParams(BaseModel):
|
22
|
+
"""Parameters for task/create method.
|
23
|
+
|
24
|
+
Attributes:
|
25
|
+
agent: The agent that the task was sent to.
|
26
|
+
task: The task to be created.
|
27
|
+
params: The parameters for the task as inputted by the user.
|
28
|
+
"""
|
29
|
+
|
30
|
+
agent: Agent = Field(..., description="The agent that the task was sent to")
|
31
|
+
task: Task = Field(..., description="The task to be created")
|
32
|
+
params: dict[str, Any] | None = Field(
|
33
|
+
None,
|
34
|
+
description="The parameters for the task as inputted by the user",
|
35
|
+
)
|
36
|
+
|
37
|
+
|
38
|
+
class SendMessageParams(BaseModel):
|
39
|
+
"""Parameters for message/send method.
|
40
|
+
|
41
|
+
Attributes:
|
42
|
+
agent: The agent that the message was sent to.
|
43
|
+
task: The task that the message was sent to.
|
44
|
+
content: The message that was sent to the agent.
|
45
|
+
stream: Whether to stream the message back to the agentex server from the agent.
|
46
|
+
"""
|
47
|
+
|
48
|
+
agent: Agent = Field(..., description="The agent that the message was sent to")
|
49
|
+
task: Task = Field(..., description="The task that the message was sent to")
|
50
|
+
content: TaskMessageContent = Field(
|
51
|
+
..., description="The message that was sent to the agent"
|
52
|
+
)
|
53
|
+
stream: bool = Field(
|
54
|
+
False,
|
55
|
+
description="Whether to stream the message back to the agentex server from the agent",
|
56
|
+
)
|
57
|
+
|
58
|
+
|
59
|
+
class SendEventParams(BaseModel):
|
60
|
+
"""Parameters for event/send method.
|
61
|
+
|
62
|
+
Attributes:
|
63
|
+
agent: The agent that the event was sent to.
|
64
|
+
task: The task that the message was sent to.
|
65
|
+
event: The event that was sent to the agent.
|
66
|
+
"""
|
67
|
+
|
68
|
+
agent: Agent = Field(..., description="The agent that the event was sent to")
|
69
|
+
task: Task = Field(..., description="The task that the message was sent to")
|
70
|
+
event: Event = Field(..., description="The event that was sent to the agent")
|
71
|
+
|
72
|
+
|
73
|
+
class CancelTaskParams(BaseModel):
|
74
|
+
"""Parameters for task/cancel method.
|
75
|
+
|
76
|
+
Attributes:
|
77
|
+
agent: The agent that the task was sent to.
|
78
|
+
task: The task that was cancelled.
|
79
|
+
"""
|
80
|
+
|
81
|
+
agent: Agent = Field(..., description="The agent that the task was sent to")
|
82
|
+
task: Task = Field(..., description="The task that was cancelled")
|
83
|
+
|
84
|
+
|
85
|
+
RPC_SYNC_METHODS = [
|
86
|
+
RPCMethod.MESSAGE_SEND,
|
87
|
+
]
|
88
|
+
|
89
|
+
PARAMS_MODEL_BY_METHOD: dict[RPCMethod, type[BaseModel]] = {
|
90
|
+
RPCMethod.EVENT_SEND: SendEventParams,
|
91
|
+
RPCMethod.TASK_CANCEL: CancelTaskParams,
|
92
|
+
RPCMethod.MESSAGE_SEND: SendMessageParams,
|
93
|
+
RPCMethod.TASK_CREATE: CreateTaskParams,
|
94
|
+
}
|
@@ -0,0 +1,79 @@
|
|
1
|
+
from pydantic import BaseModel, Field, model_validator, validator
|
2
|
+
|
3
|
+
|
4
|
+
class TemporalWorkflowConfig(BaseModel):
|
5
|
+
"""
|
6
|
+
Configuration for the temporal workflow that defines the agent.
|
7
|
+
|
8
|
+
Attributes:
|
9
|
+
name: The name of the temporal workflow that defines the agent.
|
10
|
+
queue_name: The name of the temporal queue to send tasks to.
|
11
|
+
"""
|
12
|
+
|
13
|
+
name: str = Field(
|
14
|
+
..., description="The name of the temporal workflow that defines the agent."
|
15
|
+
)
|
16
|
+
queue_name: str = Field(
|
17
|
+
..., description="The name of the temporal queue to send tasks to."
|
18
|
+
)
|
19
|
+
|
20
|
+
|
21
|
+
# TODO: Remove this class when we remove the agentex agents create
|
22
|
+
class TemporalWorkerConfig(BaseModel):
|
23
|
+
"""
|
24
|
+
Configuration for temporal worker deployment
|
25
|
+
|
26
|
+
Attributes:
|
27
|
+
image: The image to use for the temporal worker
|
28
|
+
workflow: The temporal workflow configuration
|
29
|
+
"""
|
30
|
+
|
31
|
+
image: str | None = Field(
|
32
|
+
default=None, description="Image to use for the temporal worker"
|
33
|
+
)
|
34
|
+
workflow: TemporalWorkflowConfig | None = Field(
|
35
|
+
default=None,
|
36
|
+
description="Configuration for the temporal workflow that defines the agent. Only required for agents that leverage Temporal.",
|
37
|
+
)
|
38
|
+
|
39
|
+
|
40
|
+
class TemporalConfig(BaseModel):
|
41
|
+
"""
|
42
|
+
Simplified temporal configuration for agents
|
43
|
+
|
44
|
+
Attributes:
|
45
|
+
enabled: Whether this agent uses Temporal workflows
|
46
|
+
workflow: The temporal workflow configuration
|
47
|
+
workflows: The list of temporal workflow configurations
|
48
|
+
"""
|
49
|
+
|
50
|
+
enabled: bool = Field(
|
51
|
+
default=False, description="Whether this agent uses Temporal workflows"
|
52
|
+
)
|
53
|
+
workflow: TemporalWorkflowConfig | None = Field(
|
54
|
+
default=None,
|
55
|
+
description="Temporal workflow configuration. Required when enabled=True. (deprecated: use workflows instead)",
|
56
|
+
)
|
57
|
+
workflows: list[TemporalWorkflowConfig] | None = Field(
|
58
|
+
default=None,
|
59
|
+
description="List of temporal workflow configurations. Used when enabled=true.",
|
60
|
+
)
|
61
|
+
|
62
|
+
@validator("workflows")
|
63
|
+
def validate_workflows_not_empty(cls, v):
|
64
|
+
"""Ensure workflows list is not empty when provided"""
|
65
|
+
if v is not None and len(v) == 0:
|
66
|
+
raise ValueError("workflows list cannot be empty when provided")
|
67
|
+
return v
|
68
|
+
|
69
|
+
@model_validator(mode="after")
|
70
|
+
def validate_temporal_config_when_enabled(self):
|
71
|
+
"""Validate that workflow configuration exists when enabled=true"""
|
72
|
+
if self.enabled:
|
73
|
+
# Must have either workflow (legacy) or workflows (new)
|
74
|
+
if not self.workflow and (not self.workflows or len(self.workflows) == 0):
|
75
|
+
raise ValueError(
|
76
|
+
"When temporal.enabled=true, either 'workflow' or 'workflows' must be provided and non-empty"
|
77
|
+
)
|
78
|
+
|
79
|
+
return self
|
@@ -0,0 +1,29 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
from pydantic import BaseModel
|
4
|
+
|
5
|
+
|
6
|
+
class SerializableRunResult(BaseModel):
|
7
|
+
"""
|
8
|
+
Serializable version of RunResult.
|
9
|
+
|
10
|
+
Attributes:
|
11
|
+
final_output: The final output of the run.
|
12
|
+
final_input_list: The final input list of the run.
|
13
|
+
"""
|
14
|
+
|
15
|
+
final_output: Any
|
16
|
+
final_input_list: list[dict[str, Any]]
|
17
|
+
|
18
|
+
|
19
|
+
class SerializableRunResultStreaming(BaseModel):
|
20
|
+
"""
|
21
|
+
Serializable version of RunResultStreaming.
|
22
|
+
|
23
|
+
Attributes:
|
24
|
+
final_output: The final output of the run.
|
25
|
+
final_input_list: The final input list of the run.
|
26
|
+
"""
|
27
|
+
|
28
|
+
final_output: Any
|
29
|
+
final_input_list: list[dict[str, Any]]
|
@@ -0,0 +1,34 @@
|
|
1
|
+
from pydantic import BaseModel, Field
|
2
|
+
|
3
|
+
|
4
|
+
class CredentialMapping(BaseModel):
|
5
|
+
"""Maps a Kubernetes secret to an environment variable in the agent container.
|
6
|
+
|
7
|
+
This allows agents to securely access credentials stored in Kubernetes secrets
|
8
|
+
by mapping them to environment variables. For example, you can map a secret
|
9
|
+
containing an API key to an environment variable that your agent code expects.
|
10
|
+
|
11
|
+
Example:
|
12
|
+
A mapping of {"env_var_name": "OPENAI_API_KEY",
|
13
|
+
"secret_name": "ai-credentials",
|
14
|
+
"secret_key": "openai-key"}
|
15
|
+
will make the value from the "openai-key" field in the "ai-credentials"
|
16
|
+
Kubernetes secret available to the agent as OPENAI_API_KEY environment variable.
|
17
|
+
|
18
|
+
Attributes:
|
19
|
+
env_var_name: The name of the environment variable that will be available to the agent
|
20
|
+
secret_name: The name of the Kubernetes secret containing the credential
|
21
|
+
secret_key: The key within the Kubernetes secret that contains the credential value
|
22
|
+
"""
|
23
|
+
|
24
|
+
env_var_name: str = Field(
|
25
|
+
...,
|
26
|
+
description="Name of the environment variable that will be available to the agent",
|
27
|
+
)
|
28
|
+
secret_name: str = Field(
|
29
|
+
..., description="Name of the Kubernetes secret containing the credential"
|
30
|
+
)
|
31
|
+
secret_key: str = Field(
|
32
|
+
...,
|
33
|
+
description="Key within the Kubernetes secret that contains the credential value",
|
34
|
+
)
|