@elizaos/python 2.0.0-alpha.10
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.
- package/LICENSE +26 -0
- package/README.md +239 -0
- package/elizaos/__init__.py +280 -0
- package/elizaos/action_docs.py +149 -0
- package/elizaos/advanced_capabilities/__init__.py +85 -0
- package/elizaos/advanced_capabilities/actions/__init__.py +54 -0
- package/elizaos/advanced_capabilities/actions/add_contact.py +139 -0
- package/elizaos/advanced_capabilities/actions/follow_room.py +151 -0
- package/elizaos/advanced_capabilities/actions/image_generation.py +148 -0
- package/elizaos/advanced_capabilities/actions/mute_room.py +164 -0
- package/elizaos/advanced_capabilities/actions/remove_contact.py +145 -0
- package/elizaos/advanced_capabilities/actions/roles.py +207 -0
- package/elizaos/advanced_capabilities/actions/schedule_follow_up.py +154 -0
- package/elizaos/advanced_capabilities/actions/search_contacts.py +145 -0
- package/elizaos/advanced_capabilities/actions/send_message.py +187 -0
- package/elizaos/advanced_capabilities/actions/settings.py +151 -0
- package/elizaos/advanced_capabilities/actions/unfollow_room.py +164 -0
- package/elizaos/advanced_capabilities/actions/unmute_room.py +164 -0
- package/elizaos/advanced_capabilities/actions/update_contact.py +164 -0
- package/elizaos/advanced_capabilities/actions/update_entity.py +161 -0
- package/elizaos/advanced_capabilities/evaluators/__init__.py +18 -0
- package/elizaos/advanced_capabilities/evaluators/reflection.py +134 -0
- package/elizaos/advanced_capabilities/evaluators/relationship_extraction.py +203 -0
- package/elizaos/advanced_capabilities/providers/__init__.py +36 -0
- package/elizaos/advanced_capabilities/providers/agent_settings.py +60 -0
- package/elizaos/advanced_capabilities/providers/contacts.py +77 -0
- package/elizaos/advanced_capabilities/providers/facts.py +82 -0
- package/elizaos/advanced_capabilities/providers/follow_ups.py +113 -0
- package/elizaos/advanced_capabilities/providers/knowledge.py +83 -0
- package/elizaos/advanced_capabilities/providers/relationships.py +112 -0
- package/elizaos/advanced_capabilities/providers/roles.py +97 -0
- package/elizaos/advanced_capabilities/providers/settings.py +51 -0
- package/elizaos/advanced_capabilities/services/__init__.py +18 -0
- package/elizaos/advanced_capabilities/services/follow_up.py +138 -0
- package/elizaos/advanced_capabilities/services/rolodex.py +244 -0
- package/elizaos/advanced_memory/__init__.py +3 -0
- package/elizaos/advanced_memory/evaluators.py +97 -0
- package/elizaos/advanced_memory/memory_service.py +556 -0
- package/elizaos/advanced_memory/plugin.py +30 -0
- package/elizaos/advanced_memory/prompts.py +12 -0
- package/elizaos/advanced_memory/providers.py +90 -0
- package/elizaos/advanced_memory/types.py +65 -0
- package/elizaos/advanced_planning/__init__.py +10 -0
- package/elizaos/advanced_planning/actions.py +145 -0
- package/elizaos/advanced_planning/message_classifier.py +127 -0
- package/elizaos/advanced_planning/planning_service.py +712 -0
- package/elizaos/advanced_planning/plugin.py +40 -0
- package/elizaos/advanced_planning/prompts.py +4 -0
- package/elizaos/basic_capabilities/__init__.py +66 -0
- package/elizaos/basic_capabilities/actions/__init__.py +24 -0
- package/elizaos/basic_capabilities/actions/choice.py +140 -0
- package/elizaos/basic_capabilities/actions/ignore.py +66 -0
- package/elizaos/basic_capabilities/actions/none.py +56 -0
- package/elizaos/basic_capabilities/actions/reply.py +120 -0
- package/elizaos/basic_capabilities/providers/__init__.py +54 -0
- package/elizaos/basic_capabilities/providers/action_state.py +113 -0
- package/elizaos/basic_capabilities/providers/actions.py +263 -0
- package/elizaos/basic_capabilities/providers/attachments.py +76 -0
- package/elizaos/basic_capabilities/providers/capabilities.py +62 -0
- package/elizaos/basic_capabilities/providers/character.py +113 -0
- package/elizaos/basic_capabilities/providers/choice.py +73 -0
- package/elizaos/basic_capabilities/providers/context_bench.py +44 -0
- package/elizaos/basic_capabilities/providers/current_time.py +58 -0
- package/elizaos/basic_capabilities/providers/entities.py +99 -0
- package/elizaos/basic_capabilities/providers/evaluators.py +54 -0
- package/elizaos/basic_capabilities/providers/providers_list.py +55 -0
- package/elizaos/basic_capabilities/providers/recent_messages.py +85 -0
- package/elizaos/basic_capabilities/providers/time.py +45 -0
- package/elizaos/basic_capabilities/providers/world.py +93 -0
- package/elizaos/basic_capabilities/services/__init__.py +18 -0
- package/elizaos/basic_capabilities/services/embedding.py +122 -0
- package/elizaos/basic_capabilities/services/task.py +178 -0
- package/elizaos/bootstrap/__init__.py +12 -0
- package/elizaos/bootstrap/actions/__init__.py +68 -0
- package/elizaos/bootstrap/actions/add_contact.py +149 -0
- package/elizaos/bootstrap/actions/choice.py +147 -0
- package/elizaos/bootstrap/actions/follow_room.py +151 -0
- package/elizaos/bootstrap/actions/ignore.py +80 -0
- package/elizaos/bootstrap/actions/image_generation.py +135 -0
- package/elizaos/bootstrap/actions/mute_room.py +151 -0
- package/elizaos/bootstrap/actions/none.py +71 -0
- package/elizaos/bootstrap/actions/remove_contact.py +159 -0
- package/elizaos/bootstrap/actions/reply.py +140 -0
- package/elizaos/bootstrap/actions/roles.py +193 -0
- package/elizaos/bootstrap/actions/schedule_follow_up.py +164 -0
- package/elizaos/bootstrap/actions/search_contacts.py +159 -0
- package/elizaos/bootstrap/actions/send_message.py +173 -0
- package/elizaos/bootstrap/actions/settings.py +165 -0
- package/elizaos/bootstrap/actions/unfollow_room.py +151 -0
- package/elizaos/bootstrap/actions/unmute_room.py +151 -0
- package/elizaos/bootstrap/actions/update_contact.py +178 -0
- package/elizaos/bootstrap/actions/update_entity.py +175 -0
- package/elizaos/bootstrap/autonomy/__init__.py +18 -0
- package/elizaos/bootstrap/autonomy/action.py +197 -0
- package/elizaos/bootstrap/autonomy/providers.py +165 -0
- package/elizaos/bootstrap/autonomy/routes.py +171 -0
- package/elizaos/bootstrap/autonomy/service.py +562 -0
- package/elizaos/bootstrap/autonomy/types.py +18 -0
- package/elizaos/bootstrap/evaluators/__init__.py +19 -0
- package/elizaos/bootstrap/evaluators/reflection.py +118 -0
- package/elizaos/bootstrap/evaluators/relationship_extraction.py +192 -0
- package/elizaos/bootstrap/plugin.py +140 -0
- package/elizaos/bootstrap/providers/__init__.py +80 -0
- package/elizaos/bootstrap/providers/action_state.py +71 -0
- package/elizaos/bootstrap/providers/actions.py +256 -0
- package/elizaos/bootstrap/providers/agent_settings.py +63 -0
- package/elizaos/bootstrap/providers/attachments.py +76 -0
- package/elizaos/bootstrap/providers/capabilities.py +66 -0
- package/elizaos/bootstrap/providers/character.py +128 -0
- package/elizaos/bootstrap/providers/choice.py +77 -0
- package/elizaos/bootstrap/providers/contacts.py +78 -0
- package/elizaos/bootstrap/providers/context_bench.py +49 -0
- package/elizaos/bootstrap/providers/current_time.py +56 -0
- package/elizaos/bootstrap/providers/entities.py +99 -0
- package/elizaos/bootstrap/providers/evaluators.py +58 -0
- package/elizaos/bootstrap/providers/facts.py +86 -0
- package/elizaos/bootstrap/providers/follow_ups.py +116 -0
- package/elizaos/bootstrap/providers/knowledge.py +73 -0
- package/elizaos/bootstrap/providers/providers_list.py +59 -0
- package/elizaos/bootstrap/providers/recent_messages.py +85 -0
- package/elizaos/bootstrap/providers/relationships.py +106 -0
- package/elizaos/bootstrap/providers/roles.py +95 -0
- package/elizaos/bootstrap/providers/settings.py +55 -0
- package/elizaos/bootstrap/providers/time.py +45 -0
- package/elizaos/bootstrap/providers/world.py +97 -0
- package/elizaos/bootstrap/services/__init__.py +26 -0
- package/elizaos/bootstrap/services/embedding.py +122 -0
- package/elizaos/bootstrap/services/follow_up.py +138 -0
- package/elizaos/bootstrap/services/rolodex.py +244 -0
- package/elizaos/bootstrap/services/task.py +585 -0
- package/elizaos/bootstrap/types.py +54 -0
- package/elizaos/bootstrap/utils/__init__.py +7 -0
- package/elizaos/bootstrap/utils/xml.py +69 -0
- package/elizaos/character.py +149 -0
- package/elizaos/logger.py +179 -0
- package/elizaos/media/__init__.py +45 -0
- package/elizaos/media/mime.py +315 -0
- package/elizaos/media/search.py +161 -0
- package/elizaos/media/tests/__init__.py +1 -0
- package/elizaos/media/tests/test_mime.py +117 -0
- package/elizaos/media/tests/test_search.py +156 -0
- package/elizaos/plugin.py +191 -0
- package/elizaos/prompts.py +1071 -0
- package/elizaos/py.typed +0 -0
- package/elizaos/runtime.py +2572 -0
- package/elizaos/services/__init__.py +49 -0
- package/elizaos/services/hook_service.py +511 -0
- package/elizaos/services/message_service.py +1248 -0
- package/elizaos/settings.py +182 -0
- package/elizaos/streaming_context.py +159 -0
- package/elizaos/trajectory_context.py +18 -0
- package/elizaos/types/__init__.py +512 -0
- package/elizaos/types/agent.py +31 -0
- package/elizaos/types/components.py +208 -0
- package/elizaos/types/database.py +64 -0
- package/elizaos/types/environment.py +46 -0
- package/elizaos/types/events.py +47 -0
- package/elizaos/types/memory.py +45 -0
- package/elizaos/types/model.py +393 -0
- package/elizaos/types/plugin.py +188 -0
- package/elizaos/types/primitives.py +100 -0
- package/elizaos/types/runtime.py +460 -0
- package/elizaos/types/service.py +113 -0
- package/elizaos/types/service_interfaces.py +244 -0
- package/elizaos/types/state.py +188 -0
- package/elizaos/types/task.py +29 -0
- package/elizaos/utils/__init__.py +108 -0
- package/elizaos/utils/spec_examples.py +48 -0
- package/elizaos/utils/streaming.py +426 -0
- package/elizaos_atropos_shared/__init__.py +1 -0
- package/elizaos_atropos_shared/canonical_eliza.py +282 -0
- package/package.json +19 -0
- package/pyproject.toml +143 -0
- package/requirements-dev.in +11 -0
- package/requirements-dev.lock +134 -0
- package/requirements.in +9 -0
- package/requirements.lock +64 -0
- package/tests/__init__.py +0 -0
- package/tests/test_action_parameters.py +154 -0
- package/tests/test_actions_provider_examples.py +39 -0
- package/tests/test_advanced_memory_behavior.py +96 -0
- package/tests/test_advanced_memory_flag.py +30 -0
- package/tests/test_advanced_planning_behavior.py +225 -0
- package/tests/test_advanced_planning_flag.py +26 -0
- package/tests/test_autonomy.py +445 -0
- package/tests/test_bootstrap_initialize.py +37 -0
- package/tests/test_character.py +163 -0
- package/tests/test_character_provider.py +231 -0
- package/tests/test_dynamic_prompt_exec.py +561 -0
- package/tests/test_logger_redaction.py +43 -0
- package/tests/test_plugin.py +117 -0
- package/tests/test_runtime.py +422 -0
- package/tests/test_salt_production_enforcement.py +22 -0
- package/tests/test_settings_crypto.py +118 -0
- package/tests/test_streaming.py +295 -0
- package/tests/test_types.py +221 -0
- package/tests/test_uuid_parity.py +46 -0
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
"""Task service implementation with scheduling capabilities.
|
|
2
|
+
|
|
3
|
+
This module provides parity with the TypeScript TaskService, including:
|
|
4
|
+
- Timer-based task checking (tick loop)
|
|
5
|
+
- Task workers with execute/validate callbacks
|
|
6
|
+
- Tag-based filtering ("queue", "repeat")
|
|
7
|
+
- Blocking mechanism to prevent overlapping executions
|
|
8
|
+
- Automatic deletion of non-repeating tasks after execution
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import contextlib
|
|
15
|
+
import time
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
19
|
+
from uuid import UUID, uuid4
|
|
20
|
+
|
|
21
|
+
from elizaos.types import Service, ServiceType
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from elizaos.types import IAgentRuntime, Memory, State
|
|
25
|
+
|
|
26
|
+
# Interval in milliseconds to check for tasks (parity with TypeScript TICK_INTERVAL)
|
|
27
|
+
TICK_INTERVAL_MS = 1000
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TaskStatus(str, Enum):
|
|
31
|
+
"""Task status enum."""
|
|
32
|
+
|
|
33
|
+
PENDING = "pending"
|
|
34
|
+
IN_PROGRESS = "in_progress"
|
|
35
|
+
COMPLETED = "completed"
|
|
36
|
+
FAILED = "failed"
|
|
37
|
+
CANCELLED = "cancelled"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TaskPriority(str, Enum):
|
|
41
|
+
"""Task priority enum."""
|
|
42
|
+
|
|
43
|
+
LOW = "low"
|
|
44
|
+
MEDIUM = "medium"
|
|
45
|
+
HIGH = "high"
|
|
46
|
+
URGENT = "urgent"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class TaskMetadata:
|
|
51
|
+
"""Task metadata containing scheduling and configuration information.
|
|
52
|
+
|
|
53
|
+
Provides parity with TypeScript's TaskMetadata interface.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
target_entity_id: str | None = None
|
|
57
|
+
reason: str | None = None
|
|
58
|
+
priority: str | None = None
|
|
59
|
+
message: str | None = None
|
|
60
|
+
status: str | None = None
|
|
61
|
+
scheduled_at: str | None = None
|
|
62
|
+
snoozed_at: str | None = None
|
|
63
|
+
original_scheduled_at: Any | None = None
|
|
64
|
+
created_at: str | None = None
|
|
65
|
+
completed_at: str | None = None
|
|
66
|
+
completion_notes: str | None = None
|
|
67
|
+
last_executed: str | None = None
|
|
68
|
+
updated_at: int | None = None
|
|
69
|
+
update_interval: int | None = None
|
|
70
|
+
"""Interval in milliseconds between updates or executions for recurring tasks."""
|
|
71
|
+
blocking: bool | None = None
|
|
72
|
+
"""If true (default), the task will block the next scheduled execution while running.
|
|
73
|
+
Set to false to allow overlapping executions."""
|
|
74
|
+
options: list[dict[str, str]] | None = None
|
|
75
|
+
values: dict[str, Any] | None = None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class Task:
|
|
80
|
+
"""Represents a task to be performed, often in the background or at a later time.
|
|
81
|
+
|
|
82
|
+
Tasks are managed by the TaskService and processed by registered TaskWorkers.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
name: str
|
|
86
|
+
id: UUID | None = None
|
|
87
|
+
description: str | None = None
|
|
88
|
+
status: TaskStatus | None = TaskStatus.PENDING
|
|
89
|
+
room_id: UUID | None = None
|
|
90
|
+
world_id: UUID | None = None
|
|
91
|
+
entity_id: UUID | None = None
|
|
92
|
+
tags: list[str] | None = None
|
|
93
|
+
metadata: TaskMetadata | None = None
|
|
94
|
+
created_at: int | None = None
|
|
95
|
+
updated_at: int | None = None
|
|
96
|
+
scheduled_at: int | None = None
|
|
97
|
+
repeat_interval: int | None = None
|
|
98
|
+
data: Any | None = None
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def create(cls, name: str) -> Task:
|
|
102
|
+
"""Create a new task with defaults."""
|
|
103
|
+
now = _current_timestamp()
|
|
104
|
+
return cls(
|
|
105
|
+
id=uuid4(),
|
|
106
|
+
name=name,
|
|
107
|
+
status=TaskStatus.PENDING,
|
|
108
|
+
created_at=now,
|
|
109
|
+
updated_at=now,
|
|
110
|
+
metadata=TaskMetadata(
|
|
111
|
+
updated_at=now,
|
|
112
|
+
created_at=str(now),
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def scheduled(cls, name: str, scheduled_at: int) -> Task:
|
|
118
|
+
"""Create a scheduled task."""
|
|
119
|
+
task = cls.create(name)
|
|
120
|
+
task.scheduled_at = scheduled_at
|
|
121
|
+
return task
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def repeating(cls, name: str, interval_ms: int) -> Task:
|
|
125
|
+
"""Create a repeating task with the given interval."""
|
|
126
|
+
task = cls.create(name)
|
|
127
|
+
task.tags = ["queue", "repeat"]
|
|
128
|
+
if task.metadata:
|
|
129
|
+
task.metadata.update_interval = interval_ms
|
|
130
|
+
task.metadata.blocking = True # Default to blocking
|
|
131
|
+
else:
|
|
132
|
+
task.metadata = TaskMetadata(
|
|
133
|
+
update_interval=interval_ms,
|
|
134
|
+
blocking=True,
|
|
135
|
+
)
|
|
136
|
+
return task
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def repeating_with_blocking(cls, name: str, interval_ms: int, blocking: bool) -> Task:
|
|
140
|
+
"""Create a repeating task with blocking configuration."""
|
|
141
|
+
task = cls.repeating(name, interval_ms)
|
|
142
|
+
if task.metadata:
|
|
143
|
+
task.metadata.blocking = blocking
|
|
144
|
+
return task
|
|
145
|
+
|
|
146
|
+
def is_repeating(self) -> bool:
|
|
147
|
+
"""Check if this task is a repeating task."""
|
|
148
|
+
return self.tags is not None and "repeat" in self.tags
|
|
149
|
+
|
|
150
|
+
def is_blocking(self) -> bool:
|
|
151
|
+
"""Check if this task should block overlapping executions."""
|
|
152
|
+
if self.metadata and self.metadata.blocking is not None:
|
|
153
|
+
return self.metadata.blocking
|
|
154
|
+
return True # Default to blocking
|
|
155
|
+
|
|
156
|
+
def get_update_interval(self) -> int | None:
|
|
157
|
+
"""Get the update interval in milliseconds."""
|
|
158
|
+
if self.metadata:
|
|
159
|
+
return self.metadata.update_interval
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@runtime_checkable
|
|
164
|
+
class TaskWorker(Protocol):
|
|
165
|
+
"""Task worker protocol - defines the contract for executing tasks.
|
|
166
|
+
|
|
167
|
+
Parity with TypeScript's TaskWorker interface.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def name(self) -> str:
|
|
172
|
+
"""The unique name of the task type this worker handles."""
|
|
173
|
+
...
|
|
174
|
+
|
|
175
|
+
async def execute(
|
|
176
|
+
self,
|
|
177
|
+
runtime: IAgentRuntime,
|
|
178
|
+
options: dict[str, Any],
|
|
179
|
+
task: Task,
|
|
180
|
+
) -> None:
|
|
181
|
+
"""Execute the task."""
|
|
182
|
+
...
|
|
183
|
+
|
|
184
|
+
async def validate(
|
|
185
|
+
self,
|
|
186
|
+
runtime: IAgentRuntime,
|
|
187
|
+
message: Memory,
|
|
188
|
+
state: State,
|
|
189
|
+
) -> bool:
|
|
190
|
+
"""Optional validation function."""
|
|
191
|
+
...
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class TaskService(Service):
|
|
195
|
+
"""Service for managing and scheduling tasks.
|
|
196
|
+
|
|
197
|
+
Provides parity with TypeScript's TaskService.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
name = "task"
|
|
201
|
+
service_type = ServiceType.TASK
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def capability_description(self) -> str:
|
|
205
|
+
"""Capability description for the service."""
|
|
206
|
+
return "Task management service with scheduling and worker execution."
|
|
207
|
+
|
|
208
|
+
def __init__(self) -> None:
|
|
209
|
+
"""Initialize the task service."""
|
|
210
|
+
self._workers: dict[str, TaskWorker] = {}
|
|
211
|
+
self._tasks: dict[str, Task] = {}
|
|
212
|
+
self._executing_tasks: set[str] = set()
|
|
213
|
+
self._runtime: IAgentRuntime | None = None
|
|
214
|
+
self._stop_flag = False
|
|
215
|
+
self._loop_task: asyncio.Task[None] | None = None
|
|
216
|
+
|
|
217
|
+
@classmethod
|
|
218
|
+
async def start(cls, runtime: IAgentRuntime) -> TaskService:
|
|
219
|
+
"""Start the task service."""
|
|
220
|
+
service = cls()
|
|
221
|
+
service._runtime = runtime
|
|
222
|
+
service._stop_flag = False
|
|
223
|
+
runtime.logger.info(
|
|
224
|
+
"Task service started",
|
|
225
|
+
src="service:task",
|
|
226
|
+
agentId=str(runtime.agent_id),
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Start the timer loop
|
|
230
|
+
service._loop_task = asyncio.create_task(service._run_timer())
|
|
231
|
+
|
|
232
|
+
return service
|
|
233
|
+
|
|
234
|
+
async def stop(self) -> None:
|
|
235
|
+
"""Stop the task service."""
|
|
236
|
+
self._stop_flag = True
|
|
237
|
+
|
|
238
|
+
if self._loop_task:
|
|
239
|
+
self._loop_task.cancel()
|
|
240
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
241
|
+
await self._loop_task
|
|
242
|
+
self._loop_task = None
|
|
243
|
+
|
|
244
|
+
if self._runtime:
|
|
245
|
+
self._runtime.logger.info(
|
|
246
|
+
"Task service stopped",
|
|
247
|
+
src="service:task",
|
|
248
|
+
agentId=str(self._runtime.agent_id),
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
self._tasks.clear()
|
|
252
|
+
self._executing_tasks.clear()
|
|
253
|
+
self._runtime = None
|
|
254
|
+
|
|
255
|
+
async def register_worker(self, worker: TaskWorker) -> None:
|
|
256
|
+
"""Register a task worker."""
|
|
257
|
+
self._workers[worker.name] = worker
|
|
258
|
+
if self._runtime:
|
|
259
|
+
self._runtime.logger.debug(
|
|
260
|
+
f"Registered task worker: {worker.name}",
|
|
261
|
+
src="service:task",
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def has_worker(self, name: str) -> bool:
|
|
265
|
+
"""Check if a worker exists for the given task name."""
|
|
266
|
+
return name in self._workers
|
|
267
|
+
|
|
268
|
+
async def create_task(self, task: Task) -> Task:
|
|
269
|
+
"""Create a new task."""
|
|
270
|
+
now = _current_timestamp()
|
|
271
|
+
|
|
272
|
+
# Ensure task has an ID
|
|
273
|
+
if task.id is None:
|
|
274
|
+
task.id = uuid4()
|
|
275
|
+
|
|
276
|
+
# Set timestamps
|
|
277
|
+
task.created_at = now
|
|
278
|
+
task.updated_at = now
|
|
279
|
+
|
|
280
|
+
# Ensure metadata exists with timestamps
|
|
281
|
+
if task.metadata is None:
|
|
282
|
+
task.metadata = TaskMetadata()
|
|
283
|
+
if task.metadata.updated_at is None:
|
|
284
|
+
task.metadata.updated_at = now
|
|
285
|
+
if task.metadata.created_at is None:
|
|
286
|
+
task.metadata.created_at = str(now)
|
|
287
|
+
|
|
288
|
+
task_id = str(task.id)
|
|
289
|
+
|
|
290
|
+
if self._runtime:
|
|
291
|
+
self._runtime.logger.debug(
|
|
292
|
+
f"Task created: {task_id}",
|
|
293
|
+
src="service:task",
|
|
294
|
+
taskId=task_id,
|
|
295
|
+
taskName=task.name,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
self._tasks[task_id] = task
|
|
299
|
+
return task
|
|
300
|
+
|
|
301
|
+
async def get_task(self, task_id: str) -> Task | None:
|
|
302
|
+
"""Get a task by ID."""
|
|
303
|
+
return self._tasks.get(task_id)
|
|
304
|
+
|
|
305
|
+
async def get_tasks_by_name(self, name: str) -> list[Task]:
|
|
306
|
+
"""Get tasks by name."""
|
|
307
|
+
return [t for t in self._tasks.values() if t.name == name]
|
|
308
|
+
|
|
309
|
+
async def get_tasks_by_tags(self, tags: list[str]) -> list[Task]:
|
|
310
|
+
"""Get tasks with specific tags."""
|
|
311
|
+
return [t for t in self._tasks.values() if t.tags and all(tag in t.tags for tag in tags)]
|
|
312
|
+
|
|
313
|
+
async def update_task(
|
|
314
|
+
self, task_id: str, metadata: TaskMetadata | None = None, tags: list[str] | None = None
|
|
315
|
+
) -> Task | None:
|
|
316
|
+
"""Update a task."""
|
|
317
|
+
task = self._tasks.get(task_id)
|
|
318
|
+
if task is None:
|
|
319
|
+
return None
|
|
320
|
+
|
|
321
|
+
task.updated_at = _current_timestamp()
|
|
322
|
+
|
|
323
|
+
if metadata:
|
|
324
|
+
if task.metadata is None:
|
|
325
|
+
task.metadata = metadata
|
|
326
|
+
else:
|
|
327
|
+
if metadata.updated_at is not None:
|
|
328
|
+
task.metadata.updated_at = metadata.updated_at
|
|
329
|
+
if metadata.update_interval is not None:
|
|
330
|
+
task.metadata.update_interval = metadata.update_interval
|
|
331
|
+
if metadata.blocking is not None:
|
|
332
|
+
task.metadata.blocking = metadata.blocking
|
|
333
|
+
if metadata.values is not None:
|
|
334
|
+
if task.metadata.values is None:
|
|
335
|
+
task.metadata.values = metadata.values
|
|
336
|
+
else:
|
|
337
|
+
task.metadata.values.update(metadata.values)
|
|
338
|
+
|
|
339
|
+
if tags is not None:
|
|
340
|
+
task.tags = tags
|
|
341
|
+
|
|
342
|
+
if self._runtime:
|
|
343
|
+
self._runtime.logger.debug(
|
|
344
|
+
f"Task updated: {task_id}",
|
|
345
|
+
src="service:task",
|
|
346
|
+
taskId=task_id,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
return task
|
|
350
|
+
|
|
351
|
+
async def delete_task(self, task_id: str) -> bool:
|
|
352
|
+
"""Delete a task."""
|
|
353
|
+
if task_id in self._tasks:
|
|
354
|
+
del self._tasks[task_id]
|
|
355
|
+
self._executing_tasks.discard(task_id)
|
|
356
|
+
|
|
357
|
+
if self._runtime:
|
|
358
|
+
self._runtime.logger.debug(
|
|
359
|
+
f"Task deleted: {task_id}",
|
|
360
|
+
src="service:task",
|
|
361
|
+
taskId=task_id,
|
|
362
|
+
)
|
|
363
|
+
return True
|
|
364
|
+
return False
|
|
365
|
+
|
|
366
|
+
async def _validate_tasks(self, tasks: list[Task]) -> list[Task]:
|
|
367
|
+
"""Validate an array of Task objects.
|
|
368
|
+
|
|
369
|
+
Skips tasks without IDs or if no worker is found for the task.
|
|
370
|
+
If a worker has a `validate` function, it will run validation.
|
|
371
|
+
Parity with TypeScript's validateTasks.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
tasks: An array of Task objects to validate.
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
An array of validated Task objects.
|
|
378
|
+
"""
|
|
379
|
+
if not self._runtime:
|
|
380
|
+
return []
|
|
381
|
+
|
|
382
|
+
validated_tasks: list[Task] = []
|
|
383
|
+
|
|
384
|
+
for task in tasks:
|
|
385
|
+
# Skip tasks without IDs
|
|
386
|
+
if task.id is None:
|
|
387
|
+
continue
|
|
388
|
+
|
|
389
|
+
worker = self._workers.get(task.name)
|
|
390
|
+
|
|
391
|
+
# Skip if no worker found for task
|
|
392
|
+
if worker is None:
|
|
393
|
+
continue
|
|
394
|
+
|
|
395
|
+
# If worker has validate function, run validation
|
|
396
|
+
# Pass empty dict/object for message and state since validation is time-based
|
|
397
|
+
if hasattr(worker, "validate"):
|
|
398
|
+
is_valid = await worker.validate(
|
|
399
|
+
self._runtime,
|
|
400
|
+
{}, # Empty message (dict representation)
|
|
401
|
+
{}, # Empty state (dict representation)
|
|
402
|
+
)
|
|
403
|
+
if not is_valid:
|
|
404
|
+
continue
|
|
405
|
+
|
|
406
|
+
validated_tasks.append(task)
|
|
407
|
+
|
|
408
|
+
return validated_tasks
|
|
409
|
+
|
|
410
|
+
async def _run_timer(self) -> None:
|
|
411
|
+
"""Run the timer loop for checking tasks."""
|
|
412
|
+
interval_seconds = TICK_INTERVAL_MS / 1000
|
|
413
|
+
|
|
414
|
+
while not self._stop_flag:
|
|
415
|
+
try:
|
|
416
|
+
await asyncio.sleep(interval_seconds)
|
|
417
|
+
|
|
418
|
+
if self._stop_flag:
|
|
419
|
+
break
|
|
420
|
+
|
|
421
|
+
await self._check_tasks()
|
|
422
|
+
except asyncio.CancelledError:
|
|
423
|
+
break
|
|
424
|
+
except Exception as e:
|
|
425
|
+
if self._runtime:
|
|
426
|
+
self._runtime.logger.warning(
|
|
427
|
+
f"Error checking tasks: {e}",
|
|
428
|
+
src="service:task",
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
if self._runtime:
|
|
432
|
+
self._runtime.logger.info(
|
|
433
|
+
"Task service timer loop stopped",
|
|
434
|
+
src="service:task",
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
async def _check_tasks(self) -> None:
|
|
438
|
+
"""Check tasks and execute those that are due."""
|
|
439
|
+
now = _current_timestamp()
|
|
440
|
+
|
|
441
|
+
# Get all tasks with "queue" tag
|
|
442
|
+
queue_tasks = [t for t in self._tasks.values() if t.tags and "queue" in t.tags]
|
|
443
|
+
|
|
444
|
+
# Validate the tasks (parity with TypeScript)
|
|
445
|
+
tasks = await self._validate_tasks(queue_tasks)
|
|
446
|
+
|
|
447
|
+
for task in tasks:
|
|
448
|
+
if task.id is None:
|
|
449
|
+
continue
|
|
450
|
+
|
|
451
|
+
task_id = str(task.id)
|
|
452
|
+
|
|
453
|
+
# Check if worker exists for this task
|
|
454
|
+
worker = self._workers.get(task.name)
|
|
455
|
+
if worker is None:
|
|
456
|
+
continue
|
|
457
|
+
|
|
458
|
+
# For non-repeating tasks, execute immediately
|
|
459
|
+
if not task.is_repeating():
|
|
460
|
+
await self._execute_task(task, task_id, worker)
|
|
461
|
+
continue
|
|
462
|
+
|
|
463
|
+
# For repeating tasks, check if interval has elapsed
|
|
464
|
+
task_start_time = (
|
|
465
|
+
task.updated_at or (task.metadata.updated_at if task.metadata else None) or 0
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
update_interval = task.get_update_interval() or 0
|
|
469
|
+
|
|
470
|
+
# Check for immediate execution on first run
|
|
471
|
+
metadata_updated_at = task.metadata.updated_at if task.metadata else None
|
|
472
|
+
metadata_created_at = None
|
|
473
|
+
if task.metadata and task.metadata.created_at:
|
|
474
|
+
with contextlib.suppress(ValueError):
|
|
475
|
+
metadata_created_at = int(task.metadata.created_at)
|
|
476
|
+
|
|
477
|
+
if metadata_updated_at == metadata_created_at:
|
|
478
|
+
if task.tags and "immediate" in task.tags:
|
|
479
|
+
if self._runtime:
|
|
480
|
+
self._runtime.logger.debug(
|
|
481
|
+
f"Immediately running task: {task.name}",
|
|
482
|
+
src="service:task",
|
|
483
|
+
)
|
|
484
|
+
await self._execute_task(task, task_id, worker)
|
|
485
|
+
continue
|
|
486
|
+
|
|
487
|
+
# Check if enough time has passed
|
|
488
|
+
if now - task_start_time >= update_interval:
|
|
489
|
+
# Check blocking
|
|
490
|
+
is_blocking = task.is_blocking()
|
|
491
|
+
if is_blocking and task_id in self._executing_tasks:
|
|
492
|
+
if self._runtime:
|
|
493
|
+
self._runtime.logger.debug(
|
|
494
|
+
f"Skipping task {task.name} - already executing (blocking enabled)",
|
|
495
|
+
src="service:task",
|
|
496
|
+
taskId=task_id,
|
|
497
|
+
)
|
|
498
|
+
continue
|
|
499
|
+
|
|
500
|
+
if self._runtime:
|
|
501
|
+
self._runtime.logger.debug(
|
|
502
|
+
f"Executing task {task.name} - interval elapsed",
|
|
503
|
+
src="service:task",
|
|
504
|
+
intervalMs=update_interval,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
await self._execute_task(task, task_id, worker)
|
|
508
|
+
|
|
509
|
+
async def _execute_task(self, task: Task, task_id: str, worker: TaskWorker) -> None:
|
|
510
|
+
"""Execute a single task."""
|
|
511
|
+
if self._runtime is None:
|
|
512
|
+
return
|
|
513
|
+
|
|
514
|
+
# Mark task as executing
|
|
515
|
+
self._executing_tasks.add(task_id)
|
|
516
|
+
start_time = _current_timestamp()
|
|
517
|
+
|
|
518
|
+
# For repeating tasks, update the timestamp before execution
|
|
519
|
+
if task.is_repeating():
|
|
520
|
+
task.updated_at = _current_timestamp()
|
|
521
|
+
if task.metadata:
|
|
522
|
+
task.metadata.updated_at = _current_timestamp()
|
|
523
|
+
if self._runtime:
|
|
524
|
+
self._runtime.logger.debug(
|
|
525
|
+
f"Updated repeating task timestamp: {task.name}",
|
|
526
|
+
src="service:task",
|
|
527
|
+
taskId=task_id,
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
# Execute the task
|
|
531
|
+
options = task.metadata.values if task.metadata and task.metadata.values else {}
|
|
532
|
+
|
|
533
|
+
if self._runtime:
|
|
534
|
+
self._runtime.logger.debug(
|
|
535
|
+
f"Executing task: {task.name}",
|
|
536
|
+
src="service:task",
|
|
537
|
+
taskId=task_id,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
try:
|
|
541
|
+
await worker.execute(self._runtime, options, task)
|
|
542
|
+
except Exception as e:
|
|
543
|
+
if self._runtime:
|
|
544
|
+
self._runtime.logger.warning(
|
|
545
|
+
f"Task execution failed: {task.name} - {e}",
|
|
546
|
+
src="service:task",
|
|
547
|
+
taskId=task_id,
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
# For non-repeating tasks, delete after execution
|
|
551
|
+
if not task.is_repeating():
|
|
552
|
+
self._tasks.pop(task_id, None)
|
|
553
|
+
if self._runtime:
|
|
554
|
+
self._runtime.logger.debug(
|
|
555
|
+
f"Deleted non-repeating task after execution: {task.name}",
|
|
556
|
+
src="service:task",
|
|
557
|
+
taskId=task_id,
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
# Always remove from executing set
|
|
561
|
+
self._executing_tasks.discard(task_id)
|
|
562
|
+
|
|
563
|
+
duration_ms = _current_timestamp() - start_time
|
|
564
|
+
if self._runtime:
|
|
565
|
+
self._runtime.logger.debug(
|
|
566
|
+
f"Task execution completed: {task.name}",
|
|
567
|
+
src="service:task",
|
|
568
|
+
taskId=task_id,
|
|
569
|
+
durationMs=duration_ms,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _current_timestamp() -> int:
|
|
574
|
+
"""Get the current timestamp in milliseconds."""
|
|
575
|
+
return int(time.time() * 1000)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
__all__ = [
|
|
579
|
+
"Task",
|
|
580
|
+
"TaskMetadata",
|
|
581
|
+
"TaskPriority",
|
|
582
|
+
"TaskService",
|
|
583
|
+
"TaskStatus",
|
|
584
|
+
"TaskWorker",
|
|
585
|
+
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Local type definitions for the elizaOS Bootstrap Plugin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class CapabilityConfig:
|
|
13
|
+
"""Configuration for bootstrap capabilities.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
disable_basic: If True, disables basic capabilities (reply, ignore, none, choice).
|
|
17
|
+
enable_extended: If True, enables extended/advanced capabilities.
|
|
18
|
+
advanced_capabilities: Alias for enable_extended (for consistency with TypeScript).
|
|
19
|
+
skip_character_provider: If True, excludes the CHARACTER provider.
|
|
20
|
+
enable_autonomy: If True, enables autonomy capabilities.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
disable_basic: bool = False
|
|
24
|
+
enable_extended: bool = False
|
|
25
|
+
advanced_capabilities: bool = False # Alias for enable_extended
|
|
26
|
+
skip_character_provider: bool = False
|
|
27
|
+
enable_autonomy: bool = False
|
|
28
|
+
|
|
29
|
+
def __post_init__(self) -> None:
|
|
30
|
+
"""Post-initialization to handle aliasing."""
|
|
31
|
+
# Support both enable_extended and advanced_capabilities
|
|
32
|
+
if self.advanced_capabilities and not self.enable_extended:
|
|
33
|
+
self.enable_extended = True
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class EvaluatorResult(BaseModel):
|
|
37
|
+
"""Result from an evaluator."""
|
|
38
|
+
|
|
39
|
+
score: int = Field(..., description="Numeric score 0-100")
|
|
40
|
+
passed: bool = Field(..., description="Whether evaluation passed")
|
|
41
|
+
reason: str = Field(..., description="Reason for the result")
|
|
42
|
+
details: dict[str, Any] = Field(default_factory=dict, description="Additional details")
|
|
43
|
+
|
|
44
|
+
model_config = {"populate_by_name": True}
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def pass_result(cls, score: int, reason: str) -> EvaluatorResult:
|
|
48
|
+
"""Create a passing evaluation result."""
|
|
49
|
+
return cls(score=score, passed=True, reason=reason)
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def fail_result(cls, score: int, reason: str) -> EvaluatorResult:
|
|
53
|
+
"""Create a failing evaluation result."""
|
|
54
|
+
return cls(score=score, passed=False, reason=reason)
|