Implement tasks
This commit is contained in:
0
core/assist/__init__.py
Normal file
0
core/assist/__init__.py
Normal file
13
core/assist/engine.py
Normal file
13
core/assist/engine.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from core.assist.repeat_answer import find_repeat_answer, learn_from_message
|
||||
from core.models import Message
|
||||
from core.tasks.engine import process_inbound_task_intelligence
|
||||
|
||||
|
||||
async def process_inbound_assist(message: Message) -> None:
|
||||
if message is None:
|
||||
return
|
||||
await learn_from_message(message)
|
||||
await find_repeat_answer(message.user, message)
|
||||
await process_inbound_task_intelligence(message)
|
||||
136
core/assist/repeat_answer.py
Normal file
136
core/assist/repeat_answer.py
Normal file
@@ -0,0 +1,136 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
||||
from asgiref.sync import sync_to_async
|
||||
from django.utils import timezone
|
||||
|
||||
from core.models import AnswerMemory, AnswerSuggestionEvent, Message
|
||||
|
||||
_WORD_RE = re.compile(r"[^a-z0-9\s]+", re.IGNORECASE)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class RepeatAnswerSuggestion:
|
||||
answer_memory_id: str
|
||||
answer_text: str
|
||||
score: float
|
||||
|
||||
|
||||
def _normalize_question(text: str) -> str:
|
||||
body = str(text or "").strip().lower()
|
||||
body = _WORD_RE.sub(" ", body)
|
||||
body = re.sub(r"\s+", " ", body).strip()
|
||||
return body
|
||||
|
||||
|
||||
def _fingerprint(text: str) -> str:
|
||||
norm = _normalize_question(text)
|
||||
if not norm:
|
||||
return ""
|
||||
return hashlib.sha1(norm.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _is_question(text: str) -> bool:
|
||||
body = str(text or "").strip()
|
||||
if not body:
|
||||
return False
|
||||
low = body.lower()
|
||||
return body.endswith("?") or low.startswith(("what", "why", "how", "when", "where", "who", "can ", "do ", "did ", "is ", "are "))
|
||||
|
||||
|
||||
def _is_group_channel(message: Message) -> bool:
|
||||
channel = str(getattr(message, "source_chat_id", "") or "").strip().lower()
|
||||
if channel.endswith("@g.us"):
|
||||
return True
|
||||
return str(getattr(message, "source_service", "") or "").strip().lower() == "xmpp" and "conference." in channel
|
||||
|
||||
|
||||
async def learn_from_message(message: Message) -> None:
|
||||
if message is None:
|
||||
return
|
||||
text = str(message.text or "").strip()
|
||||
if not text:
|
||||
return
|
||||
if dict(message.message_meta or {}).get("origin_tag"):
|
||||
return
|
||||
|
||||
# Build memory by linking obvious reply answers to prior questions.
|
||||
if message.reply_to_id and message.reply_to:
|
||||
q_text = str(message.reply_to.text or "").strip()
|
||||
if _is_question(q_text):
|
||||
fp = _fingerprint(q_text)
|
||||
if fp:
|
||||
await sync_to_async(AnswerMemory.objects.create)(
|
||||
user=message.user,
|
||||
service=message.source_service or "web",
|
||||
channel_identifier=message.source_chat_id or "",
|
||||
question_fingerprint=fp,
|
||||
question_text=q_text,
|
||||
answer_message=message,
|
||||
answer_text=text,
|
||||
confidence_meta={"source": "reply_pair"},
|
||||
)
|
||||
|
||||
|
||||
async def find_repeat_answer(user, message: Message) -> RepeatAnswerSuggestion | None:
|
||||
if message is None:
|
||||
return None
|
||||
if not _is_group_channel(message):
|
||||
return None
|
||||
if dict(message.message_meta or {}).get("origin_tag"):
|
||||
return None
|
||||
text = str(message.text or "").strip()
|
||||
if not _is_question(text):
|
||||
return None
|
||||
|
||||
fp = _fingerprint(text)
|
||||
if not fp:
|
||||
return None
|
||||
|
||||
# channel cooldown for repeated suggestions in short windows
|
||||
cooldown_cutoff = timezone.now() - timezone.timedelta(minutes=3)
|
||||
cooldown_exists = await sync_to_async(
|
||||
lambda: AnswerSuggestionEvent.objects.filter(
|
||||
user=user,
|
||||
message__source_service=message.source_service,
|
||||
message__source_chat_id=message.source_chat_id,
|
||||
status="suggested",
|
||||
created_at__gte=cooldown_cutoff,
|
||||
).exists()
|
||||
)()
|
||||
if cooldown_exists:
|
||||
return None
|
||||
|
||||
memory = await sync_to_async(
|
||||
lambda: AnswerMemory.objects.filter(
|
||||
user=user,
|
||||
service=message.source_service or "web",
|
||||
channel_identifier=message.source_chat_id or "",
|
||||
question_fingerprint=fp,
|
||||
)
|
||||
.order_by("-created_at")
|
||||
.first()
|
||||
)()
|
||||
if not memory:
|
||||
return None
|
||||
|
||||
answer = str(memory.answer_text or "").strip()
|
||||
if not answer:
|
||||
return None
|
||||
|
||||
score = 0.99
|
||||
await sync_to_async(AnswerSuggestionEvent.objects.create)(
|
||||
user=user,
|
||||
message=message,
|
||||
status="suggested",
|
||||
candidate_answer=memory,
|
||||
score=score,
|
||||
)
|
||||
return RepeatAnswerSuggestion(
|
||||
answer_memory_id=str(memory.id),
|
||||
answer_text=answer,
|
||||
score=score,
|
||||
)
|
||||
Reference in New Issue
Block a user