Spaces:
Paused
Paused
Rafael Uzarowski commited on
(WIP) feat: Task Scheduler Management UI/UX and tools - Part 3
Browse files- agent.py +24 -3
- docker/run/Dockerfile +13 -0
- docker/run/DockerfileKali +13 -0
- prompts/default/agent.system.datetime.md +3 -0
- prompts/default/agent.system.main.environment.md +0 -1
- prompts/default/agent.system.main.md +1 -1
- prompts/default/agent.system.tool.scheduler.md +215 -0
- prompts/default/agent.system.tools.md +2 -0
- prompts/default/fw.intervention.md +1 -1
- prompts/default/fw.user_message.md +1 -1
- python/api/chat_remove.py +13 -1
- python/api/poll.py +22 -18
- python/api/scheduler_adhoc.py_ +0 -5
- python/api/scheduler_task_create.py +61 -4
- python/api/scheduler_task_delete.py +26 -1
- python/api/scheduler_task_run.py +4 -0
- python/api/scheduler_task_update.py +26 -4
- python/api/scheduler_tasks_list.py +5 -0
- python/api/scheduler_tick.py +6 -1
- python/extensions/message_loop_prompts/_60_include_current_datetime.py +22 -0
- python/extensions/system_prompt/_10_system_prompt.py +7 -2
- python/helpers/api.py +4 -4
- python/helpers/chat_names.py +0 -137
- python/helpers/localization.py +123 -0
- python/helpers/persist_chat.py +11 -0
- python/helpers/task_scheduler.py +598 -169
- python/helpers/tool.py +7 -2
- python/tools/scheduler.py +197 -0
- run_ui.py +14 -3
- webui/css/scheduler-datepicker.css +127 -0
- webui/css/settings.css +160 -9
- webui/index.css +0 -67
- webui/index.html +327 -29
- webui/index.js +55 -12
- webui/js/scheduler.js +1187 -202
- webui/js/settings.js +76 -1
- webui/js/time-utils.js +71 -0
agent.py
CHANGED
|
@@ -27,6 +27,7 @@ from python.helpers.dirty_json import DirtyJson
|
|
| 27 |
from python.helpers.defer import DeferredTask
|
| 28 |
from typing import Callable
|
| 29 |
from python.helpers.history import OutputMessage
|
|
|
|
| 30 |
|
| 31 |
|
| 32 |
class AgentContext:
|
|
@@ -80,6 +81,21 @@ class AgentContext:
|
|
| 80 |
context.task.kill()
|
| 81 |
return context
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
def get_created_at(self):
|
| 84 |
return self.created_at
|
| 85 |
|
|
@@ -654,8 +670,13 @@ class Agent:
|
|
| 654 |
|
| 655 |
if tool_request is not None:
|
| 656 |
tool_name = tool_request.get("tool_name", "")
|
|
|
|
| 657 |
tool_args = tool_request.get("tool_args", {})
|
| 658 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 659 |
|
| 660 |
await self.handle_intervention() # wait if paused and handle intervention message if needed
|
| 661 |
await tool.before_execution(**tool_args)
|
|
@@ -685,7 +706,7 @@ class Agent:
|
|
| 685 |
except Exception as e:
|
| 686 |
pass
|
| 687 |
|
| 688 |
-
def get_tool(self, name: str, args: dict, message: str, **kwargs):
|
| 689 |
from python.tools.unknown import Unknown
|
| 690 |
from python.helpers.tool import Tool
|
| 691 |
|
|
@@ -693,7 +714,7 @@ class Agent:
|
|
| 693 |
"python/tools", name + ".py", Tool
|
| 694 |
)
|
| 695 |
tool_class = classes[0] if classes else Unknown
|
| 696 |
-
return tool_class(agent=self, name=name, args=args, message=message, **kwargs)
|
| 697 |
|
| 698 |
async def call_extensions(self, folder: str, **kwargs) -> Any:
|
| 699 |
from python.helpers.extension import Extension
|
|
|
|
| 27 |
from python.helpers.defer import DeferredTask
|
| 28 |
from typing import Callable
|
| 29 |
from python.helpers.history import OutputMessage
|
| 30 |
+
from python.helpers.localization import Localization
|
| 31 |
|
| 32 |
|
| 33 |
class AgentContext:
|
|
|
|
| 81 |
context.task.kill()
|
| 82 |
return context
|
| 83 |
|
| 84 |
+
def serialize(self):
|
| 85 |
+
return {
|
| 86 |
+
"id": self.id,
|
| 87 |
+
"name": self.name,
|
| 88 |
+
"created_at": (
|
| 89 |
+
Localization.get().serialize_datetime(self.created_at)
|
| 90 |
+
if self.created_at else Localization.get().serialize_datetime(datetime.fromtimestamp(0))
|
| 91 |
+
),
|
| 92 |
+
"no": self.no,
|
| 93 |
+
"log_guid": self.log.guid,
|
| 94 |
+
"log_version": len(self.log.updates),
|
| 95 |
+
"log_length": len(self.log.logs),
|
| 96 |
+
"paused": self.paused,
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
def get_created_at(self):
|
| 100 |
return self.created_at
|
| 101 |
|
|
|
|
| 670 |
|
| 671 |
if tool_request is not None:
|
| 672 |
tool_name = tool_request.get("tool_name", "")
|
| 673 |
+
tool_method = None
|
| 674 |
tool_args = tool_request.get("tool_args", {})
|
| 675 |
+
|
| 676 |
+
if ":" in tool_name:
|
| 677 |
+
tool_name, tool_method = tool_name.split(":", 1)
|
| 678 |
+
|
| 679 |
+
tool = self.get_tool(name=tool_name, method=tool_method, args=tool_args, message=msg)
|
| 680 |
|
| 681 |
await self.handle_intervention() # wait if paused and handle intervention message if needed
|
| 682 |
await tool.before_execution(**tool_args)
|
|
|
|
| 706 |
except Exception as e:
|
| 707 |
pass
|
| 708 |
|
| 709 |
+
def get_tool(self, name: str, method: str | None, args: dict, message: str, **kwargs):
|
| 710 |
from python.tools.unknown import Unknown
|
| 711 |
from python.helpers.tool import Tool
|
| 712 |
|
|
|
|
| 714 |
"python/tools", name + ".py", Tool
|
| 715 |
)
|
| 716 |
tool_class = classes[0] if classes else Unknown
|
| 717 |
+
return tool_class(agent=self, name=name, method=method, args=args, message=message, **kwargs)
|
| 718 |
|
| 719 |
async def call_extensions(self, folder: str, **kwargs) -> Any:
|
| 720 |
from python.helpers.extension import Extension
|
docker/run/Dockerfile
CHANGED
|
@@ -6,6 +6,19 @@ ARG BRANCH
|
|
| 6 |
RUN if [ -z "$BRANCH" ]; then echo "ERROR: BRANCH is not set!" >&2; exit 1; fi
|
| 7 |
ENV BRANCH=$BRANCH
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
# Copy contents of the project to /a0
|
| 10 |
COPY ./fs/ /
|
| 11 |
|
|
|
|
| 6 |
RUN if [ -z "$BRANCH" ]; then echo "ERROR: BRANCH is not set!" >&2; exit 1; fi
|
| 7 |
ENV BRANCH=$BRANCH
|
| 8 |
|
| 9 |
+
# Set locale to en_US.UTF-8 and timezone to UTC
|
| 10 |
+
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y locales tzdata
|
| 11 |
+
RUN sed -i -e 's/# \(en_US\.UTF-8 .*\)/\1/' /etc/locale.gen && \
|
| 12 |
+
dpkg-reconfigure --frontend=noninteractive locales && \
|
| 13 |
+
update-locale LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=en_US.UTF-8
|
| 14 |
+
RUN ln -sf /usr/share/zoneinfo/UTC /etc/localtime
|
| 15 |
+
RUN echo "UTC" > /etc/timezone
|
| 16 |
+
RUN dpkg-reconfigure -f noninteractive tzdata
|
| 17 |
+
ENV LANG=en_US.UTF-8
|
| 18 |
+
ENV LANGUAGE=en_US:en
|
| 19 |
+
ENV LC_ALL=en_US.UTF-8
|
| 20 |
+
ENV TZ=UTC
|
| 21 |
+
|
| 22 |
# Copy contents of the project to /a0
|
| 23 |
COPY ./fs/ /
|
| 24 |
|
docker/run/DockerfileKali
CHANGED
|
@@ -6,6 +6,19 @@ ARG BRANCH
|
|
| 6 |
RUN if [ -z "$BRANCH" ]; then echo "ERROR: BRANCH is not set!" >&2; exit 1; fi
|
| 7 |
ENV BRANCH=$BRANCH
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
# Copy contents of the project to /a0
|
| 10 |
COPY ./fs/ /
|
| 11 |
|
|
|
|
| 6 |
RUN if [ -z "$BRANCH" ]; then echo "ERROR: BRANCH is not set!" >&2; exit 1; fi
|
| 7 |
ENV BRANCH=$BRANCH
|
| 8 |
|
| 9 |
+
# Set locale to en_US.UTF-8 and timezone to UTC
|
| 10 |
+
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y locales tzdata
|
| 11 |
+
RUN sed -i -e 's/# \(en_US\.UTF-8 .*\)/\1/' /etc/locale.gen && \
|
| 12 |
+
dpkg-reconfigure --frontend=noninteractive locales && \
|
| 13 |
+
update-locale LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=en_US.UTF-8
|
| 14 |
+
RUN ln -sf /usr/share/zoneinfo/UTC /etc/localtime
|
| 15 |
+
RUN echo "UTC" > /etc/timezone
|
| 16 |
+
RUN dpkg-reconfigure -f noninteractive tzdata
|
| 17 |
+
ENV LANG=en_US.UTF-8
|
| 18 |
+
ENV LANGUAGE=en_US:en
|
| 19 |
+
ENV LC_ALL=en_US.UTF-8
|
| 20 |
+
ENV TZ=UTC
|
| 21 |
+
|
| 22 |
# Copy contents of the project to /a0
|
| 23 |
COPY ./fs/ /
|
| 24 |
|
prompts/default/agent.system.datetime.md
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Current system date and time of user
|
| 2 |
+
- Current Date and Time is: {{date_time}}
|
| 3 |
+
- !!! rely solely on this information for time-sensitive tasks as it is always up to date
|
prompts/default/agent.system.main.environment.md
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
## Environment
|
| 2 |
live in debian linux docker container
|
| 3 |
agent zero framework is python project in /a0 folder
|
| 4 |
-
|
|
|
|
| 1 |
## Environment
|
| 2 |
live in debian linux docker container
|
| 3 |
agent zero framework is python project in /a0 folder
|
|
|
prompts/default/agent.system.main.md
CHANGED
|
@@ -8,4 +8,4 @@
|
|
| 8 |
|
| 9 |
{{ include "./agent.system.main.solving.md" }}
|
| 10 |
|
| 11 |
-
{{ include "./agent.system.main.tips.md" }}
|
|
|
|
| 8 |
|
| 9 |
{{ include "./agent.system.main.solving.md" }}
|
| 10 |
|
| 11 |
+
{{ include "./agent.system.main.tips.md" }}
|
prompts/default/agent.system.tool.scheduler.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## Task Scheduler Subsystem:
|
| 2 |
+
The task scheduler is a part of agent-zero enabling the system to execute
|
| 3 |
+
arbitrary tasks defined by a "system prompt" and "user prompt".
|
| 4 |
+
|
| 5 |
+
When the task is executed the prompts are being run in the background in a context
|
| 6 |
+
conversation with the goal of completing the task described in the prompts.
|
| 7 |
+
|
| 8 |
+
Dedicated context means the task will run in it's own chat. If task is created without the
|
| 9 |
+
dedicated_context flag then the task will run in the chat it was created in including entire history.
|
| 10 |
+
|
| 11 |
+
There are manual and automatically executed tasks.
|
| 12 |
+
Automatic execution happens by a schedule defined when creating the task.
|
| 13 |
+
|
| 14 |
+
### Types of scheduler tasks
|
| 15 |
+
There are 3 types of scheduler tasks:
|
| 16 |
+
|
| 17 |
+
#### Scheduled - type="scheduled"
|
| 18 |
+
This type of task is run by a recurring schedule defined in the crontab syntax with 5 fields (ex. */5 * * * * means every 5 minutes).
|
| 19 |
+
It is recurring and started automatically when the crontab syntax requires next execution..
|
| 20 |
+
|
| 21 |
+
#### Planned - type="planned"
|
| 22 |
+
This type of task is run by a linear schedule defined as discrete datetimes of the upcoming executions.
|
| 23 |
+
It is started automatically when a scheduled time elapses.
|
| 24 |
+
|
| 25 |
+
#### AdHoc - type="adhoc"
|
| 26 |
+
This type of task is run manually and does not follow any schedule. It can be run explicitly by "scheduler:run_task" agent tool or by the user in the UI.
|
| 27 |
+
|
| 28 |
+
### Tools to manage the task scheduler system and it's tasks
|
| 29 |
+
|
| 30 |
+
#### scheduler:list_tasks
|
| 31 |
+
List all tasks present in the system with their 'uuid', 'name', 'type', 'state', 'schedule' and 'next_run'.
|
| 32 |
+
All runnable tasks can be listed and filtered here. The arbuments a filter fields.
|
| 33 |
+
|
| 34 |
+
##### Arguments:
|
| 35 |
+
* state: list(str) (Optional) - The state filter, one of "idle", "running", "disabled", "error". To only show tasks in given state.
|
| 36 |
+
* type: list(str) (Optional) - The task type filter, one of "adhoc", "planned", "scheduled"
|
| 37 |
+
* next_run_within: int (Optional) - The next run of the task must be within this many minutes
|
| 38 |
+
* next_run_after: int (Optional) - The next run of the task must be after not less than this many minutes
|
| 39 |
+
|
| 40 |
+
##### Usage:
|
| 41 |
+
~~~json
|
| 42 |
+
{
|
| 43 |
+
"thoughts": [
|
| 44 |
+
"I must look for planned runnable tasks with name ... and state idle or error",
|
| 45 |
+
"The tasks should run within next 20 minutes"
|
| 46 |
+
],
|
| 47 |
+
"tool_name": "scheduler:list_tasks",
|
| 48 |
+
"tool_args": {
|
| 49 |
+
"state": ["idle", "error"],
|
| 50 |
+
"type": ["planned"],
|
| 51 |
+
"next_run_within": 20
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
~~~
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
#### scheduler:show_task
|
| 58 |
+
Show task details for scheduler task with the given uuid.
|
| 59 |
+
|
| 60 |
+
##### Arguments:
|
| 61 |
+
* uuid: string - The uuid of the task to display
|
| 62 |
+
|
| 63 |
+
##### Usage (execute task with uuid "xyz-123"):
|
| 64 |
+
~~~json
|
| 65 |
+
{
|
| 66 |
+
"thoughts": [
|
| 67 |
+
"I need details of task xxx-yyy-zzz",
|
| 68 |
+
],
|
| 69 |
+
"tool_name": "scheduler:show_task",
|
| 70 |
+
"tool_args": {
|
| 71 |
+
"uuid": "xxx-yyy-zzz",
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
~~~
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
#### scheduler:run_task
|
| 78 |
+
Execute a task manually which is not in "running" state
|
| 79 |
+
This can be used to trigger tasks manually.
|
| 80 |
+
Normally you should only "run" tasks manually if they are in the "idle" state.
|
| 81 |
+
It is also advised to only run "adhoc" tasks manually but every task type can be triggered by this tool.
|
| 82 |
+
|
| 83 |
+
##### Arguments:
|
| 84 |
+
* uuid: string - The uuid of the task to run. Can be retrieved for example from "scheduler:tasks_list"
|
| 85 |
+
|
| 86 |
+
##### Usage (execute task with uuid "xyz-123"):
|
| 87 |
+
~~~json
|
| 88 |
+
{
|
| 89 |
+
"thoughts": [
|
| 90 |
+
"I must run task xyz-123",
|
| 91 |
+
],
|
| 92 |
+
"tool_name": "scheduler:run_task",
|
| 93 |
+
"tool_args": {
|
| 94 |
+
"uuid": "xyz-123",
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
~~~
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
#### scheduler:delete_task
|
| 101 |
+
Delete the task defined by the given uuid from the system.
|
| 102 |
+
|
| 103 |
+
##### Arguments:
|
| 104 |
+
* uuid: string - The uuid of the task to run. Can be retrieved for example from "scheduler:tasks_list"
|
| 105 |
+
|
| 106 |
+
##### Usage (execute task with uuid "xyz-123"):
|
| 107 |
+
~~~json
|
| 108 |
+
{
|
| 109 |
+
"thoughts": [
|
| 110 |
+
"I must delete task xyz-123",
|
| 111 |
+
],
|
| 112 |
+
"tool_name": "scheduler:delete_task",
|
| 113 |
+
"tool_args": {
|
| 114 |
+
"uuid": "xyz-123",
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
~~~
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
#### scheduler:create_scheduled_task
|
| 121 |
+
Create a task within the scheduler system with the type "scheduled".
|
| 122 |
+
The scheduled type of tasks is being run by a cron schedule that you must provide.
|
| 123 |
+
|
| 124 |
+
##### Arguments:
|
| 125 |
+
* name: str - The name of the task, will also be displayed when listing tasks
|
| 126 |
+
* system_prompt: str - The system prompt to be used when executing the task
|
| 127 |
+
* prompt: str - The actual prompt with the task definition
|
| 128 |
+
* schedule: dict[str,str] - the dict of all cron schedule values. The keys are descriptive: minute, hour, day, month, weekday. The values are cron syntax fields named by the keys.
|
| 129 |
+
* attachments: list[str] - Here you can add message attachments, valid are filesystem paths and internet urls
|
| 130 |
+
* dedicated_context: bool - if false, then the task will run in the context it was created in. If true, the task will have it's own context. If unspecified then false is assumed. The tasks run in the context they were created in by default.
|
| 131 |
+
|
| 132 |
+
##### Usage:
|
| 133 |
+
~~~json
|
| 134 |
+
{
|
| 135 |
+
"thoughts": [
|
| 136 |
+
"I must create new scheduled task with name XXX running every 20 minutes in a separate chat"
|
| 137 |
+
],
|
| 138 |
+
"tool_name": "scheduler:create_scheduled_task",
|
| 139 |
+
"tool_args": {
|
| 140 |
+
"name": "XXX",
|
| 141 |
+
"system_prompt": "You are a software developer",
|
| 142 |
+
"prompt": "Send the user an email with a greeting using python and smtp. The user's address is: xxx@yyy.zzz",
|
| 143 |
+
"attachments": [],
|
| 144 |
+
"schedule": {
|
| 145 |
+
"minute": "*/20",
|
| 146 |
+
"hour": "*",
|
| 147 |
+
"day": "*",
|
| 148 |
+
"month": "*",
|
| 149 |
+
"weekday": "*",
|
| 150 |
+
},
|
| 151 |
+
"dedicated_context": true
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
~~~
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
#### scheduler:create_adhoc_task
|
| 158 |
+
Create a task within the scheduler system with the type "adhoc".
|
| 159 |
+
The adhoc type of tasks is being run manually by "scheduler:run_task" tool or by the user via ui.
|
| 160 |
+
|
| 161 |
+
##### Arguments:
|
| 162 |
+
* name: str - The name of the task, will also be displayed when listing tasks
|
| 163 |
+
* system_prompt: str - The system prompt to be used when executing the task
|
| 164 |
+
* prompt: str - The actual prompt with the task definition
|
| 165 |
+
* attachments: list[str] - Here you can add message attachments, valid are filesystem paths and internet urls
|
| 166 |
+
* dedicated_context: bool - if false, then the task will run in the context it was created in. If true, the task will have it's own context. If unspecified then false is assumed. The tasks run in the context they were created in by default.
|
| 167 |
+
|
| 168 |
+
##### Usage:
|
| 169 |
+
~~~json
|
| 170 |
+
{
|
| 171 |
+
"thoughts": [
|
| 172 |
+
"I must create new scheduled task with name XXX running every 20 minutes"
|
| 173 |
+
],
|
| 174 |
+
"tool_name": "scheduler:create_adhoc_task",
|
| 175 |
+
"tool_args": {
|
| 176 |
+
"name": "XXX",
|
| 177 |
+
"system_prompt": "You are a software developer",
|
| 178 |
+
"prompt": "Send the user an email with a greeting using python and smtp. The user's address is: xxx@yyy.zzz",
|
| 179 |
+
"attachments": [],
|
| 180 |
+
"dedicated_context": false
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
~~~
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
#### scheduler:create_planned_task
|
| 187 |
+
Create a task within the scheduler system with the type "planned".
|
| 188 |
+
The planned type of tasks is being run by a fixed plan, a list of datetimes that you must provide.
|
| 189 |
+
|
| 190 |
+
##### Arguments:
|
| 191 |
+
* name: str - The name of the task, will also be displayed when listing tasks
|
| 192 |
+
* system_prompt: str - The system prompt to be used when executing the task
|
| 193 |
+
* prompt: str - The actual prompt with the task definition
|
| 194 |
+
* plan: list(iso datetime string) - the list of all execution timestamps. The dates should be in the 24 hour (!) strftime iso format: "%Y-%m-%dT%H:%M:%S"
|
| 195 |
+
* attachments: list[str] - Here you can add message attachments, valid are filesystem paths and internet urls
|
| 196 |
+
* dedicated_context: bool - if false, then the task will run in the context it was created in. If true, the task will have it's own context. If unspecified then false is assumed. The tasks run in the context they were created in by default.
|
| 197 |
+
|
| 198 |
+
##### Usage:
|
| 199 |
+
~~~json
|
| 200 |
+
{
|
| 201 |
+
"thoughts": [
|
| 202 |
+
"I must create new planned task to run tomorow at 6:25 PM",
|
| 203 |
+
"Today is 2025-04-29 according to system prompt"
|
| 204 |
+
],
|
| 205 |
+
"tool_name": "scheduler:create_planned_task",
|
| 206 |
+
"tool_args": {
|
| 207 |
+
"name": "XXX",
|
| 208 |
+
"system_prompt": "You are a software developer",
|
| 209 |
+
"prompt": "Send the user an email with a greeting using python and smtp. The user's address is: xxx@yyy.zzz",
|
| 210 |
+
"attachments": [],
|
| 211 |
+
"plan": ["2025-04-29T18:25:00"],
|
| 212 |
+
"dedicated_context": false
|
| 213 |
+
}
|
| 214 |
+
}
|
| 215 |
+
~~~
|
prompts/default/agent.system.tools.md
CHANGED
|
@@ -15,3 +15,5 @@
|
|
| 15 |
{{ include './agent.system.tool.input.md' }}
|
| 16 |
|
| 17 |
{{ include './agent.system.tool.browser.md' }}
|
|
|
|
|
|
|
|
|
| 15 |
{{ include './agent.system.tool.input.md' }}
|
| 16 |
|
| 17 |
{{ include './agent.system.tool.browser.md' }}
|
| 18 |
+
|
| 19 |
+
{{ include './agent.system.tool.scheduler.md' }}
|
prompts/default/fw.intervention.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
```json
|
| 2 |
{
|
| 3 |
-
"user_intervention": {{message}},
|
| 4 |
"system_message": {{system_message}},
|
|
|
|
| 5 |
"attachments": {{attachments}}
|
| 6 |
}
|
| 7 |
```
|
|
|
|
| 1 |
```json
|
| 2 |
{
|
|
|
|
| 3 |
"system_message": {{system_message}},
|
| 4 |
+
"user_intervention": {{message}},
|
| 5 |
"attachments": {{attachments}}
|
| 6 |
}
|
| 7 |
```
|
prompts/default/fw.user_message.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
```json
|
| 2 |
{
|
| 3 |
-
"user_message": {{message}},
|
| 4 |
"system_message": {{system_message}},
|
|
|
|
| 5 |
"attachments": {{attachments}}
|
| 6 |
}
|
| 7 |
```
|
|
|
|
| 1 |
```json
|
| 2 |
{
|
|
|
|
| 3 |
"system_message": {{system_message}},
|
| 4 |
+
"user_message": {{message}},
|
| 5 |
"attachments": {{attachments}}
|
| 6 |
}
|
| 7 |
```
|
python/api/chat_remove.py
CHANGED
|
@@ -1,16 +1,28 @@
|
|
| 1 |
from python.helpers.api import ApiHandler, Input, Output, Request, Response
|
| 2 |
from agent import AgentContext
|
| 3 |
from python.helpers import persist_chat
|
|
|
|
| 4 |
|
| 5 |
|
| 6 |
class RemoveChat(ApiHandler):
|
| 7 |
async def process(self, input: Input, request: Request) -> Output:
|
| 8 |
ctxid = input.get("context", "")
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
AgentContext.remove(ctxid)
|
| 12 |
persist_chat.remove_chat(ctxid)
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
return {
|
| 15 |
"message": "Context removed.",
|
| 16 |
}
|
|
|
|
| 1 |
from python.helpers.api import ApiHandler, Input, Output, Request, Response
|
| 2 |
from agent import AgentContext
|
| 3 |
from python.helpers import persist_chat
|
| 4 |
+
from python.helpers.task_scheduler import TaskScheduler
|
| 5 |
|
| 6 |
|
| 7 |
class RemoveChat(ApiHandler):
|
| 8 |
async def process(self, input: Input, request: Request) -> Output:
|
| 9 |
ctxid = input.get("context", "")
|
| 10 |
|
| 11 |
+
context = AgentContext.get(ctxid)
|
| 12 |
+
if context:
|
| 13 |
+
# stop processing any tasks
|
| 14 |
+
context.reset()
|
| 15 |
+
|
| 16 |
AgentContext.remove(ctxid)
|
| 17 |
persist_chat.remove_chat(ctxid)
|
| 18 |
|
| 19 |
+
scheduler = TaskScheduler.get()
|
| 20 |
+
await scheduler.reload()
|
| 21 |
+
|
| 22 |
+
tasks = scheduler.get_tasks_by_context_id(ctxid)
|
| 23 |
+
for task in tasks:
|
| 24 |
+
await scheduler.remove_task_by_uuid(task.uuid)
|
| 25 |
+
|
| 26 |
return {
|
| 27 |
"message": "Context removed.",
|
| 28 |
}
|
python/api/poll.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import time
|
| 2 |
-
|
| 3 |
from python.helpers.api import ApiHandler
|
| 4 |
from flask import Request, Response
|
| 5 |
|
|
@@ -7,6 +7,8 @@ from agent import AgentContext
|
|
| 7 |
|
| 8 |
from python.helpers import persist_chat
|
| 9 |
from python.helpers.task_scheduler import TaskScheduler
|
|
|
|
|
|
|
| 10 |
|
| 11 |
|
| 12 |
class Poll(ApiHandler):
|
|
@@ -15,6 +17,10 @@ class Poll(ApiHandler):
|
|
| 15 |
ctxid = input.get("context", None)
|
| 16 |
from_no = input.get("log_from", 0)
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
# context instance - get or create
|
| 19 |
context = self.get_context(ctxid)
|
| 20 |
|
|
@@ -28,7 +34,7 @@ class Poll(ApiHandler):
|
|
| 28 |
# Always reload the scheduler on each poll to ensure we have the latest task state
|
| 29 |
await scheduler.reload()
|
| 30 |
|
| 31 |
-
# loop AgentContext._contexts and
|
| 32 |
|
| 33 |
ctxs = []
|
| 34 |
tasks = []
|
|
@@ -42,21 +48,15 @@ class Poll(ApiHandler):
|
|
| 42 |
continue
|
| 43 |
|
| 44 |
# Create the base context data that will be returned
|
| 45 |
-
context_data =
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
# Determine if this is a task by checking if a task with this UUID exists
|
| 57 |
-
is_task = scheduler.get_task_by_uuid(ctx.id) is not None
|
| 58 |
-
|
| 59 |
-
if not is_task:
|
| 60 |
ctxs.append(context_data)
|
| 61 |
else:
|
| 62 |
# If this is a task, get task details from the scheduler
|
|
@@ -65,6 +65,7 @@ class Poll(ApiHandler):
|
|
| 65 |
# Add task details to context_data with the same field names
|
| 66 |
# as used in scheduler endpoints to maintain UI compatibility
|
| 67 |
context_data.update({
|
|
|
|
| 68 |
"uuid": task_details.get("uuid"),
|
| 69 |
"state": task_details.get("state"),
|
| 70 |
"type": task_details.get("type"),
|
|
@@ -72,12 +73,15 @@ class Poll(ApiHandler):
|
|
| 72 |
"prompt": task_details.get("prompt"),
|
| 73 |
"last_run": task_details.get("last_run"),
|
| 74 |
"last_result": task_details.get("last_result"),
|
| 75 |
-
"attachments": task_details.get("attachments", [])
|
|
|
|
| 76 |
})
|
| 77 |
|
| 78 |
# Add type-specific fields
|
| 79 |
if task_details.get("type") == "scheduled":
|
| 80 |
context_data["schedule"] = task_details.get("schedule")
|
|
|
|
|
|
|
| 81 |
else:
|
| 82 |
context_data["token"] = task_details.get("token")
|
| 83 |
|
|
|
|
| 1 |
import time
|
| 2 |
+
from datetime import datetime
|
| 3 |
from python.helpers.api import ApiHandler
|
| 4 |
from flask import Request, Response
|
| 5 |
|
|
|
|
| 7 |
|
| 8 |
from python.helpers import persist_chat
|
| 9 |
from python.helpers.task_scheduler import TaskScheduler
|
| 10 |
+
from python.helpers.localization import Localization
|
| 11 |
+
from python.helpers.dotenv import get_dotenv_value
|
| 12 |
|
| 13 |
|
| 14 |
class Poll(ApiHandler):
|
|
|
|
| 17 |
ctxid = input.get("context", None)
|
| 18 |
from_no = input.get("log_from", 0)
|
| 19 |
|
| 20 |
+
# Get timezone from input (default to dotenv default or UTC if not provided)
|
| 21 |
+
timezone = input.get("timezone", get_dotenv_value("DEFAULT_USER_TIMEZONE", "UTC"))
|
| 22 |
+
Localization.get().set_timezone(timezone)
|
| 23 |
+
|
| 24 |
# context instance - get or create
|
| 25 |
context = self.get_context(ctxid)
|
| 26 |
|
|
|
|
| 34 |
# Always reload the scheduler on each poll to ensure we have the latest task state
|
| 35 |
await scheduler.reload()
|
| 36 |
|
| 37 |
+
# loop AgentContext._contexts and divide into contexts and tasks
|
| 38 |
|
| 39 |
ctxs = []
|
| 40 |
tasks = []
|
|
|
|
| 48 |
continue
|
| 49 |
|
| 50 |
# Create the base context data that will be returned
|
| 51 |
+
context_data = ctx.serialize()
|
| 52 |
+
|
| 53 |
+
context_task = scheduler.get_task_by_uuid(ctx.id)
|
| 54 |
+
# Determine if this is a task-dedicated context by checking if a task with this UUID exists
|
| 55 |
+
is_task_context = (
|
| 56 |
+
context_task is not None and context_task.context_id == ctx.id
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
if not is_task_context:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
ctxs.append(context_data)
|
| 61 |
else:
|
| 62 |
# If this is a task, get task details from the scheduler
|
|
|
|
| 65 |
# Add task details to context_data with the same field names
|
| 66 |
# as used in scheduler endpoints to maintain UI compatibility
|
| 67 |
context_data.update({
|
| 68 |
+
"task_name": task_details.get("name"), # name is for context, task_name for the task name
|
| 69 |
"uuid": task_details.get("uuid"),
|
| 70 |
"state": task_details.get("state"),
|
| 71 |
"type": task_details.get("type"),
|
|
|
|
| 73 |
"prompt": task_details.get("prompt"),
|
| 74 |
"last_run": task_details.get("last_run"),
|
| 75 |
"last_result": task_details.get("last_result"),
|
| 76 |
+
"attachments": task_details.get("attachments", []),
|
| 77 |
+
"context_id": task_details.get("context_id"),
|
| 78 |
})
|
| 79 |
|
| 80 |
# Add type-specific fields
|
| 81 |
if task_details.get("type") == "scheduled":
|
| 82 |
context_data["schedule"] = task_details.get("schedule")
|
| 83 |
+
elif task_details.get("type") == "planned":
|
| 84 |
+
context_data["plan"] = task_details.get("plan")
|
| 85 |
else:
|
| 86 |
context_data["token"] = task_details.get("token")
|
| 87 |
|
python/api/scheduler_adhoc.py_
DELETED
|
@@ -1,5 +0,0 @@
|
|
| 1 |
-
## JSON Body
|
| 2 |
-
# api_key
|
| 3 |
-
# task_id
|
| 4 |
-
# params
|
| 5 |
-
# callback
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
python/api/scheduler_task_create.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
| 1 |
from python.helpers.api import ApiHandler, Input, Output, Request
|
| 2 |
from python.helpers.task_scheduler import (
|
| 3 |
-
TaskScheduler, ScheduledTask, AdHocTask, TaskSchedule,
|
| 4 |
-
serialize_task, parse_task_schedule
|
| 5 |
)
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
|
| 8 |
class SchedulerTaskCreate(ApiHandler):
|
|
@@ -10,6 +13,12 @@ class SchedulerTaskCreate(ApiHandler):
|
|
| 10 |
"""
|
| 11 |
Create a new task in the scheduler
|
| 12 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
scheduler = TaskScheduler.get()
|
| 14 |
await scheduler.reload()
|
| 15 |
|
|
@@ -18,11 +27,22 @@ class SchedulerTaskCreate(ApiHandler):
|
|
| 18 |
system_prompt = input.get("system_prompt")
|
| 19 |
prompt = input.get("prompt")
|
| 20 |
attachments = input.get("attachments", [])
|
|
|
|
| 21 |
|
| 22 |
# Check if schedule is provided (for ScheduledTask)
|
| 23 |
schedule = input.get("schedule", {})
|
| 24 |
token: str = input.get("token", "")
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
# Validate required fields
|
| 27 |
if not name or not system_prompt or not prompt:
|
| 28 |
return {"error": "Missing required fields: name, system_prompt, prompt"}
|
|
@@ -55,24 +75,61 @@ class SchedulerTaskCreate(ApiHandler):
|
|
| 55 |
system_prompt=system_prompt,
|
| 56 |
prompt=prompt,
|
| 57 |
schedule=task_schedule,
|
| 58 |
-
attachments=attachments
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
)
|
| 60 |
else:
|
| 61 |
# Create an ad-hoc task
|
|
|
|
| 62 |
task = AdHocTask.create(
|
| 63 |
name=name,
|
| 64 |
system_prompt=system_prompt,
|
| 65 |
prompt=prompt,
|
| 66 |
token=token,
|
| 67 |
-
attachments=attachments
|
|
|
|
| 68 |
)
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
# Add the task to the scheduler
|
| 71 |
await scheduler.add_task(task)
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
# Return the created task using our standardized serialization function
|
| 74 |
task_dict = serialize_task(task)
|
| 75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
return {
|
| 77 |
"task": task_dict
|
| 78 |
}
|
|
|
|
| 1 |
from python.helpers.api import ApiHandler, Input, Output, Request
|
| 2 |
from python.helpers.task_scheduler import (
|
| 3 |
+
TaskScheduler, ScheduledTask, AdHocTask, PlannedTask, TaskSchedule,
|
| 4 |
+
serialize_task, parse_task_schedule, parse_task_plan, TaskType
|
| 5 |
)
|
| 6 |
+
from python.helpers.localization import Localization
|
| 7 |
+
from python.helpers.print_style import PrintStyle
|
| 8 |
+
import random
|
| 9 |
|
| 10 |
|
| 11 |
class SchedulerTaskCreate(ApiHandler):
|
|
|
|
| 13 |
"""
|
| 14 |
Create a new task in the scheduler
|
| 15 |
"""
|
| 16 |
+
printer = PrintStyle(italic=True, font_color="blue", padding=False)
|
| 17 |
+
|
| 18 |
+
# Get timezone from input (do not set if not provided, we then rely on poll() to set it)
|
| 19 |
+
if timezone := input.get("timezone", None):
|
| 20 |
+
Localization.get().set_timezone(timezone)
|
| 21 |
+
|
| 22 |
scheduler = TaskScheduler.get()
|
| 23 |
await scheduler.reload()
|
| 24 |
|
|
|
|
| 27 |
system_prompt = input.get("system_prompt")
|
| 28 |
prompt = input.get("prompt")
|
| 29 |
attachments = input.get("attachments", [])
|
| 30 |
+
context_id = input.get("context_id", None)
|
| 31 |
|
| 32 |
# Check if schedule is provided (for ScheduledTask)
|
| 33 |
schedule = input.get("schedule", {})
|
| 34 |
token: str = input.get("token", "")
|
| 35 |
|
| 36 |
+
# Debug log the token value
|
| 37 |
+
printer.print(f"Token received from frontend: '{token}' (type: {type(token)}, length: {len(token) if token else 0})")
|
| 38 |
+
|
| 39 |
+
# Generate a random token if empty or not provided
|
| 40 |
+
if not token:
|
| 41 |
+
token = str(random.randint(1000000000000000000, 9999999999999999999))
|
| 42 |
+
printer.print(f"Generated new token: '{token}'")
|
| 43 |
+
|
| 44 |
+
plan = input.get("plan", {})
|
| 45 |
+
|
| 46 |
# Validate required fields
|
| 47 |
if not name or not system_prompt or not prompt:
|
| 48 |
return {"error": "Missing required fields: name, system_prompt, prompt"}
|
|
|
|
| 75 |
system_prompt=system_prompt,
|
| 76 |
prompt=prompt,
|
| 77 |
schedule=task_schedule,
|
| 78 |
+
attachments=attachments,
|
| 79 |
+
context_id=context_id,
|
| 80 |
+
timezone=timezone
|
| 81 |
+
)
|
| 82 |
+
elif plan:
|
| 83 |
+
# Create a planned task
|
| 84 |
+
try:
|
| 85 |
+
# Use our standardized parsing function
|
| 86 |
+
task_plan = parse_task_plan(plan)
|
| 87 |
+
except ValueError as e:
|
| 88 |
+
return {"error": str(e)}
|
| 89 |
+
|
| 90 |
+
task = PlannedTask.create(
|
| 91 |
+
name=name,
|
| 92 |
+
system_prompt=system_prompt,
|
| 93 |
+
prompt=prompt,
|
| 94 |
+
plan=task_plan,
|
| 95 |
+
attachments=attachments,
|
| 96 |
+
context_id=context_id
|
| 97 |
)
|
| 98 |
else:
|
| 99 |
# Create an ad-hoc task
|
| 100 |
+
printer.print(f"Creating AdHocTask with token: '{token}'")
|
| 101 |
task = AdHocTask.create(
|
| 102 |
name=name,
|
| 103 |
system_prompt=system_prompt,
|
| 104 |
prompt=prompt,
|
| 105 |
token=token,
|
| 106 |
+
attachments=attachments,
|
| 107 |
+
context_id=context_id
|
| 108 |
)
|
| 109 |
+
# Verify token after creation
|
| 110 |
+
if isinstance(task, AdHocTask):
|
| 111 |
+
printer.print(f"AdHocTask created with token: '{task.token}'")
|
| 112 |
|
| 113 |
# Add the task to the scheduler
|
| 114 |
await scheduler.add_task(task)
|
| 115 |
|
| 116 |
+
# Verify the task was added correctly - retrieve by UUID to check persistence
|
| 117 |
+
saved_task = scheduler.get_task_by_uuid(task.uuid)
|
| 118 |
+
if saved_task:
|
| 119 |
+
if saved_task.type == TaskType.AD_HOC and isinstance(saved_task, AdHocTask):
|
| 120 |
+
printer.print(f"Task verified after save, token: '{saved_task.token}'")
|
| 121 |
+
else:
|
| 122 |
+
printer.print("Task verified after save, not an adhoc task")
|
| 123 |
+
else:
|
| 124 |
+
printer.print("WARNING: Task not found after save!")
|
| 125 |
+
|
| 126 |
# Return the created task using our standardized serialization function
|
| 127 |
task_dict = serialize_task(task)
|
| 128 |
|
| 129 |
+
# Debug log the serialized task
|
| 130 |
+
if task_dict and task_dict.get('type') == 'adhoc':
|
| 131 |
+
printer.print(f"Serialized adhoc task, token in response: '{task_dict.get('token')}'")
|
| 132 |
+
|
| 133 |
return {
|
| 134 |
"task": task_dict
|
| 135 |
}
|
python/api/scheduler_task_delete.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
| 1 |
from python.helpers.api import ApiHandler, Input, Output, Request
|
| 2 |
-
from python.helpers.task_scheduler import TaskScheduler
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
|
| 5 |
class SchedulerTaskDelete(ApiHandler):
|
|
@@ -7,6 +10,10 @@ class SchedulerTaskDelete(ApiHandler):
|
|
| 7 |
"""
|
| 8 |
Delete a task from the scheduler by ID
|
| 9 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
scheduler = TaskScheduler.get()
|
| 11 |
await scheduler.reload()
|
| 12 |
|
|
@@ -21,6 +28,24 @@ class SchedulerTaskDelete(ApiHandler):
|
|
| 21 |
if not task:
|
| 22 |
return {"error": f"Task with ID {task_id} not found"}
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
# Remove the task
|
| 25 |
await scheduler.remove_task_by_uuid(task_id)
|
| 26 |
|
|
|
|
| 1 |
from python.helpers.api import ApiHandler, Input, Output, Request
|
| 2 |
+
from python.helpers.task_scheduler import TaskScheduler, TaskState
|
| 3 |
+
from python.helpers.localization import Localization
|
| 4 |
+
from agent import AgentContext
|
| 5 |
+
from python.helpers import persist_chat
|
| 6 |
|
| 7 |
|
| 8 |
class SchedulerTaskDelete(ApiHandler):
|
|
|
|
| 10 |
"""
|
| 11 |
Delete a task from the scheduler by ID
|
| 12 |
"""
|
| 13 |
+
# Get timezone from input (do not set if not provided, we then rely on poll() to set it)
|
| 14 |
+
if timezone := input.get("timezone", None):
|
| 15 |
+
Localization.get().set_timezone(timezone)
|
| 16 |
+
|
| 17 |
scheduler = TaskScheduler.get()
|
| 18 |
await scheduler.reload()
|
| 19 |
|
|
|
|
| 28 |
if not task:
|
| 29 |
return {"error": f"Task with ID {task_id} not found"}
|
| 30 |
|
| 31 |
+
context = None
|
| 32 |
+
if task.context_id:
|
| 33 |
+
context = self.get_context(task.context_id)
|
| 34 |
+
|
| 35 |
+
# If the task is running, update its state to IDLE first
|
| 36 |
+
if task.state == TaskState.RUNNING:
|
| 37 |
+
if context:
|
| 38 |
+
context.reset()
|
| 39 |
+
# Update the state to IDLE so any ongoing processes know to terminate
|
| 40 |
+
await scheduler.update_task(task_id, state=TaskState.IDLE)
|
| 41 |
+
# Force a save to ensure the state change is persisted
|
| 42 |
+
await scheduler.save()
|
| 43 |
+
|
| 44 |
+
# This is a dedicated context for the task, so we remove it
|
| 45 |
+
if context and context.id == task.uuid:
|
| 46 |
+
AgentContext.remove(context.id)
|
| 47 |
+
persist_chat.remove_chat(context.id)
|
| 48 |
+
|
| 49 |
# Remove the task
|
| 50 |
await scheduler.remove_task_by_uuid(task_id)
|
| 51 |
|
python/api/scheduler_task_run.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
from python.helpers.api import ApiHandler, Input, Output, Request
|
| 2 |
from python.helpers.task_scheduler import TaskScheduler, TaskState
|
| 3 |
from python.helpers.print_style import PrintStyle
|
|
|
|
| 4 |
|
| 5 |
|
| 6 |
class SchedulerTaskRun(ApiHandler):
|
|
@@ -11,6 +12,9 @@ class SchedulerTaskRun(ApiHandler):
|
|
| 11 |
"""
|
| 12 |
Manually run a task from the scheduler by ID
|
| 13 |
"""
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
# Get task ID from input
|
| 16 |
task_id: str = input.get("task_id", "")
|
|
|
|
| 1 |
from python.helpers.api import ApiHandler, Input, Output, Request
|
| 2 |
from python.helpers.task_scheduler import TaskScheduler, TaskState
|
| 3 |
from python.helpers.print_style import PrintStyle
|
| 4 |
+
from python.helpers.localization import Localization
|
| 5 |
|
| 6 |
|
| 7 |
class SchedulerTaskRun(ApiHandler):
|
|
|
|
| 12 |
"""
|
| 13 |
Manually run a task from the scheduler by ID
|
| 14 |
"""
|
| 15 |
+
# Get timezone from input (do not set if not provided, we then rely on poll() to set it)
|
| 16 |
+
if timezone := input.get("timezone", None):
|
| 17 |
+
Localization.get().set_timezone(timezone)
|
| 18 |
|
| 19 |
# Get task ID from input
|
| 20 |
task_id: str = input.get("task_id", "")
|
python/api/scheduler_task_update.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
| 1 |
from python.helpers.api import ApiHandler, Input, Output, Request
|
| 2 |
from python.helpers.task_scheduler import (
|
| 3 |
-
TaskScheduler, ScheduledTask, AdHocTask, TaskState,
|
| 4 |
-
serialize_task, parse_task_schedule
|
| 5 |
)
|
|
|
|
| 6 |
|
| 7 |
|
| 8 |
class SchedulerTaskUpdate(ApiHandler):
|
|
@@ -10,6 +11,10 @@ class SchedulerTaskUpdate(ApiHandler):
|
|
| 10 |
"""
|
| 11 |
Update an existing task in the scheduler
|
| 12 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
scheduler = TaskScheduler.get()
|
| 14 |
await scheduler.reload()
|
| 15 |
|
|
@@ -47,11 +52,28 @@ class SchedulerTaskUpdate(ApiHandler):
|
|
| 47 |
if isinstance(task, ScheduledTask) and "schedule" in input:
|
| 48 |
schedule_data = input.get("schedule", {})
|
| 49 |
try:
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
except ValueError as e:
|
| 52 |
return {"error": f"Invalid schedule format: {str(e)}"}
|
| 53 |
elif isinstance(task, AdHocTask) and "token" in input:
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
# Use atomic update method to apply changes
|
| 57 |
updated_task = await scheduler.update_task(task_id, **update_params)
|
|
|
|
| 1 |
from python.helpers.api import ApiHandler, Input, Output, Request
|
| 2 |
from python.helpers.task_scheduler import (
|
| 3 |
+
TaskScheduler, ScheduledTask, AdHocTask, PlannedTask, TaskState,
|
| 4 |
+
serialize_task, parse_task_schedule, parse_task_plan
|
| 5 |
)
|
| 6 |
+
from python.helpers.localization import Localization
|
| 7 |
|
| 8 |
|
| 9 |
class SchedulerTaskUpdate(ApiHandler):
|
|
|
|
| 11 |
"""
|
| 12 |
Update an existing task in the scheduler
|
| 13 |
"""
|
| 14 |
+
# Get timezone from input (do not set if not provided, we then rely on poll() to set it)
|
| 15 |
+
if timezone := input.get("timezone", None):
|
| 16 |
+
Localization.get().set_timezone(timezone)
|
| 17 |
+
|
| 18 |
scheduler = TaskScheduler.get()
|
| 19 |
await scheduler.reload()
|
| 20 |
|
|
|
|
| 52 |
if isinstance(task, ScheduledTask) and "schedule" in input:
|
| 53 |
schedule_data = input.get("schedule", {})
|
| 54 |
try:
|
| 55 |
+
# Parse the schedule with timezone handling
|
| 56 |
+
task_schedule = parse_task_schedule(schedule_data)
|
| 57 |
+
|
| 58 |
+
# Set the timezone from the request if not already in schedule_data
|
| 59 |
+
if not schedule_data.get('timezone', None) and timezone:
|
| 60 |
+
task_schedule.timezone = timezone
|
| 61 |
+
|
| 62 |
+
update_params["schedule"] = task_schedule
|
| 63 |
except ValueError as e:
|
| 64 |
return {"error": f"Invalid schedule format: {str(e)}"}
|
| 65 |
elif isinstance(task, AdHocTask) and "token" in input:
|
| 66 |
+
token_value = input.get("token", "")
|
| 67 |
+
if token_value: # Only update if non-empty
|
| 68 |
+
update_params["token"] = token_value
|
| 69 |
+
elif isinstance(task, PlannedTask) and "plan" in input:
|
| 70 |
+
plan_data = input.get("plan", {})
|
| 71 |
+
try:
|
| 72 |
+
# Parse the plan data
|
| 73 |
+
task_plan = parse_task_plan(plan_data)
|
| 74 |
+
update_params["plan"] = task_plan
|
| 75 |
+
except ValueError as e:
|
| 76 |
+
return {"error": f"Invalid plan format: {str(e)}"}
|
| 77 |
|
| 78 |
# Use atomic update method to apply changes
|
| 79 |
updated_task = await scheduler.update_task(task_id, **update_params)
|
python/api/scheduler_tasks_list.py
CHANGED
|
@@ -2,6 +2,7 @@ from python.helpers.api import ApiHandler, Input, Output, Request
|
|
| 2 |
from python.helpers.task_scheduler import TaskScheduler
|
| 3 |
import traceback
|
| 4 |
from python.helpers.print_style import PrintStyle
|
|
|
|
| 5 |
|
| 6 |
|
| 7 |
class SchedulerTasksList(ApiHandler):
|
|
@@ -10,6 +11,10 @@ class SchedulerTasksList(ApiHandler):
|
|
| 10 |
List all tasks in the scheduler with their types
|
| 11 |
"""
|
| 12 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
# Get task scheduler
|
| 14 |
scheduler = TaskScheduler.get()
|
| 15 |
await scheduler.reload()
|
|
|
|
| 2 |
from python.helpers.task_scheduler import TaskScheduler
|
| 3 |
import traceback
|
| 4 |
from python.helpers.print_style import PrintStyle
|
| 5 |
+
from python.helpers.localization import Localization
|
| 6 |
|
| 7 |
|
| 8 |
class SchedulerTasksList(ApiHandler):
|
|
|
|
| 11 |
List all tasks in the scheduler with their types
|
| 12 |
"""
|
| 13 |
try:
|
| 14 |
+
# Get timezone from input (do not set if not provided, we then rely on poll() to set it)
|
| 15 |
+
if timezone := input.get("timezone", None):
|
| 16 |
+
Localization.get().set_timezone(timezone)
|
| 17 |
+
|
| 18 |
# Get task scheduler
|
| 19 |
scheduler = TaskScheduler.get()
|
| 20 |
await scheduler.reload()
|
python/api/scheduler_tick.py
CHANGED
|
@@ -3,14 +3,19 @@ from datetime import datetime
|
|
| 3 |
from python.helpers.api import ApiHandler, Input, Output, Request
|
| 4 |
from python.helpers.print_style import PrintStyle
|
| 5 |
from python.helpers.task_scheduler import TaskScheduler
|
|
|
|
| 6 |
|
| 7 |
|
| 8 |
class SchedulerTick(ApiHandler):
|
| 9 |
@classmethod
|
| 10 |
-
def requires_loopback(cls):
|
| 11 |
return True
|
| 12 |
|
| 13 |
async def process(self, input: Input, request: Request) -> Output:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 15 |
printer = PrintStyle(font_color="green", padding=False)
|
| 16 |
printer.print(f"Scheduler tick - API: {timestamp}")
|
|
|
|
| 3 |
from python.helpers.api import ApiHandler, Input, Output, Request
|
| 4 |
from python.helpers.print_style import PrintStyle
|
| 5 |
from python.helpers.task_scheduler import TaskScheduler
|
| 6 |
+
from python.helpers.localization import Localization
|
| 7 |
|
| 8 |
|
| 9 |
class SchedulerTick(ApiHandler):
|
| 10 |
@classmethod
|
| 11 |
+
def requires_loopback(cls) -> bool:
|
| 12 |
return True
|
| 13 |
|
| 14 |
async def process(self, input: Input, request: Request) -> Output:
|
| 15 |
+
# Get timezone from input (do not set if not provided, we then rely on poll() to set it)
|
| 16 |
+
if timezone := input.get("timezone", None):
|
| 17 |
+
Localization.get().set_timezone(timezone)
|
| 18 |
+
|
| 19 |
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 20 |
printer = PrintStyle(font_color="green", padding=False)
|
| 21 |
printer.print(f"Scheduler tick - API: {timestamp}")
|
python/extensions/message_loop_prompts/_60_include_current_datetime.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime, timezone
|
| 2 |
+
from python.helpers.extension import Extension
|
| 3 |
+
from agent import LoopData
|
| 4 |
+
from python.helpers.localization import Localization
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class IncludeCurrentDatetime(Extension):
|
| 8 |
+
async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
|
| 9 |
+
# get current datetime
|
| 10 |
+
current_datetime = Localization.get().utc_dt_to_localtime_str(datetime.now(timezone.utc), sep=" ", timespec="seconds")
|
| 11 |
+
# remove timezone offset
|
| 12 |
+
current_datetime = current_datetime.split("+")[0]
|
| 13 |
+
|
| 14 |
+
# remove old current datetime from loop data
|
| 15 |
+
if "current_datetime" in loop_data.extras_temporary:
|
| 16 |
+
del loop_data.extras_temporary["current_datetime"]
|
| 17 |
+
|
| 18 |
+
# read prompt
|
| 19 |
+
datetime_prompt = self.agent.read_prompt("agent.system.datetime.md", date_time=current_datetime)
|
| 20 |
+
|
| 21 |
+
# add current datetime to the loop data
|
| 22 |
+
loop_data.extras_temporary["current_datetime"] = datetime_prompt
|
python/extensions/system_prompt/_10_system_prompt.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
-
from datetime import datetime
|
| 2 |
from python.helpers.extension import Extension
|
| 3 |
from agent import Agent, LoopData
|
|
|
|
| 4 |
|
| 5 |
|
| 6 |
class SystemPrompt(Extension):
|
|
@@ -28,8 +29,12 @@ def get_prompt(file: str, agent: Agent):
|
|
| 28 |
# variables for system prompts
|
| 29 |
# TODO: move variables to the end of chain
|
| 30 |
# variables in system prompt would break prompt caching, better to add them to the last message in conversation
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
vars = {
|
| 32 |
-
"date_time":
|
| 33 |
"agent_name": agent.agent_name,
|
| 34 |
}
|
| 35 |
return agent.read_prompt(file, **vars)
|
|
|
|
| 1 |
+
from datetime import datetime, timezone
|
| 2 |
from python.helpers.extension import Extension
|
| 3 |
from agent import Agent, LoopData
|
| 4 |
+
from python.helpers.localization import Localization
|
| 5 |
|
| 6 |
|
| 7 |
class SystemPrompt(Extension):
|
|
|
|
| 29 |
# variables for system prompts
|
| 30 |
# TODO: move variables to the end of chain
|
| 31 |
# variables in system prompt would break prompt caching, better to add them to the last message in conversation
|
| 32 |
+
# get current datetime
|
| 33 |
+
current_datetime = Localization.get().utc_dt_to_localtime_str(datetime.now(timezone.utc), sep=" ", timespec="seconds")
|
| 34 |
+
# remove timezone offset
|
| 35 |
+
current_datetime = current_datetime.split("+")[0]
|
| 36 |
vars = {
|
| 37 |
+
"date_time": current_datetime,
|
| 38 |
"agent_name": agent.agent_name,
|
| 39 |
}
|
| 40 |
return agent.read_prompt(file, **vars)
|
python/helpers/api.py
CHANGED
|
@@ -12,7 +12,7 @@ from werkzeug.serving import make_server
|
|
| 12 |
|
| 13 |
|
| 14 |
Input = dict
|
| 15 |
-
Output = Union[Dict[str, Any], Response, TypedDict]
|
| 16 |
|
| 17 |
|
| 18 |
class ApiHandler:
|
|
@@ -21,15 +21,15 @@ class ApiHandler:
|
|
| 21 |
self.thread_lock = thread_lock
|
| 22 |
|
| 23 |
@classmethod
|
| 24 |
-
def requires_loopback(cls):
|
| 25 |
return False
|
| 26 |
|
| 27 |
@classmethod
|
| 28 |
-
def requires_api_key(cls):
|
| 29 |
return False
|
| 30 |
|
| 31 |
@classmethod
|
| 32 |
-
def requires_auth(cls):
|
| 33 |
return True
|
| 34 |
|
| 35 |
@abstractmethod
|
|
|
|
| 12 |
|
| 13 |
|
| 14 |
Input = dict
|
| 15 |
+
Output = Union[Dict[str, Any], Response, TypedDict] # type: ignore
|
| 16 |
|
| 17 |
|
| 18 |
class ApiHandler:
|
|
|
|
| 21 |
self.thread_lock = thread_lock
|
| 22 |
|
| 23 |
@classmethod
|
| 24 |
+
def requires_loopback(cls) -> bool:
|
| 25 |
return False
|
| 26 |
|
| 27 |
@classmethod
|
| 28 |
+
def requires_api_key(cls) -> bool:
|
| 29 |
return False
|
| 30 |
|
| 31 |
@classmethod
|
| 32 |
+
def requires_auth(cls) -> bool:
|
| 33 |
return True
|
| 34 |
|
| 35 |
@abstractmethod
|
python/helpers/chat_names.py
DELETED
|
@@ -1,137 +0,0 @@
|
|
| 1 |
-
import json
|
| 2 |
-
import os
|
| 3 |
-
import time
|
| 4 |
-
from typing import Dict, Any
|
| 5 |
-
|
| 6 |
-
from python.helpers import files
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
class ChatNames:
|
| 10 |
-
_instance = None
|
| 11 |
-
_names_file = "chat_names.json"
|
| 12 |
-
_metadata_file = "chat_metadata.json"
|
| 13 |
-
|
| 14 |
-
@classmethod
|
| 15 |
-
def get_instance(cls):
|
| 16 |
-
if cls._instance is None:
|
| 17 |
-
cls._instance = ChatNames()
|
| 18 |
-
cls._instance._names_file = files.get_abs_path("tmp") + "/" + cls._names_file
|
| 19 |
-
cls._instance._metadata_file = files.get_abs_path("tmp") + "/" + cls._metadata_file
|
| 20 |
-
return cls._instance
|
| 21 |
-
|
| 22 |
-
def _read_names(self) -> dict:
|
| 23 |
-
"""Read current names from file"""
|
| 24 |
-
try:
|
| 25 |
-
if os.path.exists(self._names_file):
|
| 26 |
-
with open(self._names_file, 'r') as f:
|
| 27 |
-
return json.load(f)
|
| 28 |
-
except Exception as e:
|
| 29 |
-
print(f"Error reading chat names: {e}")
|
| 30 |
-
return {}
|
| 31 |
-
|
| 32 |
-
def _save_names(self, names: dict):
|
| 33 |
-
"""Save names to file"""
|
| 34 |
-
try:
|
| 35 |
-
with open(self._names_file, 'w') as f:
|
| 36 |
-
json.dump(names, f, indent=2)
|
| 37 |
-
except Exception as e:
|
| 38 |
-
print(f"Error saving chat names: {e}")
|
| 39 |
-
|
| 40 |
-
def _read_metadata(self) -> dict:
|
| 41 |
-
"""Read context metadata from file"""
|
| 42 |
-
try:
|
| 43 |
-
if os.path.exists(self._metadata_file):
|
| 44 |
-
with open(self._metadata_file, 'r') as f:
|
| 45 |
-
return json.load(f)
|
| 46 |
-
except Exception as e:
|
| 47 |
-
print(f"Error reading context metadata: {e}")
|
| 48 |
-
return {}
|
| 49 |
-
|
| 50 |
-
def _save_metadata(self, metadata: dict):
|
| 51 |
-
"""Save metadata to file"""
|
| 52 |
-
try:
|
| 53 |
-
with open(self._metadata_file, 'w') as f:
|
| 54 |
-
json.dump(metadata, f, indent=2)
|
| 55 |
-
except Exception as e:
|
| 56 |
-
print(f"Error saving context metadata: {e}")
|
| 57 |
-
|
| 58 |
-
def set_name(self, chat_id: str, name: str):
|
| 59 |
-
"""Set name for a chat/task"""
|
| 60 |
-
names = self._read_names()
|
| 61 |
-
names[chat_id] = name
|
| 62 |
-
self._save_names(names)
|
| 63 |
-
|
| 64 |
-
# Also update the name in metadata if it exists
|
| 65 |
-
self.update_metadata(chat_id, {"name": name})
|
| 66 |
-
|
| 67 |
-
def get_name(self, chat_id: str) -> str:
|
| 68 |
-
"""Get name for a chat/task"""
|
| 69 |
-
names = self._read_names()
|
| 70 |
-
return names.get(chat_id, f"Chat #{chat_id[:8]}")
|
| 71 |
-
|
| 72 |
-
def set_metadata(self, chat_id: str, metadata_dict: Dict[str, Any]):
|
| 73 |
-
"""Set complete metadata for a chat/task"""
|
| 74 |
-
all_metadata = self._read_metadata()
|
| 75 |
-
|
| 76 |
-
# Ensure we have created_at timestamp if not provided
|
| 77 |
-
if "created_at" not in metadata_dict:
|
| 78 |
-
metadata_dict["created_at"] = int(time.time())
|
| 79 |
-
|
| 80 |
-
all_metadata[chat_id] = metadata_dict
|
| 81 |
-
self._save_metadata(all_metadata)
|
| 82 |
-
|
| 83 |
-
# Also update the name in the names file for backward compatibility
|
| 84 |
-
if "name" in metadata_dict:
|
| 85 |
-
self.set_name(chat_id, metadata_dict["name"])
|
| 86 |
-
|
| 87 |
-
def update_metadata(self, chat_id: str, metadata_update: Dict[str, Any]):
|
| 88 |
-
"""Update specific metadata fields for a chat/task"""
|
| 89 |
-
all_metadata = self._read_metadata()
|
| 90 |
-
|
| 91 |
-
if chat_id in all_metadata:
|
| 92 |
-
all_metadata[chat_id].update(metadata_update)
|
| 93 |
-
else:
|
| 94 |
-
# Initialize with current timestamp if creating new metadata
|
| 95 |
-
metadata_update["created_at"] = metadata_update.get("created_at", int(time.time()))
|
| 96 |
-
all_metadata[chat_id] = metadata_update
|
| 97 |
-
|
| 98 |
-
self._save_metadata(all_metadata)
|
| 99 |
-
|
| 100 |
-
def get_metadata(self, chat_id: str) -> Dict[str, Any]:
|
| 101 |
-
"""Get metadata for a chat/task"""
|
| 102 |
-
all_metadata = self._read_metadata()
|
| 103 |
-
|
| 104 |
-
# Return metadata if it exists
|
| 105 |
-
if chat_id in all_metadata:
|
| 106 |
-
return all_metadata[chat_id]
|
| 107 |
-
|
| 108 |
-
# Otherwise create default metadata with just the name
|
| 109 |
-
name = self.get_name(chat_id)
|
| 110 |
-
default_metadata = {
|
| 111 |
-
"name": name,
|
| 112 |
-
"created_at": int(time.time()) # Current time as fallback
|
| 113 |
-
}
|
| 114 |
-
return default_metadata
|
| 115 |
-
|
| 116 |
-
def get_created_at(self, chat_id: str) -> int:
|
| 117 |
-
"""Get creation timestamp for a chat/task"""
|
| 118 |
-
metadata = self.get_metadata(chat_id)
|
| 119 |
-
return metadata.get("created_at", 0)
|
| 120 |
-
|
| 121 |
-
def remove_chat(self, chat_id: str):
|
| 122 |
-
"""Remove chat/task and its metadata"""
|
| 123 |
-
print("Removing chat name and metadata: ", chat_id)
|
| 124 |
-
|
| 125 |
-
# Remove from names file
|
| 126 |
-
names = self._read_names()
|
| 127 |
-
if chat_id in names:
|
| 128 |
-
del names[chat_id]
|
| 129 |
-
print("Chat name removed: ", chat_id)
|
| 130 |
-
self._save_names(names)
|
| 131 |
-
|
| 132 |
-
# Remove from metadata file
|
| 133 |
-
metadata = self._read_metadata()
|
| 134 |
-
if chat_id in metadata:
|
| 135 |
-
del metadata[chat_id]
|
| 136 |
-
print("Chat metadata removed: ", chat_id)
|
| 137 |
-
self._save_metadata(metadata)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
python/helpers/localization.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime
|
| 2 |
+
import pytz # type: ignore
|
| 3 |
+
|
| 4 |
+
from python.helpers.print_style import PrintStyle
|
| 5 |
+
from python.helpers.dotenv import get_dotenv_value, save_dotenv_value
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class Localization:
|
| 9 |
+
"""
|
| 10 |
+
Localization class for handling timezone conversions between UTC and local time.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
# singleton
|
| 14 |
+
_instance = None
|
| 15 |
+
|
| 16 |
+
@classmethod
|
| 17 |
+
def get(cls, *args, **kwargs):
|
| 18 |
+
if cls._instance is None:
|
| 19 |
+
cls._instance = cls(*args, **kwargs)
|
| 20 |
+
return cls._instance
|
| 21 |
+
|
| 22 |
+
def __init__(self, timezone: str | None = None):
|
| 23 |
+
if timezone is not None:
|
| 24 |
+
self.set_timezone(timezone) # Use the setter to validate
|
| 25 |
+
else:
|
| 26 |
+
timezone = str(get_dotenv_value("DEFAULT_USER_TIMEZONE", "UTC"))
|
| 27 |
+
self.set_timezone(timezone)
|
| 28 |
+
|
| 29 |
+
def get_timezone(self) -> str:
|
| 30 |
+
return self.timezone
|
| 31 |
+
|
| 32 |
+
def set_timezone(self, timezone: str) -> None:
|
| 33 |
+
"""Set the timezone, with validation."""
|
| 34 |
+
# Validate timezone
|
| 35 |
+
try:
|
| 36 |
+
pytz.timezone(timezone)
|
| 37 |
+
if timezone != getattr(self, 'timezone', None):
|
| 38 |
+
PrintStyle.debug(f"Changing timezone from {getattr(self, 'timezone', 'None')} to {timezone}")
|
| 39 |
+
self.timezone = timezone
|
| 40 |
+
save_dotenv_value("DEFAULT_USER_TIMEZONE", timezone)
|
| 41 |
+
except pytz.exceptions.UnknownTimeZoneError:
|
| 42 |
+
PrintStyle.error(f"Unknown timezone: {timezone}, defaulting to UTC")
|
| 43 |
+
self.timezone = "UTC"
|
| 44 |
+
# save the default timezone to the environment variable to avoid future errors on startup
|
| 45 |
+
save_dotenv_value("DEFAULT_USER_TIMEZONE", "UTC")
|
| 46 |
+
|
| 47 |
+
def localtime_str_to_utc_dt(self, localtime_str: str | None) -> datetime | None:
|
| 48 |
+
"""
|
| 49 |
+
Convert a local time ISO string to a UTC datetime object.
|
| 50 |
+
Returns None if input is None or invalid.
|
| 51 |
+
"""
|
| 52 |
+
if not localtime_str:
|
| 53 |
+
return None
|
| 54 |
+
|
| 55 |
+
try:
|
| 56 |
+
# Handle both with and without timezone info
|
| 57 |
+
try:
|
| 58 |
+
# Try parsing with timezone info first
|
| 59 |
+
local_datetime_obj = datetime.fromisoformat(localtime_str)
|
| 60 |
+
if local_datetime_obj.tzinfo is None:
|
| 61 |
+
# If no timezone info, assume it's in the configured timezone
|
| 62 |
+
local_datetime_obj = pytz.timezone(self.timezone).localize(local_datetime_obj)
|
| 63 |
+
except ValueError:
|
| 64 |
+
# If timezone parsing fails, try without timezone
|
| 65 |
+
local_datetime_obj = datetime.fromisoformat(localtime_str.split('Z')[0].split('+')[0])
|
| 66 |
+
local_datetime_obj = pytz.timezone(self.timezone).localize(local_datetime_obj)
|
| 67 |
+
|
| 68 |
+
# Convert to UTC
|
| 69 |
+
return local_datetime_obj.astimezone(pytz.utc)
|
| 70 |
+
except Exception as e:
|
| 71 |
+
PrintStyle.error(f"Error converting localtime string to UTC: {e}")
|
| 72 |
+
return None
|
| 73 |
+
|
| 74 |
+
def utc_dt_to_localtime_str(self, utc_dt: datetime | None, sep: str = "T", timespec: str = "auto") -> str | None:
|
| 75 |
+
"""
|
| 76 |
+
Convert a UTC datetime object to a local time ISO string.
|
| 77 |
+
Returns None if input is None.
|
| 78 |
+
"""
|
| 79 |
+
if utc_dt is None:
|
| 80 |
+
return None
|
| 81 |
+
|
| 82 |
+
# At this point, utc_dt is definitely not None
|
| 83 |
+
assert utc_dt is not None
|
| 84 |
+
|
| 85 |
+
try:
|
| 86 |
+
# Ensure datetime is timezone aware
|
| 87 |
+
if utc_dt.tzinfo is None:
|
| 88 |
+
utc_dt = pytz.utc.localize(utc_dt)
|
| 89 |
+
elif utc_dt.tzinfo != pytz.utc:
|
| 90 |
+
utc_dt = utc_dt.astimezone(pytz.utc)
|
| 91 |
+
|
| 92 |
+
# Convert to local time
|
| 93 |
+
local_datetime_obj = utc_dt.astimezone(pytz.timezone(self.timezone))
|
| 94 |
+
# Return the local time string
|
| 95 |
+
return local_datetime_obj.isoformat(sep=sep, timespec=timespec)
|
| 96 |
+
except Exception as e:
|
| 97 |
+
PrintStyle.error(f"Error converting UTC datetime to localtime string: {e}")
|
| 98 |
+
return None
|
| 99 |
+
|
| 100 |
+
def serialize_datetime(self, dt: datetime | None) -> str | None:
|
| 101 |
+
"""
|
| 102 |
+
Serialize a datetime object to ISO format string in the user's timezone.
|
| 103 |
+
This ensures the frontend receives dates in the correct timezone for display.
|
| 104 |
+
"""
|
| 105 |
+
if dt is None:
|
| 106 |
+
return None
|
| 107 |
+
|
| 108 |
+
# At this point, dt is definitely not None
|
| 109 |
+
assert dt is not None
|
| 110 |
+
|
| 111 |
+
try:
|
| 112 |
+
# Ensure datetime is timezone aware (if not, assume UTC)
|
| 113 |
+
if dt.tzinfo is None:
|
| 114 |
+
dt = pytz.utc.localize(dt)
|
| 115 |
+
|
| 116 |
+
# Convert to the user's timezone
|
| 117 |
+
local_timezone = pytz.timezone(self.timezone)
|
| 118 |
+
local_dt = dt.astimezone(local_timezone)
|
| 119 |
+
|
| 120 |
+
return local_dt.isoformat()
|
| 121 |
+
except Exception as e:
|
| 122 |
+
PrintStyle.error(f"Error serializing datetime: {e}")
|
| 123 |
+
return None
|
python/helpers/persist_chat.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
from collections import OrderedDict
|
|
|
|
| 2 |
from typing import Any
|
| 3 |
import uuid
|
| 4 |
from agent import Agent, AgentConfig, AgentContext
|
|
@@ -104,6 +105,10 @@ def _serialize_context(context: AgentContext):
|
|
| 104 |
return {
|
| 105 |
"id": context.id,
|
| 106 |
"name": context.name,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
"agents": agents,
|
| 108 |
"streaming_agent": (
|
| 109 |
context.streaming_agent.number if context.streaming_agent else 0
|
|
@@ -143,6 +148,12 @@ def _deserialize_context(data):
|
|
| 143 |
config=config,
|
| 144 |
id=data.get("id", None), # get new id
|
| 145 |
name=data.get("name", None),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
log=log,
|
| 147 |
paused=False,
|
| 148 |
# agent0=agent0,
|
|
|
|
| 1 |
from collections import OrderedDict
|
| 2 |
+
from datetime import datetime
|
| 3 |
from typing import Any
|
| 4 |
import uuid
|
| 5 |
from agent import Agent, AgentConfig, AgentContext
|
|
|
|
| 105 |
return {
|
| 106 |
"id": context.id,
|
| 107 |
"name": context.name,
|
| 108 |
+
"created_at": (
|
| 109 |
+
context.created_at.isoformat() if context.created_at
|
| 110 |
+
else datetime.fromtimestamp(0).isoformat()
|
| 111 |
+
),
|
| 112 |
"agents": agents,
|
| 113 |
"streaming_agent": (
|
| 114 |
context.streaming_agent.number if context.streaming_agent else 0
|
|
|
|
| 148 |
config=config,
|
| 149 |
id=data.get("id", None), # get new id
|
| 150 |
name=data.get("name", None),
|
| 151 |
+
created_at=(
|
| 152 |
+
datetime.fromisoformat(
|
| 153 |
+
# older chats may not have created_at - backcompat
|
| 154 |
+
data.get("created_at", datetime.fromtimestamp(0).isoformat())
|
| 155 |
+
)
|
| 156 |
+
),
|
| 157 |
log=log,
|
| 158 |
paused=False,
|
| 159 |
# agent0=agent0,
|
python/helpers/task_scheduler.py
CHANGED
|
@@ -1,15 +1,13 @@
|
|
| 1 |
import asyncio
|
| 2 |
from datetime import datetime, timezone, timedelta
|
| 3 |
-
import json
|
| 4 |
import os
|
| 5 |
import random
|
| 6 |
import threading
|
| 7 |
from urllib.parse import urlparse
|
| 8 |
import uuid
|
| 9 |
-
from dataclasses import dataclass
|
| 10 |
from enum import Enum
|
| 11 |
from os.path import exists
|
| 12 |
-
from typing import Any, Callable,
|
| 13 |
|
| 14 |
import nest_asyncio
|
| 15 |
nest_asyncio.apply()
|
|
@@ -17,22 +15,23 @@ nest_asyncio.apply()
|
|
| 17 |
from crontab import CronTab
|
| 18 |
from pydantic import BaseModel, Field, PrivateAttr
|
| 19 |
|
| 20 |
-
from agent import Agent,
|
| 21 |
from initialize import initialize
|
| 22 |
-
from python.helpers.persist_chat import
|
| 23 |
from python.helpers.print_style import PrintStyle
|
| 24 |
from python.helpers.defer import DeferredTask
|
| 25 |
-
from python.helpers.files import get_abs_path,
|
| 26 |
-
from python.helpers.
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
SCHEDULER_FOLDER = "memory/scheduler"
|
| 30 |
|
|
|
|
| 31 |
|
| 32 |
# ----------------------
|
| 33 |
# Task Models
|
| 34 |
# ----------------------
|
| 35 |
|
|
|
|
| 36 |
class TaskState(str, Enum):
|
| 37 |
IDLE = "idle"
|
| 38 |
RUNNING = "running"
|
|
@@ -40,49 +39,100 @@ class TaskState(str, Enum):
|
|
| 40 |
ERROR = "error"
|
| 41 |
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
class TaskSchedule(BaseModel):
|
| 44 |
minute: str
|
| 45 |
hour: str
|
| 46 |
day: str
|
| 47 |
month: str
|
| 48 |
weekday: str
|
|
|
|
| 49 |
|
| 50 |
def to_crontab(self) -> str:
|
| 51 |
return f"{self.minute} {self.hour} {self.day} {self.month} {self.weekday}"
|
| 52 |
|
| 53 |
|
| 54 |
-
class
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
uuid: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
|
|
| 56 |
state: TaskState = Field(default=TaskState.IDLE)
|
| 57 |
name: str = Field()
|
| 58 |
system_prompt: str
|
| 59 |
prompt: str
|
| 60 |
attachments: list[str] = Field(default_factory=list)
|
| 61 |
-
token: str = Field(default_factory=lambda: str(random.randint(1000000000000000000, 9999999999999999999)))
|
| 62 |
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
| 63 |
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
| 64 |
last_run: datetime | None = None
|
| 65 |
last_result: str | None = None
|
| 66 |
|
| 67 |
-
# lock: Optional[threading.Lock] = Field(exclude=True, default=threading.Lock())
|
| 68 |
-
|
| 69 |
-
@classmethod
|
| 70 |
-
def create(
|
| 71 |
-
cls,
|
| 72 |
-
name: str,
|
| 73 |
-
system_prompt: str,
|
| 74 |
-
prompt: str,
|
| 75 |
-
token: str,
|
| 76 |
-
attachments: list[str] = list()
|
| 77 |
-
):
|
| 78 |
-
return cls(name=name,
|
| 79 |
-
system_prompt=system_prompt,
|
| 80 |
-
prompt=prompt,
|
| 81 |
-
attachments=attachments,
|
| 82 |
-
token=token)
|
| 83 |
-
|
| 84 |
def __init__(self, *args, **kwargs):
|
| 85 |
super().__init__(*args, **kwargs)
|
|
|
|
|
|
|
| 86 |
self._lock = threading.RLock()
|
| 87 |
|
| 88 |
def update(self,
|
|
@@ -93,7 +143,8 @@ class AdHocTask(BaseModel):
|
|
| 93 |
attachments: list[str] | None = None,
|
| 94 |
last_run: datetime | None = None,
|
| 95 |
last_result: str | None = None,
|
| 96 |
-
|
|
|
|
| 97 |
with self._lock:
|
| 98 |
if name is not None:
|
| 99 |
self.name = name
|
|
@@ -116,29 +167,73 @@ class AdHocTask(BaseModel):
|
|
| 116 |
if last_result is not None:
|
| 117 |
self.last_result = last_result
|
| 118 |
self.updated_at = datetime.now(timezone.utc)
|
| 119 |
-
if
|
| 120 |
-
self.
|
| 121 |
self.updated_at = datetime.now(timezone.utc)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
def check_schedule(self, frequency_seconds: float = 60.0) -> bool:
|
| 124 |
-
|
| 125 |
-
return False
|
| 126 |
|
|
|
|
|
|
|
| 127 |
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
@classmethod
|
| 144 |
def create(
|
|
@@ -146,18 +241,67 @@ class ScheduledTask(BaseModel):
|
|
| 146 |
name: str,
|
| 147 |
system_prompt: str,
|
| 148 |
prompt: str,
|
| 149 |
-
|
| 150 |
-
attachments: list[str] =
|
|
|
|
| 151 |
):
|
| 152 |
return cls(name=name,
|
| 153 |
system_prompt=system_prompt,
|
| 154 |
prompt=prompt,
|
| 155 |
attachments=attachments,
|
| 156 |
-
|
|
|
|
| 157 |
|
| 158 |
-
def
|
| 159 |
-
|
| 160 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
|
| 162 |
def update(self,
|
| 163 |
name: str | None = None,
|
|
@@ -165,55 +309,140 @@ class ScheduledTask(BaseModel):
|
|
| 165 |
system_prompt: str | None = None,
|
| 166 |
prompt: str | None = None,
|
| 167 |
attachments: list[str] | None = None,
|
| 168 |
-
schedule: TaskSchedule | None = None,
|
| 169 |
last_run: datetime | None = None,
|
| 170 |
-
last_result: str | None = None
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
if attachments is not None:
|
| 185 |
-
self.attachments = attachments
|
| 186 |
-
self.updated_at = datetime.now(timezone.utc)
|
| 187 |
-
if schedule is not None:
|
| 188 |
-
self.schedule = schedule
|
| 189 |
-
self.updated_at = datetime.now(timezone.utc)
|
| 190 |
-
if last_run is not None:
|
| 191 |
-
self.last_run = last_run
|
| 192 |
-
self.updated_at = datetime.now(timezone.utc)
|
| 193 |
-
if last_result is not None:
|
| 194 |
-
self.last_result = last_result
|
| 195 |
-
self.updated_at = datetime.now(timezone.utc)
|
| 196 |
|
| 197 |
def check_schedule(self, frequency_seconds: float = 60.0) -> bool:
|
| 198 |
with self._lock:
|
| 199 |
-
crontab = CronTab(crontab=self.schedule.to_crontab())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
# Get next run time as seconds until next execution
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
now=datetime.now(timezone.utc) - timedelta(seconds=frequency_seconds),
|
| 204 |
return_datetime=False
|
| 205 |
) # type: ignore
|
|
|
|
| 206 |
if next_run_seconds is None:
|
| 207 |
return False
|
|
|
|
| 208 |
return next_run_seconds < frequency_seconds
|
| 209 |
|
| 210 |
-
def
|
| 211 |
-
|
|
|
|
|
|
|
| 212 |
|
| 213 |
|
| 214 |
-
class
|
| 215 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
# Singleton instance
|
| 218 |
__instance: ClassVar[Optional["SchedulerTaskList"]] = PrivateAttr(default=None)
|
| 219 |
|
|
@@ -245,7 +474,7 @@ class SchedulerTaskList(BaseModel):
|
|
| 245 |
self.tasks.extend(data.tasks)
|
| 246 |
return self
|
| 247 |
|
| 248 |
-
async def add_task(self, task: Union[ScheduledTask, AdHocTask]) -> "SchedulerTaskList":
|
| 249 |
with self._lock:
|
| 250 |
self.tasks.append(task)
|
| 251 |
await self.save()
|
|
@@ -253,13 +482,50 @@ class SchedulerTaskList(BaseModel):
|
|
| 253 |
|
| 254 |
async def save(self) -> "SchedulerTaskList":
|
| 255 |
with self._lock:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
path = get_abs_path(SCHEDULER_FOLDER, "tasks.json")
|
| 257 |
if not exists(path):
|
| 258 |
make_dirs(path)
|
| 259 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
return self
|
| 261 |
|
| 262 |
-
async def update_task_by_uuid(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
"""
|
| 264 |
Atomically update a task by UUID using the provided updater function.
|
| 265 |
|
|
@@ -273,7 +539,7 @@ class SchedulerTaskList(BaseModel):
|
|
| 273 |
await self.reload()
|
| 274 |
|
| 275 |
# Find the task
|
| 276 |
-
task = next((task for task in self.tasks if task.uuid == task_uuid), None)
|
| 277 |
if task is None:
|
| 278 |
return None
|
| 279 |
|
|
@@ -281,27 +547,35 @@ class SchedulerTaskList(BaseModel):
|
|
| 281 |
updater_func(task)
|
| 282 |
|
| 283 |
# Save the changes
|
| 284 |
-
|
| 285 |
-
if not exists(path):
|
| 286 |
-
make_dirs(path)
|
| 287 |
-
write_file(path, self.model_dump_json())
|
| 288 |
|
| 289 |
return task
|
| 290 |
|
| 291 |
-
def get_tasks(self) -> list[Union[ScheduledTask, AdHocTask]]:
|
| 292 |
with self._lock:
|
| 293 |
return self.tasks
|
| 294 |
|
| 295 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
with self._lock:
|
| 297 |
await self.reload()
|
| 298 |
-
return [
|
|
|
|
|
|
|
|
|
|
| 299 |
|
| 300 |
-
def get_task_by_uuid(self, task_uuid: str) -> Union[ScheduledTask, AdHocTask] | None:
|
| 301 |
with self._lock:
|
| 302 |
return next((task for task in self.tasks if task.uuid == task_uuid), None)
|
| 303 |
|
| 304 |
-
def get_task_by_name(self, name: str) -> Union[ScheduledTask, AdHocTask] | None:
|
| 305 |
with self._lock:
|
| 306 |
return next((task for task in self.tasks if task.name == name), None)
|
| 307 |
|
|
@@ -340,10 +614,13 @@ class TaskScheduler:
|
|
| 340 |
async def reload(self):
|
| 341 |
await self._tasks.reload()
|
| 342 |
|
| 343 |
-
def get_tasks(self) -> list[Union[ScheduledTask, AdHocTask]]:
|
| 344 |
return self._tasks.get_tasks()
|
| 345 |
|
| 346 |
-
|
|
|
|
|
|
|
|
|
|
| 347 |
await self._tasks.add_task(task)
|
| 348 |
return self
|
| 349 |
|
|
@@ -355,10 +632,10 @@ class TaskScheduler:
|
|
| 355 |
await self._tasks.remove_task_by_name(name)
|
| 356 |
return self
|
| 357 |
|
| 358 |
-
def get_task_by_uuid(self, task_uuid: str) -> Union[ScheduledTask, AdHocTask] | None:
|
| 359 |
return self._tasks.get_task_by_uuid(task_uuid)
|
| 360 |
|
| 361 |
-
def get_task_by_name(self, name: str) -> Union[ScheduledTask, AdHocTask] | None:
|
| 362 |
return self._tasks.get_task_by_name(name)
|
| 363 |
|
| 364 |
async def tick(self):
|
|
@@ -366,11 +643,33 @@ class TaskScheduler:
|
|
| 366 |
await self._run_task(task)
|
| 367 |
|
| 368 |
async def run_task_by_uuid(self, task_uuid: str):
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
raise ValueError(f"Task with UUID {task_uuid} not found")
|
| 372 |
|
| 373 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
await self._run_task(task)
|
| 375 |
|
| 376 |
async def run_task_by_name(self, name: str):
|
|
@@ -382,7 +681,12 @@ class TaskScheduler:
|
|
| 382 |
async def save(self):
|
| 383 |
await self._tasks.save()
|
| 384 |
|
| 385 |
-
async def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 386 |
"""
|
| 387 |
Atomically update a task by UUID with the provided parameters.
|
| 388 |
This prevents race conditions when multiple processes update tasks concurrently.
|
|
@@ -392,49 +696,52 @@ class TaskScheduler:
|
|
| 392 |
def _update_task(task):
|
| 393 |
task.update(**update_params)
|
| 394 |
|
| 395 |
-
return await self._tasks.update_task_by_uuid(task_uuid, _update_task)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
|
| 397 |
-
async def __new_context(self, task: Union[ScheduledTask, AdHocTask]) -> AgentContext:
|
| 398 |
config = initialize()
|
| 399 |
context: AgentContext = AgentContext(config)
|
| 400 |
-
context.id = task.
|
|
|
|
|
|
|
|
|
|
| 401 |
# Save the context
|
| 402 |
save_tmp_chat(context)
|
| 403 |
return context
|
| 404 |
|
| 405 |
-
async def _get_chat_context(self, task: Union[ScheduledTask, AdHocTask]) -> AgentContext:
|
| 406 |
-
|
| 407 |
|
| 408 |
-
if
|
| 409 |
-
context
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
save_tmp_chat(context)
|
| 416 |
-
return context
|
| 417 |
-
else:
|
| 418 |
-
self._printer.print(
|
| 419 |
-
f"Scheduler Task {task.name} loaded from task {task.uuid} but failed to load context"
|
| 420 |
-
)
|
| 421 |
-
return await self.__new_context(task)
|
| 422 |
else:
|
| 423 |
self._printer.print(
|
| 424 |
f"Scheduler Task {task.name} loaded from task {task.uuid} but context not found"
|
| 425 |
)
|
| 426 |
return await self.__new_context(task)
|
| 427 |
|
| 428 |
-
async def _persist_chat(self, task: Union[ScheduledTask, AdHocTask], context: AgentContext):
|
| 429 |
-
context.id = task.
|
|
|
|
| 430 |
save_tmp_chat(context)
|
| 431 |
|
| 432 |
-
async def _run_task(self, task: Union[ScheduledTask, AdHocTask]):
|
| 433 |
|
| 434 |
async def _run_task_wrapper(task_uuid: str):
|
| 435 |
|
| 436 |
# preflight checks with a snapshot of the task
|
| 437 |
-
task_snapshot: Union[ScheduledTask, AdHocTask] | None = self.get_task_by_uuid(task_uuid)
|
| 438 |
if task_snapshot is None:
|
| 439 |
self._printer.print(f"Scheduler Task with UUID '{task_uuid}' not found")
|
| 440 |
return
|
|
@@ -443,15 +750,20 @@ class TaskScheduler:
|
|
| 443 |
return
|
| 444 |
|
| 445 |
# Atomically fetch and check the task's current state
|
| 446 |
-
current_task = await self.
|
| 447 |
if not current_task:
|
| 448 |
-
self._printer.print(f"Scheduler Task with UUID '{task_uuid}' not found")
|
| 449 |
return
|
| 450 |
if current_task.state != TaskState.RUNNING:
|
| 451 |
# This means the update failed due to state conflict
|
| 452 |
self._printer.print(f"Scheduler Task '{current_task.name}' state is '{current_task.state}', skipping")
|
| 453 |
return
|
| 454 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 455 |
try:
|
| 456 |
self._printer.print(f"Scheduler Task '{current_task.name}' started")
|
| 457 |
|
|
@@ -459,9 +771,9 @@ class TaskScheduler:
|
|
| 459 |
|
| 460 |
# Ensure the context is properly registered in the AgentContext._contexts
|
| 461 |
# This is critical for the polling mechanism to find and stream logs
|
|
|
|
| 462 |
AgentContext._contexts[context.id] = context
|
| 463 |
-
|
| 464 |
-
agent = Agent(0, context.config, context)
|
| 465 |
|
| 466 |
# Prepare attachment filenames for logging
|
| 467 |
attachment_filenames = []
|
|
@@ -507,34 +819,46 @@ class TaskScheduler:
|
|
| 507 |
|
| 508 |
result = await agent.monologue()
|
| 509 |
|
| 510 |
-
#
|
| 511 |
-
await self.update_task(
|
| 512 |
-
task_uuid,
|
| 513 |
-
state=TaskState.IDLE,
|
| 514 |
-
last_run=datetime.now(timezone.utc),
|
| 515 |
-
last_result="SUCCESS: " + result
|
| 516 |
-
)
|
| 517 |
-
|
| 518 |
self._printer.print(f"Scheduler Task '{current_task.name}' completed: {result}")
|
| 519 |
-
|
| 520 |
await self._persist_chat(current_task, context)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 521 |
|
| 522 |
except Exception as e:
|
|
|
|
| 523 |
self._printer.print(f"Scheduler Task '{current_task.name}' failed: {e}")
|
|
|
|
| 524 |
|
| 525 |
-
#
|
| 526 |
-
await self.
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
|
| 532 |
if agent:
|
| 533 |
agent.handle_critical_exception(e)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 534 |
|
| 535 |
deferred_task = DeferredTask(thread_name=self.__class__.__name__)
|
| 536 |
deferred_task.start_task(_run_task_wrapper, task.uuid)
|
| 537 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 538 |
def serialize_all_tasks(self) -> list[Dict[str, Any]]:
|
| 539 |
"""
|
| 540 |
Serialize all tasks in the scheduler to a list of dictionaries.
|
|
@@ -558,18 +882,35 @@ class TaskScheduler:
|
|
| 558 |
# ----------------------
|
| 559 |
|
| 560 |
def serialize_datetime(dt: Optional[datetime]) -> Optional[str]:
|
| 561 |
-
"""
|
| 562 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 563 |
|
| 564 |
|
| 565 |
def parse_datetime(dt_str: Optional[str]) -> Optional[datetime]:
|
| 566 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 567 |
if not dt_str:
|
| 568 |
return None
|
|
|
|
| 569 |
try:
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
|
|
|
| 573 |
|
| 574 |
|
| 575 |
def serialize_task_schedule(schedule: TaskSchedule) -> Dict[str, str]:
|
|
@@ -579,7 +920,8 @@ def serialize_task_schedule(schedule: TaskSchedule) -> Dict[str, str]:
|
|
| 579 |
'hour': schedule.hour,
|
| 580 |
'day': schedule.day,
|
| 581 |
'month': schedule.month,
|
| 582 |
-
'weekday': schedule.weekday
|
|
|
|
| 583 |
}
|
| 584 |
|
| 585 |
|
|
@@ -591,16 +933,84 @@ def parse_task_schedule(schedule_data: Dict[str, str]) -> TaskSchedule:
|
|
| 591 |
hour=schedule_data.get('hour', '*'),
|
| 592 |
day=schedule_data.get('day', '*'),
|
| 593 |
month=schedule_data.get('month', '*'),
|
| 594 |
-
weekday=schedule_data.get('weekday', '*')
|
|
|
|
| 595 |
)
|
| 596 |
except Exception as e:
|
| 597 |
raise ValueError(f"Invalid schedule format: {e}") from e
|
| 598 |
|
| 599 |
|
| 600 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 601 |
|
| 602 |
|
| 603 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 604 |
"""
|
| 605 |
Standardized serialization for task objects with proper handling of all complex types.
|
| 606 |
"""
|
|
@@ -615,22 +1025,28 @@ def serialize_task(task: Union[ScheduledTask, AdHocTask]) -> Dict[str, Any]:
|
|
| 615 |
"created_at": serialize_datetime(task.created_at),
|
| 616 |
"updated_at": serialize_datetime(task.updated_at),
|
| 617 |
"last_run": serialize_datetime(task.last_run),
|
| 618 |
-
"
|
|
|
|
|
|
|
| 619 |
}
|
| 620 |
|
| 621 |
# Add type-specific fields
|
| 622 |
if isinstance(task, ScheduledTask):
|
| 623 |
task_dict['type'] = 'scheduled'
|
| 624 |
-
task_dict['schedule'] = serialize_task_schedule(task.schedule)
|
| 625 |
-
|
| 626 |
task_dict['type'] = 'adhoc'
|
| 627 |
adhoc_task = cast(AdHocTask, task)
|
| 628 |
task_dict['token'] = adhoc_task.token
|
|
|
|
|
|
|
|
|
|
|
|
|
| 629 |
|
| 630 |
return task_dict
|
| 631 |
|
| 632 |
|
| 633 |
-
def serialize_tasks(tasks: list[Union[ScheduledTask, AdHocTask]]) -> list[Dict[str, Any]]:
|
| 634 |
"""
|
| 635 |
Serialize a list of tasks to a list of dictionaries.
|
| 636 |
"""
|
|
@@ -651,10 +1067,18 @@ def deserialize_task(task_data: Dict[str, Any], task_class: Optional[Type[T]] =
|
|
| 651 |
determined_class = cast(Type[T], ScheduledTask)
|
| 652 |
elif task_type_str == 'adhoc':
|
| 653 |
determined_class = cast(Type[T], AdHocTask)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 654 |
else:
|
| 655 |
raise ValueError(f"Unknown task type: {task_type_str}")
|
| 656 |
else:
|
| 657 |
determined_class = task_class
|
|
|
|
|
|
|
|
|
|
| 658 |
|
| 659 |
common_args = {
|
| 660 |
"uuid": task_data.get("uuid"),
|
|
@@ -666,14 +1090,19 @@ def deserialize_task(task_data: Dict[str, Any], task_class: Optional[Type[T]] =
|
|
| 666 |
"created_at": parse_datetime(task_data.get("created_at")),
|
| 667 |
"updated_at": parse_datetime(task_data.get("updated_at")),
|
| 668 |
"last_run": parse_datetime(task_data.get("last_run")),
|
| 669 |
-
"last_result": task_data.get("last_result")
|
|
|
|
| 670 |
}
|
| 671 |
|
| 672 |
# Add type-specific fields
|
| 673 |
-
if determined_class == ScheduledTask:
|
| 674 |
schedule_data = task_data.get("schedule", {})
|
| 675 |
common_args["schedule"] = parse_task_schedule(schedule_data)
|
| 676 |
return ScheduledTask(**common_args) # type: ignore
|
| 677 |
-
|
| 678 |
common_args["token"] = task_data.get("token", "")
|
| 679 |
return AdHocTask(**common_args) # type: ignore
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import asyncio
|
| 2 |
from datetime import datetime, timezone, timedelta
|
|
|
|
| 3 |
import os
|
| 4 |
import random
|
| 5 |
import threading
|
| 6 |
from urllib.parse import urlparse
|
| 7 |
import uuid
|
|
|
|
| 8 |
from enum import Enum
|
| 9 |
from os.path import exists
|
| 10 |
+
from typing import Any, Callable, Dict, Literal, Optional, Type, TypeVar, Union, cast, ClassVar
|
| 11 |
|
| 12 |
import nest_asyncio
|
| 13 |
nest_asyncio.apply()
|
|
|
|
| 15 |
from crontab import CronTab
|
| 16 |
from pydantic import BaseModel, Field, PrivateAttr
|
| 17 |
|
| 18 |
+
from agent import Agent, AgentContext, UserMessage
|
| 19 |
from initialize import initialize
|
| 20 |
+
from python.helpers.persist_chat import save_tmp_chat
|
| 21 |
from python.helpers.print_style import PrintStyle
|
| 22 |
from python.helpers.defer import DeferredTask
|
| 23 |
+
from python.helpers.files import get_abs_path, make_dirs, read_file, write_file
|
| 24 |
+
from python.helpers.localization import Localization
|
| 25 |
+
import pytz
|
| 26 |
+
from typing import Annotated
|
|
|
|
| 27 |
|
| 28 |
+
SCHEDULER_FOLDER = "tmp/scheduler"
|
| 29 |
|
| 30 |
# ----------------------
|
| 31 |
# Task Models
|
| 32 |
# ----------------------
|
| 33 |
|
| 34 |
+
|
| 35 |
class TaskState(str, Enum):
|
| 36 |
IDLE = "idle"
|
| 37 |
RUNNING = "running"
|
|
|
|
| 39 |
ERROR = "error"
|
| 40 |
|
| 41 |
|
| 42 |
+
class TaskType(str, Enum):
|
| 43 |
+
AD_HOC = "adhoc"
|
| 44 |
+
SCHEDULED = "scheduled"
|
| 45 |
+
PLANNED = "planned"
|
| 46 |
+
|
| 47 |
+
|
| 48 |
class TaskSchedule(BaseModel):
|
| 49 |
minute: str
|
| 50 |
hour: str
|
| 51 |
day: str
|
| 52 |
month: str
|
| 53 |
weekday: str
|
| 54 |
+
timezone: str = Field(default_factory=lambda: Localization.get().get_timezone())
|
| 55 |
|
| 56 |
def to_crontab(self) -> str:
|
| 57 |
return f"{self.minute} {self.hour} {self.day} {self.month} {self.weekday}"
|
| 58 |
|
| 59 |
|
| 60 |
+
class TaskPlan(BaseModel):
|
| 61 |
+
todo: list[datetime] = Field(default_factory=list)
|
| 62 |
+
in_progress: datetime | None = None
|
| 63 |
+
done: list[datetime] = Field(default_factory=list)
|
| 64 |
+
|
| 65 |
+
@classmethod
|
| 66 |
+
def create(cls, todo: list[datetime] = list(), in_progress: datetime | None = None, done: list[datetime] = list()):
|
| 67 |
+
if todo:
|
| 68 |
+
for idx, dt in enumerate(todo):
|
| 69 |
+
if dt.tzinfo is None:
|
| 70 |
+
todo[idx] = pytz.timezone("UTC").localize(dt)
|
| 71 |
+
if in_progress:
|
| 72 |
+
if in_progress.tzinfo is None:
|
| 73 |
+
in_progress = pytz.timezone("UTC").localize(in_progress)
|
| 74 |
+
if done:
|
| 75 |
+
for idx, dt in enumerate(done):
|
| 76 |
+
if dt.tzinfo is None:
|
| 77 |
+
done[idx] = pytz.timezone("UTC").localize(dt)
|
| 78 |
+
return cls(todo=todo, in_progress=in_progress, done=done)
|
| 79 |
+
|
| 80 |
+
def add_todo(self, launch_time: datetime):
|
| 81 |
+
if launch_time.tzinfo is None:
|
| 82 |
+
launch_time = pytz.timezone("UTC").localize(launch_time)
|
| 83 |
+
self.todo.append(launch_time)
|
| 84 |
+
self.todo = sorted(self.todo)
|
| 85 |
+
|
| 86 |
+
def set_in_progress(self, launch_time: datetime):
|
| 87 |
+
if launch_time.tzinfo is None:
|
| 88 |
+
launch_time = pytz.timezone("UTC").localize(launch_time)
|
| 89 |
+
if launch_time not in self.todo:
|
| 90 |
+
raise ValueError(f"Launch time {launch_time} not in todo list")
|
| 91 |
+
self.todo.remove(launch_time)
|
| 92 |
+
self.todo = sorted(self.todo)
|
| 93 |
+
self.in_progress = launch_time
|
| 94 |
+
|
| 95 |
+
def set_done(self, launch_time: datetime):
|
| 96 |
+
if launch_time.tzinfo is None:
|
| 97 |
+
launch_time = pytz.timezone("UTC").localize(launch_time)
|
| 98 |
+
if launch_time != self.in_progress:
|
| 99 |
+
raise ValueError(f"Launch time {launch_time} is not the same as in progress time {self.in_progress}")
|
| 100 |
+
if launch_time in self.done:
|
| 101 |
+
raise ValueError(f"Launch time {launch_time} already in done list")
|
| 102 |
+
self.in_progress = None
|
| 103 |
+
self.done.append(launch_time)
|
| 104 |
+
self.done = sorted(self.done)
|
| 105 |
+
|
| 106 |
+
def get_next_launch_time(self) -> datetime | None:
|
| 107 |
+
return self.todo[0] if self.todo else None
|
| 108 |
+
|
| 109 |
+
def should_launch(self) -> datetime | None:
|
| 110 |
+
next_launch_time = self.get_next_launch_time()
|
| 111 |
+
if next_launch_time is None:
|
| 112 |
+
return None
|
| 113 |
+
# return next launch time if current datetime utc is later than next launch time
|
| 114 |
+
if datetime.now(timezone.utc) > next_launch_time:
|
| 115 |
+
return next_launch_time
|
| 116 |
+
return None
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
class BaseTask(BaseModel):
|
| 120 |
uuid: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
| 121 |
+
context_id: Optional[str] = Field(default=None)
|
| 122 |
state: TaskState = Field(default=TaskState.IDLE)
|
| 123 |
name: str = Field()
|
| 124 |
system_prompt: str
|
| 125 |
prompt: str
|
| 126 |
attachments: list[str] = Field(default_factory=list)
|
|
|
|
| 127 |
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
| 128 |
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
| 129 |
last_run: datetime | None = None
|
| 130 |
last_result: str | None = None
|
| 131 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
def __init__(self, *args, **kwargs):
|
| 133 |
super().__init__(*args, **kwargs)
|
| 134 |
+
if not self.context_id:
|
| 135 |
+
self.context_id = self.uuid
|
| 136 |
self._lock = threading.RLock()
|
| 137 |
|
| 138 |
def update(self,
|
|
|
|
| 143 |
attachments: list[str] | None = None,
|
| 144 |
last_run: datetime | None = None,
|
| 145 |
last_result: str | None = None,
|
| 146 |
+
context_id: str | None = None,
|
| 147 |
+
**kwargs):
|
| 148 |
with self._lock:
|
| 149 |
if name is not None:
|
| 150 |
self.name = name
|
|
|
|
| 167 |
if last_result is not None:
|
| 168 |
self.last_result = last_result
|
| 169 |
self.updated_at = datetime.now(timezone.utc)
|
| 170 |
+
if context_id is not None:
|
| 171 |
+
self.context_id = context_id
|
| 172 |
self.updated_at = datetime.now(timezone.utc)
|
| 173 |
+
for key, value in kwargs.items():
|
| 174 |
+
if value is not None:
|
| 175 |
+
setattr(self, key, value)
|
| 176 |
+
self.updated_at = datetime.now(timezone.utc)
|
| 177 |
|
| 178 |
def check_schedule(self, frequency_seconds: float = 60.0) -> bool:
|
| 179 |
+
return False
|
|
|
|
| 180 |
|
| 181 |
+
def get_next_run(self) -> datetime | None:
|
| 182 |
+
return None
|
| 183 |
|
| 184 |
+
def get_next_run_minutes(self) -> int | None:
|
| 185 |
+
next_run = self.get_next_run()
|
| 186 |
+
if next_run is None:
|
| 187 |
+
return None
|
| 188 |
+
return int((next_run - datetime.now(timezone.utc)).total_seconds() / 60)
|
| 189 |
+
|
| 190 |
+
async def on_run(self):
|
| 191 |
+
pass
|
| 192 |
+
|
| 193 |
+
async def on_finish(self):
|
| 194 |
+
# Ensure that updated_at is refreshed to reflect completion time
|
| 195 |
+
# This helps track when the task actually finished, regardless of success/error
|
| 196 |
+
await TaskScheduler.get().update_task(
|
| 197 |
+
self.uuid,
|
| 198 |
+
updated_at=datetime.now(timezone.utc)
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
async def on_error(self, error: str):
|
| 202 |
+
# Update task state to ERROR and set last result
|
| 203 |
+
scheduler = TaskScheduler.get()
|
| 204 |
+
await scheduler.reload() # Ensure we have the latest state
|
| 205 |
+
updated_task = await scheduler.update_task(
|
| 206 |
+
self.uuid,
|
| 207 |
+
state=TaskState.ERROR,
|
| 208 |
+
last_run=datetime.now(timezone.utc),
|
| 209 |
+
last_result=f"ERROR: {error}"
|
| 210 |
+
)
|
| 211 |
+
if not updated_task:
|
| 212 |
+
PrintStyle(italic=True, font_color="red", padding=False).print(
|
| 213 |
+
f"Failed to update task {self.uuid} state to ERROR after error: {error}"
|
| 214 |
+
)
|
| 215 |
+
await scheduler.save() # Force save after update
|
| 216 |
+
|
| 217 |
+
async def on_success(self, result: str):
|
| 218 |
+
# Update task state to IDLE and set last result
|
| 219 |
+
scheduler = TaskScheduler.get()
|
| 220 |
+
await scheduler.reload() # Ensure we have the latest state
|
| 221 |
+
updated_task = await scheduler.update_task(
|
| 222 |
+
self.uuid,
|
| 223 |
+
state=TaskState.IDLE,
|
| 224 |
+
last_run=datetime.now(timezone.utc),
|
| 225 |
+
last_result=result
|
| 226 |
+
)
|
| 227 |
+
if not updated_task:
|
| 228 |
+
PrintStyle(italic=True, font_color="red", padding=False).print(
|
| 229 |
+
f"Failed to update task {self.uuid} state to IDLE after success"
|
| 230 |
+
)
|
| 231 |
+
await scheduler.save() # Force save after update
|
| 232 |
|
| 233 |
+
|
| 234 |
+
class AdHocTask(BaseTask):
|
| 235 |
+
type: Literal[TaskType.AD_HOC] = TaskType.AD_HOC
|
| 236 |
+
token: str = Field(default_factory=lambda: str(random.randint(1000000000000000000, 9999999999999999999)))
|
| 237 |
|
| 238 |
@classmethod
|
| 239 |
def create(
|
|
|
|
| 241 |
name: str,
|
| 242 |
system_prompt: str,
|
| 243 |
prompt: str,
|
| 244 |
+
token: str,
|
| 245 |
+
attachments: list[str] = list(),
|
| 246 |
+
context_id: str | None = None
|
| 247 |
):
|
| 248 |
return cls(name=name,
|
| 249 |
system_prompt=system_prompt,
|
| 250 |
prompt=prompt,
|
| 251 |
attachments=attachments,
|
| 252 |
+
token=token,
|
| 253 |
+
context_id=context_id)
|
| 254 |
|
| 255 |
+
def update(self,
|
| 256 |
+
name: str | None = None,
|
| 257 |
+
state: TaskState | None = None,
|
| 258 |
+
system_prompt: str | None = None,
|
| 259 |
+
prompt: str | None = None,
|
| 260 |
+
attachments: list[str] | None = None,
|
| 261 |
+
last_run: datetime | None = None,
|
| 262 |
+
last_result: str | None = None,
|
| 263 |
+
context_id: str | None = None,
|
| 264 |
+
token: str | None = None,
|
| 265 |
+
**kwargs):
|
| 266 |
+
super().update(name=name,
|
| 267 |
+
state=state,
|
| 268 |
+
system_prompt=system_prompt,
|
| 269 |
+
prompt=prompt,
|
| 270 |
+
attachments=attachments,
|
| 271 |
+
last_run=last_run,
|
| 272 |
+
last_result=last_result,
|
| 273 |
+
context_id=context_id,
|
| 274 |
+
token=token,
|
| 275 |
+
**kwargs)
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
class ScheduledTask(BaseTask):
|
| 279 |
+
type: Literal[TaskType.SCHEDULED] = TaskType.SCHEDULED
|
| 280 |
+
schedule: TaskSchedule
|
| 281 |
+
|
| 282 |
+
@classmethod
|
| 283 |
+
def create(
|
| 284 |
+
cls,
|
| 285 |
+
name: str,
|
| 286 |
+
system_prompt: str,
|
| 287 |
+
prompt: str,
|
| 288 |
+
schedule: TaskSchedule,
|
| 289 |
+
attachments: list[str] = list(),
|
| 290 |
+
context_id: str | None = None,
|
| 291 |
+
timezone: str | None = None
|
| 292 |
+
):
|
| 293 |
+
# Set timezone in schedule if provided
|
| 294 |
+
if timezone is not None:
|
| 295 |
+
schedule.timezone = timezone
|
| 296 |
+
else:
|
| 297 |
+
schedule.timezone = Localization.get().get_timezone()
|
| 298 |
+
|
| 299 |
+
return cls(name=name,
|
| 300 |
+
system_prompt=system_prompt,
|
| 301 |
+
prompt=prompt,
|
| 302 |
+
attachments=attachments,
|
| 303 |
+
schedule=schedule,
|
| 304 |
+
context_id=context_id)
|
| 305 |
|
| 306 |
def update(self,
|
| 307 |
name: str | None = None,
|
|
|
|
| 309 |
system_prompt: str | None = None,
|
| 310 |
prompt: str | None = None,
|
| 311 |
attachments: list[str] | None = None,
|
|
|
|
| 312 |
last_run: datetime | None = None,
|
| 313 |
+
last_result: str | None = None,
|
| 314 |
+
context_id: str | None = None,
|
| 315 |
+
schedule: TaskSchedule | None = None,
|
| 316 |
+
**kwargs):
|
| 317 |
+
super().update(name=name,
|
| 318 |
+
state=state,
|
| 319 |
+
system_prompt=system_prompt,
|
| 320 |
+
prompt=prompt,
|
| 321 |
+
attachments=attachments,
|
| 322 |
+
last_run=last_run,
|
| 323 |
+
last_result=last_result,
|
| 324 |
+
context_id=context_id,
|
| 325 |
+
schedule=schedule,
|
| 326 |
+
**kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
|
| 328 |
def check_schedule(self, frequency_seconds: float = 60.0) -> bool:
|
| 329 |
with self._lock:
|
| 330 |
+
crontab = CronTab(crontab=self.schedule.to_crontab()) # type: ignore
|
| 331 |
+
|
| 332 |
+
# Get the timezone from the schedule or use UTC as fallback
|
| 333 |
+
task_timezone = pytz.timezone(self.schedule.timezone or Localization.get().get_timezone())
|
| 334 |
+
|
| 335 |
+
# Get reference time in task's timezone (by default now - frequency_seconds)
|
| 336 |
+
reference_time = datetime.now(timezone.utc) - timedelta(seconds=frequency_seconds)
|
| 337 |
+
reference_time = reference_time.astimezone(task_timezone)
|
| 338 |
+
|
| 339 |
# Get next run time as seconds until next execution
|
| 340 |
+
next_run_seconds: Optional[float] = crontab.next( # type: ignore
|
| 341 |
+
now=reference_time,
|
|
|
|
| 342 |
return_datetime=False
|
| 343 |
) # type: ignore
|
| 344 |
+
|
| 345 |
if next_run_seconds is None:
|
| 346 |
return False
|
| 347 |
+
|
| 348 |
return next_run_seconds < frequency_seconds
|
| 349 |
|
| 350 |
+
def get_next_run(self) -> datetime | None:
|
| 351 |
+
with self._lock:
|
| 352 |
+
crontab = CronTab(crontab=self.schedule.to_crontab()) # type: ignore
|
| 353 |
+
return crontab.next(now=datetime.now(timezone.utc), return_datetime=True) # type: ignore
|
| 354 |
|
| 355 |
|
| 356 |
+
class PlannedTask(BaseTask):
|
| 357 |
+
type: Literal[TaskType.PLANNED] = TaskType.PLANNED
|
| 358 |
+
plan: TaskPlan
|
| 359 |
+
|
| 360 |
+
@classmethod
|
| 361 |
+
def create(
|
| 362 |
+
cls,
|
| 363 |
+
name: str,
|
| 364 |
+
system_prompt: str,
|
| 365 |
+
prompt: str,
|
| 366 |
+
plan: TaskPlan,
|
| 367 |
+
attachments: list[str] = list(),
|
| 368 |
+
context_id: str | None = None
|
| 369 |
+
):
|
| 370 |
+
return cls(name=name,
|
| 371 |
+
system_prompt=system_prompt,
|
| 372 |
+
prompt=prompt,
|
| 373 |
+
plan=plan,
|
| 374 |
+
attachments=attachments,
|
| 375 |
+
context_id=context_id)
|
| 376 |
+
|
| 377 |
+
def update(self,
|
| 378 |
+
name: str | None = None,
|
| 379 |
+
state: TaskState | None = None,
|
| 380 |
+
system_prompt: str | None = None,
|
| 381 |
+
prompt: str | None = None,
|
| 382 |
+
attachments: list[str] | None = None,
|
| 383 |
+
last_run: datetime | None = None,
|
| 384 |
+
last_result: str | None = None,
|
| 385 |
+
context_id: str | None = None,
|
| 386 |
+
plan: TaskPlan | None = None,
|
| 387 |
+
**kwargs):
|
| 388 |
+
super().update(name=name,
|
| 389 |
+
state=state,
|
| 390 |
+
system_prompt=system_prompt,
|
| 391 |
+
prompt=prompt,
|
| 392 |
+
attachments=attachments,
|
| 393 |
+
last_run=last_run,
|
| 394 |
+
last_result=last_result,
|
| 395 |
+
context_id=context_id,
|
| 396 |
+
plan=plan,
|
| 397 |
+
**kwargs)
|
| 398 |
+
|
| 399 |
+
def check_schedule(self, frequency_seconds: float = 60.0) -> bool:
|
| 400 |
+
with self._lock:
|
| 401 |
+
return self.plan.should_launch() is not None
|
| 402 |
+
|
| 403 |
+
def get_next_run(self) -> datetime | None:
|
| 404 |
+
with self._lock:
|
| 405 |
+
return self.plan.get_next_launch_time()
|
| 406 |
+
|
| 407 |
+
async def on_run(self):
|
| 408 |
+
with self._lock:
|
| 409 |
+
# Get the next launch time and set it as in_progress
|
| 410 |
+
next_launch_time = self.plan.should_launch()
|
| 411 |
+
if next_launch_time is not None:
|
| 412 |
+
self.plan.set_in_progress(next_launch_time)
|
| 413 |
+
await super().on_run()
|
| 414 |
+
|
| 415 |
+
async def on_finish(self):
|
| 416 |
+
# Handle plan item progression regardless of success or error
|
| 417 |
+
plan_updated = False
|
| 418 |
+
|
| 419 |
+
with self._lock:
|
| 420 |
+
# If there's an in_progress time, mark it as done
|
| 421 |
+
if self.plan.in_progress is not None:
|
| 422 |
+
self.plan.set_done(self.plan.in_progress)
|
| 423 |
+
plan_updated = True
|
| 424 |
|
| 425 |
+
# If we updated the plan, make sure to persist it
|
| 426 |
+
if plan_updated:
|
| 427 |
+
scheduler = TaskScheduler.get()
|
| 428 |
+
await scheduler.reload()
|
| 429 |
+
await scheduler.update_task(self.uuid, plan=self.plan)
|
| 430 |
+
await scheduler.save() # Force save
|
| 431 |
+
|
| 432 |
+
# Call the parent implementation for any additional cleanup
|
| 433 |
+
await super().on_finish()
|
| 434 |
+
|
| 435 |
+
async def on_success(self, result: str):
|
| 436 |
+
# Call parent implementation to update state, etc.
|
| 437 |
+
await super().on_success(result)
|
| 438 |
+
|
| 439 |
+
async def on_error(self, error: str):
|
| 440 |
+
# Call parent implementation to update state, etc.
|
| 441 |
+
await super().on_error(error)
|
| 442 |
+
|
| 443 |
+
|
| 444 |
+
class SchedulerTaskList(BaseModel):
|
| 445 |
+
tasks: list[Annotated[Union[ScheduledTask, AdHocTask, PlannedTask], Field(discriminator="type")]] = Field(default_factory=list)
|
| 446 |
# Singleton instance
|
| 447 |
__instance: ClassVar[Optional["SchedulerTaskList"]] = PrivateAttr(default=None)
|
| 448 |
|
|
|
|
| 474 |
self.tasks.extend(data.tasks)
|
| 475 |
return self
|
| 476 |
|
| 477 |
+
async def add_task(self, task: Union[ScheduledTask, AdHocTask, PlannedTask]) -> "SchedulerTaskList":
|
| 478 |
with self._lock:
|
| 479 |
self.tasks.append(task)
|
| 480 |
await self.save()
|
|
|
|
| 482 |
|
| 483 |
async def save(self) -> "SchedulerTaskList":
|
| 484 |
with self._lock:
|
| 485 |
+
# Debug: check for AdHocTasks with null tokens before saving
|
| 486 |
+
for task in self.tasks:
|
| 487 |
+
if isinstance(task, AdHocTask):
|
| 488 |
+
if task.token is None or task.token == "":
|
| 489 |
+
PrintStyle(italic=True, font_color="red", padding=False).print(
|
| 490 |
+
f"WARNING: AdHocTask {task.name} ({task.uuid}) has a null or empty token before saving: '{task.token}'"
|
| 491 |
+
)
|
| 492 |
+
# Generate a new token to prevent errors
|
| 493 |
+
task.token = str(random.randint(1000000000000000000, 9999999999999999999))
|
| 494 |
+
PrintStyle(italic=True, font_color="red", padding=False).print(
|
| 495 |
+
f"Fixed: Generated new token '{task.token}' for task {task.name}"
|
| 496 |
+
)
|
| 497 |
+
|
| 498 |
path = get_abs_path(SCHEDULER_FOLDER, "tasks.json")
|
| 499 |
if not exists(path):
|
| 500 |
make_dirs(path)
|
| 501 |
+
|
| 502 |
+
# Get the JSON string before writing
|
| 503 |
+
json_data = self.model_dump_json()
|
| 504 |
+
|
| 505 |
+
# Debug: check if 'null' appears as token value in JSON
|
| 506 |
+
if '"type": "adhoc"' in json_data and '"token": null' in json_data:
|
| 507 |
+
PrintStyle(italic=True, font_color="red", padding=False).print(
|
| 508 |
+
"ERROR: Found null token in JSON output for an adhoc task"
|
| 509 |
+
)
|
| 510 |
+
|
| 511 |
+
write_file(path, json_data)
|
| 512 |
+
|
| 513 |
+
# Debug: Verify after saving
|
| 514 |
+
if exists(path):
|
| 515 |
+
loaded_json = read_file(path)
|
| 516 |
+
if '"type": "adhoc"' in loaded_json and '"token": null' in loaded_json:
|
| 517 |
+
PrintStyle(italic=True, font_color="red", padding=False).print(
|
| 518 |
+
"ERROR: Null token persisted in JSON file for an adhoc task"
|
| 519 |
+
)
|
| 520 |
+
|
| 521 |
return self
|
| 522 |
|
| 523 |
+
async def update_task_by_uuid(
|
| 524 |
+
self,
|
| 525 |
+
task_uuid: str,
|
| 526 |
+
updater_func: Callable[[Union[ScheduledTask, AdHocTask, PlannedTask]], None],
|
| 527 |
+
verify_func: Callable[[Union[ScheduledTask, AdHocTask, PlannedTask]], bool] = lambda task: True
|
| 528 |
+
) -> Union[ScheduledTask, AdHocTask, PlannedTask] | None:
|
| 529 |
"""
|
| 530 |
Atomically update a task by UUID using the provided updater function.
|
| 531 |
|
|
|
|
| 539 |
await self.reload()
|
| 540 |
|
| 541 |
# Find the task
|
| 542 |
+
task = next((task for task in self.tasks if task.uuid == task_uuid and verify_func(task)), None)
|
| 543 |
if task is None:
|
| 544 |
return None
|
| 545 |
|
|
|
|
| 547 |
updater_func(task)
|
| 548 |
|
| 549 |
# Save the changes
|
| 550 |
+
await self.save()
|
|
|
|
|
|
|
|
|
|
| 551 |
|
| 552 |
return task
|
| 553 |
|
| 554 |
+
def get_tasks(self) -> list[Union[ScheduledTask, AdHocTask, PlannedTask]]:
|
| 555 |
with self._lock:
|
| 556 |
return self.tasks
|
| 557 |
|
| 558 |
+
def get_tasks_by_context_id(self, context_id: str, only_running: bool = False) -> list[Union[ScheduledTask, AdHocTask, PlannedTask]]:
|
| 559 |
+
with self._lock:
|
| 560 |
+
return [
|
| 561 |
+
task for task in self.tasks
|
| 562 |
+
if task.context_id == context_id
|
| 563 |
+
and (not only_running or task.state == TaskState.RUNNING)
|
| 564 |
+
]
|
| 565 |
+
|
| 566 |
+
async def get_due_tasks(self) -> list[Union[ScheduledTask, AdHocTask, PlannedTask]]:
|
| 567 |
with self._lock:
|
| 568 |
await self.reload()
|
| 569 |
+
return [
|
| 570 |
+
task for task in self.tasks
|
| 571 |
+
if task.check_schedule() and task.state == TaskState.IDLE
|
| 572 |
+
]
|
| 573 |
|
| 574 |
+
def get_task_by_uuid(self, task_uuid: str) -> Union[ScheduledTask, AdHocTask, PlannedTask] | None:
|
| 575 |
with self._lock:
|
| 576 |
return next((task for task in self.tasks if task.uuid == task_uuid), None)
|
| 577 |
|
| 578 |
+
def get_task_by_name(self, name: str) -> Union[ScheduledTask, AdHocTask, PlannedTask] | None:
|
| 579 |
with self._lock:
|
| 580 |
return next((task for task in self.tasks if task.name == name), None)
|
| 581 |
|
|
|
|
| 614 |
async def reload(self):
|
| 615 |
await self._tasks.reload()
|
| 616 |
|
| 617 |
+
def get_tasks(self) -> list[Union[ScheduledTask, AdHocTask, PlannedTask]]:
|
| 618 |
return self._tasks.get_tasks()
|
| 619 |
|
| 620 |
+
def get_tasks_by_context_id(self, context_id: str, only_running: bool = False) -> list[Union[ScheduledTask, AdHocTask, PlannedTask]]:
|
| 621 |
+
return self._tasks.get_tasks_by_context_id(context_id, only_running)
|
| 622 |
+
|
| 623 |
+
async def add_task(self, task: Union[ScheduledTask, AdHocTask, PlannedTask]) -> "TaskScheduler":
|
| 624 |
await self._tasks.add_task(task)
|
| 625 |
return self
|
| 626 |
|
|
|
|
| 632 |
await self._tasks.remove_task_by_name(name)
|
| 633 |
return self
|
| 634 |
|
| 635 |
+
def get_task_by_uuid(self, task_uuid: str) -> Union[ScheduledTask, AdHocTask, PlannedTask] | None:
|
| 636 |
return self._tasks.get_task_by_uuid(task_uuid)
|
| 637 |
|
| 638 |
+
def get_task_by_name(self, name: str) -> Union[ScheduledTask, AdHocTask, PlannedTask] | None:
|
| 639 |
return self._tasks.get_task_by_name(name)
|
| 640 |
|
| 641 |
async def tick(self):
|
|
|
|
| 643 |
await self._run_task(task)
|
| 644 |
|
| 645 |
async def run_task_by_uuid(self, task_uuid: str):
|
| 646 |
+
# First reload tasks to ensure we have the latest state
|
| 647 |
+
await self._tasks.reload()
|
|
|
|
| 648 |
|
| 649 |
+
# Get the task to run
|
| 650 |
+
task = self.get_task_by_uuid(task_uuid)
|
| 651 |
+
if not task:
|
| 652 |
+
raise ValueError(f"Task with UUID '{task_uuid}' not found")
|
| 653 |
+
|
| 654 |
+
# If the task is already running, raise an error
|
| 655 |
+
if task.state == TaskState.RUNNING:
|
| 656 |
+
raise ValueError(f"Task '{task.name}' is already running")
|
| 657 |
+
|
| 658 |
+
# If the task is disabled, raise an error
|
| 659 |
+
if task.state == TaskState.DISABLED:
|
| 660 |
+
raise ValueError(f"Task '{task.name}' is disabled")
|
| 661 |
+
|
| 662 |
+
# If the task is in error state, reset it to IDLE first
|
| 663 |
+
if task.state == TaskState.ERROR:
|
| 664 |
+
self._printer.print(f"Resetting task '{task.name}' from ERROR to IDLE state before running")
|
| 665 |
+
await self.update_task(task_uuid, state=TaskState.IDLE)
|
| 666 |
+
# Force a reload to ensure we have the updated state
|
| 667 |
+
await self._tasks.reload()
|
| 668 |
+
task = self.get_task_by_uuid(task_uuid)
|
| 669 |
+
if not task:
|
| 670 |
+
raise ValueError(f"Task with UUID '{task_uuid}' not found after state reset")
|
| 671 |
+
|
| 672 |
+
# Run the task
|
| 673 |
await self._run_task(task)
|
| 674 |
|
| 675 |
async def run_task_by_name(self, name: str):
|
|
|
|
| 681 |
async def save(self):
|
| 682 |
await self._tasks.save()
|
| 683 |
|
| 684 |
+
async def update_task_checked(
|
| 685 |
+
self,
|
| 686 |
+
task_uuid: str,
|
| 687 |
+
verify_func: Callable[[Union[ScheduledTask, AdHocTask, PlannedTask]], bool] = lambda task: True,
|
| 688 |
+
**update_params
|
| 689 |
+
) -> Union[ScheduledTask, AdHocTask, PlannedTask] | None:
|
| 690 |
"""
|
| 691 |
Atomically update a task by UUID with the provided parameters.
|
| 692 |
This prevents race conditions when multiple processes update tasks concurrently.
|
|
|
|
| 696 |
def _update_task(task):
|
| 697 |
task.update(**update_params)
|
| 698 |
|
| 699 |
+
return await self._tasks.update_task_by_uuid(task_uuid, _update_task, verify_func)
|
| 700 |
+
|
| 701 |
+
async def update_task(self, task_uuid: str, **update_params) -> Union[ScheduledTask, AdHocTask, PlannedTask] | None:
|
| 702 |
+
return await self.update_task_checked(task_uuid, lambda task: True, **update_params)
|
| 703 |
+
|
| 704 |
+
async def __new_context(self, task: Union[ScheduledTask, AdHocTask, PlannedTask]) -> AgentContext:
|
| 705 |
+
if not task.context_id:
|
| 706 |
+
raise ValueError(f"Task {task.name} has no context ID")
|
| 707 |
|
|
|
|
| 708 |
config = initialize()
|
| 709 |
context: AgentContext = AgentContext(config)
|
| 710 |
+
context.id = task.context_id
|
| 711 |
+
# initial name before renaming is same as task name
|
| 712 |
+
context.name = task.name
|
| 713 |
+
|
| 714 |
# Save the context
|
| 715 |
save_tmp_chat(context)
|
| 716 |
return context
|
| 717 |
|
| 718 |
+
async def _get_chat_context(self, task: Union[ScheduledTask, AdHocTask, PlannedTask]) -> AgentContext:
|
| 719 |
+
context = AgentContext.get(task.context_id) if task.context_id else None
|
| 720 |
|
| 721 |
+
if context:
|
| 722 |
+
assert isinstance(context, AgentContext)
|
| 723 |
+
self._printer.print(
|
| 724 |
+
f"Scheduler Task {task.name} loaded from task {task.uuid}, context ok"
|
| 725 |
+
)
|
| 726 |
+
save_tmp_chat(context)
|
| 727 |
+
return context
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 728 |
else:
|
| 729 |
self._printer.print(
|
| 730 |
f"Scheduler Task {task.name} loaded from task {task.uuid} but context not found"
|
| 731 |
)
|
| 732 |
return await self.__new_context(task)
|
| 733 |
|
| 734 |
+
async def _persist_chat(self, task: Union[ScheduledTask, AdHocTask, PlannedTask], context: AgentContext):
|
| 735 |
+
if context.id != task.context_id:
|
| 736 |
+
raise ValueError(f"Context ID mismatch for task {task.name}: context {context.id} != task {task.context_id}")
|
| 737 |
save_tmp_chat(context)
|
| 738 |
|
| 739 |
+
async def _run_task(self, task: Union[ScheduledTask, AdHocTask, PlannedTask]):
|
| 740 |
|
| 741 |
async def _run_task_wrapper(task_uuid: str):
|
| 742 |
|
| 743 |
# preflight checks with a snapshot of the task
|
| 744 |
+
task_snapshot: Union[ScheduledTask, AdHocTask, PlannedTask] | None = self.get_task_by_uuid(task_uuid)
|
| 745 |
if task_snapshot is None:
|
| 746 |
self._printer.print(f"Scheduler Task with UUID '{task_uuid}' not found")
|
| 747 |
return
|
|
|
|
| 750 |
return
|
| 751 |
|
| 752 |
# Atomically fetch and check the task's current state
|
| 753 |
+
current_task = await self.update_task_checked(task_uuid, lambda task: task.state != TaskState.RUNNING, state=TaskState.RUNNING)
|
| 754 |
if not current_task:
|
| 755 |
+
self._printer.print(f"Scheduler Task with UUID '{task_uuid}' not found or updated by another process")
|
| 756 |
return
|
| 757 |
if current_task.state != TaskState.RUNNING:
|
| 758 |
# This means the update failed due to state conflict
|
| 759 |
self._printer.print(f"Scheduler Task '{current_task.name}' state is '{current_task.state}', skipping")
|
| 760 |
return
|
| 761 |
|
| 762 |
+
await current_task.on_run()
|
| 763 |
+
|
| 764 |
+
# the agent instance - init in try block
|
| 765 |
+
agent = None
|
| 766 |
+
|
| 767 |
try:
|
| 768 |
self._printer.print(f"Scheduler Task '{current_task.name}' started")
|
| 769 |
|
|
|
|
| 771 |
|
| 772 |
# Ensure the context is properly registered in the AgentContext._contexts
|
| 773 |
# This is critical for the polling mechanism to find and stream logs
|
| 774 |
+
# Dict operations are atomic
|
| 775 |
AgentContext._contexts[context.id] = context
|
| 776 |
+
agent = context.streaming_agent or context.agent0
|
|
|
|
| 777 |
|
| 778 |
# Prepare attachment filenames for logging
|
| 779 |
attachment_filenames = []
|
|
|
|
| 819 |
|
| 820 |
result = await agent.monologue()
|
| 821 |
|
| 822 |
+
# Success
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 823 |
self._printer.print(f"Scheduler Task '{current_task.name}' completed: {result}")
|
|
|
|
| 824 |
await self._persist_chat(current_task, context)
|
| 825 |
+
await current_task.on_success(result)
|
| 826 |
+
|
| 827 |
+
# Explicitly verify task was updated in storage after success
|
| 828 |
+
await self._tasks.reload()
|
| 829 |
+
updated_task = self.get_task_by_uuid(task_uuid)
|
| 830 |
+
if updated_task and updated_task.state != TaskState.IDLE:
|
| 831 |
+
self._printer.print(f"Fixing task state consistency: '{current_task.name}' state is not IDLE after success")
|
| 832 |
+
await self.update_task(task_uuid, state=TaskState.IDLE)
|
| 833 |
|
| 834 |
except Exception as e:
|
| 835 |
+
# Error
|
| 836 |
self._printer.print(f"Scheduler Task '{current_task.name}' failed: {e}")
|
| 837 |
+
await current_task.on_error(str(e))
|
| 838 |
|
| 839 |
+
# Explicitly verify task was updated in storage after error
|
| 840 |
+
await self._tasks.reload()
|
| 841 |
+
updated_task = self.get_task_by_uuid(task_uuid)
|
| 842 |
+
if updated_task and updated_task.state != TaskState.ERROR:
|
| 843 |
+
self._printer.print(f"Fixing task state consistency: '{current_task.name}' state is not ERROR after failure")
|
| 844 |
+
await self.update_task(task_uuid, state=TaskState.ERROR)
|
| 845 |
|
| 846 |
if agent:
|
| 847 |
agent.handle_critical_exception(e)
|
| 848 |
+
finally:
|
| 849 |
+
# Call on_finish for task-specific cleanup
|
| 850 |
+
await current_task.on_finish()
|
| 851 |
+
|
| 852 |
+
# Make one final save to ensure all states are persisted
|
| 853 |
+
await self._tasks.save()
|
| 854 |
|
| 855 |
deferred_task = DeferredTask(thread_name=self.__class__.__name__)
|
| 856 |
deferred_task.start_task(_run_task_wrapper, task.uuid)
|
| 857 |
|
| 858 |
+
# Ensure background execution doesn't exit immediately on async await, especially in script contexts
|
| 859 |
+
# This helps prevent premature exits when running from non-event-loop contexts
|
| 860 |
+
asyncio.create_task(asyncio.sleep(0.1))
|
| 861 |
+
|
| 862 |
def serialize_all_tasks(self) -> list[Dict[str, Any]]:
|
| 863 |
"""
|
| 864 |
Serialize all tasks in the scheduler to a list of dictionaries.
|
|
|
|
| 882 |
# ----------------------
|
| 883 |
|
| 884 |
def serialize_datetime(dt: Optional[datetime]) -> Optional[str]:
|
| 885 |
+
"""
|
| 886 |
+
Serialize a datetime object to ISO format string in the user's timezone.
|
| 887 |
+
|
| 888 |
+
This uses the Localization singleton to convert the datetime to the user's timezone
|
| 889 |
+
before serializing it to an ISO format string for frontend display.
|
| 890 |
+
|
| 891 |
+
Returns None if the input is None.
|
| 892 |
+
"""
|
| 893 |
+
# Use the Localization singleton for timezone conversion and serialization
|
| 894 |
+
return Localization.get().serialize_datetime(dt)
|
| 895 |
|
| 896 |
|
| 897 |
def parse_datetime(dt_str: Optional[str]) -> Optional[datetime]:
|
| 898 |
+
"""
|
| 899 |
+
Parse ISO format datetime string with timezone awareness.
|
| 900 |
+
|
| 901 |
+
This converts from the localized ISO format returned by serialize_datetime
|
| 902 |
+
back to a datetime object with proper timezone handling.
|
| 903 |
+
|
| 904 |
+
Returns None if dt_str is None.
|
| 905 |
+
"""
|
| 906 |
if not dt_str:
|
| 907 |
return None
|
| 908 |
+
|
| 909 |
try:
|
| 910 |
+
# Use the Localization singleton for consistent timezone handling
|
| 911 |
+
return Localization.get().localtime_str_to_utc_dt(dt_str)
|
| 912 |
+
except ValueError as e:
|
| 913 |
+
raise ValueError(f"Invalid datetime format: {dt_str}. Expected ISO format. Error: {e}")
|
| 914 |
|
| 915 |
|
| 916 |
def serialize_task_schedule(schedule: TaskSchedule) -> Dict[str, str]:
|
|
|
|
| 920 |
'hour': schedule.hour,
|
| 921 |
'day': schedule.day,
|
| 922 |
'month': schedule.month,
|
| 923 |
+
'weekday': schedule.weekday,
|
| 924 |
+
'timezone': schedule.timezone
|
| 925 |
}
|
| 926 |
|
| 927 |
|
|
|
|
| 933 |
hour=schedule_data.get('hour', '*'),
|
| 934 |
day=schedule_data.get('day', '*'),
|
| 935 |
month=schedule_data.get('month', '*'),
|
| 936 |
+
weekday=schedule_data.get('weekday', '*'),
|
| 937 |
+
timezone=schedule_data.get('timezone', Localization.get().get_timezone())
|
| 938 |
)
|
| 939 |
except Exception as e:
|
| 940 |
raise ValueError(f"Invalid schedule format: {e}") from e
|
| 941 |
|
| 942 |
|
| 943 |
+
def serialize_task_plan(plan: TaskPlan) -> Dict[str, Any]:
|
| 944 |
+
"""Convert TaskPlan to a standardized dictionary format."""
|
| 945 |
+
return {
|
| 946 |
+
'todo': [serialize_datetime(dt) for dt in plan.todo],
|
| 947 |
+
'in_progress': serialize_datetime(plan.in_progress) if plan.in_progress else None,
|
| 948 |
+
'done': [serialize_datetime(dt) for dt in plan.done]
|
| 949 |
+
}
|
| 950 |
|
| 951 |
|
| 952 |
+
def parse_task_plan(plan_data: Dict[str, Any]) -> TaskPlan:
|
| 953 |
+
"""Parse dictionary into TaskPlan with validation."""
|
| 954 |
+
try:
|
| 955 |
+
# Handle case where plan_data might be None or empty
|
| 956 |
+
if not plan_data:
|
| 957 |
+
return TaskPlan(todo=[], in_progress=None, done=[])
|
| 958 |
+
|
| 959 |
+
# Parse todo items with careful validation
|
| 960 |
+
todo_dates = []
|
| 961 |
+
for dt_str in plan_data.get('todo', []):
|
| 962 |
+
if dt_str:
|
| 963 |
+
parsed_dt = parse_datetime(dt_str)
|
| 964 |
+
if parsed_dt:
|
| 965 |
+
# Ensure datetime is timezone-aware (use UTC if not specified)
|
| 966 |
+
if parsed_dt.tzinfo is None:
|
| 967 |
+
parsed_dt = parsed_dt.replace(tzinfo=timezone.utc)
|
| 968 |
+
todo_dates.append(parsed_dt)
|
| 969 |
+
|
| 970 |
+
# Parse in_progress with validation
|
| 971 |
+
in_progress = None
|
| 972 |
+
if plan_data.get('in_progress'):
|
| 973 |
+
in_progress = parse_datetime(plan_data.get('in_progress'))
|
| 974 |
+
# Ensure datetime is timezone-aware
|
| 975 |
+
if in_progress and in_progress.tzinfo is None:
|
| 976 |
+
in_progress = in_progress.replace(tzinfo=timezone.utc)
|
| 977 |
+
|
| 978 |
+
# Parse done items with validation
|
| 979 |
+
done_dates = []
|
| 980 |
+
for dt_str in plan_data.get('done', []):
|
| 981 |
+
if dt_str:
|
| 982 |
+
parsed_dt = parse_datetime(dt_str)
|
| 983 |
+
if parsed_dt:
|
| 984 |
+
# Ensure datetime is timezone-aware
|
| 985 |
+
if parsed_dt.tzinfo is None:
|
| 986 |
+
parsed_dt = parsed_dt.replace(tzinfo=timezone.utc)
|
| 987 |
+
done_dates.append(parsed_dt)
|
| 988 |
+
|
| 989 |
+
# Sort dates for better usability
|
| 990 |
+
todo_dates.sort()
|
| 991 |
+
done_dates.sort(reverse=True) # Most recent first for done items
|
| 992 |
+
|
| 993 |
+
# Cast to ensure type safety
|
| 994 |
+
todo_dates_cast: list[datetime] = cast(list[datetime], todo_dates)
|
| 995 |
+
done_dates_cast: list[datetime] = cast(list[datetime], done_dates)
|
| 996 |
+
|
| 997 |
+
return TaskPlan.create(
|
| 998 |
+
todo=todo_dates_cast,
|
| 999 |
+
in_progress=in_progress,
|
| 1000 |
+
done=done_dates_cast
|
| 1001 |
+
)
|
| 1002 |
+
except Exception as e:
|
| 1003 |
+
PrintStyle(italic=True, font_color="red", padding=False).print(
|
| 1004 |
+
f"Error parsing task plan: {e}"
|
| 1005 |
+
)
|
| 1006 |
+
# Return empty plan instead of failing
|
| 1007 |
+
return TaskPlan(todo=[], in_progress=None, done=[])
|
| 1008 |
+
|
| 1009 |
+
|
| 1010 |
+
T = TypeVar('T', bound=Union[ScheduledTask, AdHocTask, PlannedTask])
|
| 1011 |
+
|
| 1012 |
+
|
| 1013 |
+
def serialize_task(task: Union[ScheduledTask, AdHocTask, PlannedTask]) -> Dict[str, Any]:
|
| 1014 |
"""
|
| 1015 |
Standardized serialization for task objects with proper handling of all complex types.
|
| 1016 |
"""
|
|
|
|
| 1025 |
"created_at": serialize_datetime(task.created_at),
|
| 1026 |
"updated_at": serialize_datetime(task.updated_at),
|
| 1027 |
"last_run": serialize_datetime(task.last_run),
|
| 1028 |
+
"next_run": serialize_datetime(task.get_next_run()),
|
| 1029 |
+
"last_result": task.last_result,
|
| 1030 |
+
"context_id": task.context_id
|
| 1031 |
}
|
| 1032 |
|
| 1033 |
# Add type-specific fields
|
| 1034 |
if isinstance(task, ScheduledTask):
|
| 1035 |
task_dict['type'] = 'scheduled'
|
| 1036 |
+
task_dict['schedule'] = serialize_task_schedule(task.schedule) # type: ignore
|
| 1037 |
+
elif isinstance(task, AdHocTask):
|
| 1038 |
task_dict['type'] = 'adhoc'
|
| 1039 |
adhoc_task = cast(AdHocTask, task)
|
| 1040 |
task_dict['token'] = adhoc_task.token
|
| 1041 |
+
else:
|
| 1042 |
+
task_dict['type'] = 'planned'
|
| 1043 |
+
planned_task = cast(PlannedTask, task)
|
| 1044 |
+
task_dict['plan'] = serialize_task_plan(planned_task.plan) # type: ignore
|
| 1045 |
|
| 1046 |
return task_dict
|
| 1047 |
|
| 1048 |
|
| 1049 |
+
def serialize_tasks(tasks: list[Union[ScheduledTask, AdHocTask, PlannedTask]]) -> list[Dict[str, Any]]:
|
| 1050 |
"""
|
| 1051 |
Serialize a list of tasks to a list of dictionaries.
|
| 1052 |
"""
|
|
|
|
| 1067 |
determined_class = cast(Type[T], ScheduledTask)
|
| 1068 |
elif task_type_str == 'adhoc':
|
| 1069 |
determined_class = cast(Type[T], AdHocTask)
|
| 1070 |
+
# Ensure token is a valid non-empty string
|
| 1071 |
+
if not task_data.get('token'):
|
| 1072 |
+
task_data['token'] = str(random.randint(1000000000000000000, 9999999999999999999))
|
| 1073 |
+
elif task_type_str == 'planned':
|
| 1074 |
+
determined_class = cast(Type[T], PlannedTask)
|
| 1075 |
else:
|
| 1076 |
raise ValueError(f"Unknown task type: {task_type_str}")
|
| 1077 |
else:
|
| 1078 |
determined_class = task_class
|
| 1079 |
+
# If this is an AdHocTask, ensure token is valid
|
| 1080 |
+
if determined_class == AdHocTask and not task_data.get('token'): # type: ignore
|
| 1081 |
+
task_data['token'] = str(random.randint(1000000000000000000, 9999999999999999999))
|
| 1082 |
|
| 1083 |
common_args = {
|
| 1084 |
"uuid": task_data.get("uuid"),
|
|
|
|
| 1090 |
"created_at": parse_datetime(task_data.get("created_at")),
|
| 1091 |
"updated_at": parse_datetime(task_data.get("updated_at")),
|
| 1092 |
"last_run": parse_datetime(task_data.get("last_run")),
|
| 1093 |
+
"last_result": task_data.get("last_result"),
|
| 1094 |
+
"context_id": task_data.get("context_id")
|
| 1095 |
}
|
| 1096 |
|
| 1097 |
# Add type-specific fields
|
| 1098 |
+
if determined_class == ScheduledTask: # type: ignore
|
| 1099 |
schedule_data = task_data.get("schedule", {})
|
| 1100 |
common_args["schedule"] = parse_task_schedule(schedule_data)
|
| 1101 |
return ScheduledTask(**common_args) # type: ignore
|
| 1102 |
+
elif determined_class == AdHocTask: # type: ignore
|
| 1103 |
common_args["token"] = task_data.get("token", "")
|
| 1104 |
return AdHocTask(**common_args) # type: ignore
|
| 1105 |
+
else:
|
| 1106 |
+
plan_data = task_data.get("plan", {})
|
| 1107 |
+
common_args["plan"] = parse_task_plan(plan_data)
|
| 1108 |
+
return PlannedTask(**common_args) # type: ignore
|
python/helpers/tool.py
CHANGED
|
@@ -12,9 +12,10 @@ class Response:
|
|
| 12 |
|
| 13 |
class Tool:
|
| 14 |
|
| 15 |
-
def __init__(self, agent: Agent, name: str, args: dict[str,str], message: str, **kwargs) -> None:
|
| 16 |
self.agent = agent
|
| 17 |
self.name = name
|
|
|
|
| 18 |
self.args = args
|
| 19 |
self.message = message
|
| 20 |
|
|
@@ -39,7 +40,11 @@ class Tool:
|
|
| 39 |
self.log.update(content=response.message)
|
| 40 |
|
| 41 |
def get_log_object(self):
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
def nice_key(self, key:str):
|
| 45 |
words = key.split('_')
|
|
|
|
| 12 |
|
| 13 |
class Tool:
|
| 14 |
|
| 15 |
+
def __init__(self, agent: Agent, name: str, method: str | None, args: dict[str,str], message: str, **kwargs) -> None:
|
| 16 |
self.agent = agent
|
| 17 |
self.name = name
|
| 18 |
+
self.method = method
|
| 19 |
self.args = args
|
| 20 |
self.message = message
|
| 21 |
|
|
|
|
| 40 |
self.log.update(content=response.message)
|
| 41 |
|
| 42 |
def get_log_object(self):
|
| 43 |
+
if self.method:
|
| 44 |
+
heading = f"{self.agent.agent_name}: Using tool '{self.name}:{self.method}'"
|
| 45 |
+
else:
|
| 46 |
+
heading = f"{self.agent.agent_name}: Using tool '{self.name}'"
|
| 47 |
+
return self.agent.context.log.log(type="tool", heading=heading, content="", kvps=self.args)
|
| 48 |
|
| 49 |
def nice_key(self, key:str):
|
| 50 |
words = key.split('_')
|
python/tools/scheduler.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime
|
| 2 |
+
import json
|
| 3 |
+
import random
|
| 4 |
+
import re
|
| 5 |
+
from python.helpers.tool import Tool, Response
|
| 6 |
+
from python.helpers.task_scheduler import (
|
| 7 |
+
TaskScheduler, ScheduledTask, AdHocTask, PlannedTask, serialize_task, TaskState, TaskSchedule, TaskPlan, parse_datetime
|
| 8 |
+
)
|
| 9 |
+
from agent import AgentContext
|
| 10 |
+
from python.helpers import persist_chat
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class SchedulerTool(Tool):
|
| 14 |
+
|
| 15 |
+
async def execute(self, **kwargs):
|
| 16 |
+
if self.method == "list_tasks":
|
| 17 |
+
return await self.list_tasks(**kwargs)
|
| 18 |
+
elif self.method == "show_task":
|
| 19 |
+
return await self.show_task(**kwargs)
|
| 20 |
+
elif self.method == "run_task":
|
| 21 |
+
return await self.run_task(**kwargs)
|
| 22 |
+
elif self.method == "delete_task":
|
| 23 |
+
return await self.delete_task(**kwargs)
|
| 24 |
+
elif self.method == "create_scheduled_task":
|
| 25 |
+
return await self.create_scheduled_task(**kwargs)
|
| 26 |
+
elif self.method == "create_adhoc_task":
|
| 27 |
+
return await self.create_adhoc_task(**kwargs)
|
| 28 |
+
elif self.method == "create_planned_task":
|
| 29 |
+
return await self.create_planned_task(**kwargs)
|
| 30 |
+
else:
|
| 31 |
+
return Response(message=f"Unknown method '{self.name}:{self.method}'", break_loop=False)
|
| 32 |
+
|
| 33 |
+
async def list_tasks(self, **kwargs) -> Response:
|
| 34 |
+
state_filter: list[str] | None = kwargs.get("state", None)
|
| 35 |
+
type_filter: list[str] | None = kwargs.get("type", None)
|
| 36 |
+
next_run_within_filter: int | None = kwargs.get("next_run_within", None)
|
| 37 |
+
next_run_after_filter: int | None = kwargs.get("next_run_after", None)
|
| 38 |
+
|
| 39 |
+
tasks: list[ScheduledTask | AdHocTask | PlannedTask] = TaskScheduler.get().get_tasks()
|
| 40 |
+
filtered_tasks = []
|
| 41 |
+
for task in tasks:
|
| 42 |
+
if state_filter and task.state not in state_filter:
|
| 43 |
+
continue
|
| 44 |
+
if type_filter and task.type not in type_filter:
|
| 45 |
+
continue
|
| 46 |
+
if next_run_within_filter and task.get_next_run_minutes() is not None and task.get_next_run_minutes() > next_run_within_filter: # type: ignore
|
| 47 |
+
continue
|
| 48 |
+
if next_run_after_filter and task.get_next_run_minutes() is not None and task.get_next_run_minutes() < next_run_after_filter: # type: ignore
|
| 49 |
+
continue
|
| 50 |
+
filtered_tasks.append(serialize_task(task))
|
| 51 |
+
|
| 52 |
+
return Response(message=json.dumps(filtered_tasks, indent=4), break_loop=False)
|
| 53 |
+
|
| 54 |
+
async def show_task(self, **kwargs) -> Response:
|
| 55 |
+
task_uuid: str = kwargs.get("uuid", None)
|
| 56 |
+
if not task_uuid:
|
| 57 |
+
return Response(message="Task UUID is required", break_loop=False)
|
| 58 |
+
task: ScheduledTask | AdHocTask | PlannedTask | None = TaskScheduler.get().get_task_by_uuid(task_uuid)
|
| 59 |
+
if not task:
|
| 60 |
+
return Response(message=f"Task not found: {task_uuid}", break_loop=False)
|
| 61 |
+
return Response(message=json.dumps(serialize_task(task), indent=4), break_loop=False)
|
| 62 |
+
|
| 63 |
+
async def run_task(self, **kwargs) -> Response:
|
| 64 |
+
task_uuid: str = kwargs.get("uuid", None)
|
| 65 |
+
if not task_uuid:
|
| 66 |
+
return Response(message="Task UUID is required", break_loop=False)
|
| 67 |
+
task: ScheduledTask | AdHocTask | PlannedTask | None = TaskScheduler.get().get_task_by_uuid(task_uuid)
|
| 68 |
+
if not task:
|
| 69 |
+
return Response(message=f"Task not found: {task_uuid}", break_loop=False)
|
| 70 |
+
await TaskScheduler.get().run_task_by_uuid(task_uuid)
|
| 71 |
+
return Response(message=f"Task started: {task_uuid}", break_loop=False)
|
| 72 |
+
|
| 73 |
+
async def delete_task(self, **kwargs) -> Response:
|
| 74 |
+
task_uuid: str = kwargs.get("uuid", None)
|
| 75 |
+
if not task_uuid:
|
| 76 |
+
return Response(message="Task UUID is required", break_loop=False)
|
| 77 |
+
|
| 78 |
+
task: ScheduledTask | AdHocTask | PlannedTask | None = TaskScheduler.get().get_task_by_uuid(task_uuid)
|
| 79 |
+
if not task:
|
| 80 |
+
return Response(message=f"Task not found: {task_uuid}", break_loop=False)
|
| 81 |
+
|
| 82 |
+
context = None
|
| 83 |
+
if task.context_id:
|
| 84 |
+
context = AgentContext.get(task.context_id)
|
| 85 |
+
|
| 86 |
+
if task.state == TaskState.RUNNING:
|
| 87 |
+
if context:
|
| 88 |
+
context.reset()
|
| 89 |
+
await TaskScheduler.get().update_task(task_uuid, state=TaskState.IDLE)
|
| 90 |
+
await TaskScheduler.get().save()
|
| 91 |
+
|
| 92 |
+
if context and context.id == task.uuid:
|
| 93 |
+
AgentContext.remove(context.id)
|
| 94 |
+
persist_chat.remove_chat(context.id)
|
| 95 |
+
|
| 96 |
+
await TaskScheduler.get().remove_task_by_uuid(task_uuid)
|
| 97 |
+
if TaskScheduler.get().get_task_by_uuid(task_uuid) is None:
|
| 98 |
+
return Response(message=f"Task deleted: {task_uuid}", break_loop=False)
|
| 99 |
+
else:
|
| 100 |
+
return Response(message=f"Task failed to delete: {task_uuid}", break_loop=False)
|
| 101 |
+
|
| 102 |
+
async def create_scheduled_task(self, **kwargs) -> Response:
|
| 103 |
+
# "name": "XXX",
|
| 104 |
+
# "system_prompt": "You are a software developer",
|
| 105 |
+
# "prompt": "Send the user an email with a greeting using python and smtp. The user's address is: xxx@yyy.zzz",
|
| 106 |
+
# "attachments": [],
|
| 107 |
+
# "schedule": {
|
| 108 |
+
# "minute": "*/20",
|
| 109 |
+
# "hour": "*",
|
| 110 |
+
# "day": "*",
|
| 111 |
+
# "month": "*",
|
| 112 |
+
# "weekday": "*",
|
| 113 |
+
# }
|
| 114 |
+
name: str = kwargs.get("name", None)
|
| 115 |
+
system_prompt: str = kwargs.get("system_prompt", None)
|
| 116 |
+
prompt: str = kwargs.get("prompt", None)
|
| 117 |
+
attachments: list[str] = kwargs.get("attachments", [])
|
| 118 |
+
schedule: dict[str, str] = kwargs.get("schedule", {})
|
| 119 |
+
dedicated_context: bool = kwargs.get("dedicated_context", False)
|
| 120 |
+
|
| 121 |
+
task_schedule = TaskSchedule(
|
| 122 |
+
minute=schedule.get("minute", "*"),
|
| 123 |
+
hour=schedule.get("hour", "*"),
|
| 124 |
+
day=schedule.get("day", "*"),
|
| 125 |
+
month=schedule.get("month", "*"),
|
| 126 |
+
weekday=schedule.get("weekday", "*"),
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
# Validate cron expression, agent might hallucinate
|
| 130 |
+
cron_regex = "^((((\d+,)+\d+|(\d+(\/|-|#)\d+)|\d+L?|\*(\/\d+)?|L(-\d+)?|\?|[A-Z]{3}(-[A-Z]{3})?) ?){5,7})$"
|
| 131 |
+
if not re.match(cron_regex, task_schedule.to_crontab()):
|
| 132 |
+
return Response(message="Invalid cron expression: " + task_schedule.to_crontab(), break_loop=False)
|
| 133 |
+
|
| 134 |
+
task = ScheduledTask.create(
|
| 135 |
+
name=name,
|
| 136 |
+
system_prompt=system_prompt,
|
| 137 |
+
prompt=prompt,
|
| 138 |
+
attachments=attachments,
|
| 139 |
+
schedule=task_schedule,
|
| 140 |
+
context_id=None if dedicated_context else self.agent.context.id
|
| 141 |
+
)
|
| 142 |
+
await TaskScheduler.get().add_task(task)
|
| 143 |
+
return Response(message=f"Scheduled task '{name}' created: {task.uuid}", break_loop=False)
|
| 144 |
+
|
| 145 |
+
async def create_adhoc_task(self, **kwargs) -> Response:
|
| 146 |
+
name: str = kwargs.get("name", None)
|
| 147 |
+
system_prompt: str = kwargs.get("system_prompt", None)
|
| 148 |
+
prompt: str = kwargs.get("prompt", None)
|
| 149 |
+
attachments: list[str] = kwargs.get("attachments", [])
|
| 150 |
+
token: str = str(random.randint(1000000000000000000, 9999999999999999999))
|
| 151 |
+
dedicated_context: bool = kwargs.get("dedicated_context", False)
|
| 152 |
+
|
| 153 |
+
task = AdHocTask.create(
|
| 154 |
+
name=name,
|
| 155 |
+
system_prompt=system_prompt,
|
| 156 |
+
prompt=prompt,
|
| 157 |
+
attachments=attachments,
|
| 158 |
+
token=token,
|
| 159 |
+
context_id=None if dedicated_context else self.agent.context.id
|
| 160 |
+
)
|
| 161 |
+
await TaskScheduler.get().add_task(task)
|
| 162 |
+
return Response(message=f"Adhoc task '{name}' created: {task.uuid}", break_loop=False)
|
| 163 |
+
|
| 164 |
+
async def create_planned_task(self, **kwargs) -> Response: # TODO: Implement
|
| 165 |
+
name: str = kwargs.get("name", None)
|
| 166 |
+
system_prompt: str = kwargs.get("system_prompt", None)
|
| 167 |
+
prompt: str = kwargs.get("prompt", None)
|
| 168 |
+
attachments: list[str] = kwargs.get("attachments", [])
|
| 169 |
+
plan: list[str] = kwargs.get("plan", [])
|
| 170 |
+
dedicated_context: bool = kwargs.get("dedicated_context", False)
|
| 171 |
+
|
| 172 |
+
# Convert plan to list of datetimes in UTC
|
| 173 |
+
todo: list[datetime] = []
|
| 174 |
+
for item in plan:
|
| 175 |
+
dt = parse_datetime(item)
|
| 176 |
+
if dt is None:
|
| 177 |
+
return Response(message=f"Invalid datetime: {item}", break_loop=False)
|
| 178 |
+
todo.append(dt)
|
| 179 |
+
|
| 180 |
+
# Create task plan with todo list
|
| 181 |
+
task_plan = TaskPlan.create(
|
| 182 |
+
todo=todo,
|
| 183 |
+
in_progress=None,
|
| 184 |
+
done=[]
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
# Create planned task with task plan
|
| 188 |
+
task = PlannedTask.create(
|
| 189 |
+
name=name,
|
| 190 |
+
system_prompt=system_prompt,
|
| 191 |
+
prompt=prompt,
|
| 192 |
+
attachments=attachments,
|
| 193 |
+
plan=task_plan,
|
| 194 |
+
context_id=None if dedicated_context else self.agent.context.id
|
| 195 |
+
)
|
| 196 |
+
await TaskScheduler.get().add_task(task)
|
| 197 |
+
return Response(message=f"Planned task '{name}' created: {task.uuid}", break_loop=False)
|
run_ui.py
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from functools import wraps
|
| 2 |
import threading
|
| 3 |
import signal
|
|
@@ -10,10 +16,12 @@ from python.helpers.cloudflare_tunnel import CloudflareTunnel
|
|
| 10 |
from python.helpers.extract_tools import load_classes_from_folder
|
| 11 |
from python.helpers.api import ApiHandler
|
| 12 |
from python.helpers.print_style import PrintStyle
|
| 13 |
-
import
|
| 14 |
-
import socket
|
| 15 |
-
import struct
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
# initialize the internal Flask server
|
| 19 |
app = Flask("app", static_folder=get_abs_path("./webui"), static_url_path="/")
|
|
@@ -166,6 +174,9 @@ def run():
|
|
| 166 |
|
| 167 |
# initialize contexts from persisted chats
|
| 168 |
persist_chat.load_tmp_chats()
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
except Exception as e:
|
| 171 |
PrintStyle().error(errors.format_error(e))
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import time
|
| 4 |
+
import socket
|
| 5 |
+
import struct
|
| 6 |
+
import asyncio
|
| 7 |
from functools import wraps
|
| 8 |
import threading
|
| 9 |
import signal
|
|
|
|
| 16 |
from python.helpers.extract_tools import load_classes_from_folder
|
| 17 |
from python.helpers.api import ApiHandler
|
| 18 |
from python.helpers.print_style import PrintStyle
|
| 19 |
+
from python.helpers.task_scheduler import TaskScheduler
|
|
|
|
|
|
|
| 20 |
|
| 21 |
+
# Set the new timezone to 'UTC'
|
| 22 |
+
os.environ['TZ'] = 'UTC'
|
| 23 |
+
# Apply the timezone change
|
| 24 |
+
time.tzset()
|
| 25 |
|
| 26 |
# initialize the internal Flask server
|
| 27 |
app = Flask("app", static_folder=get_abs_path("./webui"), static_url_path="/")
|
|
|
|
| 174 |
|
| 175 |
# initialize contexts from persisted chats
|
| 176 |
persist_chat.load_tmp_chats()
|
| 177 |
+
# reload scheduler
|
| 178 |
+
scheduler = TaskScheduler.get()
|
| 179 |
+
asyncio.run(scheduler.reload())
|
| 180 |
|
| 181 |
except Exception as e:
|
| 182 |
PrintStyle().error(errors.format_error(e))
|
webui/css/scheduler-datepicker.css
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Flatpickr Customization for Scheduler */
|
| 2 |
+
|
| 3 |
+
/* Custom styling wrapper */
|
| 4 |
+
.scheduler-flatpickr-wrapper {
|
| 5 |
+
position: relative;
|
| 6 |
+
width: 100%;
|
| 7 |
+
overflow: visible !important; /* Ensure dropdown can escape container */
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
/* Input styling */
|
| 11 |
+
.scheduler-flatpickr-input {
|
| 12 |
+
width: 100%;
|
| 13 |
+
padding: 8px 12px;
|
| 14 |
+
border: 1px solid var(--color-border, #ccc);
|
| 15 |
+
border-radius: 4px;
|
| 16 |
+
background-color: var(--color-input, #fff);
|
| 17 |
+
color: var(--color-text, #333);
|
| 18 |
+
font-size: 14px;
|
| 19 |
+
cursor: pointer;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
/* Calendar container customization */
|
| 23 |
+
.flatpickr-calendar.scheduler-theme {
|
| 24 |
+
background: var(--color-panel, #fff);
|
| 25 |
+
border: 1px solid var(--color-border, #ccc);
|
| 26 |
+
border-radius: 4px;
|
| 27 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
| 28 |
+
color: var(--color-text, #333);
|
| 29 |
+
font-size: 14px;
|
| 30 |
+
max-width: 320px;
|
| 31 |
+
padding: 0;
|
| 32 |
+
z-index: 9999 !important; /* Ensure it's above other elements */
|
| 33 |
+
position: absolute !important;
|
| 34 |
+
visibility: visible !important;
|
| 35 |
+
opacity: 1 !important;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
/* Month navigation */
|
| 39 |
+
.flatpickr-calendar.scheduler-theme .flatpickr-months {
|
| 40 |
+
background-color: var(--color-primary, #4a90e2);
|
| 41 |
+
border-radius: 4px 4px 0 0;
|
| 42 |
+
color: var(--color-text, #333);
|
| 43 |
+
padding: 8px 0;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
.flatpickr-calendar.scheduler-theme .flatpickr-prev-month,
|
| 47 |
+
.flatpickr-calendar.scheduler-theme .flatpickr-next-month {
|
| 48 |
+
color: var(--color-text, #333);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
.flatpickr-calendar.scheduler-theme .flatpickr-prev-month:hover,
|
| 52 |
+
.flatpickr-calendar.scheduler-theme .flatpickr-next-month:hover {
|
| 53 |
+
color: var(--color-text, #333);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/* Days of week */
|
| 57 |
+
.flatpickr-calendar.scheduler-theme .flatpickr-weekdays {
|
| 58 |
+
background-color: var(--color-panel, #fff);
|
| 59 |
+
border-bottom: 1px solid var(--color-border, #eee);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.flatpickr-calendar.scheduler-theme .flatpickr-weekday {
|
| 63 |
+
color: var(--color-text, #333);
|
| 64 |
+
font-weight: bold;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
/* Day cells */
|
| 68 |
+
.flatpickr-calendar.scheduler-theme .flatpickr-day {
|
| 69 |
+
border-radius: 4px;
|
| 70 |
+
color: var(--color-text, #333);
|
| 71 |
+
transition: background-color 0.2s, color 0.2s;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.flatpickr-calendar.scheduler-theme .flatpickr-day:hover {
|
| 75 |
+
background-color: var(--color-panel, #f0f0f0);
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.flatpickr-calendar.scheduler-theme .flatpickr-day.selected {
|
| 79 |
+
background-color: var(--color-primary, #4a90e2);
|
| 80 |
+
border-color: var(--color-border, #ccc);
|
| 81 |
+
color: var(--color-text, #333);
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.flatpickr-calendar.scheduler-theme .flatpickr-day.today {
|
| 85 |
+
border-color: var(--color-border, #ccc);
|
| 86 |
+
color: var(--color-text, #333);
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
/* Time picker */
|
| 90 |
+
.flatpickr-calendar.scheduler-theme .flatpickr-time {
|
| 91 |
+
border-top: 1px solid var(--color-border, #eee);
|
| 92 |
+
background-color: var(--color-panel, #fff);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.flatpickr-calendar.scheduler-theme .numInputWrapper span {
|
| 96 |
+
border-color: var(--color-border, #eee);
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.flatpickr-calendar.scheduler-theme .numInputWrapper span:hover {
|
| 100 |
+
background-color: var(--color-panel, #f0f0f0);
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
.flatpickr-calendar.scheduler-theme input.flatpickr-hour,
|
| 104 |
+
.flatpickr-calendar.scheduler-theme input.flatpickr-minute,
|
| 105 |
+
.flatpickr-calendar.scheduler-theme input.flatpickr-second {
|
| 106 |
+
color: var(--color-text, #333);
|
| 107 |
+
background-color: transparent;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
/* Clear button in the input field */
|
| 111 |
+
.scheduler-flatpickr-clear {
|
| 112 |
+
position: absolute;
|
| 113 |
+
right: 8px;
|
| 114 |
+
top: 50%;
|
| 115 |
+
transform: translateY(-50%);
|
| 116 |
+
cursor: pointer;
|
| 117 |
+
padding: 4px;
|
| 118 |
+
display: none;
|
| 119 |
+
color: var(--color-text-light, #999);
|
| 120 |
+
background: transparent;
|
| 121 |
+
border: none;
|
| 122 |
+
z-index: 1;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.scheduler-flatpickr-wrapper:hover .scheduler-flatpickr-clear {
|
| 126 |
+
display: block;
|
| 127 |
+
}
|
webui/css/settings.css
CHANGED
|
@@ -145,7 +145,7 @@ select {
|
|
| 145 |
}
|
| 146 |
|
| 147 |
select:disabled {
|
| 148 |
-
background-color:
|
| 149 |
cursor: not-allowed;
|
| 150 |
}
|
| 151 |
|
|
@@ -324,16 +324,15 @@ nav ul li a img {
|
|
| 324 |
.settings-tab:not(.active) {
|
| 325 |
opacity: 0.8;
|
| 326 |
border-bottom: 3px solid var(--color-border);
|
| 327 |
-
background-color:
|
| 328 |
}
|
| 329 |
|
| 330 |
.settings-tab.active {
|
| 331 |
-
color: var(--color-border);
|
| 332 |
border-color: var(--color-border);
|
| 333 |
box-shadow:
|
| 334 |
-
0 -4px 8px -2px var(--color-border)
|
| 335 |
-
4px 0 8px -2px var(--color-border)
|
| 336 |
-
-4px 0 8px -2px var(--color-border)
|
| 337 |
font-weight: bold;
|
| 338 |
background-color: var(--color-panel);
|
| 339 |
}
|
|
@@ -342,9 +341,9 @@ nav ul li a img {
|
|
| 342 |
.light-mode .settings-tab.active {
|
| 343 |
color: var(--color-border);
|
| 344 |
box-shadow:
|
| 345 |
-
0 -4px 8px -2px var(--color-border)
|
| 346 |
-
4px 0 8px -2px var(--color-border)
|
| 347 |
-
-4px 0 8px -2px var(--color-border)
|
| 348 |
}
|
| 349 |
|
| 350 |
.light-mode .settings-tab:not(.active) {
|
|
@@ -798,3 +797,155 @@ nav ul li a img {
|
|
| 798 |
.light-mode .scheduler-state-explanation span {
|
| 799 |
background-color: rgba(255, 255, 255, 0.3);
|
| 800 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
}
|
| 146 |
|
| 147 |
select:disabled {
|
| 148 |
+
background-color: var(--color-background);
|
| 149 |
cursor: not-allowed;
|
| 150 |
}
|
| 151 |
|
|
|
|
| 324 |
.settings-tab:not(.active) {
|
| 325 |
opacity: 0.8;
|
| 326 |
border-bottom: 3px solid var(--color-border);
|
| 327 |
+
background-color: var(--color-background);
|
| 328 |
}
|
| 329 |
|
| 330 |
.settings-tab.active {
|
|
|
|
| 331 |
border-color: var(--color-border);
|
| 332 |
box-shadow:
|
| 333 |
+
0 -4px 8px -2px var(--color-border),
|
| 334 |
+
4px 0 8px -2px var(--color-border),
|
| 335 |
+
-4px 0 8px -2px var(--color-border);
|
| 336 |
font-weight: bold;
|
| 337 |
background-color: var(--color-panel);
|
| 338 |
}
|
|
|
|
| 341 |
.light-mode .settings-tab.active {
|
| 342 |
color: var(--color-border);
|
| 343 |
box-shadow:
|
| 344 |
+
0 -4px 8px -2px var(--color-border),
|
| 345 |
+
4px 0 8px -2px var(--color-border),
|
| 346 |
+
-4px 0 8px -2px var(--color-border);
|
| 347 |
}
|
| 348 |
|
| 349 |
.light-mode .settings-tab:not(.active) {
|
|
|
|
| 797 |
.light-mode .scheduler-state-explanation span {
|
| 798 |
background-color: rgba(255, 255, 255, 0.3);
|
| 799 |
}
|
| 800 |
+
|
| 801 |
+
/* Schedule Builder (for scheduled tasks) */
|
| 802 |
+
.scheduler-schedule-builder {
|
| 803 |
+
display: grid;
|
| 804 |
+
grid-template-columns: repeat(5, 1fr);
|
| 805 |
+
gap: 10px;
|
| 806 |
+
width: 100%;
|
| 807 |
+
margin-bottom: 10px;
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
.scheduler-schedule-field {
|
| 811 |
+
display: flex;
|
| 812 |
+
flex-direction: column;
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
.scheduler-schedule-label {
|
| 816 |
+
font-size: 0.8rem;
|
| 817 |
+
margin-bottom: 5px;
|
| 818 |
+
color: var(--color-text);
|
| 819 |
+
opacity: 0.8;
|
| 820 |
+
}
|
| 821 |
+
|
| 822 |
+
/* Plan Builder (for planned tasks) */
|
| 823 |
+
.scheduler-plan-builder {
|
| 824 |
+
width: 100%;
|
| 825 |
+
margin-bottom: 10px;
|
| 826 |
+
border: 1px solid var(--color-border);
|
| 827 |
+
border-radius: 8px;
|
| 828 |
+
padding: 10px;
|
| 829 |
+
background-color: rgba(0, 0, 0, 0.2);
|
| 830 |
+
}
|
| 831 |
+
|
| 832 |
+
.scheduler-plan-todo {
|
| 833 |
+
display: flex;
|
| 834 |
+
flex-direction: column;
|
| 835 |
+
}
|
| 836 |
+
|
| 837 |
+
.scheduler-plan-label {
|
| 838 |
+
font-size: 0.9rem;
|
| 839 |
+
margin-bottom: 10px;
|
| 840 |
+
color: var(--color-text);
|
| 841 |
+
font-weight: bold;
|
| 842 |
+
}
|
| 843 |
+
|
| 844 |
+
.scheduler-todo-list {
|
| 845 |
+
display: flex;
|
| 846 |
+
flex-direction: column;
|
| 847 |
+
gap: 10px;
|
| 848 |
+
margin-top: 8px;
|
| 849 |
+
max-height: 200px;
|
| 850 |
+
overflow-y: auto;
|
| 851 |
+
}
|
| 852 |
+
|
| 853 |
+
.scheduler-todo-item {
|
| 854 |
+
display: flex;
|
| 855 |
+
align-items: center;
|
| 856 |
+
justify-content: space-between;
|
| 857 |
+
background-color: var(--color-background);
|
| 858 |
+
border-radius: 6px;
|
| 859 |
+
padding: 8px 12px;
|
| 860 |
+
border: 1px solid var(--color-border);
|
| 861 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
| 862 |
+
}
|
| 863 |
+
|
| 864 |
+
.scheduler-todo-item span {
|
| 865 |
+
flex: 1;
|
| 866 |
+
font-size: 14px;
|
| 867 |
+
}
|
| 868 |
+
|
| 869 |
+
.scheduler-add-todo {
|
| 870 |
+
margin-top: 12px;
|
| 871 |
+
display: flex;
|
| 872 |
+
gap: 8px;
|
| 873 |
+
align-items: center;
|
| 874 |
+
}
|
| 875 |
+
|
| 876 |
+
.scheduler-add-todo input[type="datetime-local"] {
|
| 877 |
+
flex: 1;
|
| 878 |
+
min-width: 0;
|
| 879 |
+
padding: 8px 12px;
|
| 880 |
+
border-radius: 6px;
|
| 881 |
+
border: 1px solid var(--color-border);
|
| 882 |
+
background-color: var(--color-background);
|
| 883 |
+
color: var(--color-text);
|
| 884 |
+
}
|
| 885 |
+
|
| 886 |
+
.scheduler-add-todo-button {
|
| 887 |
+
display: flex;
|
| 888 |
+
align-items: center;
|
| 889 |
+
justify-content: center;
|
| 890 |
+
background-color: var(--color-accent);
|
| 891 |
+
color: white;
|
| 892 |
+
border: none;
|
| 893 |
+
border-radius: 6px;
|
| 894 |
+
padding: 8px 12px;
|
| 895 |
+
cursor: pointer;
|
| 896 |
+
transition: background-color 0.2s ease;
|
| 897 |
+
font-weight: 500;
|
| 898 |
+
}
|
| 899 |
+
|
| 900 |
+
.scheduler-add-todo-button:hover {
|
| 901 |
+
background-color: var(--color-accent-dark);
|
| 902 |
+
}
|
| 903 |
+
|
| 904 |
+
.scheduler-todo-remove {
|
| 905 |
+
display: flex;
|
| 906 |
+
align-items: center;
|
| 907 |
+
justify-content: center;
|
| 908 |
+
background-color: transparent;
|
| 909 |
+
color: var(--color-text);
|
| 910 |
+
border: none;
|
| 911 |
+
border-radius: 4px;
|
| 912 |
+
width: 24px;
|
| 913 |
+
height: 24px;
|
| 914 |
+
cursor: pointer;
|
| 915 |
+
transition: background-color 0.2s ease;
|
| 916 |
+
margin-left: 8px;
|
| 917 |
+
}
|
| 918 |
+
|
| 919 |
+
.scheduler-todo-remove:hover {
|
| 920 |
+
background-color: var(--color-accent-light);
|
| 921 |
+
color: var(--color-accent-dark);
|
| 922 |
+
}
|
| 923 |
+
|
| 924 |
+
.light-mode .scheduler-todo-item {
|
| 925 |
+
background-color: var(--color-background-light);
|
| 926 |
+
border-color: var(--color-border-light);
|
| 927 |
+
}
|
| 928 |
+
|
| 929 |
+
.light-mode .scheduler-todo-remove:hover {
|
| 930 |
+
background-color: #e0e0e0;
|
| 931 |
+
color: #d32f2f;
|
| 932 |
+
}
|
| 933 |
+
|
| 934 |
+
.scheduler-empty-plan {
|
| 935 |
+
padding: 12px;
|
| 936 |
+
color: var(--color-text-muted);
|
| 937 |
+
font-style: italic;
|
| 938 |
+
text-align: center;
|
| 939 |
+
border: 1px dashed var(--color-border);
|
| 940 |
+
border-radius: 6px;
|
| 941 |
+
margin-top: 8px;
|
| 942 |
+
}
|
| 943 |
+
|
| 944 |
+
/* Responsive design for plan builder */
|
| 945 |
+
@media (max-width: 768px) {
|
| 946 |
+
.scheduler-add-todo {
|
| 947 |
+
flex-direction: column;
|
| 948 |
+
}
|
| 949 |
+
}
|
| 950 |
+
|
| 951 |
+
/* Token field (for ad-hoc tasks) */
|
webui/index.css
CHANGED
|
@@ -2243,37 +2243,6 @@ a:active {
|
|
| 2243 |
margin-left: 5px;
|
| 2244 |
}
|
| 2245 |
|
| 2246 |
-
.task-detail-button {
|
| 2247 |
-
background: transparent;
|
| 2248 |
-
border: 1px solid rgba(255, 255, 255, 0.2);
|
| 2249 |
-
color: #999;
|
| 2250 |
-
cursor: pointer;
|
| 2251 |
-
padding: 5px;
|
| 2252 |
-
margin-left: auto;
|
| 2253 |
-
border-radius: 4px;
|
| 2254 |
-
display: flex;
|
| 2255 |
-
align-items: center;
|
| 2256 |
-
justify-content: center;
|
| 2257 |
-
transition: all 0.2s ease;
|
| 2258 |
-
}
|
| 2259 |
-
|
| 2260 |
-
.task-detail-button:hover {
|
| 2261 |
-
color: #fff;
|
| 2262 |
-
background-color: rgba(255, 255, 255, 0.15);
|
| 2263 |
-
border-color: rgba(255, 255, 255, 0.3);
|
| 2264 |
-
}
|
| 2265 |
-
|
| 2266 |
-
.light-mode .task-detail-button {
|
| 2267 |
-
color: #666;
|
| 2268 |
-
border: 1px solid rgba(0, 0, 0, 0.1);
|
| 2269 |
-
}
|
| 2270 |
-
|
| 2271 |
-
.light-mode .task-detail-button:hover {
|
| 2272 |
-
color: #222;
|
| 2273 |
-
background-color: rgba(0, 0, 0, 0.08);
|
| 2274 |
-
border-color: rgba(0, 0, 0, 0.2);
|
| 2275 |
-
}
|
| 2276 |
-
|
| 2277 |
.task-name:hover {
|
| 2278 |
background-color: rgba(255, 255, 255, 0.1);
|
| 2279 |
text-decoration: none;
|
|
@@ -2330,7 +2299,6 @@ a:active {
|
|
| 2330 |
}
|
| 2331 |
|
| 2332 |
.tab.active {
|
| 2333 |
-
color: var(--color-primary);
|
| 2334 |
border-color: var(--color-border);
|
| 2335 |
box-shadow:
|
| 2336 |
0 -4px 8px -2px var(--color-border),
|
|
@@ -2341,7 +2309,6 @@ a:active {
|
|
| 2341 |
}
|
| 2342 |
|
| 2343 |
.light-mode .tab.active {
|
| 2344 |
-
color: var(--color-primary);
|
| 2345 |
box-shadow:
|
| 2346 |
0 -4px 8px -2px var(--color-border),
|
| 2347 |
4px 0 8px -2px var(--color-border),
|
|
@@ -2509,40 +2476,6 @@ a:active {
|
|
| 2509 |
overflow: auto;
|
| 2510 |
}
|
| 2511 |
|
| 2512 |
-
/* Settings Tabs */
|
| 2513 |
-
.settings-tabs {
|
| 2514 |
-
display: flex;
|
| 2515 |
-
border-bottom: 1px solid var(--color-border);
|
| 2516 |
-
margin-bottom: 24px;
|
| 2517 |
-
gap: 8px;
|
| 2518 |
-
overflow-x: auto;
|
| 2519 |
-
scrollbar-width: none;
|
| 2520 |
-
-ms-overflow-style: none;
|
| 2521 |
-
}
|
| 2522 |
-
|
| 2523 |
-
.settings-tabs::-webkit-scrollbar {
|
| 2524 |
-
display: none;
|
| 2525 |
-
}
|
| 2526 |
-
|
| 2527 |
-
.settings-tab {
|
| 2528 |
-
padding: 8px 16px;
|
| 2529 |
-
cursor: pointer;
|
| 2530 |
-
border-bottom: 3px solid transparent;
|
| 2531 |
-
color: var(--color-text-secondary);
|
| 2532 |
-
transition: all 0.2s ease;
|
| 2533 |
-
white-space: nowrap;
|
| 2534 |
-
}
|
| 2535 |
-
|
| 2536 |
-
.settings-tab:hover {
|
| 2537 |
-
color: var(--color-text);
|
| 2538 |
-
}
|
| 2539 |
-
|
| 2540 |
-
.settings-tab.active {
|
| 2541 |
-
border-bottom-color: var(--color-primary);
|
| 2542 |
-
color: var(--color-primary);
|
| 2543 |
-
font-weight: 500;
|
| 2544 |
-
}
|
| 2545 |
-
|
| 2546 |
/* Settings Sections */
|
| 2547 |
nav ul {
|
| 2548 |
display: grid;
|
|
|
|
| 2243 |
margin-left: 5px;
|
| 2244 |
}
|
| 2245 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2246 |
.task-name:hover {
|
| 2247 |
background-color: rgba(255, 255, 255, 0.1);
|
| 2248 |
text-decoration: none;
|
|
|
|
| 2299 |
}
|
| 2300 |
|
| 2301 |
.tab.active {
|
|
|
|
| 2302 |
border-color: var(--color-border);
|
| 2303 |
box-shadow:
|
| 2304 |
0 -4px 8px -2px var(--color-border),
|
|
|
|
| 2309 |
}
|
| 2310 |
|
| 2311 |
.light-mode .tab.active {
|
|
|
|
| 2312 |
box-shadow:
|
| 2313 |
0 -4px 8px -2px var(--color-border),
|
| 2314 |
4px 0 8px -2px var(--color-border),
|
|
|
|
| 2476 |
overflow: auto;
|
| 2477 |
}
|
| 2478 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2479 |
/* Settings Sections */
|
| 2480 |
nav ul {
|
| 2481 |
display: grid;
|
webui/index.html
CHANGED
|
@@ -13,6 +13,11 @@
|
|
| 13 |
<link rel="stylesheet" href="css/modals.css">
|
| 14 |
<link rel="stylesheet" href="css/speech.css">
|
| 15 |
<link rel="stylesheet" href="css/history.css">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
<script>
|
| 18 |
window.safeCall = function (name, ...args) {
|
|
@@ -20,6 +25,80 @@
|
|
| 20 |
}
|
| 21 |
</script>
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.14.3/dist/cdn.min.js"></script>
|
| 24 |
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.3/dist/cdn.min.js"></script>
|
| 25 |
|
|
@@ -33,15 +112,10 @@
|
|
| 33 |
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/contrib/auto-render.min.js"
|
| 34 |
crossorigin="anonymous"></script>
|
| 35 |
|
| 36 |
-
|
| 37 |
-
<script type="module" src="index.js"></script>
|
| 38 |
<script type="text/javascript" src="js/settings.js"></script>
|
| 39 |
<script type="text/javascript" src="js/file_browser.js"></script>
|
| 40 |
<script type="text/javascript" src="js/modal.js"></script>
|
| 41 |
-
<script type="text/javascript" src="js/scheduler.js"></script>
|
| 42 |
-
<script type="module" src="js/speech.js"></script>
|
| 43 |
-
<script type="module" src="js/history.js"></script>
|
| 44 |
-
|
| 45 |
</head>
|
| 46 |
|
| 47 |
<body>
|
|
@@ -106,7 +180,8 @@
|
|
| 106 |
<li>
|
| 107 |
<div :class="{'chat-list-button': true, 'font-bold': context.id === selected}"
|
| 108 |
@click="selected = context.id; selectChat(context.id)">
|
| 109 |
-
<span class="chat-name"
|
|
|
|
| 110 |
</div>
|
| 111 |
<button class="edit-button" @click="killChat(context.id)">X</button>
|
| 112 |
</li>
|
|
@@ -136,8 +211,8 @@
|
|
| 136 |
<!-- Task container with a vertical layout -->
|
| 137 |
<div class="task-container task-container-vertical">
|
| 138 |
<!-- Task name on its own line with full width -->
|
| 139 |
-
<span class="task-name"
|
| 140 |
-
x-text="task.
|
| 141 |
:data-task-id="task.id"></span>
|
| 142 |
<!-- Second line with status badge and action button -->
|
| 143 |
<div class="task-info-line">
|
|
@@ -145,15 +220,22 @@
|
|
| 145 |
<span class="scheduler-status-badge scheduler-status-badge-small"
|
| 146 |
:class="task.state ? `scheduler-status-${task.state}` : 'scheduler-status-idle'"
|
| 147 |
x-text="task.state || 'idle'"></span>
|
| 148 |
-
<!-- Action
|
| 149 |
-
<button class="
|
| 150 |
@click.stop="openTaskDetail(task.id)"
|
| 151 |
-
title="View task details">
|
| 152 |
-
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0
|
| 153 |
-
<path d="
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
</svg>
|
| 156 |
</button>
|
|
|
|
| 157 |
</div>
|
| 158 |
</div>
|
| 159 |
</div>
|
|
@@ -653,15 +735,16 @@
|
|
| 653 |
<input type="text" x-model="editingTask.name" placeholder="Enter task name">
|
| 654 |
</div>
|
| 655 |
|
| 656 |
-
<!-- Task Type
|
| 657 |
<div class="scheduler-form-field">
|
| 658 |
<div class="label-help-wrapper">
|
| 659 |
-
<label class="scheduler-form-label">
|
| 660 |
-
<div class="scheduler-form-help">Task
|
| 661 |
</div>
|
| 662 |
-
<select x-model="editingTask.type"
|
| 663 |
-
<option value="scheduled">Scheduled
|
| 664 |
-
<option value="adhoc">Ad-hoc
|
|
|
|
| 665 |
</select>
|
| 666 |
</div>
|
| 667 |
|
|
@@ -695,11 +778,11 @@
|
|
| 695 |
</div>
|
| 696 |
</div>
|
| 697 |
|
| 698 |
-
<!-- Schedule (for scheduled tasks) -->
|
| 699 |
-
<div class="scheduler-form-field full-width" x-show="editingTask
|
| 700 |
<div class="label-help-wrapper">
|
| 701 |
-
<label class="scheduler-form-label">Schedule
|
| 702 |
-
<div class="scheduler-form-help">
|
| 703 |
</div>
|
| 704 |
<div class="scheduler-schedule-builder">
|
| 705 |
<div class="scheduler-schedule-field">
|
|
@@ -725,6 +808,94 @@
|
|
| 725 |
</div>
|
| 726 |
</div>
|
| 727 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 728 |
<!-- Token (for ad-hoc tasks) -->
|
| 729 |
<div class="scheduler-form-field full-width" x-show="editingTask.type === 'adhoc'">
|
| 730 |
<div class="label-help-wrapper">
|
|
@@ -801,6 +972,7 @@
|
|
| 801 |
<select x-model="editingTask.type" disabled>
|
| 802 |
<option value="scheduled">Scheduled Task</option>
|
| 803 |
<option value="adhoc">Ad-hoc Task</option>
|
|
|
|
| 804 |
</select>
|
| 805 |
</div>
|
| 806 |
|
|
@@ -864,6 +1036,94 @@
|
|
| 864 |
</div>
|
| 865 |
</div>
|
| 866 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 867 |
<!-- Token (for ad-hoc tasks) -->
|
| 868 |
<div class="scheduler-form-field full-width" x-show="editingTask.type === 'adhoc'">
|
| 869 |
<div class="label-help-wrapper">
|
|
@@ -927,6 +1187,7 @@
|
|
| 927 |
<option value="all">All Types</option>
|
| 928 |
<option value="scheduled">Scheduled</option>
|
| 929 |
<option value="adhoc">Ad-hoc</option>
|
|
|
|
| 930 |
</select>
|
| 931 |
</div>
|
| 932 |
|
|
@@ -943,19 +1204,23 @@
|
|
| 943 |
</div>
|
| 944 |
|
| 945 |
<!-- Loading State -->
|
| 946 |
-
<div class="scheduler-loading" x-show="isLoading">
|
| 947 |
Loading tasks...
|
| 948 |
-
</div>
|
| 949 |
|
| 950 |
<!-- Empty State -->
|
| 951 |
-
<div class="scheduler-empty"
|
|
|
|
|
|
|
| 952 |
<div class="scheduler-empty-icon">📋</div>
|
| 953 |
<div class="scheduler-empty-text">No tasks found</div>
|
| 954 |
<button class="btn btn-ok" @click="startCreateTask()">Create your first task</button>
|
| 955 |
</div>
|
| 956 |
|
| 957 |
<!-- Task List Table -->
|
| 958 |
-
<table class="scheduler-task-list"
|
|
|
|
|
|
|
| 959 |
<thead>
|
| 960 |
<tr>
|
| 961 |
<th @click="changeSort('name')">
|
|
@@ -991,6 +1256,7 @@
|
|
| 991 |
<td>
|
| 992 |
<span x-show="task.type === 'scheduled'" x-text="formatSchedule(task)"></span>
|
| 993 |
<span x-show="task.type === 'adhoc'" class="scheduler-no-schedule">—</span>
|
|
|
|
| 994 |
</td>
|
| 995 |
<td x-text="formatDate(task.last_run)"></td>
|
| 996 |
<td @click.stop>
|
|
@@ -1054,6 +1320,38 @@
|
|
| 1054 |
<div class="scheduler-details-label" x-show="selectedTaskForDetail && selectedTaskForDetail.type === 'adhoc'">Token:</div>
|
| 1055 |
<div class="scheduler-details-value" x-show="selectedTaskForDetail && selectedTaskForDetail.type === 'adhoc'" x-text="selectedTaskForDetail ? selectedTaskForDetail.token : ''"></div>
|
| 1056 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1057 |
<div class="scheduler-details-label">Last Result:</div>
|
| 1058 |
<div class="scheduler-details-value" x-text="selectedTaskForDetail && selectedTaskForDetail.last_result ? selectedTaskForDetail.last_result : 'No results yet'"></div>
|
| 1059 |
|
|
|
|
| 13 |
<link rel="stylesheet" href="css/modals.css">
|
| 14 |
<link rel="stylesheet" href="css/speech.css">
|
| 15 |
<link rel="stylesheet" href="css/history.css">
|
| 16 |
+
<link rel="stylesheet" href="css/scheduler-datepicker.css">
|
| 17 |
+
|
| 18 |
+
<!-- Flatpickr for datetime picker -->
|
| 19 |
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
|
| 20 |
+
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
| 21 |
|
| 22 |
<script>
|
| 23 |
window.safeCall = function (name, ...args) {
|
|
|
|
| 25 |
}
|
| 26 |
</script>
|
| 27 |
|
| 28 |
+
<!-- Pre-initialize schedulerSettings to ensure Alpine doesn't miss it -->
|
| 29 |
+
<script>
|
| 30 |
+
// Pre-define schedulerSettings skeleton to ensure it's available to Alpine
|
| 31 |
+
window.schedulerSettings = function() {
|
| 32 |
+
return {
|
| 33 |
+
tasks: [],
|
| 34 |
+
isLoading: true,
|
| 35 |
+
selectedTask: null,
|
| 36 |
+
expandedTaskId: null,
|
| 37 |
+
sortField: 'name',
|
| 38 |
+
sortDirection: 'asc',
|
| 39 |
+
filterType: 'all',
|
| 40 |
+
filterState: 'all',
|
| 41 |
+
pollingInterval: null,
|
| 42 |
+
pollingActive: false,
|
| 43 |
+
editingTask: {
|
| 44 |
+
name: '',
|
| 45 |
+
type: 'scheduled',
|
| 46 |
+
state: 'idle',
|
| 47 |
+
schedule: {
|
| 48 |
+
minute: '*',
|
| 49 |
+
hour: '*',
|
| 50 |
+
day: '*',
|
| 51 |
+
month: '*',
|
| 52 |
+
weekday: '*',
|
| 53 |
+
timezone: ''
|
| 54 |
+
},
|
| 55 |
+
token: '',
|
| 56 |
+
plan: {
|
| 57 |
+
todo: [],
|
| 58 |
+
in_progress: null,
|
| 59 |
+
done: []
|
| 60 |
+
},
|
| 61 |
+
system_prompt: '',
|
| 62 |
+
prompt: '',
|
| 63 |
+
attachments: []
|
| 64 |
+
},
|
| 65 |
+
isCreating: false,
|
| 66 |
+
isEditing: false,
|
| 67 |
+
showLoadingState: false,
|
| 68 |
+
viewMode: 'list',
|
| 69 |
+
selectedTaskForDetail: null,
|
| 70 |
+
attachmentsText: '',
|
| 71 |
+
filteredTasks: [],
|
| 72 |
+
// Minimal init to avoid errors
|
| 73 |
+
init() {
|
| 74 |
+
console.log('Basic schedulerSettings initialized');
|
| 75 |
+
// Watch for task type changes
|
| 76 |
+
this.$watch('editingTask.type', (newType) => {
|
| 77 |
+
if (newType === 'planned') {
|
| 78 |
+
// When switching to planned task type, initialize the datetime picker
|
| 79 |
+
this.$nextTick(() => {
|
| 80 |
+
if (this.initFlatpickr) {
|
| 81 |
+
if (this.isCreating) {
|
| 82 |
+
this.initFlatpickr('create');
|
| 83 |
+
} else if (this.isEditing) {
|
| 84 |
+
this.initFlatpickr('edit');
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
});
|
| 88 |
+
}
|
| 89 |
+
});
|
| 90 |
+
}
|
| 91 |
+
};
|
| 92 |
+
};
|
| 93 |
+
</script>
|
| 94 |
+
|
| 95 |
+
<!-- Load module scripts first -->
|
| 96 |
+
<script type="module" src="js/scheduler.js"></script>
|
| 97 |
+
<script type="module" src="js/speech.js"></script>
|
| 98 |
+
<script type="module" src="js/history.js"></script>
|
| 99 |
+
<script type="module" src="index.js"></script>
|
| 100 |
+
|
| 101 |
+
<!-- Then load Alpine.js -->
|
| 102 |
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.14.3/dist/cdn.min.js"></script>
|
| 103 |
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.3/dist/cdn.min.js"></script>
|
| 104 |
|
|
|
|
| 112 |
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/contrib/auto-render.min.js"
|
| 113 |
crossorigin="anonymous"></script>
|
| 114 |
|
| 115 |
+
<!-- Non-module scripts after Alpine.js -->
|
|
|
|
| 116 |
<script type="text/javascript" src="js/settings.js"></script>
|
| 117 |
<script type="text/javascript" src="js/file_browser.js"></script>
|
| 118 |
<script type="text/javascript" src="js/modal.js"></script>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
</head>
|
| 120 |
|
| 121 |
<body>
|
|
|
|
| 180 |
<li>
|
| 181 |
<div :class="{'chat-list-button': true, 'font-bold': context.id === selected}"
|
| 182 |
@click="selected = context.id; selectChat(context.id)">
|
| 183 |
+
<span class="chat-name" :title="context.name ? context.name : 'Chat #' + context.no"
|
| 184 |
+
x-text="context.name ? context.name : 'Chat #' + context.no"></span>
|
| 185 |
</div>
|
| 186 |
<button class="edit-button" @click="killChat(context.id)">X</button>
|
| 187 |
</li>
|
|
|
|
| 211 |
<!-- Task container with a vertical layout -->
|
| 212 |
<div class="task-container task-container-vertical">
|
| 213 |
<!-- Task name on its own line with full width -->
|
| 214 |
+
<span class="task-name" :title="(task.task_name || `Task #${task.no}`) + ' (' + (task.name || `Chat #${task.no}`) + ')'"
|
| 215 |
+
x-text="(task.task_name || `Task #${task.no}`) + ' (' + (task.name || `Chat #${task.no}`) + ')'"
|
| 216 |
:data-task-id="task.id"></span>
|
| 217 |
<!-- Second line with status badge and action button -->
|
| 218 |
<div class="task-info-line">
|
|
|
|
| 220 |
<span class="scheduler-status-badge scheduler-status-badge-small"
|
| 221 |
:class="task.state ? `scheduler-status-${task.state}` : 'scheduler-status-idle'"
|
| 222 |
x-text="task.state || 'idle'"></span>
|
| 223 |
+
<!-- Action buttons -->
|
| 224 |
+
<button class="edit-button"
|
| 225 |
@click.stop="openTaskDetail(task.id)"
|
| 226 |
+
title="View task details" style="margin-left: auto; margin-right: 5px;">
|
| 227 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 512 512" fill="var(--color-primary)" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 228 |
+
<path d="M256 0c70.69 0 134.69 28.66 181.02 74.98C483.34 121.3 512 185.31 512 256c0 70.69-28.66 134.7-74.98 181.02C390.69 483.34 326.69 512 256 512c-70.69 0-134.69-28.66-181.02-74.98C28.66 390.69 0 326.69 0 256c0-70.69 28.66-134.69 74.98-181.02C121.31 28.66 185.31 0 256 0zm-9.96 161.03c0-4.28.76-8.26 2.27-11.91 1.5-3.63 3.77-6.94 6.79-9.91 3-2.95 6.29-5.2 9.84-6.7 3.57-1.5 7.41-2.28 11.52-2.28 4.12 0 7.96.78 11.49 2.27 3.54 1.51 6.78 3.76 9.75 6.73 2.95 2.97 5.16 6.26 6.64 9.91 1.49 3.63 2.22 7.61 2.22 11.89 0 4.17-.73 8.08-2.21 11.69-1.48 3.6-3.68 6.94-6.65 9.97-2.94 3.03-6.18 5.32-9.72 6.84-3.54 1.51-7.38 2.29-11.52 2.29-4.22 0-8.14-.76-11.75-2.26-3.58-1.51-6.86-3.79-9.83-6.79-2.94-3.02-5.16-6.34-6.63-9.97-1.48-3.62-2.21-7.54-2.21-11.77zm13.4 178.16c-1.11 3.97-3.35 11.76 3.3 11.76 1.44 0 3.27-.81 5.46-2.4 2.37-1.71 5.09-4.31 8.13-7.75 3.09-3.5 6.32-7.65 9.67-12.42 3.33-4.76 6.84-10.22 10.49-16.31.37-.65 1.23-.87 1.89-.48l12.36 9.18c.6.43.73 1.25.35 1.86-5.69 9.88-11.44 18.51-17.26 25.88-5.85 7.41-11.79 13.57-17.8 18.43l-.1.06c-6.02 4.88-12.19 8.55-18.51 11.01-17.58 6.81-45.36 5.7-53.32-14.83-5.02-12.96-.9-27.69 3.06-40.37l19.96-60.44c1.28-4.58 2.89-9.62 3.47-14.33.97-7.87-2.49-12.96-11.06-12.96h-17.45c-.76 0-1.38-.62-1.38-1.38l.08-.48 4.58-16.68c.16-.62.73-1.04 1.35-1.02l89.12-2.79c.76-.03 1.41.57 1.44 1.33l-.07.43-37.76 124.7zm158.3-244.93c-41.39-41.39-98.58-67-161.74-67-63.16 0-120.35 25.61-161.74 67-41.39 41.39-67 98.58-67 161.74 0 63.16 25.61 120.35 67 161.74 41.39 41.39 98.58 67 161.74 67 63.16 0 120.35-25.61 161.74-67 41.39-41.39 67-98.58 67-161.74 0-63.16-25.61-120.35-67-161.74z"/>
|
| 229 |
+
</svg>
|
| 230 |
+
</button>
|
| 231 |
+
<button class="edit-button"
|
| 232 |
+
@click.stop="resetChat(task.id)"
|
| 233 |
+
title="Reset chat">
|
| 234 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 122.88 121.1" fill="var(--color-primary)" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 235 |
+
<path d="M62.89,56.03c1.11-0.35,2.34-0.25,3.72,0.37l10.4,7.87c2.26,1.71,4.24,3.78,2.73,6.9 c-0.51,1.06-1.4,2.1-2.38,3.49l-0.53,0.75c-1.97,2.8-2.61,2-5.71,1.83c0.56,13.37,1.75,27.82-2.64,40.88 c-0.87,2.7-3.32,3.44-6.95,2.71l-6.1-2.03c4.11-6.14,6.16-13.85,6.44-22.89c-3.46,8.58-6.8,16.96-10.68,20.86l-6.28-2.08 c0.61-3.05,1.05-5.43,0.35-6.9l-4.07,4.24l-9.33-5.77c6.36-3.36,11.62-7.87,15.6-13.73c-6.69,5.01-12.76,8.1-18.14,8.99 c-2.75,0.83-4.49,0.35-5.16-1.53c-0.48-1.34-0.05-1.77,0.81-2.86c1.11-1.41,2.61-2.67,4.35-3.79c-3.13,1.1-4.64,0.95-6.37,1.51 c-4.9,1.59-9.94-1.86-8.26-6.9c1.07-3.23,3.54-3.09,6.67-4.07l5.42-1.69c-5.19,0.28-10.32,0.45-15.02-0.25 c-5.4-0.8-5.31-0.99-8.24-5.38c-3.94-5.91-6.25-11.45,2.52-9.16c16.73,3.18,33.56,5.34,51.25-0.98c-0.76-1.32-0.9-2.57-0.5-3.73 C57.37,60.94,61.13,56.58,62.89,56.03L62.89,56.03z M113.8,2.42L74.45,51.53c-4.71,6.68,3.2,11.91,8.39,5.64l39.2-49.27 C125.12,1.86,119.13-3.16,113.8,2.42L113.8,2.42z"/>
|
| 236 |
</svg>
|
| 237 |
</button>
|
| 238 |
+
|
| 239 |
</div>
|
| 240 |
</div>
|
| 241 |
</div>
|
|
|
|
| 735 |
<input type="text" x-model="editingTask.name" placeholder="Enter task name">
|
| 736 |
</div>
|
| 737 |
|
| 738 |
+
<!-- Task Type Selection -->
|
| 739 |
<div class="scheduler-form-field">
|
| 740 |
<div class="label-help-wrapper">
|
| 741 |
+
<label class="scheduler-form-label">Type</label>
|
| 742 |
+
<div class="scheduler-form-help">Task execution method</div>
|
| 743 |
</div>
|
| 744 |
+
<select x-model="editingTask.type">
|
| 745 |
+
<option value="scheduled">Scheduled (Cron)</option>
|
| 746 |
+
<option value="adhoc">Ad-hoc (Manual)</option>
|
| 747 |
+
<option value="planned">Planned (Specific Times)</option>
|
| 748 |
</select>
|
| 749 |
</div>
|
| 750 |
|
|
|
|
| 778 |
</div>
|
| 779 |
</div>
|
| 780 |
|
| 781 |
+
<!-- Schedule (only for scheduled tasks) -->
|
| 782 |
+
<div class="scheduler-form-field full-width" x-show="editingTask.type === 'scheduled'">
|
| 783 |
<div class="label-help-wrapper">
|
| 784 |
+
<label class="scheduler-form-label">Schedule</label>
|
| 785 |
+
<div class="scheduler-form-help">Cron schedule for automated execution (minute hour day month weekday)</div>
|
| 786 |
</div>
|
| 787 |
<div class="scheduler-schedule-builder">
|
| 788 |
<div class="scheduler-schedule-field">
|
|
|
|
| 808 |
</div>
|
| 809 |
</div>
|
| 810 |
|
| 811 |
+
<!-- Plan (for planned tasks) -->
|
| 812 |
+
<div class="scheduler-form-field full-width" x-show="editingTask.type === 'planned'">
|
| 813 |
+
<div class="label-help-wrapper">
|
| 814 |
+
<label class="scheduler-form-label">Plan</label>
|
| 815 |
+
<div class="scheduler-form-help">Specific execution times for this task</div>
|
| 816 |
+
</div>
|
| 817 |
+
<div class="scheduler-plan-builder">
|
| 818 |
+
<div class="scheduler-plan-todo">
|
| 819 |
+
<span class="scheduler-plan-label">Upcoming Executions</span>
|
| 820 |
+
<div class="scheduler-todo-list">
|
| 821 |
+
<template x-if="editingTask.plan && Array.isArray(editingTask.plan.todo) && editingTask.plan.todo.length > 0">
|
| 822 |
+
<template x-for="(time, index) in editingTask.plan.todo" :key="index">
|
| 823 |
+
<div class="scheduler-todo-item">
|
| 824 |
+
<span x-text="formatDate(time)"></span>
|
| 825 |
+
<button @click.prevent="editingTask.plan.todo.splice(index, 1)" class="scheduler-todo-remove">
|
| 826 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 827 |
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
| 828 |
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
| 829 |
+
</svg>
|
| 830 |
+
</button>
|
| 831 |
+
</div>
|
| 832 |
+
</template>
|
| 833 |
+
</template>
|
| 834 |
+
<template x-if="!editingTask.plan || !Array.isArray(editingTask.plan.todo) || editingTask.plan.todo.length === 0">
|
| 835 |
+
<div class="scheduler-empty-plan">
|
| 836 |
+
No scheduled execution times yet. Add one below.
|
| 837 |
+
</div>
|
| 838 |
+
</template>
|
| 839 |
+
<div class="scheduler-add-todo">
|
| 840 |
+
<!-- Create form planned task input -->
|
| 841 |
+
<input type="text" id="newPlannedTime-create"
|
| 842 |
+
x-ref="plannedTimeCreate"
|
| 843 |
+
class="scheduler-flatpickr-input"
|
| 844 |
+
placeholder="Select date and time">
|
| 845 |
+
<!-- Create Task Form Add Time Button -->
|
| 846 |
+
<button @click.prevent="
|
| 847 |
+
const input = $refs.plannedTimeCreate;
|
| 848 |
+
if (!input) {
|
| 849 |
+
console.error('Input reference not found for plannedTimeCreate');
|
| 850 |
+
return;
|
| 851 |
+
}
|
| 852 |
+
|
| 853 |
+
// Ensure plan structure exists
|
| 854 |
+
if (!editingTask.plan) {
|
| 855 |
+
editingTask.plan = { todo: [], in_progress: null, done: [] };
|
| 856 |
+
}
|
| 857 |
+
if (!Array.isArray(editingTask.plan.todo)) {
|
| 858 |
+
editingTask.plan.todo = [];
|
| 859 |
+
}
|
| 860 |
+
|
| 861 |
+
// Get date from Flatpickr if available
|
| 862 |
+
let selectedDate;
|
| 863 |
+
if (input._flatpickr && input._flatpickr.selectedDates.length > 0) {
|
| 864 |
+
selectedDate = input._flatpickr.selectedDates[0];
|
| 865 |
+
} else if (input.value) {
|
| 866 |
+
selectedDate = new Date(input.value);
|
| 867 |
+
}
|
| 868 |
+
|
| 869 |
+
if (!selectedDate || isNaN(selectedDate.getTime())) {
|
| 870 |
+
alert('Please select a valid date and time');
|
| 871 |
+
return;
|
| 872 |
+
}
|
| 873 |
+
|
| 874 |
+
// Convert to ISO string and add to plan
|
| 875 |
+
editingTask.plan.todo.push(selectedDate.toISOString());
|
| 876 |
+
|
| 877 |
+
// Sort by date (earliest first)
|
| 878 |
+
editingTask.plan.todo.sort();
|
| 879 |
+
|
| 880 |
+
// Clear the input
|
| 881 |
+
if (input._flatpickr) {
|
| 882 |
+
input._flatpickr.clear();
|
| 883 |
+
} else {
|
| 884 |
+
input.value = '';
|
| 885 |
+
}
|
| 886 |
+
" class="scheduler-add-todo-button">
|
| 887 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 4px;">
|
| 888 |
+
<line x1="12" y1="5" x2="12" y2="19"></line>
|
| 889 |
+
<line x1="5" y1="12" x2="19" y2="12"></line>
|
| 890 |
+
</svg>
|
| 891 |
+
Add Time
|
| 892 |
+
</button>
|
| 893 |
+
</div>
|
| 894 |
+
</div>
|
| 895 |
+
</div>
|
| 896 |
+
</div>
|
| 897 |
+
</div>
|
| 898 |
+
|
| 899 |
<!-- Token (for ad-hoc tasks) -->
|
| 900 |
<div class="scheduler-form-field full-width" x-show="editingTask.type === 'adhoc'">
|
| 901 |
<div class="label-help-wrapper">
|
|
|
|
| 972 |
<select x-model="editingTask.type" disabled>
|
| 973 |
<option value="scheduled">Scheduled Task</option>
|
| 974 |
<option value="adhoc">Ad-hoc Task</option>
|
| 975 |
+
<option value="planned">Planned Task</option>
|
| 976 |
</select>
|
| 977 |
</div>
|
| 978 |
|
|
|
|
| 1036 |
</div>
|
| 1037 |
</div>
|
| 1038 |
|
| 1039 |
+
<!-- Plan (for planned tasks) -->
|
| 1040 |
+
<div class="scheduler-form-field full-width" x-show="editingTask.type === 'planned'">
|
| 1041 |
+
<div class="label-help-wrapper">
|
| 1042 |
+
<label class="scheduler-form-label">Plan</label>
|
| 1043 |
+
<div class="scheduler-form-help">Specific execution times for this task</div>
|
| 1044 |
+
</div>
|
| 1045 |
+
<div class="scheduler-plan-builder">
|
| 1046 |
+
<div class="scheduler-plan-todo">
|
| 1047 |
+
<span class="scheduler-plan-label">Upcoming Executions</span>
|
| 1048 |
+
<div class="scheduler-todo-list">
|
| 1049 |
+
<template x-if="editingTask.plan && Array.isArray(editingTask.plan.todo) && editingTask.plan.todo.length > 0">
|
| 1050 |
+
<template x-for="(time, index) in editingTask.plan.todo" :key="index">
|
| 1051 |
+
<div class="scheduler-todo-item">
|
| 1052 |
+
<span x-text="formatDate(time)"></span>
|
| 1053 |
+
<button @click.prevent="editingTask.plan.todo.splice(index, 1)" class="scheduler-todo-remove">
|
| 1054 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 1055 |
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
| 1056 |
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
| 1057 |
+
</svg>
|
| 1058 |
+
</button>
|
| 1059 |
+
</div>
|
| 1060 |
+
</template>
|
| 1061 |
+
</template>
|
| 1062 |
+
<template x-if="!editingTask.plan || !Array.isArray(editingTask.plan.todo) || editingTask.plan.todo.length === 0">
|
| 1063 |
+
<div class="scheduler-empty-plan">
|
| 1064 |
+
No scheduled execution times yet. Add one below.
|
| 1065 |
+
</div>
|
| 1066 |
+
</template>
|
| 1067 |
+
<div class="scheduler-add-todo">
|
| 1068 |
+
<!-- Edit form planned task input -->
|
| 1069 |
+
<input type="text" id="newPlannedTime-edit"
|
| 1070 |
+
x-ref="plannedTimeEdit"
|
| 1071 |
+
class="scheduler-flatpickr-input"
|
| 1072 |
+
placeholder="Select date and time">
|
| 1073 |
+
<!-- Edit Task Form Add Time Button -->
|
| 1074 |
+
<button @click.prevent="
|
| 1075 |
+
const input = $refs.plannedTimeEdit;
|
| 1076 |
+
if (!input) {
|
| 1077 |
+
console.error('Input reference not found for plannedTimeEdit');
|
| 1078 |
+
return;
|
| 1079 |
+
}
|
| 1080 |
+
|
| 1081 |
+
// Ensure plan structure exists
|
| 1082 |
+
if (!editingTask.plan) {
|
| 1083 |
+
editingTask.plan = { todo: [], in_progress: null, done: [] };
|
| 1084 |
+
}
|
| 1085 |
+
if (!Array.isArray(editingTask.plan.todo)) {
|
| 1086 |
+
editingTask.plan.todo = [];
|
| 1087 |
+
}
|
| 1088 |
+
|
| 1089 |
+
// Get date from Flatpickr if available
|
| 1090 |
+
let selectedDate;
|
| 1091 |
+
if (input._flatpickr && input._flatpickr.selectedDates.length > 0) {
|
| 1092 |
+
selectedDate = input._flatpickr.selectedDates[0];
|
| 1093 |
+
} else if (input.value) {
|
| 1094 |
+
selectedDate = new Date(input.value);
|
| 1095 |
+
}
|
| 1096 |
+
|
| 1097 |
+
if (!selectedDate || isNaN(selectedDate.getTime())) {
|
| 1098 |
+
alert('Please select a valid date and time');
|
| 1099 |
+
return;
|
| 1100 |
+
}
|
| 1101 |
+
|
| 1102 |
+
// Convert to ISO string and add to plan
|
| 1103 |
+
editingTask.plan.todo.push(selectedDate.toISOString());
|
| 1104 |
+
|
| 1105 |
+
// Sort by date (earliest first)
|
| 1106 |
+
editingTask.plan.todo.sort();
|
| 1107 |
+
|
| 1108 |
+
// Clear the input
|
| 1109 |
+
if (input._flatpickr) {
|
| 1110 |
+
input._flatpickr.clear();
|
| 1111 |
+
} else {
|
| 1112 |
+
input.value = '';
|
| 1113 |
+
}
|
| 1114 |
+
" class="scheduler-add-todo-button">
|
| 1115 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 4px;">
|
| 1116 |
+
<line x1="12" y1="5" x2="12" y2="19"></line>
|
| 1117 |
+
<line x1="5" y1="12" x2="19" y2="12"></line>
|
| 1118 |
+
</svg>
|
| 1119 |
+
Add Time
|
| 1120 |
+
</button>
|
| 1121 |
+
</div>
|
| 1122 |
+
</div>
|
| 1123 |
+
</div>
|
| 1124 |
+
</div>
|
| 1125 |
+
</div>
|
| 1126 |
+
|
| 1127 |
<!-- Token (for ad-hoc tasks) -->
|
| 1128 |
<div class="scheduler-form-field full-width" x-show="editingTask.type === 'adhoc'">
|
| 1129 |
<div class="label-help-wrapper">
|
|
|
|
| 1187 |
<option value="all">All Types</option>
|
| 1188 |
<option value="scheduled">Scheduled</option>
|
| 1189 |
<option value="adhoc">Ad-hoc</option>
|
| 1190 |
+
<option value="planned">Planned</option>
|
| 1191 |
</select>
|
| 1192 |
</div>
|
| 1193 |
|
|
|
|
| 1204 |
</div>
|
| 1205 |
|
| 1206 |
<!-- Loading State -->
|
| 1207 |
+
<!-- <div class="scheduler-loading" x-show="isLoading">
|
| 1208 |
Loading tasks...
|
| 1209 |
+
</div> -->
|
| 1210 |
|
| 1211 |
<!-- Empty State -->
|
| 1212 |
+
<div class="scheduler-empty"
|
| 1213 |
+
x-show="!isLoading && filteredTasks.length === 0"
|
| 1214 |
+
x-effect="$el.style.display = (!isLoading && filteredTasks.length === 0) ? '' : 'none'">
|
| 1215 |
<div class="scheduler-empty-icon">📋</div>
|
| 1216 |
<div class="scheduler-empty-text">No tasks found</div>
|
| 1217 |
<button class="btn btn-ok" @click="startCreateTask()">Create your first task</button>
|
| 1218 |
</div>
|
| 1219 |
|
| 1220 |
<!-- Task List Table -->
|
| 1221 |
+
<table class="scheduler-task-list"
|
| 1222 |
+
x-show="!isLoading && filteredTasks.length > 0"
|
| 1223 |
+
x-effect="$el.style.display = (!isLoading && filteredTasks.length > 0) ? '' : 'none'">
|
| 1224 |
<thead>
|
| 1225 |
<tr>
|
| 1226 |
<th @click="changeSort('name')">
|
|
|
|
| 1256 |
<td>
|
| 1257 |
<span x-show="task.type === 'scheduled'" x-text="formatSchedule(task)"></span>
|
| 1258 |
<span x-show="task.type === 'adhoc'" class="scheduler-no-schedule">—</span>
|
| 1259 |
+
<span x-show="task.type === 'planned'" x-html="formatPlan(task).replace(/\n/g, '<br>')"></span>
|
| 1260 |
</td>
|
| 1261 |
<td x-text="formatDate(task.last_run)"></td>
|
| 1262 |
<td @click.stop>
|
|
|
|
| 1320 |
<div class="scheduler-details-label" x-show="selectedTaskForDetail && selectedTaskForDetail.type === 'adhoc'">Token:</div>
|
| 1321 |
<div class="scheduler-details-value" x-show="selectedTaskForDetail && selectedTaskForDetail.type === 'adhoc'" x-text="selectedTaskForDetail ? selectedTaskForDetail.token : ''"></div>
|
| 1322 |
|
| 1323 |
+
<div class="scheduler-details-label" x-show="selectedTaskForDetail && selectedTaskForDetail.type === 'planned'">Plan:</div>
|
| 1324 |
+
<div class="scheduler-details-value" x-show="selectedTaskForDetail && selectedTaskForDetail.type === 'planned'">
|
| 1325 |
+
<div x-show="selectedTaskForDetail && selectedTaskForDetail.plan">
|
| 1326 |
+
<div><strong>Upcoming:</strong></div>
|
| 1327 |
+
<template x-if="selectedTaskForDetail && selectedTaskForDetail.plan && selectedTaskForDetail.plan.todo && selectedTaskForDetail.plan.todo.length > 0">
|
| 1328 |
+
<div>
|
| 1329 |
+
<template x-for="(time, index) in selectedTaskForDetail.plan.todo" :key="index">
|
| 1330 |
+
<div x-text="formatDate(time)"></div>
|
| 1331 |
+
</template>
|
| 1332 |
+
</div>
|
| 1333 |
+
</template>
|
| 1334 |
+
<template x-if="!selectedTaskForDetail || !selectedTaskForDetail.plan || !selectedTaskForDetail.plan.todo || selectedTaskForDetail.plan.todo.length === 0">
|
| 1335 |
+
<div>No upcoming executions</div>
|
| 1336 |
+
</template>
|
| 1337 |
+
|
| 1338 |
+
<div><strong>In Progress:</strong></div>
|
| 1339 |
+
<div x-text="selectedTaskForDetail && selectedTaskForDetail.plan && selectedTaskForDetail.plan.in_progress ? formatDate(selectedTaskForDetail.plan.in_progress) : 'None'"></div>
|
| 1340 |
+
|
| 1341 |
+
<div><strong>Completed:</strong></div>
|
| 1342 |
+
<template x-if="selectedTaskForDetail && selectedTaskForDetail.plan && selectedTaskForDetail.plan.done && selectedTaskForDetail.plan.done.length > 0">
|
| 1343 |
+
<div>
|
| 1344 |
+
<template x-for="(time, index) in selectedTaskForDetail.plan.done" :key="index">
|
| 1345 |
+
<div x-text="formatDate(time)"></div>
|
| 1346 |
+
</template>
|
| 1347 |
+
</div>
|
| 1348 |
+
</template>
|
| 1349 |
+
<template x-if="!selectedTaskForDetail || !selectedTaskForDetail.plan || !selectedTaskForDetail.plan.done || selectedTaskForDetail.plan.done.length === 0">
|
| 1350 |
+
<div>No completed executions</div>
|
| 1351 |
+
</template>
|
| 1352 |
+
</div>
|
| 1353 |
+
</div>
|
| 1354 |
+
|
| 1355 |
<div class="scheduler-details-label">Last Result:</div>
|
| 1356 |
<div class="scheduler-details-value" x-text="selectedTaskForDetail && selectedTaskForDetail.last_result ? selectedTaskForDetail.last_result : 'No results yet'"></div>
|
| 1357 |
|
webui/index.js
CHANGED
|
@@ -342,7 +342,17 @@ let lastSpokenNo = 0
|
|
| 342 |
async function poll() {
|
| 343 |
let updated = false
|
| 344 |
try {
|
| 345 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
|
| 347 |
// Check if the response is valid
|
| 348 |
if (!response) {
|
|
@@ -421,7 +431,13 @@ async function poll() {
|
|
| 421 |
|
| 422 |
// If it doesn't exist in the chats list but we're in chats tab, try to select the first chat
|
| 423 |
if (!contextExists && contexts.length > 0) {
|
|
|
|
|
|
|
|
|
|
| 424 |
const firstChatId = contexts[0].id;
|
|
|
|
|
|
|
|
|
|
| 425 |
setContext(firstChatId);
|
| 426 |
chatsAD.selected = firstChatId;
|
| 427 |
localStorage.setItem('lastSelectedChat', firstChatId);
|
|
@@ -454,11 +470,18 @@ async function poll() {
|
|
| 454 |
} else if (contexts.length > 0 && localStorage.getItem('activeTab') === 'chats') {
|
| 455 |
// If we're in chats tab with no selection but have chats, select the first one
|
| 456 |
const firstChatId = contexts[0].id;
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 460 |
}
|
| 461 |
|
|
|
|
|
|
|
|
|
|
| 462 |
} catch (error) {
|
| 463 |
console.error('Error:', error);
|
| 464 |
setConnectionStatus(false)
|
|
@@ -509,12 +532,12 @@ window.pauseAgent = async function (paused) {
|
|
| 509 |
}
|
| 510 |
}
|
| 511 |
|
| 512 |
-
window.resetChat = async function () {
|
| 513 |
try {
|
| 514 |
-
const resp = await sendJsonData("/chat_reset", { context });
|
| 515 |
-
updateAfterScroll()
|
| 516 |
} catch (e) {
|
| 517 |
-
window.toastFetchError("Error resetting chat", e)
|
| 518 |
}
|
| 519 |
}
|
| 520 |
|
|
@@ -1193,10 +1216,20 @@ function activateTab(tabName) {
|
|
| 1193 |
chatsTab.classList.add('active');
|
| 1194 |
chatsSection.style.display = '';
|
| 1195 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1196 |
// Restore previous chat selection
|
| 1197 |
const lastSelectedChat = localStorage.getItem('lastSelectedChat');
|
| 1198 |
-
|
| 1199 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1200 |
setContext(lastSelectedChat);
|
| 1201 |
}
|
| 1202 |
} else if (tabName === 'tasks') {
|
|
@@ -1204,10 +1237,20 @@ function activateTab(tabName) {
|
|
| 1204 |
tasksSection.style.display = 'flex';
|
| 1205 |
tasksSection.style.flexDirection = 'column';
|
| 1206 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1207 |
// Restore previous task selection
|
| 1208 |
const lastSelectedTask = localStorage.getItem('lastSelectedTask');
|
| 1209 |
-
|
| 1210 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1211 |
setContext(lastSelectedTask);
|
| 1212 |
}
|
| 1213 |
}
|
|
|
|
| 342 |
async function poll() {
|
| 343 |
let updated = false
|
| 344 |
try {
|
| 345 |
+
// Get timezone from navigator
|
| 346 |
+
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
| 347 |
+
|
| 348 |
+
const response = await sendJsonData(
|
| 349 |
+
"/poll",
|
| 350 |
+
{
|
| 351 |
+
log_from: lastLogVersion,
|
| 352 |
+
context: context || null,
|
| 353 |
+
timezone: timezone
|
| 354 |
+
}
|
| 355 |
+
);
|
| 356 |
|
| 357 |
// Check if the response is valid
|
| 358 |
if (!response) {
|
|
|
|
| 431 |
|
| 432 |
// If it doesn't exist in the chats list but we're in chats tab, try to select the first chat
|
| 433 |
if (!contextExists && contexts.length > 0) {
|
| 434 |
+
// Check if the current context is empty before creating a new one
|
| 435 |
+
// If there's already a current context and we're just updating UI, don't automatically
|
| 436 |
+
// create a new context by calling setContext
|
| 437 |
const firstChatId = contexts[0].id;
|
| 438 |
+
|
| 439 |
+
// Only create a new context if we're not currently in an existing context
|
| 440 |
+
// This helps prevent duplicate contexts when switching tabs
|
| 441 |
setContext(firstChatId);
|
| 442 |
chatsAD.selected = firstChatId;
|
| 443 |
localStorage.setItem('lastSelectedChat', firstChatId);
|
|
|
|
| 470 |
} else if (contexts.length > 0 && localStorage.getItem('activeTab') === 'chats') {
|
| 471 |
// If we're in chats tab with no selection but have chats, select the first one
|
| 472 |
const firstChatId = contexts[0].id;
|
| 473 |
+
|
| 474 |
+
// Only set context if we don't already have one to avoid duplicates
|
| 475 |
+
if (!context) {
|
| 476 |
+
setContext(firstChatId);
|
| 477 |
+
chatsAD.selected = firstChatId;
|
| 478 |
+
localStorage.setItem('lastSelectedChat', firstChatId);
|
| 479 |
+
}
|
| 480 |
}
|
| 481 |
|
| 482 |
+
lastLogVersion = response.log_version;
|
| 483 |
+
lastLogGuid = response.log_guid;
|
| 484 |
+
|
| 485 |
} catch (error) {
|
| 486 |
console.error('Error:', error);
|
| 487 |
setConnectionStatus(false)
|
|
|
|
| 532 |
}
|
| 533 |
}
|
| 534 |
|
| 535 |
+
window.resetChat = async function (ctxid=null) {
|
| 536 |
try {
|
| 537 |
+
const resp = await sendJsonData("/chat_reset", { "context": ctxid === null ? context : ctxid });
|
| 538 |
+
if (ctxid === null) updateAfterScroll();
|
| 539 |
} catch (e) {
|
| 540 |
+
window.toastFetchError("Error resetting chat", e);
|
| 541 |
}
|
| 542 |
}
|
| 543 |
|
|
|
|
| 1216 |
chatsTab.classList.add('active');
|
| 1217 |
chatsSection.style.display = '';
|
| 1218 |
|
| 1219 |
+
// Get the available contexts from Alpine.js data
|
| 1220 |
+
const chatsAD = Alpine.$data(chatsSection);
|
| 1221 |
+
const availableContexts = chatsAD.contexts || [];
|
| 1222 |
+
|
| 1223 |
// Restore previous chat selection
|
| 1224 |
const lastSelectedChat = localStorage.getItem('lastSelectedChat');
|
| 1225 |
+
|
| 1226 |
+
// Only switch if:
|
| 1227 |
+
// 1. lastSelectedChat exists AND
|
| 1228 |
+
// 2. It's different from current context AND
|
| 1229 |
+
// 3. The context actually exists in our contexts list OR there are no contexts yet
|
| 1230 |
+
if (lastSelectedChat &&
|
| 1231 |
+
lastSelectedChat !== currentContext &&
|
| 1232 |
+
(availableContexts.some(ctx => ctx.id === lastSelectedChat) || availableContexts.length === 0)) {
|
| 1233 |
setContext(lastSelectedChat);
|
| 1234 |
}
|
| 1235 |
} else if (tabName === 'tasks') {
|
|
|
|
| 1237 |
tasksSection.style.display = 'flex';
|
| 1238 |
tasksSection.style.flexDirection = 'column';
|
| 1239 |
|
| 1240 |
+
// Get the available tasks from Alpine.js data
|
| 1241 |
+
const tasksAD = Alpine.$data(tasksSection);
|
| 1242 |
+
const availableTasks = tasksAD.tasks || [];
|
| 1243 |
+
|
| 1244 |
// Restore previous task selection
|
| 1245 |
const lastSelectedTask = localStorage.getItem('lastSelectedTask');
|
| 1246 |
+
|
| 1247 |
+
// Only switch if:
|
| 1248 |
+
// 1. lastSelectedTask exists AND
|
| 1249 |
+
// 2. It's different from current context AND
|
| 1250 |
+
// 3. The task actually exists in our tasks list
|
| 1251 |
+
if (lastSelectedTask &&
|
| 1252 |
+
lastSelectedTask !== currentContext &&
|
| 1253 |
+
availableTasks.some(task => task.id === lastSelectedTask)) {
|
| 1254 |
setContext(lastSelectedTask);
|
| 1255 |
}
|
| 1256 |
}
|
webui/js/scheduler.js
CHANGED
|
@@ -3,141 +3,222 @@
|
|
| 3 |
* Manages scheduled and ad-hoc tasks through a dedicated settings tab
|
| 4 |
*/
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
tasks: [],
|
| 46 |
isLoading: true,
|
| 47 |
selectedTask: null,
|
| 48 |
expandedTaskId: null,
|
| 49 |
sortField: 'name',
|
| 50 |
sortDirection: 'asc',
|
| 51 |
-
filterType: 'all', // all, scheduled, adhoc
|
| 52 |
filterState: 'all', // all, idle, running, disabled, error
|
| 53 |
pollingInterval: null,
|
| 54 |
pollingActive: false, // Track if polling is currently active
|
| 55 |
-
editingTask:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
isCreating: false,
|
| 57 |
isEditing: false,
|
| 58 |
showLoadingState: false,
|
| 59 |
viewMode: 'list', // Controls whether to show list or detail view
|
| 60 |
selectedTaskForDetail: null, // Task object for detail view
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
// Initialize the component
|
| 63 |
init() {
|
| 64 |
-
// Initialize
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
this.editingTask = {
|
| 66 |
name: '',
|
| 67 |
type: 'scheduled',
|
| 68 |
-
state: 'idle',
|
| 69 |
schedule: {
|
| 70 |
minute: '*',
|
| 71 |
hour: '*',
|
| 72 |
day: '*',
|
| 73 |
month: '*',
|
| 74 |
-
weekday: '*'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
},
|
| 76 |
-
token: '',
|
| 77 |
system_prompt: '',
|
| 78 |
prompt: '',
|
| 79 |
attachments: []
|
| 80 |
};
|
| 81 |
|
| 82 |
-
//
|
| 83 |
-
this.
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
hour: '*',
|
| 93 |
-
day: '*',
|
| 94 |
-
month: '*',
|
| 95 |
-
weekday: '*'
|
| 96 |
-
};
|
| 97 |
-
} else if (newType === 'adhoc' && !this.editingTask.token) {
|
| 98 |
-
// Initialize token if changing to adhoc type
|
| 99 |
-
this.editingTask.token = this.generateRandomToken();
|
| 100 |
-
}
|
| 101 |
});
|
| 102 |
|
| 103 |
-
//
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
this.fetchTasks();
|
| 108 |
|
| 109 |
-
//
|
| 110 |
-
const
|
| 111 |
-
if (
|
| 112 |
-
|
| 113 |
}
|
| 114 |
-
}, 100);
|
| 115 |
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
if (typeof isOpen !== 'undefined') {
|
| 122 |
-
if (isOpen === true) {
|
| 123 |
-
// Modal just opened
|
| 124 |
-
this.startPolling();
|
| 125 |
-
} else if (isOpen === false) {
|
| 126 |
-
// Modal closed, stop polling
|
| 127 |
-
this.stopPolling();
|
| 128 |
-
}
|
| 129 |
-
}
|
| 130 |
-
});
|
| 131 |
-
});
|
| 132 |
},
|
| 133 |
|
| 134 |
// Start polling for task updates
|
| 135 |
startPolling() {
|
| 136 |
// Don't start if already polling
|
| 137 |
if (this.pollingInterval) {
|
|
|
|
| 138 |
return;
|
| 139 |
}
|
| 140 |
|
|
|
|
| 141 |
this.pollingActive = true;
|
| 142 |
|
| 143 |
// Fetch immediately, then set up interval for every 2 seconds
|
|
@@ -151,6 +232,7 @@ document.addEventListener('alpine:init', () => {
|
|
| 151 |
|
| 152 |
// Stop polling when tab is inactive
|
| 153 |
stopPolling() {
|
|
|
|
| 154 |
this.pollingActive = false;
|
| 155 |
|
| 156 |
if (this.pollingInterval) {
|
|
@@ -177,7 +259,10 @@ document.addEventListener('alpine:init', () => {
|
|
| 177 |
method: 'POST',
|
| 178 |
headers: {
|
| 179 |
'Content-Type': 'application/json'
|
| 180 |
-
}
|
|
|
|
|
|
|
|
|
|
| 181 |
});
|
| 182 |
|
| 183 |
if (!response.ok) {
|
|
@@ -185,68 +270,58 @@ document.addEventListener('alpine:init', () => {
|
|
| 185 |
}
|
| 186 |
|
| 187 |
const data = await response.json();
|
| 188 |
-
|
| 189 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
} catch (error) {
|
| 191 |
console.error('Error fetching tasks:', error);
|
| 192 |
// Only show toast for errors on manual refresh, not during polling
|
| 193 |
if (!this.pollingInterval) {
|
| 194 |
showToast('Failed to fetch tasks: ' + error.message, 'error');
|
| 195 |
}
|
|
|
|
|
|
|
| 196 |
} finally {
|
| 197 |
this.isLoading = false;
|
| 198 |
}
|
| 199 |
},
|
| 200 |
|
| 201 |
-
// Computed property for filtered tasks
|
| 202 |
-
get filteredTasks() {
|
| 203 |
-
return this.tasks
|
| 204 |
-
.filter(task => {
|
| 205 |
-
// Filter by type
|
| 206 |
-
if (this.filterType !== 'all') {
|
| 207 |
-
if (this.filterType === 'scheduled' && !task.schedule) return false;
|
| 208 |
-
if (this.filterType === 'adhoc' && !task.token) return false;
|
| 209 |
-
}
|
| 210 |
-
|
| 211 |
-
// Filter by state
|
| 212 |
-
if (this.filterState !== 'all' && task.state !== this.filterState) {
|
| 213 |
-
return false;
|
| 214 |
-
}
|
| 215 |
-
|
| 216 |
-
return true;
|
| 217 |
-
})
|
| 218 |
-
.sort((a, b) => {
|
| 219 |
-
// Handle sorting
|
| 220 |
-
let valueA, valueB;
|
| 221 |
-
|
| 222 |
-
switch (this.sortField) {
|
| 223 |
-
case 'name':
|
| 224 |
-
valueA = a.name.toLowerCase();
|
| 225 |
-
valueB = b.name.toLowerCase();
|
| 226 |
-
break;
|
| 227 |
-
case 'state':
|
| 228 |
-
valueA = a.state;
|
| 229 |
-
valueB = b.state;
|
| 230 |
-
break;
|
| 231 |
-
case 'last_run':
|
| 232 |
-
valueA = a.last_run ? new Date(a.last_run).getTime() : 0;
|
| 233 |
-
valueB = b.last_run ? new Date(b.last_run).getTime() : 0;
|
| 234 |
-
break;
|
| 235 |
-
default:
|
| 236 |
-
valueA = a.name.toLowerCase();
|
| 237 |
-
valueB = b.name.toLowerCase();
|
| 238 |
-
}
|
| 239 |
-
|
| 240 |
-
// Determine sort direction
|
| 241 |
-
const direction = this.sortDirection === 'asc' ? 1 : -1;
|
| 242 |
-
|
| 243 |
-
// Compare values
|
| 244 |
-
if (valueA < valueB) return -1 * direction;
|
| 245 |
-
if (valueA > valueB) return 1 * direction;
|
| 246 |
-
return 0;
|
| 247 |
-
});
|
| 248 |
-
},
|
| 249 |
-
|
| 250 |
// Change sort field/direction
|
| 251 |
changeSort(field) {
|
| 252 |
if (this.sortField === field) {
|
|
@@ -296,9 +371,38 @@ document.addEventListener('alpine:init', () => {
|
|
| 296 |
// Format date for display
|
| 297 |
formatDate(dateString) {
|
| 298 |
if (!dateString) return 'Never';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 299 |
|
| 300 |
-
|
| 301 |
-
return date.toLocaleString();
|
| 302 |
},
|
| 303 |
|
| 304 |
// Format schedule for display
|
|
@@ -309,6 +413,7 @@ document.addEventListener('alpine:init', () => {
|
|
| 309 |
if (typeof task.schedule === 'string') {
|
| 310 |
schedule = task.schedule;
|
| 311 |
} else if (typeof task.schedule === 'object') {
|
|
|
|
| 312 |
schedule = `${task.schedule.minute || '*'} ${task.schedule.hour || '*'} ${task.schedule.day || '*'} ${task.schedule.month || '*'} ${task.schedule.weekday || '*'}`;
|
| 313 |
}
|
| 314 |
|
|
@@ -333,20 +438,31 @@ document.addEventListener('alpine:init', () => {
|
|
| 333 |
document.querySelector('[x-data="schedulerSettings"]')?.setAttribute('data-editing-state', 'creating');
|
| 334 |
this.editingTask = {
|
| 335 |
name: '',
|
| 336 |
-
type: 'scheduled',
|
| 337 |
state: 'idle', // Initialize with idle state
|
| 338 |
schedule: {
|
| 339 |
minute: '*',
|
| 340 |
hour: '*',
|
| 341 |
day: '*',
|
| 342 |
month: '*',
|
| 343 |
-
weekday: '*'
|
|
|
|
| 344 |
},
|
| 345 |
token: this.generateRandomToken(), // Generate token even for scheduled tasks to prevent undefined errors
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
system_prompt: '',
|
| 347 |
prompt: '',
|
| 348 |
attachments: [], // Always initialize as an empty array
|
| 349 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
},
|
| 351 |
|
| 352 |
// Edit an existing task
|
|
@@ -371,6 +487,38 @@ document.addEventListener('alpine:init', () => {
|
|
| 371 |
// Ensure state is set with a default if missing
|
| 372 |
if (!this.editingTask.state) this.editingTask.state = 'idle';
|
| 373 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
// Ensure attachments is always an array
|
| 375 |
if (!this.editingTask.attachments) {
|
| 376 |
this.editingTask.attachments = [];
|
|
@@ -387,50 +535,92 @@ document.addEventListener('alpine:init', () => {
|
|
| 387 |
|
| 388 |
// Ensure appropriate properties are initialized based on task type
|
| 389 |
if (this.editingTask.type === 'scheduled') {
|
| 390 |
-
// Ensure proper structure for schedule
|
| 391 |
-
if (typeof this.editingTask.schedule === 'string') {
|
| 392 |
-
const parts = this.editingTask.schedule.split(' ');
|
| 393 |
-
this.editingTask.schedule = {
|
| 394 |
-
minute: parts[0] || '*',
|
| 395 |
-
hour: parts[1] || '*',
|
| 396 |
-
day: parts[2] || '*',
|
| 397 |
-
month: parts[3] || '*',
|
| 398 |
-
weekday: parts[4] || '*'
|
| 399 |
-
};
|
| 400 |
-
} else if (!this.editingTask.schedule) {
|
| 401 |
-
// Initialize schedule if it doesn't exist
|
| 402 |
-
this.editingTask.schedule = {
|
| 403 |
-
minute: '*',
|
| 404 |
-
hour: '*',
|
| 405 |
-
day: '*',
|
| 406 |
-
month: '*',
|
| 407 |
-
weekday: '*'
|
| 408 |
-
};
|
| 409 |
-
}
|
| 410 |
// Initialize token for scheduled tasks to prevent undefined errors if UI accesses it
|
| 411 |
if (!this.editingTask.token) {
|
| 412 |
this.editingTask.token = '';
|
| 413 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 414 |
} else if (this.editingTask.type === 'adhoc') {
|
| 415 |
// Initialize token if it doesn't exist
|
| 416 |
if (!this.editingTask.token) {
|
| 417 |
this.editingTask.token = this.generateRandomToken();
|
|
|
|
| 418 |
}
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
};
|
| 428 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 429 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
},
|
| 431 |
|
| 432 |
// Cancel editing
|
| 433 |
cancelEdit() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
// Reset to initial state but keep default values to prevent errors
|
| 435 |
this.editingTask = {
|
| 436 |
name: '',
|
|
@@ -441,9 +631,15 @@ document.addEventListener('alpine:init', () => {
|
|
| 441 |
hour: '*',
|
| 442 |
day: '*',
|
| 443 |
month: '*',
|
| 444 |
-
weekday: '*'
|
|
|
|
| 445 |
},
|
| 446 |
token: '',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 447 |
system_prompt: '',
|
| 448 |
prompt: '',
|
| 449 |
attachments: [], // Always initialize as an empty array
|
|
@@ -469,7 +665,8 @@ document.addEventListener('alpine:init', () => {
|
|
| 469 |
name: this.editingTask.name,
|
| 470 |
system_prompt: this.editingTask.system_prompt || '',
|
| 471 |
prompt: this.editingTask.prompt || '',
|
| 472 |
-
state: this.editingTask.state || 'idle' // Include state in task data
|
|
|
|
| 473 |
};
|
| 474 |
|
| 475 |
// Process attachments - now always stored as array
|
|
@@ -479,7 +676,7 @@ document.addEventListener('alpine:init', () => {
|
|
| 479 |
.filter(line => line && line.trim().length > 0)
|
| 480 |
: [];
|
| 481 |
|
| 482 |
-
// Handle
|
| 483 |
if (this.editingTask.type === 'scheduled') {
|
| 484 |
// Ensure schedule is properly formatted as an object
|
| 485 |
if (typeof this.editingTask.schedule === 'string') {
|
|
@@ -490,19 +687,87 @@ document.addEventListener('alpine:init', () => {
|
|
| 490 |
hour: parts[1] || '*',
|
| 491 |
day: parts[2] || '*',
|
| 492 |
month: parts[3] || '*',
|
| 493 |
-
weekday: parts[4] || '*'
|
|
|
|
| 494 |
};
|
| 495 |
} else {
|
| 496 |
-
// Use object schedule directly
|
| 497 |
-
taskData.schedule =
|
|
|
|
|
|
|
|
|
|
| 498 |
}
|
| 499 |
-
// Don't send token for scheduled tasks
|
| 500 |
delete taskData.token;
|
| 501 |
-
|
|
|
|
| 502 |
// Ad-hoc task with token
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 503 |
taskData.token = this.editingTask.token;
|
| 504 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 505 |
delete taskData.schedule;
|
|
|
|
| 506 |
}
|
| 507 |
|
| 508 |
// Determine if creating or updating
|
|
@@ -513,6 +778,9 @@ document.addEventListener('alpine:init', () => {
|
|
| 513 |
taskData.task_id = this.editingTask.uuid;
|
| 514 |
}
|
| 515 |
|
|
|
|
|
|
|
|
|
|
| 516 |
// Make API request
|
| 517 |
const response = await fetch(apiEndpoint, {
|
| 518 |
method: 'POST',
|
|
@@ -527,14 +795,73 @@ document.addEventListener('alpine:init', () => {
|
|
| 527 |
throw new Error(errorData.error || 'Failed to save task');
|
| 528 |
}
|
| 529 |
|
|
|
|
|
|
|
|
|
|
| 530 |
// Show success message
|
| 531 |
showToast(this.isCreating ? 'Task created successfully' : 'Task updated successfully', 'success');
|
| 532 |
|
| 533 |
-
//
|
| 534 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 535 |
|
| 536 |
-
//
|
| 537 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 538 |
document.querySelector('[x-data="schedulerSettings"]')?.removeAttribute('data-editing-state');
|
| 539 |
} catch (error) {
|
| 540 |
console.error('Error saving task:', error);
|
|
@@ -550,7 +877,10 @@ document.addEventListener('alpine:init', () => {
|
|
| 550 |
headers: {
|
| 551 |
'Content-Type': 'application/json'
|
| 552 |
},
|
| 553 |
-
body: JSON.stringify({
|
|
|
|
|
|
|
|
|
|
| 554 |
});
|
| 555 |
|
| 556 |
if (!response.ok) {
|
|
@@ -593,7 +923,8 @@ document.addEventListener('alpine:init', () => {
|
|
| 593 |
},
|
| 594 |
body: JSON.stringify({
|
| 595 |
task_id: taskId,
|
| 596 |
-
state: 'idle' // Always reset to idle state
|
|
|
|
| 597 |
})
|
| 598 |
});
|
| 599 |
|
|
@@ -627,7 +958,10 @@ document.addEventListener('alpine:init', () => {
|
|
| 627 |
headers: {
|
| 628 |
'Content-Type': 'application/json'
|
| 629 |
},
|
| 630 |
-
body: JSON.stringify({
|
|
|
|
|
|
|
|
|
|
| 631 |
});
|
| 632 |
|
| 633 |
if (!response.ok) {
|
|
@@ -637,19 +971,44 @@ document.addEventListener('alpine:init', () => {
|
|
| 637 |
|
| 638 |
showToast('Task deleted successfully', 'success');
|
| 639 |
|
| 640 |
-
//
|
| 641 |
-
this.
|
| 642 |
-
|
| 643 |
-
// Close expanded view if this task was expanded
|
| 644 |
-
if (this.expandedTaskId === taskId) {
|
| 645 |
-
this.expandedTaskId = null;
|
| 646 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 647 |
} catch (error) {
|
| 648 |
console.error('Error deleting task:', error);
|
| 649 |
showToast('Failed to delete task: ' + error.message, 'error');
|
| 650 |
}
|
| 651 |
},
|
| 652 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 653 |
// Generate a random token for ad-hoc tasks
|
| 654 |
generateRandomToken() {
|
| 655 |
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
@@ -660,6 +1019,72 @@ document.addEventListener('alpine:init', () => {
|
|
| 660 |
return token;
|
| 661 |
},
|
| 662 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 663 |
// Computed property for attachments text representation
|
| 664 |
get attachmentsText() {
|
| 665 |
// Ensure we always have an array to work with
|
|
@@ -667,16 +1092,12 @@ document.addEventListener('alpine:init', () => {
|
|
| 667 |
? this.editingTask.attachments
|
| 668 |
: [];
|
| 669 |
|
| 670 |
-
console.log('attachmentsText getter called, source:', this.editingTask.attachments);
|
| 671 |
-
|
| 672 |
// Join array items with newlines
|
| 673 |
return attachments.join('\n');
|
| 674 |
},
|
| 675 |
|
| 676 |
// Setter for attachments text - preserves empty lines during editing
|
| 677 |
set attachmentsText(value) {
|
| 678 |
-
console.log('attachmentsText setter called with:', value);
|
| 679 |
-
|
| 680 |
if (typeof value === 'string') {
|
| 681 |
// Just split by newlines without filtering to preserve editing experience
|
| 682 |
this.editingTask.attachments = value.split('\n');
|
|
@@ -684,8 +1105,572 @@ document.addEventListener('alpine:init', () => {
|
|
| 684 |
// Fallback to empty array if not a string
|
| 685 |
this.editingTask.attachments = [];
|
| 686 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 687 |
|
| 688 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 689 |
}
|
| 690 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 691 |
});
|
|
|
|
| 3 |
* Manages scheduled and ad-hoc tasks through a dedicated settings tab
|
| 4 |
*/
|
| 5 |
|
| 6 |
+
import { formatDateTime, getUserTimezone } from './time-utils.js';
|
| 7 |
+
|
| 8 |
+
// Ensure the showToast function is available
|
| 9 |
+
// if (typeof window.showToast !== 'function') {
|
| 10 |
+
// window.showToast = function(message, type = 'info') {
|
| 11 |
+
// console.log(`[Toast ${type}]: ${message}`);
|
| 12 |
+
// // Create toast element if not already present
|
| 13 |
+
// let toastContainer = document.getElementById('toast-container');
|
| 14 |
+
// if (!toastContainer) {
|
| 15 |
+
// toastContainer = document.createElement('div');
|
| 16 |
+
// toastContainer.id = 'toast-container';
|
| 17 |
+
// toastContainer.style.position = 'fixed';
|
| 18 |
+
// toastContainer.style.bottom = '20px';
|
| 19 |
+
// toastContainer.style.right = '20px';
|
| 20 |
+
// toastContainer.style.zIndex = '9999';
|
| 21 |
+
// document.body.appendChild(toastContainer);
|
| 22 |
+
// }
|
| 23 |
+
|
| 24 |
+
// // Create the toast
|
| 25 |
+
// const toast = document.createElement('div');
|
| 26 |
+
// toast.className = `toast toast-${type}`;
|
| 27 |
+
// toast.style.padding = '10px 15px';
|
| 28 |
+
// toast.style.margin = '5px 0';
|
| 29 |
+
// toast.style.backgroundColor = type === 'error' ? '#f44336' :
|
| 30 |
+
// type === 'success' ? '#4CAF50' :
|
| 31 |
+
// type === 'warning' ? '#ff9800' : '#2196F3';
|
| 32 |
+
// toast.style.color = 'white';
|
| 33 |
+
// toast.style.borderRadius = '4px';
|
| 34 |
+
// toast.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
|
| 35 |
+
// toast.style.width = 'auto';
|
| 36 |
+
// toast.style.maxWidth = '300px';
|
| 37 |
+
// toast.style.wordWrap = 'break-word';
|
| 38 |
+
|
| 39 |
+
// toast.innerHTML = message;
|
| 40 |
+
|
| 41 |
+
// // Add to container
|
| 42 |
+
// toastContainer.appendChild(toast);
|
| 43 |
+
|
| 44 |
+
// // Auto remove after 3 seconds
|
| 45 |
+
// setTimeout(() => {
|
| 46 |
+
// if (toast.parentNode) {
|
| 47 |
+
// toast.style.opacity = '0';
|
| 48 |
+
// toast.style.transition = 'opacity 0.5s ease';
|
| 49 |
+
// setTimeout(() => {
|
| 50 |
+
// if (toast.parentNode) {
|
| 51 |
+
// toast.parentNode.removeChild(toast);
|
| 52 |
+
// }
|
| 53 |
+
// }, 500);
|
| 54 |
+
// }
|
| 55 |
+
// }, 3000);
|
| 56 |
+
// };
|
| 57 |
+
// }
|
| 58 |
+
|
| 59 |
+
// Add this near the top of the scheduler.js file, outside of any function
|
| 60 |
+
const showToast = function(message, type = 'info') {
|
| 61 |
+
// Use the global toast function if available, otherwise fallback to console
|
| 62 |
+
if (typeof window.toast === 'function') {
|
| 63 |
+
window.toast(message, type);
|
| 64 |
+
} else {
|
| 65 |
+
console.log(`Toast (${type}): ${message}`);
|
| 66 |
+
}
|
| 67 |
+
};
|
| 68 |
+
|
| 69 |
+
// Define the full component implementation
|
| 70 |
+
const fullComponentImplementation = function() {
|
| 71 |
+
return {
|
| 72 |
tasks: [],
|
| 73 |
isLoading: true,
|
| 74 |
selectedTask: null,
|
| 75 |
expandedTaskId: null,
|
| 76 |
sortField: 'name',
|
| 77 |
sortDirection: 'asc',
|
| 78 |
+
filterType: 'all', // all, scheduled, adhoc, planned
|
| 79 |
filterState: 'all', // all, idle, running, disabled, error
|
| 80 |
pollingInterval: null,
|
| 81 |
pollingActive: false, // Track if polling is currently active
|
| 82 |
+
editingTask: {
|
| 83 |
+
name: '',
|
| 84 |
+
type: 'scheduled',
|
| 85 |
+
state: 'idle',
|
| 86 |
+
schedule: {
|
| 87 |
+
minute: '*',
|
| 88 |
+
hour: '*',
|
| 89 |
+
day: '*',
|
| 90 |
+
month: '*',
|
| 91 |
+
weekday: '*',
|
| 92 |
+
timezone: getUserTimezone()
|
| 93 |
+
},
|
| 94 |
+
token: '',
|
| 95 |
+
plan: {
|
| 96 |
+
todo: [],
|
| 97 |
+
in_progress: null,
|
| 98 |
+
done: []
|
| 99 |
+
},
|
| 100 |
+
system_prompt: '',
|
| 101 |
+
prompt: '',
|
| 102 |
+
attachments: []
|
| 103 |
+
},
|
| 104 |
isCreating: false,
|
| 105 |
isEditing: false,
|
| 106 |
showLoadingState: false,
|
| 107 |
viewMode: 'list', // Controls whether to show list or detail view
|
| 108 |
selectedTaskForDetail: null, // Task object for detail view
|
| 109 |
+
attachmentsText: '',
|
| 110 |
+
filteredTasks: [],
|
| 111 |
+
hasNoTasks: true, // Add explicit reactive property
|
| 112 |
|
| 113 |
// Initialize the component
|
| 114 |
init() {
|
| 115 |
+
// Initialize component data
|
| 116 |
+
this.tasks = [];
|
| 117 |
+
this.isLoading = true;
|
| 118 |
+
this.hasNoTasks = true; // Add explicit reactive property
|
| 119 |
+
this.filterType = 'all';
|
| 120 |
+
this.filterState = 'all';
|
| 121 |
+
this.sortField = 'name';
|
| 122 |
+
this.sortDirection = 'asc';
|
| 123 |
+
this.pollingInterval = null;
|
| 124 |
+
this.pollingActive = false;
|
| 125 |
+
|
| 126 |
+
// Start polling for tasks
|
| 127 |
+
this.startPolling();
|
| 128 |
+
|
| 129 |
+
// Refresh initial data
|
| 130 |
+
this.fetchTasks();
|
| 131 |
+
|
| 132 |
+
// Set up event handler for tab selection to ensure view is refreshed when tab becomes visible
|
| 133 |
+
document.addEventListener('click', (event) => {
|
| 134 |
+
// Check if a tab was clicked
|
| 135 |
+
const clickedTab = event.target.closest('.settings-tab');
|
| 136 |
+
if (clickedTab && clickedTab.getAttribute('data-tab') === 'scheduler') {
|
| 137 |
+
setTimeout(() => {
|
| 138 |
+
this.fetchTasks();
|
| 139 |
+
}, 100);
|
| 140 |
+
}
|
| 141 |
+
});
|
| 142 |
+
|
| 143 |
+
// Watch for changes to the tasks array to update UI
|
| 144 |
+
this.$watch('tasks', (newTasks) => {
|
| 145 |
+
this.updateTasksUI();
|
| 146 |
+
});
|
| 147 |
+
|
| 148 |
+
this.$watch('filterType', () => {
|
| 149 |
+
this.updateTasksUI();
|
| 150 |
+
});
|
| 151 |
+
|
| 152 |
+
this.$watch('filterState', () => {
|
| 153 |
+
this.updateTasksUI();
|
| 154 |
+
});
|
| 155 |
+
|
| 156 |
+
// Set up default configuration
|
| 157 |
+
this.viewMode = localStorage.getItem('scheduler_view_mode') || 'list';
|
| 158 |
+
this.selectedTask = null;
|
| 159 |
+
this.expandedTaskId = null;
|
| 160 |
this.editingTask = {
|
| 161 |
name: '',
|
| 162 |
type: 'scheduled',
|
| 163 |
+
state: 'idle',
|
| 164 |
schedule: {
|
| 165 |
minute: '*',
|
| 166 |
hour: '*',
|
| 167 |
day: '*',
|
| 168 |
month: '*',
|
| 169 |
+
weekday: '*',
|
| 170 |
+
timezone: getUserTimezone()
|
| 171 |
+
},
|
| 172 |
+
token: this.generateRandomToken ? this.generateRandomToken() : '',
|
| 173 |
+
plan: {
|
| 174 |
+
todo: [],
|
| 175 |
+
in_progress: null,
|
| 176 |
+
done: []
|
| 177 |
},
|
|
|
|
| 178 |
system_prompt: '',
|
| 179 |
prompt: '',
|
| 180 |
attachments: []
|
| 181 |
};
|
| 182 |
|
| 183 |
+
// Initialize Flatpickr for date/time pickers after Alpine is fully initialized
|
| 184 |
+
this.$nextTick(() => {
|
| 185 |
+
// Wait until DOM is updated
|
| 186 |
+
setTimeout(() => {
|
| 187 |
+
if (this.isCreating) {
|
| 188 |
+
this.initFlatpickr('create');
|
| 189 |
+
} else if (this.isEditing) {
|
| 190 |
+
this.initFlatpickr('edit');
|
| 191 |
+
}
|
| 192 |
+
}, 100);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
});
|
| 194 |
|
| 195 |
+
// Cleanup on component destruction
|
| 196 |
+
this.$cleanup = () => {
|
| 197 |
+
console.log('Cleaning up schedulerSettings component');
|
| 198 |
+
this.stopPolling();
|
|
|
|
| 199 |
|
| 200 |
+
// Clean up any Flatpickr instances
|
| 201 |
+
const createInput = document.getElementById('newPlannedTime-create');
|
| 202 |
+
if (createInput && createInput._flatpickr) {
|
| 203 |
+
createInput._flatpickr.destroy();
|
| 204 |
}
|
|
|
|
| 205 |
|
| 206 |
+
const editInput = document.getElementById('newPlannedTime-edit');
|
| 207 |
+
if (editInput && editInput._flatpickr) {
|
| 208 |
+
editInput._flatpickr.destroy();
|
| 209 |
+
}
|
| 210 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
},
|
| 212 |
|
| 213 |
// Start polling for task updates
|
| 214 |
startPolling() {
|
| 215 |
// Don't start if already polling
|
| 216 |
if (this.pollingInterval) {
|
| 217 |
+
console.log('Polling already active, not starting again');
|
| 218 |
return;
|
| 219 |
}
|
| 220 |
|
| 221 |
+
console.log('Starting task polling');
|
| 222 |
this.pollingActive = true;
|
| 223 |
|
| 224 |
// Fetch immediately, then set up interval for every 2 seconds
|
|
|
|
| 232 |
|
| 233 |
// Stop polling when tab is inactive
|
| 234 |
stopPolling() {
|
| 235 |
+
console.log('Stopping task polling');
|
| 236 |
this.pollingActive = false;
|
| 237 |
|
| 238 |
if (this.pollingInterval) {
|
|
|
|
| 259 |
method: 'POST',
|
| 260 |
headers: {
|
| 261 |
'Content-Type': 'application/json'
|
| 262 |
+
},
|
| 263 |
+
body: JSON.stringify({
|
| 264 |
+
timezone: getUserTimezone()
|
| 265 |
+
})
|
| 266 |
});
|
| 267 |
|
| 268 |
if (!response.ok) {
|
|
|
|
| 270 |
}
|
| 271 |
|
| 272 |
const data = await response.json();
|
| 273 |
+
|
| 274 |
+
// Check if data.tasks exists and is an array
|
| 275 |
+
if (!data || !data.tasks) {
|
| 276 |
+
console.error('Invalid response: data.tasks is missing', data);
|
| 277 |
+
this.tasks = [];
|
| 278 |
+
} else if (!Array.isArray(data.tasks)) {
|
| 279 |
+
console.error('Invalid response: data.tasks is not an array', data.tasks);
|
| 280 |
+
this.tasks = [];
|
| 281 |
+
} else {
|
| 282 |
+
// Verify each task has necessary properties
|
| 283 |
+
const validTasks = data.tasks.filter(task => {
|
| 284 |
+
if (!task || typeof task !== 'object') {
|
| 285 |
+
console.error('Invalid task (not an object):', task);
|
| 286 |
+
return false;
|
| 287 |
+
}
|
| 288 |
+
if (!task.uuid) {
|
| 289 |
+
console.error('Task missing uuid:', task);
|
| 290 |
+
return false;
|
| 291 |
+
}
|
| 292 |
+
if (!task.name) {
|
| 293 |
+
console.error('Task missing name:', task);
|
| 294 |
+
return false;
|
| 295 |
+
}
|
| 296 |
+
if (!task.type) {
|
| 297 |
+
console.error('Task missing type:', task);
|
| 298 |
+
return false;
|
| 299 |
+
}
|
| 300 |
+
return true;
|
| 301 |
+
});
|
| 302 |
+
|
| 303 |
+
if (validTasks.length !== data.tasks.length) {
|
| 304 |
+
console.warn(`Filtered out ${data.tasks.length - validTasks.length} invalid tasks`);
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
this.tasks = validTasks;
|
| 308 |
+
|
| 309 |
+
// Update UI using the shared function
|
| 310 |
+
this.updateTasksUI();
|
| 311 |
+
}
|
| 312 |
} catch (error) {
|
| 313 |
console.error('Error fetching tasks:', error);
|
| 314 |
// Only show toast for errors on manual refresh, not during polling
|
| 315 |
if (!this.pollingInterval) {
|
| 316 |
showToast('Failed to fetch tasks: ' + error.message, 'error');
|
| 317 |
}
|
| 318 |
+
// Reset tasks to empty array on error
|
| 319 |
+
this.tasks = [];
|
| 320 |
} finally {
|
| 321 |
this.isLoading = false;
|
| 322 |
}
|
| 323 |
},
|
| 324 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
// Change sort field/direction
|
| 326 |
changeSort(field) {
|
| 327 |
if (this.sortField === field) {
|
|
|
|
| 371 |
// Format date for display
|
| 372 |
formatDate(dateString) {
|
| 373 |
if (!dateString) return 'Never';
|
| 374 |
+
return formatDateTime(dateString, 'full');
|
| 375 |
+
},
|
| 376 |
+
|
| 377 |
+
// Format plan for display
|
| 378 |
+
formatPlan(task) {
|
| 379 |
+
if (!task || !task.plan) return 'No plan';
|
| 380 |
+
|
| 381 |
+
const todoCount = Array.isArray(task.plan.todo) ? task.plan.todo.length : 0;
|
| 382 |
+
const inProgress = task.plan.in_progress ? 'Yes' : 'No';
|
| 383 |
+
const doneCount = Array.isArray(task.plan.done) ? task.plan.done.length : 0;
|
| 384 |
+
|
| 385 |
+
let nextRun = '';
|
| 386 |
+
if (Array.isArray(task.plan.todo) && task.plan.todo.length > 0) {
|
| 387 |
+
try {
|
| 388 |
+
const nextTime = new Date(task.plan.todo[0]);
|
| 389 |
+
|
| 390 |
+
// Verify it's a valid date before formatting
|
| 391 |
+
if (!isNaN(nextTime.getTime())) {
|
| 392 |
+
nextRun = formatDateTime(nextTime, 'short');
|
| 393 |
+
} else {
|
| 394 |
+
nextRun = 'Invalid date';
|
| 395 |
+
console.warn(`Invalid date format in plan.todo[0]: ${task.plan.todo[0]}`);
|
| 396 |
+
}
|
| 397 |
+
} catch (error) {
|
| 398 |
+
console.error(`Error formatting next run time: ${error.message}`);
|
| 399 |
+
nextRun = 'Error';
|
| 400 |
+
}
|
| 401 |
+
} else {
|
| 402 |
+
nextRun = 'None';
|
| 403 |
+
}
|
| 404 |
|
| 405 |
+
return `Next: ${nextRun}\nTodo: ${todoCount}\nIn Progress: ${inProgress}\nDone: ${doneCount}`;
|
|
|
|
| 406 |
},
|
| 407 |
|
| 408 |
// Format schedule for display
|
|
|
|
| 413 |
if (typeof task.schedule === 'string') {
|
| 414 |
schedule = task.schedule;
|
| 415 |
} else if (typeof task.schedule === 'object') {
|
| 416 |
+
// Display only the cron parts, not the timezone
|
| 417 |
schedule = `${task.schedule.minute || '*'} ${task.schedule.hour || '*'} ${task.schedule.day || '*'} ${task.schedule.month || '*'} ${task.schedule.weekday || '*'}`;
|
| 418 |
}
|
| 419 |
|
|
|
|
| 438 |
document.querySelector('[x-data="schedulerSettings"]')?.setAttribute('data-editing-state', 'creating');
|
| 439 |
this.editingTask = {
|
| 440 |
name: '',
|
| 441 |
+
type: 'scheduled', // Default to scheduled
|
| 442 |
state: 'idle', // Initialize with idle state
|
| 443 |
schedule: {
|
| 444 |
minute: '*',
|
| 445 |
hour: '*',
|
| 446 |
day: '*',
|
| 447 |
month: '*',
|
| 448 |
+
weekday: '*',
|
| 449 |
+
timezone: getUserTimezone()
|
| 450 |
},
|
| 451 |
token: this.generateRandomToken(), // Generate token even for scheduled tasks to prevent undefined errors
|
| 452 |
+
plan: { // Initialize plan for all task types to prevent undefined errors
|
| 453 |
+
todo: [],
|
| 454 |
+
in_progress: null,
|
| 455 |
+
done: []
|
| 456 |
+
},
|
| 457 |
system_prompt: '',
|
| 458 |
prompt: '',
|
| 459 |
attachments: [], // Always initialize as an empty array
|
| 460 |
};
|
| 461 |
+
|
| 462 |
+
// Set up Flatpickr after the component is visible
|
| 463 |
+
this.$nextTick(() => {
|
| 464 |
+
this.initFlatpickr('create');
|
| 465 |
+
});
|
| 466 |
},
|
| 467 |
|
| 468 |
// Edit an existing task
|
|
|
|
| 487 |
// Ensure state is set with a default if missing
|
| 488 |
if (!this.editingTask.state) this.editingTask.state = 'idle';
|
| 489 |
|
| 490 |
+
// Always initialize schedule to prevent UI errors
|
| 491 |
+
// All task types need this structure for the form to work properly
|
| 492 |
+
if (!this.editingTask.schedule || typeof this.editingTask.schedule === 'string') {
|
| 493 |
+
let scheduleObj = {
|
| 494 |
+
minute: '*',
|
| 495 |
+
hour: '*',
|
| 496 |
+
day: '*',
|
| 497 |
+
month: '*',
|
| 498 |
+
weekday: '*',
|
| 499 |
+
timezone: getUserTimezone()
|
| 500 |
+
};
|
| 501 |
+
|
| 502 |
+
// If it's a string, parse it
|
| 503 |
+
if (typeof this.editingTask.schedule === 'string') {
|
| 504 |
+
const parts = this.editingTask.schedule.split(' ');
|
| 505 |
+
if (parts.length >= 5) {
|
| 506 |
+
scheduleObj.minute = parts[0] || '*';
|
| 507 |
+
scheduleObj.hour = parts[1] || '*';
|
| 508 |
+
scheduleObj.day = parts[2] || '*';
|
| 509 |
+
scheduleObj.month = parts[3] || '*';
|
| 510 |
+
scheduleObj.weekday = parts[4] || '*';
|
| 511 |
+
}
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
this.editingTask.schedule = scheduleObj;
|
| 515 |
+
} else {
|
| 516 |
+
// Ensure timezone exists in the schedule
|
| 517 |
+
if (!this.editingTask.schedule.timezone) {
|
| 518 |
+
this.editingTask.schedule.timezone = getUserTimezone();
|
| 519 |
+
}
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
// Ensure attachments is always an array
|
| 523 |
if (!this.editingTask.attachments) {
|
| 524 |
this.editingTask.attachments = [];
|
|
|
|
| 535 |
|
| 536 |
// Ensure appropriate properties are initialized based on task type
|
| 537 |
if (this.editingTask.type === 'scheduled') {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 538 |
// Initialize token for scheduled tasks to prevent undefined errors if UI accesses it
|
| 539 |
if (!this.editingTask.token) {
|
| 540 |
this.editingTask.token = '';
|
| 541 |
}
|
| 542 |
+
|
| 543 |
+
// Initialize plan stub for scheduled tasks to prevent undefined errors
|
| 544 |
+
if (!this.editingTask.plan) {
|
| 545 |
+
this.editingTask.plan = {
|
| 546 |
+
todo: [],
|
| 547 |
+
in_progress: null,
|
| 548 |
+
done: []
|
| 549 |
+
};
|
| 550 |
+
}
|
| 551 |
} else if (this.editingTask.type === 'adhoc') {
|
| 552 |
// Initialize token if it doesn't exist
|
| 553 |
if (!this.editingTask.token) {
|
| 554 |
this.editingTask.token = this.generateRandomToken();
|
| 555 |
+
console.log('Generated new token for adhoc task:', this.editingTask.token);
|
| 556 |
}
|
| 557 |
+
|
| 558 |
+
console.log('Setting token for adhoc task:', this.editingTask.token);
|
| 559 |
+
|
| 560 |
+
// Initialize plan stub for adhoc tasks to prevent undefined errors
|
| 561 |
+
if (!this.editingTask.plan) {
|
| 562 |
+
this.editingTask.plan = {
|
| 563 |
+
todo: [],
|
| 564 |
+
in_progress: null,
|
| 565 |
+
done: []
|
| 566 |
+
};
|
| 567 |
+
}
|
| 568 |
+
} else if (this.editingTask.type === 'planned') {
|
| 569 |
+
// Initialize plan if it doesn't exist
|
| 570 |
+
if (!this.editingTask.plan) {
|
| 571 |
+
this.editingTask.plan = {
|
| 572 |
+
todo: [],
|
| 573 |
+
in_progress: null,
|
| 574 |
+
done: []
|
| 575 |
};
|
| 576 |
}
|
| 577 |
+
|
| 578 |
+
// Ensure todo is an array
|
| 579 |
+
if (!Array.isArray(this.editingTask.plan.todo)) {
|
| 580 |
+
this.editingTask.plan.todo = [];
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
// Initialize token to prevent undefined errors
|
| 584 |
+
if (!this.editingTask.token) {
|
| 585 |
+
this.editingTask.token = '';
|
| 586 |
+
}
|
| 587 |
}
|
| 588 |
+
|
| 589 |
+
// Set up Flatpickr after the component is visible and task data is loaded
|
| 590 |
+
this.$nextTick(() => {
|
| 591 |
+
this.initFlatpickr('edit');
|
| 592 |
+
});
|
| 593 |
},
|
| 594 |
|
| 595 |
// Cancel editing
|
| 596 |
cancelEdit() {
|
| 597 |
+
// Clean up Flatpickr instances
|
| 598 |
+
const destroyFlatpickr = (inputId) => {
|
| 599 |
+
const input = document.getElementById(inputId);
|
| 600 |
+
if (input && input._flatpickr) {
|
| 601 |
+
console.log(`Destroying Flatpickr instance for ${inputId}`);
|
| 602 |
+
input._flatpickr.destroy();
|
| 603 |
+
|
| 604 |
+
// Also remove any wrapper elements that might have been created
|
| 605 |
+
const wrapper = input.closest('.scheduler-flatpickr-wrapper');
|
| 606 |
+
if (wrapper && wrapper.parentNode) {
|
| 607 |
+
// Move the input back to its original position
|
| 608 |
+
wrapper.parentNode.insertBefore(input, wrapper);
|
| 609 |
+
// Remove the wrapper
|
| 610 |
+
wrapper.parentNode.removeChild(wrapper);
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
// Remove any added classes
|
| 614 |
+
input.classList.remove('scheduler-flatpickr-input');
|
| 615 |
+
}
|
| 616 |
+
};
|
| 617 |
+
|
| 618 |
+
if (this.isCreating) {
|
| 619 |
+
destroyFlatpickr('newPlannedTime-create');
|
| 620 |
+
} else if (this.isEditing) {
|
| 621 |
+
destroyFlatpickr('newPlannedTime-edit');
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
// Reset to initial state but keep default values to prevent errors
|
| 625 |
this.editingTask = {
|
| 626 |
name: '',
|
|
|
|
| 631 |
hour: '*',
|
| 632 |
day: '*',
|
| 633 |
month: '*',
|
| 634 |
+
weekday: '*',
|
| 635 |
+
timezone: getUserTimezone()
|
| 636 |
},
|
| 637 |
token: '',
|
| 638 |
+
plan: { // Initialize plan for planned tasks
|
| 639 |
+
todo: [],
|
| 640 |
+
in_progress: null,
|
| 641 |
+
done: []
|
| 642 |
+
},
|
| 643 |
system_prompt: '',
|
| 644 |
prompt: '',
|
| 645 |
attachments: [], // Always initialize as an empty array
|
|
|
|
| 665 |
name: this.editingTask.name,
|
| 666 |
system_prompt: this.editingTask.system_prompt || '',
|
| 667 |
prompt: this.editingTask.prompt || '',
|
| 668 |
+
state: this.editingTask.state || 'idle', // Include state in task data
|
| 669 |
+
timezone: getUserTimezone()
|
| 670 |
};
|
| 671 |
|
| 672 |
// Process attachments - now always stored as array
|
|
|
|
| 676 |
.filter(line => line && line.trim().length > 0)
|
| 677 |
: [];
|
| 678 |
|
| 679 |
+
// Handle task type specific data
|
| 680 |
if (this.editingTask.type === 'scheduled') {
|
| 681 |
// Ensure schedule is properly formatted as an object
|
| 682 |
if (typeof this.editingTask.schedule === 'string') {
|
|
|
|
| 687 |
hour: parts[1] || '*',
|
| 688 |
day: parts[2] || '*',
|
| 689 |
month: parts[3] || '*',
|
| 690 |
+
weekday: parts[4] || '*',
|
| 691 |
+
timezone: getUserTimezone() // Add timezone to schedule object
|
| 692 |
};
|
| 693 |
} else {
|
| 694 |
+
// Use object schedule directly but ensure timezone is included
|
| 695 |
+
taskData.schedule = {
|
| 696 |
+
...this.editingTask.schedule,
|
| 697 |
+
timezone: this.editingTask.schedule.timezone || getUserTimezone()
|
| 698 |
+
};
|
| 699 |
}
|
| 700 |
+
// Don't send token or plan for scheduled tasks
|
| 701 |
delete taskData.token;
|
| 702 |
+
delete taskData.plan;
|
| 703 |
+
} else if (this.editingTask.type === 'adhoc') {
|
| 704 |
// Ad-hoc task with token
|
| 705 |
+
// Ensure token is a non-empty string, generate a new one if needed
|
| 706 |
+
if (!this.editingTask.token) {
|
| 707 |
+
this.editingTask.token = this.generateRandomToken();
|
| 708 |
+
console.log('Generated new token for adhoc task:', this.editingTask.token);
|
| 709 |
+
}
|
| 710 |
+
|
| 711 |
+
console.log('Setting token in taskData:', this.editingTask.token);
|
| 712 |
taskData.token = this.editingTask.token;
|
| 713 |
+
|
| 714 |
+
// Don't send schedule or plan for adhoc tasks
|
| 715 |
+
delete taskData.schedule;
|
| 716 |
+
delete taskData.plan;
|
| 717 |
+
} else if (this.editingTask.type === 'planned') {
|
| 718 |
+
// Planned task with plan
|
| 719 |
+
// Make sure plan exists and has required properties
|
| 720 |
+
if (!this.editingTask.plan) {
|
| 721 |
+
this.editingTask.plan = {
|
| 722 |
+
todo: [],
|
| 723 |
+
in_progress: null,
|
| 724 |
+
done: []
|
| 725 |
+
};
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
+
// Ensure todo and done are arrays
|
| 729 |
+
if (!Array.isArray(this.editingTask.plan.todo)) {
|
| 730 |
+
this.editingTask.plan.todo = [];
|
| 731 |
+
}
|
| 732 |
+
|
| 733 |
+
if (!Array.isArray(this.editingTask.plan.done)) {
|
| 734 |
+
this.editingTask.plan.done = [];
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
+
// Validate each date in the todo list to ensure it's a valid ISO string
|
| 738 |
+
const validatedTodo = [];
|
| 739 |
+
for (const dateStr of this.editingTask.plan.todo) {
|
| 740 |
+
try {
|
| 741 |
+
const date = new Date(dateStr);
|
| 742 |
+
if (!isNaN(date.getTime())) {
|
| 743 |
+
validatedTodo.push(date.toISOString());
|
| 744 |
+
} else {
|
| 745 |
+
console.warn(`Skipping invalid date in todo list: ${dateStr}`);
|
| 746 |
+
}
|
| 747 |
+
} catch (error) {
|
| 748 |
+
console.warn(`Error processing date: ${error.message}`);
|
| 749 |
+
}
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
+
// Replace with validated list
|
| 753 |
+
this.editingTask.plan.todo = validatedTodo;
|
| 754 |
+
|
| 755 |
+
// Sort the todo items by date (earliest first)
|
| 756 |
+
this.editingTask.plan.todo.sort();
|
| 757 |
+
|
| 758 |
+
// Set the plan in taskData
|
| 759 |
+
taskData.plan = {
|
| 760 |
+
todo: this.editingTask.plan.todo,
|
| 761 |
+
in_progress: this.editingTask.plan.in_progress,
|
| 762 |
+
done: this.editingTask.plan.done || []
|
| 763 |
+
};
|
| 764 |
+
|
| 765 |
+
// Log the plan data for debugging
|
| 766 |
+
console.log('Planned task plan data:', JSON.stringify(taskData.plan, null, 2));
|
| 767 |
+
|
| 768 |
+
// Don't send schedule or token for planned tasks
|
| 769 |
delete taskData.schedule;
|
| 770 |
+
delete taskData.token;
|
| 771 |
}
|
| 772 |
|
| 773 |
// Determine if creating or updating
|
|
|
|
| 778 |
taskData.task_id = this.editingTask.uuid;
|
| 779 |
}
|
| 780 |
|
| 781 |
+
// Debug: Log the final task data being sent
|
| 782 |
+
console.log('Final task data being sent to API:', JSON.stringify(taskData, null, 2));
|
| 783 |
+
|
| 784 |
// Make API request
|
| 785 |
const response = await fetch(apiEndpoint, {
|
| 786 |
method: 'POST',
|
|
|
|
| 795 |
throw new Error(errorData.error || 'Failed to save task');
|
| 796 |
}
|
| 797 |
|
| 798 |
+
// Parse response data to get the created/updated task
|
| 799 |
+
const responseData = await response.json();
|
| 800 |
+
|
| 801 |
// Show success message
|
| 802 |
showToast(this.isCreating ? 'Task created successfully' : 'Task updated successfully', 'success');
|
| 803 |
|
| 804 |
+
// Immediately update the UI if the response includes the task
|
| 805 |
+
if (responseData && responseData.task) {
|
| 806 |
+
console.log('Task received in response:', responseData.task);
|
| 807 |
+
|
| 808 |
+
// Update the tasks array
|
| 809 |
+
if (this.isCreating) {
|
| 810 |
+
// For new tasks, add to the array
|
| 811 |
+
this.tasks = [...this.tasks, responseData.task];
|
| 812 |
+
} else {
|
| 813 |
+
// For updated tasks, replace the existing one
|
| 814 |
+
this.tasks = this.tasks.map(t =>
|
| 815 |
+
t.uuid === responseData.task.uuid ? responseData.task : t
|
| 816 |
+
);
|
| 817 |
+
}
|
| 818 |
+
|
| 819 |
+
// Update UI using the shared function
|
| 820 |
+
this.updateTasksUI();
|
| 821 |
+
} else {
|
| 822 |
+
// Fallback to fetching tasks if no task in response
|
| 823 |
+
await this.fetchTasks();
|
| 824 |
+
}
|
| 825 |
|
| 826 |
+
// Clean up Flatpickr instances
|
| 827 |
+
const destroyFlatpickr = (inputId) => {
|
| 828 |
+
const input = document.getElementById(inputId);
|
| 829 |
+
if (input && input._flatpickr) {
|
| 830 |
+
input._flatpickr.destroy();
|
| 831 |
+
}
|
| 832 |
+
};
|
| 833 |
+
|
| 834 |
+
if (this.isCreating) {
|
| 835 |
+
destroyFlatpickr('newPlannedTime-create');
|
| 836 |
+
} else if (this.isEditing) {
|
| 837 |
+
destroyFlatpickr('newPlannedTime-edit');
|
| 838 |
+
}
|
| 839 |
+
|
| 840 |
+
// Reset task data and form state
|
| 841 |
+
this.editingTask = {
|
| 842 |
+
name: '',
|
| 843 |
+
type: 'scheduled',
|
| 844 |
+
state: 'idle',
|
| 845 |
+
schedule: {
|
| 846 |
+
minute: '*',
|
| 847 |
+
hour: '*',
|
| 848 |
+
day: '*',
|
| 849 |
+
month: '*',
|
| 850 |
+
weekday: '*',
|
| 851 |
+
timezone: getUserTimezone()
|
| 852 |
+
},
|
| 853 |
+
token: '',
|
| 854 |
+
plan: {
|
| 855 |
+
todo: [],
|
| 856 |
+
in_progress: null,
|
| 857 |
+
done: []
|
| 858 |
+
},
|
| 859 |
+
system_prompt: '',
|
| 860 |
+
prompt: '',
|
| 861 |
+
attachments: []
|
| 862 |
+
};
|
| 863 |
+
this.isCreating = false;
|
| 864 |
+
this.isEditing = false;
|
| 865 |
document.querySelector('[x-data="schedulerSettings"]')?.removeAttribute('data-editing-state');
|
| 866 |
} catch (error) {
|
| 867 |
console.error('Error saving task:', error);
|
|
|
|
| 877 |
headers: {
|
| 878 |
'Content-Type': 'application/json'
|
| 879 |
},
|
| 880 |
+
body: JSON.stringify({
|
| 881 |
+
task_id: taskId,
|
| 882 |
+
timezone: getUserTimezone()
|
| 883 |
+
})
|
| 884 |
});
|
| 885 |
|
| 886 |
if (!response.ok) {
|
|
|
|
| 923 |
},
|
| 924 |
body: JSON.stringify({
|
| 925 |
task_id: taskId,
|
| 926 |
+
state: 'idle', // Always reset to idle state
|
| 927 |
+
timezone: getUserTimezone()
|
| 928 |
})
|
| 929 |
});
|
| 930 |
|
|
|
|
| 958 |
headers: {
|
| 959 |
'Content-Type': 'application/json'
|
| 960 |
},
|
| 961 |
+
body: JSON.stringify({
|
| 962 |
+
task_id: taskId,
|
| 963 |
+
timezone: getUserTimezone()
|
| 964 |
+
})
|
| 965 |
});
|
| 966 |
|
| 967 |
if (!response.ok) {
|
|
|
|
| 971 |
|
| 972 |
showToast('Task deleted successfully', 'success');
|
| 973 |
|
| 974 |
+
// If we were viewing the detail of the deleted task, close the detail view
|
| 975 |
+
if (this.selectedTaskForDetail && this.selectedTaskForDetail.uuid === taskId) {
|
| 976 |
+
this.closeTaskDetail();
|
|
|
|
|
|
|
|
|
|
| 977 |
}
|
| 978 |
+
|
| 979 |
+
// Immediately update UI without waiting for polling
|
| 980 |
+
this.tasks = this.tasks.filter(t => t.uuid !== taskId);
|
| 981 |
+
|
| 982 |
+
// Update UI using the shared function
|
| 983 |
+
this.updateTasksUI();
|
| 984 |
} catch (error) {
|
| 985 |
console.error('Error deleting task:', error);
|
| 986 |
showToast('Failed to delete task: ' + error.message, 'error');
|
| 987 |
}
|
| 988 |
},
|
| 989 |
|
| 990 |
+
// Initialize datetime input with default value (30 minutes from now)
|
| 991 |
+
initDateTimeInput(event) {
|
| 992 |
+
if (!event.target.value) {
|
| 993 |
+
const now = new Date();
|
| 994 |
+
now.setMinutes(now.getMinutes() + 30);
|
| 995 |
+
|
| 996 |
+
// Format as YYYY-MM-DDThh:mm
|
| 997 |
+
const year = now.getFullYear();
|
| 998 |
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
| 999 |
+
const day = String(now.getDate()).padStart(2, '0');
|
| 1000 |
+
const hours = String(now.getHours()).padStart(2, '0');
|
| 1001 |
+
const minutes = String(now.getMinutes()).padStart(2, '0');
|
| 1002 |
+
|
| 1003 |
+
event.target.value = `${year}-${month}-${day}T${hours}:${minutes}`;
|
| 1004 |
+
|
| 1005 |
+
// If using Flatpickr, update it as well
|
| 1006 |
+
if (event.target._flatpickr) {
|
| 1007 |
+
event.target._flatpickr.setDate(event.target.value);
|
| 1008 |
+
}
|
| 1009 |
+
}
|
| 1010 |
+
},
|
| 1011 |
+
|
| 1012 |
// Generate a random token for ad-hoc tasks
|
| 1013 |
generateRandomToken() {
|
| 1014 |
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
|
|
| 1019 |
return token;
|
| 1020 |
},
|
| 1021 |
|
| 1022 |
+
// Getter for filtered tasks
|
| 1023 |
+
get filteredTasks() {
|
| 1024 |
+
// Make sure we have tasks to filter
|
| 1025 |
+
if (!Array.isArray(this.tasks)) {
|
| 1026 |
+
console.warn('Tasks is not an array:', this.tasks);
|
| 1027 |
+
return [];
|
| 1028 |
+
}
|
| 1029 |
+
|
| 1030 |
+
let filtered = [...this.tasks];
|
| 1031 |
+
|
| 1032 |
+
// Apply type filter with case-insensitive comparison
|
| 1033 |
+
if (this.filterType && this.filterType !== 'all') {
|
| 1034 |
+
filtered = filtered.filter(task => {
|
| 1035 |
+
if (!task.type) return false;
|
| 1036 |
+
return String(task.type).toLowerCase() === this.filterType.toLowerCase();
|
| 1037 |
+
});
|
| 1038 |
+
}
|
| 1039 |
+
|
| 1040 |
+
// Apply state filter with case-insensitive comparison
|
| 1041 |
+
if (this.filterState && this.filterState !== 'all') {
|
| 1042 |
+
filtered = filtered.filter(task => {
|
| 1043 |
+
if (!task.state) return false;
|
| 1044 |
+
return String(task.state).toLowerCase() === this.filterState.toLowerCase();
|
| 1045 |
+
});
|
| 1046 |
+
}
|
| 1047 |
+
|
| 1048 |
+
// Sort the filtered tasks
|
| 1049 |
+
return this.sortTasks(filtered);
|
| 1050 |
+
},
|
| 1051 |
+
|
| 1052 |
+
// Sort the tasks based on sort field and direction
|
| 1053 |
+
sortTasks(tasks) {
|
| 1054 |
+
if (!Array.isArray(tasks) || tasks.length === 0) {
|
| 1055 |
+
return tasks;
|
| 1056 |
+
}
|
| 1057 |
+
|
| 1058 |
+
return [...tasks].sort((a, b) => {
|
| 1059 |
+
if (!this.sortField) return 0;
|
| 1060 |
+
|
| 1061 |
+
const fieldA = a[this.sortField];
|
| 1062 |
+
const fieldB = b[this.sortField];
|
| 1063 |
+
|
| 1064 |
+
// Handle cases where fields might be undefined
|
| 1065 |
+
if (fieldA === undefined && fieldB === undefined) return 0;
|
| 1066 |
+
if (fieldA === undefined) return 1;
|
| 1067 |
+
if (fieldB === undefined) return -1;
|
| 1068 |
+
|
| 1069 |
+
// For dates, convert to timestamps
|
| 1070 |
+
if (this.sortField === 'createdAt' || this.sortField === 'updatedAt') {
|
| 1071 |
+
const dateA = new Date(fieldA).getTime();
|
| 1072 |
+
const dateB = new Date(fieldB).getTime();
|
| 1073 |
+
return this.sortDirection === 'asc' ? dateA - dateB : dateB - dateA;
|
| 1074 |
+
}
|
| 1075 |
+
|
| 1076 |
+
// For string comparisons
|
| 1077 |
+
if (typeof fieldA === 'string' && typeof fieldB === 'string') {
|
| 1078 |
+
return this.sortDirection === 'asc'
|
| 1079 |
+
? fieldA.localeCompare(fieldB)
|
| 1080 |
+
: fieldB.localeCompare(fieldA);
|
| 1081 |
+
}
|
| 1082 |
+
|
| 1083 |
+
// For numerical comparisons
|
| 1084 |
+
return this.sortDirection === 'asc' ? fieldA - fieldB : fieldB - fieldA;
|
| 1085 |
+
});
|
| 1086 |
+
},
|
| 1087 |
+
|
| 1088 |
// Computed property for attachments text representation
|
| 1089 |
get attachmentsText() {
|
| 1090 |
// Ensure we always have an array to work with
|
|
|
|
| 1092 |
? this.editingTask.attachments
|
| 1093 |
: [];
|
| 1094 |
|
|
|
|
|
|
|
| 1095 |
// Join array items with newlines
|
| 1096 |
return attachments.join('\n');
|
| 1097 |
},
|
| 1098 |
|
| 1099 |
// Setter for attachments text - preserves empty lines during editing
|
| 1100 |
set attachmentsText(value) {
|
|
|
|
|
|
|
| 1101 |
if (typeof value === 'string') {
|
| 1102 |
// Just split by newlines without filtering to preserve editing experience
|
| 1103 |
this.editingTask.attachments = value.split('\n');
|
|
|
|
| 1105 |
// Fallback to empty array if not a string
|
| 1106 |
this.editingTask.attachments = [];
|
| 1107 |
}
|
| 1108 |
+
},
|
| 1109 |
+
|
| 1110 |
+
// Debug method to test filtering logic
|
| 1111 |
+
testFiltering() {
|
| 1112 |
+
console.group('SchedulerSettings Debug: Filter Test');
|
| 1113 |
+
console.log('Current Filter Settings:');
|
| 1114 |
+
console.log('- Filter Type:', this.filterType);
|
| 1115 |
+
console.log('- Filter State:', this.filterState);
|
| 1116 |
+
console.log('- Sort Field:', this.sortField);
|
| 1117 |
+
console.log('- Sort Direction:', this.sortDirection);
|
| 1118 |
+
|
| 1119 |
+
// Check if tasks is an array
|
| 1120 |
+
if (!Array.isArray(this.tasks)) {
|
| 1121 |
+
console.error('ERROR: this.tasks is not an array!', this.tasks);
|
| 1122 |
+
console.groupEnd();
|
| 1123 |
+
return;
|
| 1124 |
+
}
|
| 1125 |
+
|
| 1126 |
+
console.log(`Raw Tasks (${this.tasks.length}):`, this.tasks);
|
| 1127 |
+
|
| 1128 |
+
// Test filtering by type
|
| 1129 |
+
console.group('Filter by Type Test');
|
| 1130 |
+
['all', 'adhoc', 'scheduled', 'recurring'].forEach(type => {
|
| 1131 |
+
const filtered = this.tasks.filter(task =>
|
| 1132 |
+
type === 'all' ||
|
| 1133 |
+
(task.type && String(task.type).toLowerCase() === type)
|
| 1134 |
+
);
|
| 1135 |
+
console.log(`Type "${type}": ${filtered.length} tasks`, filtered);
|
| 1136 |
+
});
|
| 1137 |
+
console.groupEnd();
|
| 1138 |
+
|
| 1139 |
+
// Test filtering by state
|
| 1140 |
+
console.group('Filter by State Test');
|
| 1141 |
+
['all', 'idle', 'running', 'completed', 'failed'].forEach(state => {
|
| 1142 |
+
const filtered = this.tasks.filter(task =>
|
| 1143 |
+
state === 'all' ||
|
| 1144 |
+
(task.state && String(task.state).toLowerCase() === state)
|
| 1145 |
+
);
|
| 1146 |
+
console.log(`State "${state}": ${filtered.length} tasks`, filtered);
|
| 1147 |
+
});
|
| 1148 |
+
console.groupEnd();
|
| 1149 |
+
|
| 1150 |
+
// Show current filtered tasks
|
| 1151 |
+
console.log('Current Filtered Tasks:', this.filteredTasks);
|
| 1152 |
+
|
| 1153 |
+
console.groupEnd();
|
| 1154 |
+
},
|
| 1155 |
+
|
| 1156 |
+
// New comprehensive debug method
|
| 1157 |
+
debugTasks() {
|
| 1158 |
+
console.group('SchedulerSettings Comprehensive Debug');
|
| 1159 |
+
|
| 1160 |
+
// Component state
|
| 1161 |
+
console.log('Component State:');
|
| 1162 |
+
console.log({
|
| 1163 |
+
filterType: this.filterType,
|
| 1164 |
+
filterState: this.filterState,
|
| 1165 |
+
sortField: this.sortField,
|
| 1166 |
+
sortDirection: this.sortDirection,
|
| 1167 |
+
isLoading: this.isLoading,
|
| 1168 |
+
isEditing: this.isEditing,
|
| 1169 |
+
isCreating: this.isCreating,
|
| 1170 |
+
viewMode: this.viewMode
|
| 1171 |
+
});
|
| 1172 |
+
|
| 1173 |
+
// Tasks validation
|
| 1174 |
+
if (!this.tasks) {
|
| 1175 |
+
console.error('ERROR: this.tasks is undefined or null!');
|
| 1176 |
+
console.groupEnd();
|
| 1177 |
+
return;
|
| 1178 |
+
}
|
| 1179 |
+
|
| 1180 |
+
if (!Array.isArray(this.tasks)) {
|
| 1181 |
+
console.error('ERROR: this.tasks is not an array!', typeof this.tasks, this.tasks);
|
| 1182 |
+
console.groupEnd();
|
| 1183 |
+
return;
|
| 1184 |
+
}
|
| 1185 |
+
|
| 1186 |
+
// Raw tasks
|
| 1187 |
+
console.group('Raw Tasks');
|
| 1188 |
+
console.log(`Count: ${this.tasks.length}`);
|
| 1189 |
+
if (this.tasks.length > 0) {
|
| 1190 |
+
console.table(this.tasks.map(t => ({
|
| 1191 |
+
uuid: t.uuid,
|
| 1192 |
+
name: t.name,
|
| 1193 |
+
type: t.type,
|
| 1194 |
+
state: t.state
|
| 1195 |
+
})));
|
| 1196 |
+
|
| 1197 |
+
// Inspect first task in detail
|
| 1198 |
+
console.log('First Task Structure:', JSON.stringify(this.tasks[0], null, 2));
|
| 1199 |
+
} else {
|
| 1200 |
+
console.log('No tasks available');
|
| 1201 |
+
}
|
| 1202 |
+
console.groupEnd();
|
| 1203 |
+
|
| 1204 |
+
// Filtered tasks
|
| 1205 |
+
console.group('Filtered Tasks');
|
| 1206 |
+
const filteredTasks = this.filteredTasks;
|
| 1207 |
+
console.log(`Count: ${filteredTasks.length}`);
|
| 1208 |
+
if (filteredTasks.length > 0) {
|
| 1209 |
+
console.table(filteredTasks.map(t => ({
|
| 1210 |
+
uuid: t.uuid,
|
| 1211 |
+
name: t.name,
|
| 1212 |
+
type: t.type,
|
| 1213 |
+
state: t.state
|
| 1214 |
+
})));
|
| 1215 |
+
} else {
|
| 1216 |
+
console.log('No filtered tasks');
|
| 1217 |
+
}
|
| 1218 |
+
console.groupEnd();
|
| 1219 |
+
|
| 1220 |
+
// Check for potential issues
|
| 1221 |
+
console.group('Potential Issues');
|
| 1222 |
+
|
| 1223 |
+
// Check for case mismatches
|
| 1224 |
+
if (this.tasks.length > 0 && filteredTasks.length === 0) {
|
| 1225 |
+
console.warn('Filter seems to exclude all tasks. Checking why:');
|
| 1226 |
+
|
| 1227 |
+
// Check type values
|
| 1228 |
+
const uniqueTypes = [...new Set(this.tasks.map(t => t.type))];
|
| 1229 |
+
console.log('Unique task types in data:', uniqueTypes);
|
| 1230 |
+
|
| 1231 |
+
// Check state values
|
| 1232 |
+
const uniqueStates = [...new Set(this.tasks.map(t => t.state))];
|
| 1233 |
+
console.log('Unique task states in data:', uniqueStates);
|
| 1234 |
+
|
| 1235 |
+
// Check for exact mismatches
|
| 1236 |
+
if (this.filterType !== 'all') {
|
| 1237 |
+
const typeMatch = this.tasks.some(t =>
|
| 1238 |
+
t.type && String(t.type).toLowerCase() === this.filterType.toLowerCase()
|
| 1239 |
+
);
|
| 1240 |
+
console.log(`Type "${this.filterType}" matches found:`, typeMatch);
|
| 1241 |
+
}
|
| 1242 |
+
|
| 1243 |
+
if (this.filterState !== 'all') {
|
| 1244 |
+
const stateMatch = this.tasks.some(t =>
|
| 1245 |
+
t.state && String(t.state).toLowerCase() === this.filterState.toLowerCase()
|
| 1246 |
+
);
|
| 1247 |
+
console.log(`State "${this.filterState}" matches found:`, stateMatch);
|
| 1248 |
+
}
|
| 1249 |
+
}
|
| 1250 |
+
|
| 1251 |
+
// Check for undefined or null values
|
| 1252 |
+
const hasUndefinedType = this.tasks.some(t => t.type === undefined || t.type === null);
|
| 1253 |
+
const hasUndefinedState = this.tasks.some(t => t.state === undefined || t.state === null);
|
| 1254 |
+
|
| 1255 |
+
if (hasUndefinedType) {
|
| 1256 |
+
console.warn('Some tasks have undefined or null type values!');
|
| 1257 |
+
}
|
| 1258 |
+
|
| 1259 |
+
if (hasUndefinedState) {
|
| 1260 |
+
console.warn('Some tasks have undefined or null state values!');
|
| 1261 |
+
}
|
| 1262 |
+
|
| 1263 |
+
console.groupEnd();
|
| 1264 |
+
|
| 1265 |
+
console.groupEnd();
|
| 1266 |
+
},
|
| 1267 |
+
|
| 1268 |
+
// Initialize Flatpickr datetime pickers for both create and edit forms
|
| 1269 |
+
/**
|
| 1270 |
+
* Initialize Flatpickr date/time pickers for scheduler forms
|
| 1271 |
+
*
|
| 1272 |
+
* @param {string} mode - Which pickers to initialize: 'all', 'create', or 'edit'
|
| 1273 |
+
* @returns {void}
|
| 1274 |
+
*/
|
| 1275 |
+
initFlatpickr(mode = 'all') {
|
| 1276 |
+
const initPicker = (inputId, refName, wrapperClass, options = {}) => {
|
| 1277 |
+
// Try to get input using Alpine.js x-ref first (more reliable)
|
| 1278 |
+
let input = this.$refs[refName];
|
| 1279 |
+
|
| 1280 |
+
// Fall back to getElementById if x-ref is not available
|
| 1281 |
+
if (!input) {
|
| 1282 |
+
input = document.getElementById(inputId);
|
| 1283 |
+
console.log(`Using getElementById fallback for ${inputId}`);
|
| 1284 |
+
}
|
| 1285 |
+
|
| 1286 |
+
if (!input) {
|
| 1287 |
+
console.warn(`Input element ${inputId} not found by ID or ref`);
|
| 1288 |
+
return null;
|
| 1289 |
+
}
|
| 1290 |
+
|
| 1291 |
+
// Create a wrapper around the input
|
| 1292 |
+
const wrapper = document.createElement('div');
|
| 1293 |
+
wrapper.className = wrapperClass || 'scheduler-flatpickr-wrapper';
|
| 1294 |
+
wrapper.style.overflow = 'visible'; // Ensure dropdown can escape container
|
| 1295 |
+
|
| 1296 |
+
// Replace the input with our wrapped version
|
| 1297 |
+
input.parentNode.insertBefore(wrapper, input);
|
| 1298 |
+
wrapper.appendChild(input);
|
| 1299 |
+
input.classList.add('scheduler-flatpickr-input');
|
| 1300 |
+
|
| 1301 |
+
// Default options
|
| 1302 |
+
const defaultOptions = {
|
| 1303 |
+
dateFormat: "Y-m-d H:i",
|
| 1304 |
+
enableTime: true,
|
| 1305 |
+
time_24hr: true,
|
| 1306 |
+
static: false, // Not static so it will float
|
| 1307 |
+
appendTo: document.body, // Append to body to avoid overflow issues
|
| 1308 |
+
theme: "scheduler-theme",
|
| 1309 |
+
allowInput: true,
|
| 1310 |
+
positionElement: wrapper, // Position relative to wrapper
|
| 1311 |
+
onOpen: function(selectedDates, dateStr, instance) {
|
| 1312 |
+
// Ensure calendar is properly positioned and visible
|
| 1313 |
+
instance.calendarContainer.style.zIndex = '9999';
|
| 1314 |
+
instance.calendarContainer.style.position = 'absolute';
|
| 1315 |
+
instance.calendarContainer.style.visibility = 'visible';
|
| 1316 |
+
instance.calendarContainer.style.opacity = '1';
|
| 1317 |
+
|
| 1318 |
+
// Add class to calendar container for our custom styling
|
| 1319 |
+
instance.calendarContainer.classList.add('scheduler-theme');
|
| 1320 |
+
},
|
| 1321 |
+
// Set default date to 30 minutes from now if no date selected
|
| 1322 |
+
onReady: function(selectedDates, dateStr, instance) {
|
| 1323 |
+
if (!dateStr) {
|
| 1324 |
+
const now = new Date();
|
| 1325 |
+
now.setMinutes(now.getMinutes() + 30);
|
| 1326 |
+
instance.setDate(now, true);
|
| 1327 |
+
}
|
| 1328 |
+
}
|
| 1329 |
+
};
|
| 1330 |
+
|
| 1331 |
+
// Merge options
|
| 1332 |
+
const mergedOptions = {...defaultOptions, ...options};
|
| 1333 |
+
|
| 1334 |
+
// Initialize flatpickr
|
| 1335 |
+
const fp = flatpickr(input, mergedOptions);
|
| 1336 |
+
|
| 1337 |
+
// Add a clear button
|
| 1338 |
+
const clearButton = document.createElement('button');
|
| 1339 |
+
clearButton.className = 'scheduler-flatpickr-clear';
|
| 1340 |
+
clearButton.innerHTML = '×';
|
| 1341 |
+
clearButton.type = 'button';
|
| 1342 |
+
clearButton.addEventListener('click', (e) => {
|
| 1343 |
+
e.preventDefault();
|
| 1344 |
+
e.stopPropagation();
|
| 1345 |
+
if (fp) {
|
| 1346 |
+
fp.clear();
|
| 1347 |
+
}
|
| 1348 |
+
});
|
| 1349 |
+
wrapper.appendChild(clearButton);
|
| 1350 |
+
|
| 1351 |
+
return fp;
|
| 1352 |
+
};
|
| 1353 |
+
|
| 1354 |
+
// Clear any existing Flatpickr instances to prevent duplication
|
| 1355 |
+
if (mode === 'all' || mode === 'create') {
|
| 1356 |
+
const createInput = document.getElementById('newPlannedTime-create');
|
| 1357 |
+
if (createInput && createInput._flatpickr) {
|
| 1358 |
+
createInput._flatpickr.destroy();
|
| 1359 |
+
}
|
| 1360 |
+
}
|
| 1361 |
+
|
| 1362 |
+
if (mode === 'all' || mode === 'edit') {
|
| 1363 |
+
const editInput = document.getElementById('newPlannedTime-edit');
|
| 1364 |
+
if (editInput && editInput._flatpickr) {
|
| 1365 |
+
editInput._flatpickr.destroy();
|
| 1366 |
+
}
|
| 1367 |
+
}
|
| 1368 |
+
|
| 1369 |
+
// Initialize new instances
|
| 1370 |
+
if (mode === 'all' || mode === 'create') {
|
| 1371 |
+
initPicker('newPlannedTime-create', 'plannedTimeCreate', 'scheduler-flatpickr-wrapper', {
|
| 1372 |
+
minuteIncrement: 5,
|
| 1373 |
+
defaultHour: new Date().getHours(),
|
| 1374 |
+
defaultMinute: Math.ceil(new Date().getMinutes() / 5) * 5
|
| 1375 |
+
});
|
| 1376 |
+
}
|
| 1377 |
|
| 1378 |
+
if (mode === 'all' || mode === 'edit') {
|
| 1379 |
+
initPicker('newPlannedTime-edit', 'plannedTimeEdit', 'scheduler-flatpickr-wrapper', {
|
| 1380 |
+
minuteIncrement: 5,
|
| 1381 |
+
defaultHour: new Date().getHours(),
|
| 1382 |
+
defaultMinute: Math.ceil(new Date().getMinutes() / 5) * 5
|
| 1383 |
+
});
|
| 1384 |
+
}
|
| 1385 |
+
},
|
| 1386 |
+
|
| 1387 |
+
// Update tasks UI
|
| 1388 |
+
updateTasksUI() {
|
| 1389 |
+
// First update filteredTasks if that method exists
|
| 1390 |
+
if (typeof this.updateFilteredTasks === 'function') {
|
| 1391 |
+
this.updateFilteredTasks();
|
| 1392 |
+
}
|
| 1393 |
+
|
| 1394 |
+
// Wait for UI to update
|
| 1395 |
+
this.$nextTick(() => {
|
| 1396 |
+
// Get empty state and task list elements
|
| 1397 |
+
const emptyElement = document.querySelector('.scheduler-empty');
|
| 1398 |
+
const tableElement = document.querySelector('.scheduler-task-list');
|
| 1399 |
+
|
| 1400 |
+
// Calculate visibility state based on filtered tasks
|
| 1401 |
+
const hasFilteredTasks = Array.isArray(this.filteredTasks) && this.filteredTasks.length > 0;
|
| 1402 |
+
|
| 1403 |
+
// Update visibility directly
|
| 1404 |
+
if (emptyElement) {
|
| 1405 |
+
emptyElement.style.display = !hasFilteredTasks ? '' : 'none';
|
| 1406 |
+
}
|
| 1407 |
+
|
| 1408 |
+
if (tableElement) {
|
| 1409 |
+
tableElement.style.display = hasFilteredTasks ? '' : 'none';
|
| 1410 |
+
}
|
| 1411 |
+
});
|
| 1412 |
+
}
|
| 1413 |
+
};
|
| 1414 |
+
};
|
| 1415 |
+
|
| 1416 |
+
// Only define the component if it doesn't already exist or extend the existing one
|
| 1417 |
+
if (!window.schedulerSettings) {
|
| 1418 |
+
console.log('Defining schedulerSettings component from scratch');
|
| 1419 |
+
window.schedulerSettings = fullComponentImplementation;
|
| 1420 |
+
} else {
|
| 1421 |
+
console.log('Extending existing schedulerSettings component');
|
| 1422 |
+
// Store the original function
|
| 1423 |
+
const originalSchedulerSettings = window.schedulerSettings;
|
| 1424 |
+
|
| 1425 |
+
// Replace with enhanced version that merges the pre-initialized stub with the full implementation
|
| 1426 |
+
window.schedulerSettings = function() {
|
| 1427 |
+
// Get the base pre-initialized component
|
| 1428 |
+
const baseComponent = originalSchedulerSettings();
|
| 1429 |
+
|
| 1430 |
+
// Create a backup of the original init function
|
| 1431 |
+
const originalInit = baseComponent.init || function() {};
|
| 1432 |
+
|
| 1433 |
+
// Create our enhanced init function that adds the missing functionality
|
| 1434 |
+
baseComponent.init = function() {
|
| 1435 |
+
// Call the original init if it exists
|
| 1436 |
+
originalInit.call(this);
|
| 1437 |
+
|
| 1438 |
+
console.log('Enhanced init running: adding missing methods to component');
|
| 1439 |
+
|
| 1440 |
+
// Get the full implementation
|
| 1441 |
+
const fullImpl = fullComponentImplementation();
|
| 1442 |
+
|
| 1443 |
+
// Add essential methods directly
|
| 1444 |
+
const essentialMethods = [
|
| 1445 |
+
'fetchTasks', 'startPolling', 'stopPolling',
|
| 1446 |
+
'startCreateTask', 'startEditTask', 'cancelEdit',
|
| 1447 |
+
'saveTask', 'runTask', 'resetTaskState', 'deleteTask',
|
| 1448 |
+
'toggleTaskExpand', 'showTaskDetail', 'closeTaskDetail',
|
| 1449 |
+
'changeSort', 'formatDate', 'formatPlan', 'formatSchedule',
|
| 1450 |
+
'getStateBadgeClass', 'generateRandomToken', 'testFiltering',
|
| 1451 |
+
'debugTasks', 'sortTasks', 'initFlatpickr', 'initDateTimeInput',
|
| 1452 |
+
'updateTasksUI'
|
| 1453 |
+
];
|
| 1454 |
+
|
| 1455 |
+
essentialMethods.forEach(method => {
|
| 1456 |
+
if (typeof this[method] !== 'function' && typeof fullImpl[method] === 'function') {
|
| 1457 |
+
console.log(`Adding missing method: ${method}`);
|
| 1458 |
+
this[method] = fullImpl[method];
|
| 1459 |
+
}
|
| 1460 |
+
});
|
| 1461 |
+
|
| 1462 |
+
// Make sure we have a filteredTasks array initialized
|
| 1463 |
+
this.filteredTasks = [];
|
| 1464 |
+
|
| 1465 |
+
// Initialize essential properties if missing
|
| 1466 |
+
if (!Array.isArray(this.tasks)) {
|
| 1467 |
+
this.tasks = [];
|
| 1468 |
+
}
|
| 1469 |
+
|
| 1470 |
+
// Make sure attachmentsText getter/setter are defined
|
| 1471 |
+
if (!Object.getOwnPropertyDescriptor(this, 'attachmentsText')?.get) {
|
| 1472 |
+
Object.defineProperty(this, 'attachmentsText', {
|
| 1473 |
+
get: function() {
|
| 1474 |
+
// Ensure we always have an array to work with
|
| 1475 |
+
const attachments = Array.isArray(this.editingTask?.attachments)
|
| 1476 |
+
? this.editingTask.attachments
|
| 1477 |
+
: [];
|
| 1478 |
+
|
| 1479 |
+
// Join array items with newlines
|
| 1480 |
+
return attachments.join('\n');
|
| 1481 |
+
},
|
| 1482 |
+
set: function(value) {
|
| 1483 |
+
if (!this.editingTask) {
|
| 1484 |
+
this.editingTask = { attachments: [] };
|
| 1485 |
+
}
|
| 1486 |
+
|
| 1487 |
+
if (typeof value === 'string') {
|
| 1488 |
+
// Just split by newlines without filtering to preserve editing experience
|
| 1489 |
+
this.editingTask.attachments = value.split('\n');
|
| 1490 |
+
} else {
|
| 1491 |
+
// Fallback to empty array if not a string
|
| 1492 |
+
this.editingTask.attachments = [];
|
| 1493 |
+
}
|
| 1494 |
+
}
|
| 1495 |
+
});
|
| 1496 |
+
}
|
| 1497 |
+
|
| 1498 |
+
// Add methods for updating filteredTasks directly
|
| 1499 |
+
if (typeof this.updateFilteredTasks !== 'function') {
|
| 1500 |
+
this.updateFilteredTasks = function() {
|
| 1501 |
+
// Make sure we have tasks to filter
|
| 1502 |
+
if (!Array.isArray(this.tasks)) {
|
| 1503 |
+
this.filteredTasks = [];
|
| 1504 |
+
return;
|
| 1505 |
+
}
|
| 1506 |
+
|
| 1507 |
+
let filtered = [...this.tasks];
|
| 1508 |
+
|
| 1509 |
+
// Apply type filter with case-insensitive comparison
|
| 1510 |
+
if (this.filterType && this.filterType !== 'all') {
|
| 1511 |
+
filtered = filtered.filter(task => {
|
| 1512 |
+
if (!task.type) return false;
|
| 1513 |
+
return String(task.type).toLowerCase() === this.filterType.toLowerCase();
|
| 1514 |
+
});
|
| 1515 |
+
}
|
| 1516 |
+
|
| 1517 |
+
// Apply state filter with case-insensitive comparison
|
| 1518 |
+
if (this.filterState && this.filterState !== 'all') {
|
| 1519 |
+
filtered = filtered.filter(task => {
|
| 1520 |
+
if (!task.state) return false;
|
| 1521 |
+
return String(task.state).toLowerCase() === this.filterState.toLowerCase();
|
| 1522 |
+
});
|
| 1523 |
+
}
|
| 1524 |
+
|
| 1525 |
+
// Sort the filtered tasks
|
| 1526 |
+
if (typeof this.sortTasks === 'function') {
|
| 1527 |
+
filtered = this.sortTasks(filtered);
|
| 1528 |
+
}
|
| 1529 |
+
|
| 1530 |
+
// Directly update the filteredTasks property
|
| 1531 |
+
this.filteredTasks = filtered;
|
| 1532 |
+
};
|
| 1533 |
+
}
|
| 1534 |
+
|
| 1535 |
+
// Set up watchers to update filtered tasks when dependencies change
|
| 1536 |
+
this.$nextTick(() => {
|
| 1537 |
+
// Update filtered tasks when raw tasks change
|
| 1538 |
+
this.$watch('tasks', () => {
|
| 1539 |
+
this.updateFilteredTasks();
|
| 1540 |
+
});
|
| 1541 |
+
|
| 1542 |
+
// Update filtered tasks when filter type changes
|
| 1543 |
+
this.$watch('filterType', () => {
|
| 1544 |
+
this.updateFilteredTasks();
|
| 1545 |
+
});
|
| 1546 |
+
|
| 1547 |
+
// Update filtered tasks when filter state changes
|
| 1548 |
+
this.$watch('filterState', () => {
|
| 1549 |
+
this.updateFilteredTasks();
|
| 1550 |
+
});
|
| 1551 |
+
|
| 1552 |
+
// Update filtered tasks when sort field or direction changes
|
| 1553 |
+
this.$watch('sortField', () => {
|
| 1554 |
+
this.updateFilteredTasks();
|
| 1555 |
+
});
|
| 1556 |
+
|
| 1557 |
+
this.$watch('sortDirection', () => {
|
| 1558 |
+
this.updateFilteredTasks();
|
| 1559 |
+
});
|
| 1560 |
+
|
| 1561 |
+
// Initial update
|
| 1562 |
+
this.updateFilteredTasks();
|
| 1563 |
+
|
| 1564 |
+
// Set up watcher for task type changes to initialize Flatpickr for planned tasks
|
| 1565 |
+
this.$watch('editingTask.type', (newType) => {
|
| 1566 |
+
if (newType === 'planned') {
|
| 1567 |
+
this.$nextTick(() => {
|
| 1568 |
+
// Reinitialize Flatpickr when switching to planned task type
|
| 1569 |
+
if (this.isCreating) {
|
| 1570 |
+
this.initFlatpickr('create');
|
| 1571 |
+
} else if (this.isEditing) {
|
| 1572 |
+
this.initFlatpickr('edit');
|
| 1573 |
+
}
|
| 1574 |
+
});
|
| 1575 |
+
}
|
| 1576 |
+
});
|
| 1577 |
+
|
| 1578 |
+
// Initialize Flatpickr
|
| 1579 |
+
this.$nextTick(() => {
|
| 1580 |
+
if (typeof this.initFlatpickr === 'function') {
|
| 1581 |
+
this.initFlatpickr();
|
| 1582 |
+
} else {
|
| 1583 |
+
console.error('initFlatpickr is not available');
|
| 1584 |
+
}
|
| 1585 |
+
});
|
| 1586 |
+
});
|
| 1587 |
+
|
| 1588 |
+
// Try fetching tasks after a short delay
|
| 1589 |
+
setTimeout(() => {
|
| 1590 |
+
if (typeof this.fetchTasks === 'function') {
|
| 1591 |
+
this.fetchTasks();
|
| 1592 |
+
} else {
|
| 1593 |
+
console.error('fetchTasks still not available after enhancement');
|
| 1594 |
+
}
|
| 1595 |
+
}, 100);
|
| 1596 |
+
|
| 1597 |
+
console.log('Enhanced init complete');
|
| 1598 |
+
};
|
| 1599 |
+
|
| 1600 |
+
return baseComponent;
|
| 1601 |
+
};
|
| 1602 |
+
}
|
| 1603 |
+
|
| 1604 |
+
// Force Alpine.js to register the component immediately
|
| 1605 |
+
if (window.Alpine) {
|
| 1606 |
+
// Alpine is already loaded, register now
|
| 1607 |
+
console.log('Alpine already loaded, registering schedulerSettings component now');
|
| 1608 |
+
window.Alpine.data('schedulerSettings', window.schedulerSettings);
|
| 1609 |
+
} else {
|
| 1610 |
+
// Wait for Alpine to load
|
| 1611 |
+
document.addEventListener('alpine:init', () => {
|
| 1612 |
+
console.log('Alpine:init - immediately registering schedulerSettings component');
|
| 1613 |
+
Alpine.data('schedulerSettings', window.schedulerSettings);
|
| 1614 |
+
});
|
| 1615 |
+
}
|
| 1616 |
+
|
| 1617 |
+
// Add a document ready event handler to ensure the scheduler tab can be clicked on first load
|
| 1618 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 1619 |
+
console.log('DOMContentLoaded - setting up scheduler tab click handler');
|
| 1620 |
+
// Setup scheduler tab click handling
|
| 1621 |
+
const setupSchedulerTab = () => {
|
| 1622 |
+
const settingsModal = document.getElementById('settingsModal');
|
| 1623 |
+
if (!settingsModal) {
|
| 1624 |
+
setTimeout(setupSchedulerTab, 100);
|
| 1625 |
+
return;
|
| 1626 |
}
|
| 1627 |
+
|
| 1628 |
+
// Create a global event listener for clicks on the scheduler tab
|
| 1629 |
+
document.addEventListener('click', function(e) {
|
| 1630 |
+
// Find if the click was on the scheduler tab or its children
|
| 1631 |
+
const schedulerTab = e.target.closest('.settings-tab[title="Task Scheduler"]');
|
| 1632 |
+
if (!schedulerTab) return;
|
| 1633 |
+
|
| 1634 |
+
e.preventDefault();
|
| 1635 |
+
e.stopPropagation();
|
| 1636 |
+
|
| 1637 |
+
// Get the settings modal data
|
| 1638 |
+
try {
|
| 1639 |
+
const modalData = Alpine.$data(settingsModal);
|
| 1640 |
+
if (modalData.activeTab !== 'scheduler') {
|
| 1641 |
+
// Directly call the modal's switchTab method
|
| 1642 |
+
modalData.switchTab('scheduler');
|
| 1643 |
+
}
|
| 1644 |
+
|
| 1645 |
+
// Force start polling and fetch tasks immediately when tab is selected
|
| 1646 |
+
setTimeout(() => {
|
| 1647 |
+
// Get the scheduler component data
|
| 1648 |
+
const schedulerElement = document.querySelector('[x-data="schedulerSettings"]');
|
| 1649 |
+
if (schedulerElement) {
|
| 1650 |
+
const schedulerData = Alpine.$data(schedulerElement);
|
| 1651 |
+
|
| 1652 |
+
// Force fetch tasks and start polling
|
| 1653 |
+
if (typeof schedulerData.fetchTasks === 'function') {
|
| 1654 |
+
schedulerData.fetchTasks();
|
| 1655 |
+
} else {
|
| 1656 |
+
console.error('fetchTasks is not a function on scheduler component');
|
| 1657 |
+
}
|
| 1658 |
+
|
| 1659 |
+
if (typeof schedulerData.startPolling === 'function') {
|
| 1660 |
+
schedulerData.startPolling();
|
| 1661 |
+
} else {
|
| 1662 |
+
console.error('startPolling is not a function on scheduler component');
|
| 1663 |
+
}
|
| 1664 |
+
} else {
|
| 1665 |
+
console.error('Could not find scheduler component element');
|
| 1666 |
+
}
|
| 1667 |
+
}, 100);
|
| 1668 |
+
} catch (err) {
|
| 1669 |
+
console.error('Error handling scheduler tab click:', err);
|
| 1670 |
+
}
|
| 1671 |
+
}, true); // Use capture phase to intercept before Alpine.js handlers
|
| 1672 |
+
};
|
| 1673 |
+
|
| 1674 |
+
// Initialize the tab handling
|
| 1675 |
+
setupSchedulerTab();
|
| 1676 |
});
|
webui/js/settings.js
CHANGED
|
@@ -36,6 +36,36 @@ const settingsModalProxy = {
|
|
| 36 |
if (activeTab) {
|
| 37 |
activeTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
|
| 38 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
}, 10);
|
| 40 |
},
|
| 41 |
|
|
@@ -103,9 +133,25 @@ const settingsModalProxy = {
|
|
| 103 |
}
|
| 104 |
// Debug log
|
| 105 |
const schedulerTab = document.querySelector('.settings-tab[title="Task Scheduler"]');
|
| 106 |
-
|
| 107 |
console.log('Scheduler tab active after direct initialization?',
|
| 108 |
schedulerTab && schedulerTab.classList.contains('active'));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
}, 10); // Small delay just for scrolling
|
| 110 |
|
| 111 |
}, 5); // Keep a minimal delay for modal opening reactivity
|
|
@@ -170,6 +216,9 @@ const settingsModalProxy = {
|
|
| 170 |
this.handleCancel();
|
| 171 |
}
|
| 172 |
|
|
|
|
|
|
|
|
|
|
| 173 |
// First update our component state
|
| 174 |
this.isOpen = false;
|
| 175 |
|
|
@@ -189,6 +238,9 @@ const settingsModalProxy = {
|
|
| 189 |
data: null
|
| 190 |
});
|
| 191 |
|
|
|
|
|
|
|
|
|
|
| 192 |
// First update our component state
|
| 193 |
this.isOpen = false;
|
| 194 |
|
|
@@ -202,6 +254,19 @@ const settingsModalProxy = {
|
|
| 202 |
}
|
| 203 |
},
|
| 204 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
handleFieldButton(field) {
|
| 206 |
console.log(`Button clicked: ${field.action}`);
|
| 207 |
}
|
|
@@ -463,6 +528,16 @@ document.addEventListener('alpine:init', function () {
|
|
| 463 |
},
|
| 464 |
|
| 465 |
closeModal() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 466 |
this.$store.root.isOpen = false;
|
| 467 |
}
|
| 468 |
};
|
|
|
|
| 36 |
if (activeTab) {
|
| 37 |
activeTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
|
| 38 |
}
|
| 39 |
+
|
| 40 |
+
// When switching to the scheduler tab, initialize Flatpickr components
|
| 41 |
+
if (tabName === 'scheduler') {
|
| 42 |
+
console.log('Switching to scheduler tab, initializing Flatpickr');
|
| 43 |
+
const schedulerElement = document.querySelector('[x-data="schedulerSettings"]');
|
| 44 |
+
if (schedulerElement) {
|
| 45 |
+
const schedulerData = Alpine.$data(schedulerElement);
|
| 46 |
+
if (schedulerData) {
|
| 47 |
+
// Start polling
|
| 48 |
+
if (typeof schedulerData.startPolling === 'function') {
|
| 49 |
+
schedulerData.startPolling();
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// Initialize Flatpickr if editing or creating
|
| 53 |
+
if (typeof schedulerData.initFlatpickr === 'function') {
|
| 54 |
+
// Check if we're creating or editing and initialize accordingly
|
| 55 |
+
if (schedulerData.isCreating) {
|
| 56 |
+
schedulerData.initFlatpickr('create');
|
| 57 |
+
} else if (schedulerData.isEditing) {
|
| 58 |
+
schedulerData.initFlatpickr('edit');
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// Force an immediate fetch
|
| 63 |
+
if (typeof schedulerData.fetchTasks === 'function') {
|
| 64 |
+
schedulerData.fetchTasks();
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
}, 10);
|
| 70 |
},
|
| 71 |
|
|
|
|
| 133 |
}
|
| 134 |
// Debug log
|
| 135 |
const schedulerTab = document.querySelector('.settings-tab[title="Task Scheduler"]');
|
| 136 |
+
console.log(`Current active tab after direct set: ${modalAD.activeTab}`);
|
| 137 |
console.log('Scheduler tab active after direct initialization?',
|
| 138 |
schedulerTab && schedulerTab.classList.contains('active'));
|
| 139 |
+
|
| 140 |
+
// Explicitly start polling if we're on the scheduler tab
|
| 141 |
+
if (modalAD.activeTab === 'scheduler') {
|
| 142 |
+
console.log('Settings opened directly to scheduler tab, initializing polling');
|
| 143 |
+
const schedulerElement = document.querySelector('[x-data="schedulerSettings"]');
|
| 144 |
+
if (schedulerElement) {
|
| 145 |
+
const schedulerData = Alpine.$data(schedulerElement);
|
| 146 |
+
if (schedulerData && typeof schedulerData.startPolling === 'function') {
|
| 147 |
+
schedulerData.startPolling();
|
| 148 |
+
// Also force an immediate fetch
|
| 149 |
+
if (typeof schedulerData.fetchTasks === 'function') {
|
| 150 |
+
schedulerData.fetchTasks();
|
| 151 |
+
}
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
}, 10); // Small delay just for scrolling
|
| 156 |
|
| 157 |
}, 5); // Keep a minimal delay for modal opening reactivity
|
|
|
|
| 216 |
this.handleCancel();
|
| 217 |
}
|
| 218 |
|
| 219 |
+
// Stop scheduler polling if it's running
|
| 220 |
+
this.stopSchedulerPolling();
|
| 221 |
+
|
| 222 |
// First update our component state
|
| 223 |
this.isOpen = false;
|
| 224 |
|
|
|
|
| 238 |
data: null
|
| 239 |
});
|
| 240 |
|
| 241 |
+
// Stop scheduler polling if it's running
|
| 242 |
+
this.stopSchedulerPolling();
|
| 243 |
+
|
| 244 |
// First update our component state
|
| 245 |
this.isOpen = false;
|
| 246 |
|
|
|
|
| 254 |
}
|
| 255 |
},
|
| 256 |
|
| 257 |
+
// Add a helper method to stop scheduler polling
|
| 258 |
+
stopSchedulerPolling() {
|
| 259 |
+
// Find the scheduler component and stop polling if it exists
|
| 260 |
+
const schedulerElement = document.querySelector('[x-data="schedulerSettings"]');
|
| 261 |
+
if (schedulerElement) {
|
| 262 |
+
const schedulerData = Alpine.$data(schedulerElement);
|
| 263 |
+
if (schedulerData && typeof schedulerData.stopPolling === 'function') {
|
| 264 |
+
console.log('Stopping scheduler polling on modal close');
|
| 265 |
+
schedulerData.stopPolling();
|
| 266 |
+
}
|
| 267 |
+
}
|
| 268 |
+
},
|
| 269 |
+
|
| 270 |
handleFieldButton(field) {
|
| 271 |
console.log(`Button clicked: ${field.action}`);
|
| 272 |
}
|
|
|
|
| 528 |
},
|
| 529 |
|
| 530 |
closeModal() {
|
| 531 |
+
// Stop scheduler polling before closing the modal
|
| 532 |
+
const schedulerElement = document.querySelector('[x-data="schedulerSettings"]');
|
| 533 |
+
if (schedulerElement) {
|
| 534 |
+
const schedulerData = Alpine.$data(schedulerElement);
|
| 535 |
+
if (schedulerData && typeof schedulerData.stopPolling === 'function') {
|
| 536 |
+
console.log('Stopping scheduler polling on modal close');
|
| 537 |
+
schedulerData.stopPolling();
|
| 538 |
+
}
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
this.$store.root.isOpen = false;
|
| 542 |
}
|
| 543 |
};
|
webui/js/time-utils.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Time utilities for handling UTC to local time conversion
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
/**
|
| 6 |
+
* Convert a UTC ISO string to a local time string
|
| 7 |
+
* @param {string} utcIsoString - UTC time in ISO format
|
| 8 |
+
* @param {Object} options - Formatting options for Intl.DateTimeFormat
|
| 9 |
+
* @returns {string} Formatted local time string
|
| 10 |
+
*/
|
| 11 |
+
export function toLocalTime(utcIsoString, options = {}) {
|
| 12 |
+
if (!utcIsoString) return '';
|
| 13 |
+
|
| 14 |
+
const date = new Date(utcIsoString);
|
| 15 |
+
const defaultOptions = {
|
| 16 |
+
dateStyle: 'medium',
|
| 17 |
+
timeStyle: 'medium'
|
| 18 |
+
};
|
| 19 |
+
|
| 20 |
+
return new Intl.DateTimeFormat(
|
| 21 |
+
undefined, // Use browser's locale
|
| 22 |
+
{ ...defaultOptions, ...options }
|
| 23 |
+
).format(date);
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
/**
|
| 27 |
+
* Convert a local Date object to UTC ISO string
|
| 28 |
+
* @param {Date} date - Date object in local time
|
| 29 |
+
* @returns {string} UTC ISO string
|
| 30 |
+
*/
|
| 31 |
+
export function toUTCISOString(date) {
|
| 32 |
+
if (!date) return '';
|
| 33 |
+
return date.toISOString();
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/**
|
| 37 |
+
* Get current time as UTC ISO string
|
| 38 |
+
* @returns {string} Current UTC time in ISO format
|
| 39 |
+
*/
|
| 40 |
+
export function getCurrentUTCISOString() {
|
| 41 |
+
return new Date().toISOString();
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
/**
|
| 45 |
+
* Format a UTC ISO string for display in local time with configurable format
|
| 46 |
+
* @param {string} utcIsoString - UTC time in ISO format
|
| 47 |
+
* @param {string} format - Format type ('full', 'date', 'time', 'short')
|
| 48 |
+
* @returns {string} Formatted local time string
|
| 49 |
+
*/
|
| 50 |
+
export function formatDateTime(utcIsoString, format = 'full') {
|
| 51 |
+
if (!utcIsoString) return '';
|
| 52 |
+
|
| 53 |
+
const date = new Date(utcIsoString);
|
| 54 |
+
|
| 55 |
+
const formatOptions = {
|
| 56 |
+
full: { dateStyle: 'medium', timeStyle: 'medium' },
|
| 57 |
+
date: { dateStyle: 'medium' },
|
| 58 |
+
time: { timeStyle: 'medium' },
|
| 59 |
+
short: { dateStyle: 'short', timeStyle: 'short' }
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
return toLocalTime(utcIsoString, formatOptions[format] || formatOptions.full);
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
/**
|
| 66 |
+
* Get the user's local timezone name
|
| 67 |
+
* @returns {string} Timezone name (e.g., 'America/New_York')
|
| 68 |
+
*/
|
| 69 |
+
export function getUserTimezone() {
|
| 70 |
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
| 71 |
+
}
|