agentexec 0.1.0__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.
- agentexec/__init__.py +73 -0
- agentexec/activity/__init__.py +50 -0
- agentexec/activity/models.py +294 -0
- agentexec/activity/schemas.py +70 -0
- agentexec/activity/tracker.py +267 -0
- agentexec/config.py +72 -0
- agentexec/core/__init__.py +0 -0
- agentexec/core/models.py +23 -0
- agentexec/core/queue.py +109 -0
- agentexec/core/redis_client.py +40 -0
- agentexec/core/task.py +132 -0
- agentexec/core/worker.py +304 -0
- agentexec/runners/__init__.py +13 -0
- agentexec/runners/base.py +135 -0
- agentexec/runners/openai.py +237 -0
- agentexec-0.1.0.dist-info/METADATA +370 -0
- agentexec-0.1.0.dist-info/RECORD +18 -0
- agentexec-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
from sqlalchemy.orm import Session
|
|
4
|
+
|
|
5
|
+
from agentexec.activity.models import Activity, ActivityLog, Status
|
|
6
|
+
from agentexec.activity.schemas import (
|
|
7
|
+
ActivityDetailSchema,
|
|
8
|
+
ActivityListItemSchema,
|
|
9
|
+
ActivityListSchema,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def generate_agent_id() -> uuid.UUID:
|
|
14
|
+
"""Generate a new UUID for an agent.
|
|
15
|
+
|
|
16
|
+
This is the centralized function for generating agent IDs.
|
|
17
|
+
Users can override this if they need custom ID generation logic.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
A new UUID4 object
|
|
21
|
+
"""
|
|
22
|
+
return uuid.uuid4()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def normalize_agent_id(agent_id: str | uuid.UUID) -> uuid.UUID:
|
|
26
|
+
"""Normalize agent_id to UUID object.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
agent_id: Either a string UUID or UUID object
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
UUID object
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
ValueError: If string is not a valid UUID
|
|
36
|
+
"""
|
|
37
|
+
if isinstance(agent_id, str):
|
|
38
|
+
return uuid.UUID(agent_id)
|
|
39
|
+
return agent_id
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def create(
|
|
43
|
+
task_name: str,
|
|
44
|
+
message: str = "Agent queued",
|
|
45
|
+
agent_id: str | uuid.UUID | None = None,
|
|
46
|
+
session: Session | None = None,
|
|
47
|
+
) -> uuid.UUID:
|
|
48
|
+
"""Create a new agent activity record with initial queued status.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
task_name: Name/type of the task (e.g., "research", "analysis")
|
|
52
|
+
initial_message: Initial log message (default: "Agent queued")
|
|
53
|
+
agent_id: Optional custom agent ID (string or UUID). If not provided, one will be auto-generated.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
The agent_id (as UUID object) of the created record
|
|
57
|
+
"""
|
|
58
|
+
agent_id = normalize_agent_id(agent_id) if agent_id else generate_agent_id()
|
|
59
|
+
|
|
60
|
+
if session is None:
|
|
61
|
+
from agentexec.core.worker import get_worker_session
|
|
62
|
+
db = get_worker_session()
|
|
63
|
+
else:
|
|
64
|
+
db = session
|
|
65
|
+
|
|
66
|
+
activity_record = Activity(
|
|
67
|
+
agent_id=agent_id,
|
|
68
|
+
agent_type=task_name,
|
|
69
|
+
)
|
|
70
|
+
db.add(activity_record)
|
|
71
|
+
db.flush()
|
|
72
|
+
|
|
73
|
+
log = ActivityLog(
|
|
74
|
+
activity_id=activity_record.id,
|
|
75
|
+
message=message,
|
|
76
|
+
status=Status.QUEUED,
|
|
77
|
+
completion_percentage=0,
|
|
78
|
+
)
|
|
79
|
+
db.add(log)
|
|
80
|
+
db.commit()
|
|
81
|
+
|
|
82
|
+
return agent_id
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def update(
|
|
86
|
+
agent_id: str | uuid.UUID,
|
|
87
|
+
message: str,
|
|
88
|
+
completion_percentage: int | None = None,
|
|
89
|
+
status: Status | None = None,
|
|
90
|
+
session: Session | None = None,
|
|
91
|
+
) -> bool:
|
|
92
|
+
"""Update an agent's activity by adding a new log message.
|
|
93
|
+
|
|
94
|
+
This function will set the status to RUNNING unless a different status is explicitly provided.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
agent_id: The agent_id of the agent to update
|
|
98
|
+
message: Log message to append
|
|
99
|
+
completion_percentage: Optional completion percentage (0-100)
|
|
100
|
+
status: Optional status to set (default: RUNNING)
|
|
101
|
+
session: Optional SQLAlchemy session. If not provided, uses global session factory.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
True if successful
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
ValueError: If agent_id not found
|
|
108
|
+
"""
|
|
109
|
+
if session is None:
|
|
110
|
+
from agentexec.core.worker import get_worker_session
|
|
111
|
+
db = get_worker_session()
|
|
112
|
+
else:
|
|
113
|
+
db = session
|
|
114
|
+
|
|
115
|
+
Activity.append_log(
|
|
116
|
+
session=db,
|
|
117
|
+
agent_id=normalize_agent_id(agent_id),
|
|
118
|
+
message=message,
|
|
119
|
+
status=status if status else Status.RUNNING,
|
|
120
|
+
completion_percentage=completion_percentage,
|
|
121
|
+
)
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def complete(
|
|
126
|
+
agent_id: str | uuid.UUID,
|
|
127
|
+
message: str = "Agent completed",
|
|
128
|
+
completion_percentage: int = 100,
|
|
129
|
+
session: Session | None = None,
|
|
130
|
+
) -> bool:
|
|
131
|
+
"""Mark an agent activity as complete.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
agent_id: The agent_id of the agent to mark as complete
|
|
135
|
+
message: Log message (default: "Agent completed")
|
|
136
|
+
completion_percentage: Completion percentage (default: 100)
|
|
137
|
+
session: Optional SQLAlchemy session. If not provided, uses global session factory.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
True if successful
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
ValueError: If agent_id not found
|
|
144
|
+
"""
|
|
145
|
+
if session is None:
|
|
146
|
+
from agentexec.core.worker import get_worker_session
|
|
147
|
+
db = get_worker_session()
|
|
148
|
+
else:
|
|
149
|
+
db = session
|
|
150
|
+
|
|
151
|
+
Activity.append_log(
|
|
152
|
+
session=db,
|
|
153
|
+
agent_id=normalize_agent_id(agent_id),
|
|
154
|
+
message=message,
|
|
155
|
+
status=Status.COMPLETE,
|
|
156
|
+
completion_percentage=completion_percentage,
|
|
157
|
+
)
|
|
158
|
+
return True
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def error(
|
|
162
|
+
agent_id: str | uuid.UUID,
|
|
163
|
+
message: str = "Agent failed",
|
|
164
|
+
completion_percentage: int = 100,
|
|
165
|
+
session: Session | None = None,
|
|
166
|
+
) -> bool:
|
|
167
|
+
"""Mark an agent activity as failed.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
agent_id: The agent_id of the agent to mark as failed
|
|
171
|
+
message: Log message (default: "Agent failed")
|
|
172
|
+
completion_percentage: Completion percentage (default: 100)
|
|
173
|
+
session: Optional SQLAlchemy session. If not provided, uses global session factory.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
True if successful
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
ValueError: If agent_id not found
|
|
180
|
+
"""
|
|
181
|
+
if session is None:
|
|
182
|
+
from agentexec.core.worker import get_worker_session
|
|
183
|
+
db = get_worker_session()
|
|
184
|
+
else:
|
|
185
|
+
db = session
|
|
186
|
+
|
|
187
|
+
Activity.append_log(
|
|
188
|
+
session=db,
|
|
189
|
+
agent_id=normalize_agent_id(agent_id),
|
|
190
|
+
message=message,
|
|
191
|
+
status=Status.ERROR,
|
|
192
|
+
completion_percentage=completion_percentage,
|
|
193
|
+
)
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def cancel_pending(
|
|
198
|
+
session: Session | None = None,
|
|
199
|
+
) -> int:
|
|
200
|
+
"""Mark all queued and running agents as canceled.
|
|
201
|
+
|
|
202
|
+
Useful during application shutdown to clean up pending tasks.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Number of agents that were canceled
|
|
206
|
+
"""
|
|
207
|
+
if session is None:
|
|
208
|
+
from agentexec.core.worker import get_worker_session
|
|
209
|
+
db = get_worker_session()
|
|
210
|
+
else:
|
|
211
|
+
db = session
|
|
212
|
+
|
|
213
|
+
pending_agent_ids = Activity.get_pending_ids(db)
|
|
214
|
+
for agent_id in pending_agent_ids:
|
|
215
|
+
Activity.append_log(
|
|
216
|
+
session=db,
|
|
217
|
+
agent_id=agent_id,
|
|
218
|
+
message="Canceled due to shutdown",
|
|
219
|
+
status=Status.CANCELED,
|
|
220
|
+
completion_percentage=None,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
return len(pending_agent_ids)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def list(
|
|
227
|
+
session: Session,
|
|
228
|
+
page: int = 1,
|
|
229
|
+
page_size: int = 50,
|
|
230
|
+
) -> ActivityListSchema:
|
|
231
|
+
"""List activities with pagination.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
session: SQLAlchemy session to use for the query
|
|
235
|
+
page: Page number (1-indexed)
|
|
236
|
+
page_size: Number of items per page
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
ActivityList with list of ActivityListItemSchema items
|
|
240
|
+
"""
|
|
241
|
+
total = session.query(Activity).count()
|
|
242
|
+
rows = Activity.get_list(session, page=page, page_size=page_size)
|
|
243
|
+
|
|
244
|
+
return ActivityListSchema(
|
|
245
|
+
items=[ActivityListItemSchema.model_validate(row) for row in rows],
|
|
246
|
+
total=total,
|
|
247
|
+
page=page,
|
|
248
|
+
page_size=page_size,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def detail(
|
|
253
|
+
session: Session,
|
|
254
|
+
agent_id: str | uuid.UUID,
|
|
255
|
+
) -> ActivityDetailSchema | None:
|
|
256
|
+
"""Get a single activity by agent_id with all logs.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
session: SQLAlchemy session to use for the query
|
|
260
|
+
agent_id: The agent_id to look up
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
ActivityDetailSchema with full log history, or None if not found
|
|
264
|
+
"""
|
|
265
|
+
if item := Activity.get_by_agent_id(session, agent_id):
|
|
266
|
+
return ActivityDetailSchema.model_validate(item)
|
|
267
|
+
return None
|
agentexec/config.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from pydantic import AliasChoices, Field
|
|
2
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Config(BaseSettings):
|
|
6
|
+
model_config = SettingsConfigDict(
|
|
7
|
+
env_file=".env",
|
|
8
|
+
env_file_encoding="utf-8",
|
|
9
|
+
case_sensitive=False,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
debug: bool = Field(
|
|
13
|
+
default=False,
|
|
14
|
+
description="Enable debug logging",
|
|
15
|
+
validation_alias=AliasChoices("AGENTEXEC_DEBUG", "DEBUG"),
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
table_prefix: str = Field(
|
|
19
|
+
default="agentexec_",
|
|
20
|
+
description="Prefix for database table names",
|
|
21
|
+
validation_alias="AGENTEXEC_TABLE_PREFIX",
|
|
22
|
+
)
|
|
23
|
+
queue_name: str = Field(
|
|
24
|
+
default="agentexec_tasks",
|
|
25
|
+
description="Name of the Redis list to use as task queue",
|
|
26
|
+
validation_alias="AGENTEXEC_QUEUE_NAME",
|
|
27
|
+
)
|
|
28
|
+
num_workers: int = Field(
|
|
29
|
+
default=4,
|
|
30
|
+
description="Number of worker processes to spawn",
|
|
31
|
+
validation_alias="AGENTEXEC_NUM_WORKERS",
|
|
32
|
+
)
|
|
33
|
+
graceful_shutdown_timeout: int = Field(
|
|
34
|
+
default=300,
|
|
35
|
+
description="Maximum seconds to wait for workers to finish on shutdown",
|
|
36
|
+
validation_alias="AGENTEXEC_GRACEFUL_SHUTDOWN_TIMEOUT",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
activity_message_create: str = Field(
|
|
40
|
+
default="Waiting to start.",
|
|
41
|
+
description="Default message when creating a new agent activity",
|
|
42
|
+
validation_alias="AGENTEXEC_ACTIVITY_MESSAGE_CREATE",
|
|
43
|
+
)
|
|
44
|
+
activity_message_complete: str = Field(
|
|
45
|
+
default="Completed successfully.",
|
|
46
|
+
description="Default message when an agent activity completes successfully",
|
|
47
|
+
validation_alias="AGENTEXEC_ACTIVITY_MESSAGE_COMPLETE",
|
|
48
|
+
)
|
|
49
|
+
activity_message_error: str = Field(
|
|
50
|
+
default="An error occurred during execution.",
|
|
51
|
+
description="Default message when an agent activity encounters an error",
|
|
52
|
+
validation_alias="AGENTEXEC_ACTIVITY_MESSAGE_ERROR",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
redis_url: str = Field(
|
|
56
|
+
default="redis://localhost:6379/0",
|
|
57
|
+
description="Redis connection URL",
|
|
58
|
+
validation_alias=AliasChoices("AGENTEXEC_REDIS_URL", "REDIS_URL"),
|
|
59
|
+
)
|
|
60
|
+
redis_pool_size: int = Field(
|
|
61
|
+
default=10,
|
|
62
|
+
description="Redis connection pool size",
|
|
63
|
+
validation_alias=AliasChoices("AGENTEXEC_REDIS_POOL_SIZE", "REDIS_POOL_SIZE"),
|
|
64
|
+
)
|
|
65
|
+
redis_pool_timeout: int = Field(
|
|
66
|
+
default=5,
|
|
67
|
+
description="Redis connection pool timeout in seconds",
|
|
68
|
+
validation_alias=AliasChoices("AGENTEXEC_REDIS_POOL_TIMEOUT", "REDIS_POOL_TIMEOUT"),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
CONF = Config()
|
|
File without changes
|
agentexec/core/models.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Database configuration for agent-runner.
|
|
2
|
+
|
|
3
|
+
This module provides the declarative base class for all SQLAlchemy models.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Base(DeclarativeBase):
|
|
10
|
+
"""Base class for all SQLAlchemy models in agent-runner.
|
|
11
|
+
|
|
12
|
+
Users can reference this base in their Alembic migrations to include
|
|
13
|
+
agent-runner's tables alongside their own application tables.
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
# In alembic/env.py
|
|
17
|
+
import agentexec as ax
|
|
18
|
+
from models import Base
|
|
19
|
+
|
|
20
|
+
target_metadata = [Base.metadata, ax.Base.metadata]
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
pass
|
agentexec/core/queue.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Any, cast
|
|
4
|
+
|
|
5
|
+
from agentexec.config import CONF
|
|
6
|
+
from agentexec.core.redis_client import get_redis
|
|
7
|
+
from agentexec.core.task import Task
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Priority(str, Enum):
|
|
13
|
+
"""Task priority levels.
|
|
14
|
+
|
|
15
|
+
HIGH: Push to front of queue (processed first).
|
|
16
|
+
LOW: Push to back of queue (processed later).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
HIGH = "high"
|
|
20
|
+
LOW = "low"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def enqueue(
|
|
24
|
+
task_name: str,
|
|
25
|
+
payload: dict[str, Any],
|
|
26
|
+
*,
|
|
27
|
+
priority: Priority = Priority.LOW,
|
|
28
|
+
queue_name: str | None = None,
|
|
29
|
+
) -> Task:
|
|
30
|
+
"""Enqueue a task for background execution with automatic activity tracking.
|
|
31
|
+
|
|
32
|
+
This function automatically creates an activity record for the task and
|
|
33
|
+
enqueues it to Redis for worker processing.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
task: Task to enqueue.
|
|
37
|
+
db: Database session for activity tracking.
|
|
38
|
+
priority: Task priority (Priority.HIGH or Priority.LOW).
|
|
39
|
+
queue_name: Redis list name to use. If not provided, uses
|
|
40
|
+
agentexec.CONF.queue_name.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
agent_id: UUID for tracking the task's progress.
|
|
44
|
+
|
|
45
|
+
Example:
|
|
46
|
+
task = Task(task_type="research", payload={"company": "Acme"})
|
|
47
|
+
agent_id = enqueue(task, db=session, priority=Priority.HIGH)
|
|
48
|
+
# Track progress: ax.activity.get_activity(db, agent_id)
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
redis = get_redis()
|
|
53
|
+
redis_push = {
|
|
54
|
+
Priority.HIGH: redis.rpush,
|
|
55
|
+
Priority.LOW: redis.lpush,
|
|
56
|
+
}[priority]
|
|
57
|
+
except KeyError:
|
|
58
|
+
raise ValueError(f"Invalid priority: {priority}. Must be Priority.HIGH or Priority.LOW")
|
|
59
|
+
|
|
60
|
+
task = Task.create(
|
|
61
|
+
task_name=task_name,
|
|
62
|
+
payload=payload,
|
|
63
|
+
)
|
|
64
|
+
redis_push(
|
|
65
|
+
queue_name or CONF.queue_name,
|
|
66
|
+
task.model_dump_json(),
|
|
67
|
+
)
|
|
68
|
+
logger.info(f"Enqueued task {task.task_name} with agent_id {task.agent_id}")
|
|
69
|
+
# TODO track errors
|
|
70
|
+
|
|
71
|
+
return task
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def dequeue(queue_name: str | None = None, timeout: int = 1) -> Task | None:
|
|
75
|
+
"""Dequeue a task from the queue.
|
|
76
|
+
|
|
77
|
+
Blocks for up to timeout seconds waiting for a task. Returns None if
|
|
78
|
+
no task is available within the timeout period.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
queue_name: Redis list name to use. If not provided, uses
|
|
82
|
+
agentexec.CONF.queue_name.
|
|
83
|
+
timeout: Maximum seconds to wait for a task.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Task if available, None otherwise.
|
|
87
|
+
|
|
88
|
+
Example:
|
|
89
|
+
task = dequeue(timeout=5)
|
|
90
|
+
if task is not None:
|
|
91
|
+
# Process task
|
|
92
|
+
pass
|
|
93
|
+
"""
|
|
94
|
+
redis = get_redis()
|
|
95
|
+
|
|
96
|
+
result = cast(
|
|
97
|
+
tuple[str, str] | None,
|
|
98
|
+
redis.brpop(
|
|
99
|
+
[queue_name or CONF.queue_name],
|
|
100
|
+
timeout=timeout,
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if result is None:
|
|
105
|
+
# No task available within the timeout
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
_, task_json = result
|
|
109
|
+
return Task.model_validate_json(task_json)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from redis import Redis
|
|
2
|
+
from agentexec.config import CONF
|
|
3
|
+
|
|
4
|
+
_redis_client: Redis | None = None
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_redis() -> Redis:
|
|
8
|
+
"""Get the Redis client instance.
|
|
9
|
+
|
|
10
|
+
Creates and caches a Redis client on first call. Subsequent calls
|
|
11
|
+
return the cached instance.
|
|
12
|
+
|
|
13
|
+
The client is configured from agentexec.CONF settings:
|
|
14
|
+
- redis_url: Connection URL
|
|
15
|
+
- redis_pool_size: Connection pool size
|
|
16
|
+
- redis_pool_timeout: Connection timeout
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Redis client instance.
|
|
20
|
+
"""
|
|
21
|
+
global _redis_client
|
|
22
|
+
|
|
23
|
+
if _redis_client is None:
|
|
24
|
+
_redis_client = Redis.from_url(
|
|
25
|
+
CONF.redis_url,
|
|
26
|
+
max_connections=CONF.redis_pool_size,
|
|
27
|
+
socket_connect_timeout=CONF.redis_pool_timeout,
|
|
28
|
+
decode_responses=True, # Automatically decode bytes to strings
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return _redis_client
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def close_redis() -> None:
|
|
35
|
+
"""Close the Redis connection and reset the client."""
|
|
36
|
+
global _redis_client
|
|
37
|
+
|
|
38
|
+
if _redis_client is not None:
|
|
39
|
+
_redis_client.close()
|
|
40
|
+
_redis_client = None
|
agentexec/core/task.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, Protocol, TypedDict, Unpack
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
from agentexec import activity
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TaskHandlerKwargs(TypedDict):
|
|
10
|
+
"""Type for kwargs passed to task handlers.
|
|
11
|
+
|
|
12
|
+
Handlers receive the payload as a dict and agent_id as a separate parameter.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
agent_id: UUID
|
|
16
|
+
payload: dict[str, Any]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TaskHandler(Protocol):
|
|
20
|
+
"""Protocol for task handler functions.
|
|
21
|
+
|
|
22
|
+
Handlers accept **kwargs matching HandlerKwargs structure.
|
|
23
|
+
Return value is ignored. Can be sync or async.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __call__(self, **kwargs: Unpack[TaskHandlerKwargs]) -> None: ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Task(BaseModel):
|
|
30
|
+
"""Represents a background task.
|
|
31
|
+
|
|
32
|
+
Tasks are serialized to JSON and enqueued to Redis for workers to process.
|
|
33
|
+
Each task has a type (matching a registered handler), a payload with
|
|
34
|
+
parameters, and an agent_id for tracking.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
task_name: str
|
|
38
|
+
payload: dict[str, Any] = Field(default_factory=dict)
|
|
39
|
+
agent_id: UUID
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def create(cls, task_name: str, payload: dict[str, Any]) -> Task:
|
|
43
|
+
"""Create a new task with automatic activity tracking.
|
|
44
|
+
|
|
45
|
+
This is a convenience method that creates both a Task instance and
|
|
46
|
+
its corresponding activity record in one step.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
task_name: Name/type of the task (e.g., "research", "analysis")
|
|
50
|
+
payload: Task parameters to pass to the handler
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Task instance with agent_id set
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
task = Task.create("research_company", {"company": "Acme Corp"})
|
|
57
|
+
# Activity record is created automatically
|
|
58
|
+
"""
|
|
59
|
+
agent_id = activity.create(
|
|
60
|
+
task_name=task_name,
|
|
61
|
+
message="Waiting to start.",
|
|
62
|
+
)
|
|
63
|
+
return cls(
|
|
64
|
+
task_name=task_name,
|
|
65
|
+
payload=payload,
|
|
66
|
+
agent_id=agent_id,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def started(self) -> None:
|
|
70
|
+
"""Mark the task as started in the activity log.
|
|
71
|
+
|
|
72
|
+
Updates the activity status to RUNNING with a starting message.
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
task = Task.create("research", {"company": "Acme"})
|
|
76
|
+
task.started()
|
|
77
|
+
"""
|
|
78
|
+
activity.update(
|
|
79
|
+
agent_id=self.agent_id,
|
|
80
|
+
message="Task started.",
|
|
81
|
+
completion_percentage=0,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def completed(self) -> None:
|
|
85
|
+
"""Mark the task as completed in the activity log.
|
|
86
|
+
|
|
87
|
+
Updates the activity status to COMPLETE with a completion message.
|
|
88
|
+
|
|
89
|
+
Example:
|
|
90
|
+
task = Task.create("research", {"company": "Acme"})
|
|
91
|
+
task.completed()
|
|
92
|
+
"""
|
|
93
|
+
activity.update(
|
|
94
|
+
agent_id=self.agent_id,
|
|
95
|
+
message="Task completed successfully.",
|
|
96
|
+
completion_percentage=100,
|
|
97
|
+
status=activity.Status.COMPLETE,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def errored(self, exception: Exception) -> None:
|
|
101
|
+
"""Mark the task as errored in the activity log.
|
|
102
|
+
|
|
103
|
+
Updates the activity status to ERROR with the provided error message.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
error_message: Description of the error that occurred
|
|
107
|
+
|
|
108
|
+
Example:
|
|
109
|
+
task = Task.create("research", {"company": "Acme"})
|
|
110
|
+
task.error("Failed to fetch data from API.")
|
|
111
|
+
"""
|
|
112
|
+
activity.update(
|
|
113
|
+
agent_id=self.agent_id,
|
|
114
|
+
message=f"Task failed with error: {exception}",
|
|
115
|
+
status=activity.Status.ERROR,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def handler_kwargs(self) -> TaskHandlerKwargs:
|
|
120
|
+
"""Get kwargs to pass to the task handler.
|
|
121
|
+
|
|
122
|
+
Builds a dictionary containing the task's payload and agent_id,
|
|
123
|
+
matching the TaskHandlerKwargs structure expected by handlers.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Dict with 'payload' (task parameters) and 'agent_id' (tracking ID)
|
|
127
|
+
|
|
128
|
+
Example:
|
|
129
|
+
kwargs = task._handler_kwargs
|
|
130
|
+
# Returns: {"payload": {"company": "Acme"}, "agent_id": UUID(...)}
|
|
131
|
+
"""
|
|
132
|
+
return {"agent_id": self.agent_id, "payload": self.payload}
|