ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: интерактивный предпросмотр Voice Tasker

This commit is contained in:
DCCONSTRUCTIONS
2026-04-26 16:14:49 +03:00
parent a13ff3b954
commit 323b4b964e
4 changed files with 801 additions and 209 deletions
@@ -691,6 +691,8 @@ def harden_voice_task_intent(parsed, transcript):
if parsed.get("intent") != "update_task":
return parsed
if parsed.get("target_task_id"):
return parsed
target_memory_ref = normalize_string(parsed.get("target_memory_ref"), 80)
has_explicit_issue_ref = bool(parse_issue_key_reference(target_memory_ref)) or transcript_has_issue_key_reference(
@@ -716,6 +718,9 @@ def harden_voice_task_intent(parsed, transcript):
def voice_task_has_safe_existing_task_anchor(draft, transcript):
if draft.get("target_task_id"):
return True
target_memory_ref = normalize_string(draft.get("target_memory_ref"), 80)
return bool(
parse_issue_key_reference(target_memory_ref)
@@ -971,6 +976,26 @@ def resolve_voice_task_assignee(project, draft):
return serialize_resolved_assignee(best_member, best_score, "assignee_hint")
def resolve_explicit_voice_task_assignees(project, draft):
if "assignee_ids" not in draft:
return None
assignee_ids = draft.get("assignee_ids") if isinstance(draft.get("assignee_ids"), list) else []
if not assignee_ids:
return []
member_by_id = {
str(project_member.member_id): project_member.member
for project_member in get_voice_task_project_assignable_members(project)
}
return [
serialize_resolved_assignee(member_by_id[assignee_id], 1.0, "explicit_assignee_ids")
for assignee_id in assignee_ids
if assignee_id in member_by_id
]
def transcript_requests_all_project_assignees(transcript, draft=None):
normalized = normalize_match_value(transcript)
assignee_hint = normalize_match_value((draft or {}).get("assignee_hint"))
@@ -988,6 +1013,10 @@ def transcript_requests_all_project_assignees(transcript, draft=None):
def resolve_voice_task_assignees(project, draft, transcript=None):
explicit_assignees = resolve_explicit_voice_task_assignees(project, draft)
if explicit_assignees is not None:
return explicit_assignees
if transcript_requests_all_project_assignees(transcript, draft):
return [
serialize_resolved_assignee(project_member.member, 1.0, "all_project_members")
@@ -1081,8 +1110,14 @@ def resolve_voice_task_state(project, draft, allow_default=True):
if not project:
return None
state_hint = draft.get("state_hint")
states = list(State.objects.filter(project=project).order_by("sequence"))
explicit_state_id = normalize_uuid_string(draft.get("state_id"))
if explicit_state_id:
explicit_state = next((state for state in states if str(state.id) == explicit_state_id), None)
if explicit_state:
return serialize_resolved_state(explicit_state, 1.0, "explicit_state_id")
state_hint = draft.get("state_hint")
if state_hint:
best_state = None
best_score = 0.0
@@ -1452,9 +1487,22 @@ def find_latest_voice_task_issue(memory_sessions, project_id=None):
def resolve_voice_task_memory_target(workspace, user, draft, current_session=None, client_context=None, transcript=None):
target_memory_ref = normalize_string(draft.get("target_memory_ref"), 80)
explicit_target_task_id = normalize_uuid_string(draft.get("target_task_id"))
memory_sessions = get_committed_voice_task_memory_sessions(workspace, user, current_session=current_session)
generic_memory_reference = transcript_has_generic_memory_reference(transcript)
if explicit_target_task_id:
target_issue = (
Issue.issue_objects.filter(workspace=workspace, id=explicit_target_task_id)
.select_related("project")
.first()
)
if (
is_voice_task_issue_available(target_issue)
and get_accessible_projects(workspace, user).filter(id=target_issue.project_id).exists()
):
return target_issue, "explicit_target_task_id", None
if target_memory_ref:
target_uuid = None
try:
@@ -1523,6 +1571,8 @@ def voice_task_has_update_fields(draft, resolution):
or draft.get("due_time")
or (draft.get("priority") and draft.get("priority") != "none")
or draft.get("checklist")
or "assignee_ids" in draft
or draft.get("state_id")
or (resolution.get("assignee") and resolution["assignee"]["confidence"] >= VOICE_TASK_ASSIGNEE_MATCH_THRESHOLD)
or (draft.get("state_hint") and resolution.get("state") and resolution["state"]["confidence"] >= VOICE_TASK_STATE_MATCH_THRESHOLD)
or resolution.get("labels")
@@ -1846,7 +1896,13 @@ def build_voice_task_issue_update_payload(issue, draft, resolution, transcript=N
if draft.get("priority") and draft["priority"] != "none":
payload["priority"] = draft["priority"]
if assignees:
if "assignee_ids" in draft:
payload["assignee_ids"] = [
assignee["id"]
for assignee in assignees
if assignee and assignee["confidence"] >= VOICE_TASK_ASSIGNEE_MATCH_THRESHOLD
]
elif assignees:
payload["assignee_ids"] = [
assignee["id"]
for assignee in assignees
@@ -1855,7 +1911,7 @@ def build_voice_task_issue_update_payload(issue, draft, resolution, transcript=N
elif assignee and assignee["confidence"] >= VOICE_TASK_ASSIGNEE_MATCH_THRESHOLD:
payload["assignee_ids"] = [assignee["id"]]
if (draft.get("state_hint") or project_change) and state and state["confidence"] >= VOICE_TASK_STATE_MATCH_THRESHOLD:
if (draft.get("state_hint") or draft.get("state_id") or project_change) and state and state["confidence"] >= VOICE_TASK_STATE_MATCH_THRESHOLD:
payload["state_id"] = state["id"]
if labels:
@@ -2043,6 +2099,29 @@ def normalize_string(value, max_length=None):
return normalized[:max_length] if max_length else normalized
def normalize_uuid_string(value):
normalized = normalize_string(value, 80)
if not normalized:
return None
try:
return str(uuid.UUID(normalized))
except (TypeError, ValueError):
return None
def normalize_uuid_list(value, limit=50):
if not isinstance(value, list):
return []
result = []
for item in value[:limit]:
normalized = normalize_uuid_string(item)
if normalized and normalized not in result:
result.append(normalized)
return result
def normalize_string_list(value, limit=20, item_max_length=120):
if not isinstance(value, list):
return []
@@ -2097,6 +2176,9 @@ def normalize_voice_task_parse(parsed):
normalized = {
"intent": intent,
"target_memory_ref": normalize_string(parsed.get("target_memory_ref"), 80),
"project_id": normalize_uuid_string(parsed.get("project_id")),
"state_id": normalize_uuid_string(parsed.get("state_id")),
"target_task_id": normalize_uuid_string(parsed.get("target_task_id") or parsed.get("target_issue_id")),
"project_hint": normalize_string(parsed.get("project_hint"), 255),
"state_hint": normalize_string(parsed.get("state_hint"), 120),
"assignee_hint": normalize_string(parsed.get("assignee_hint"), 255),
@@ -2115,6 +2197,8 @@ def normalize_voice_task_parse(parsed):
},
"questions": normalize_string_list(parsed.get("questions"), limit=10, item_max_length=255),
}
if "assignee_ids" in parsed:
normalized["assignee_ids"] = normalize_uuid_list(parsed.get("assignee_ids"))
return normalized