Implement 3 plans

This commit is contained in:
2026-03-06 19:38:32 +00:00
parent 49aaed5dec
commit ff66bc9e1f
13 changed files with 1650 additions and 74 deletions

View File

@@ -1,11 +1,13 @@
from __future__ import annotations
import datetime
import re
from asgiref.sync import sync_to_async
from django.conf import settings
from core.clients.transport import send_message_raw
from core.memory.retrieval import retrieve_memories_for_prompt
from core.messaging import ai as ai_runner
from core.models import (
AI,
@@ -43,6 +45,78 @@ _EPIC_CREATE_RE = re.compile(
re.IGNORECASE | re.DOTALL,
)
_EPIC_TOKEN_RE = re.compile(r"\[\s*epic\s*:\s*([^\]]+?)\s*\]", re.IGNORECASE)
_LIST_TASKS_CMD_RE = re.compile(
r"^\s*\.task\s+list\s*$",
re.IGNORECASE,
)
_TASK_SHOW_RE = re.compile(
r"^\s*\.task\s+show\s+#?(?P<reference>[A-Za-z0-9_-]+)\s*$",
re.IGNORECASE,
)
_TASK_COMPLETE_CMD_RE = re.compile(
r"^\s*\.task\s+(?:complete|done|close)\s+#?(?P<reference>[A-Za-z0-9_-]+)\s*$",
re.IGNORECASE,
)
_DUE_ISO_RE = re.compile(
r"\b(?:due|by)\s+(\d{4}-\d{2}-\d{2})\b",
re.IGNORECASE,
)
_DUE_RELATIVE_RE = re.compile(
r"\b(?:due|by)\s+(?P<token>today|tomorrow|monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b",
re.IGNORECASE,
)
_ASSIGNEE_AT_RE = re.compile(r"@([A-Za-z0-9_.-]+)")
_ASSIGNEE_PHRASE_RE = re.compile(
r"\b(?:assign(?:ed)?\s+to|for)\s+([A-Za-z0-9_.-]+)\b",
re.IGNORECASE,
)
_WEEKDAY_MAP = {
"monday": 0,
"tuesday": 1,
"wednesday": 2,
"thursday": 3,
"friday": 4,
"saturday": 5,
"sunday": 6,
}
def _parse_due_date(text: str) -> datetime.date | None:
body = str(text or "")
m = _DUE_ISO_RE.search(body)
if m:
try:
return datetime.date.fromisoformat(m.group(1))
except ValueError:
pass
m = _DUE_RELATIVE_RE.search(body)
if not m:
return None
token = m.group("token").strip().lower()
today = datetime.date.today()
if token == "today":
return today
if token == "tomorrow":
return today + datetime.timedelta(days=1)
target_weekday = _WEEKDAY_MAP.get(token)
if target_weekday is None:
return None
days_ahead = (target_weekday - today.weekday()) % 7
if days_ahead == 0:
days_ahead = 7
return today + datetime.timedelta(days=days_ahead)
def _parse_assignee(text: str) -> str:
body = str(text or "")
m = _ASSIGNEE_AT_RE.search(body)
if m:
return str(m.group(1) or "").strip()
m = _ASSIGNEE_PHRASE_RE.search(body)
if m:
return str(m.group(1) or "").strip()
return ""
def _channel_variants(service: str, channel: str) -> list[str]:
@@ -319,11 +393,22 @@ async def _emit_sync_event(task: DerivedTask, event: DerivedTaskEvent, action: s
cached_epic = task._state.fields_cache.get("epic")
project_name = str(getattr(cached_project, "name", "") or "")
epic_name = str(getattr(cached_epic, "name", "") or "")
memory_context: list = []
try:
memory_context = await sync_to_async(retrieve_memories_for_prompt)(
user_id=int(task.user_id),
query=str(task.title or ""),
limit=10,
)
except Exception:
pass
request_payload = {
"task_id": str(task.id),
"reference_code": str(task.reference_code or ""),
"title": str(task.title or ""),
"external_key": str(task.external_key or ""),
"due_date": task.due_date.isoformat() if task.due_date else "",
"assignee_identifier": str(task.assignee_identifier or ""),
"project_name": project_name,
"epic_name": epic_name,
"source_service": str(task.source_service or ""),
@@ -333,6 +418,7 @@ async def _emit_sync_event(task: DerivedTask, event: DerivedTaskEvent, action: s
"trigger_message_id": str(getattr(event, "source_message_id", "") or getattr(task, "origin_message_id", "") or ""),
"mode": "default",
"payload": event.payload,
"memory_context": memory_context,
}
codex_run = await sync_to_async(CodexRun.objects.create)(
user=task.user,
@@ -439,7 +525,7 @@ async def _handle_scope_task_commands(message: Message, sources: list[ChatTaskSo
return False
body = str(text or "").strip()
source = sources[0]
if _LIST_TASKS_RE.match(body):
if _LIST_TASKS_RE.match(body) or _LIST_TASKS_CMD_RE.match(body):
open_rows = await sync_to_async(list)(
DerivedTask.objects.filter(
user=message.user,
@@ -494,6 +580,64 @@ async def _handle_scope_task_commands(message: Message, sources: list[ChatTaskSo
await _send_scope_message(source, message, f"[task] removed #{ref}: {title}")
return True
show_match = _TASK_SHOW_RE.match(body)
if show_match:
reference = str(show_match.group("reference") or "").strip()
task = await sync_to_async(
lambda: DerivedTask.objects.filter(
user=message.user,
project=source.project,
source_service=source.service,
source_channel=source.channel_identifier,
reference_code=reference,
)
.order_by("-created_at")
.first()
)()
if task is None:
await _send_scope_message(source, message, f"[task] #{reference} not found.")
return True
due_str = f"\ndue: {task.due_date}" if task.due_date else ""
assignee_str = f"\nassignee: {task.assignee_identifier}" if task.assignee_identifier else ""
detail = (
f"[task] #{task.reference_code}: {task.title}"
f"\nstatus: {task.status_snapshot}"
f"{due_str}"
f"{assignee_str}"
)
await _send_scope_message(source, message, detail)
return True
complete_match = _TASK_COMPLETE_CMD_RE.match(body)
if complete_match:
reference = str(complete_match.group("reference") or "").strip()
task = await sync_to_async(
lambda: DerivedTask.objects.filter(
user=message.user,
project=source.project,
source_service=source.service,
source_channel=source.channel_identifier,
reference_code=reference,
)
.order_by("-created_at")
.first()
)()
if task is None:
await _send_scope_message(source, message, f"[task] #{reference} not found.")
return True
task.status_snapshot = "completed"
await sync_to_async(task.save)(update_fields=["status_snapshot"])
event = await sync_to_async(DerivedTaskEvent.objects.create)(
task=task,
event_type="completion_marked",
actor_identifier=str(message.sender_uuid or ""),
source_message=message,
payload={"marker": reference, "command": ".task complete", "via": "chat_command"},
)
await _emit_sync_event(task, event, "complete")
await _send_scope_message(source, message, f"[task] completed #{task.reference_code}: {task.title}")
return True
return False
@@ -543,7 +687,14 @@ def _is_task_command_candidate(text: str) -> bool:
body = str(text or "").strip()
if not body:
return False
if _LIST_TASKS_RE.match(body) or _UNDO_TASK_RE.match(body) or _EPIC_CREATE_RE.match(body):
if (
_LIST_TASKS_RE.match(body)
or _LIST_TASKS_CMD_RE.match(body)
or _TASK_SHOW_RE.match(body)
or _TASK_COMPLETE_CMD_RE.match(body)
or _UNDO_TASK_RE.match(body)
or _EPIC_CREATE_RE.match(body)
):
return True
return _has_task_prefix(body.lower(), ["task:", "todo:"])
@@ -646,6 +797,8 @@ async def process_inbound_task_intelligence(message: Message) -> None:
)
title = await _derive_title_with_flags(cloned_message, flags)
reference = await sync_to_async(_next_reference)(message.user, source.project)
parsed_due_date = _parse_due_date(task_text)
parsed_assignee = _parse_assignee(task_text)
task = await sync_to_async(DerivedTask.objects.create)(
user=message.user,
project=source.project,
@@ -656,6 +809,8 @@ async def process_inbound_task_intelligence(message: Message) -> None:
origin_message=message,
reference_code=reference,
status_snapshot="open",
due_date=parsed_due_date,
assignee_identifier=parsed_assignee,
immutable_payload={"origin_text": text, "task_text": task_text, "flags": flags},
)
event = await sync_to_async(DerivedTaskEvent.objects.create)(