fix: improve maintenance and synchronize live conversations
This commit is contained in:
1 parent
bb541bd212
commit
ef86a5d3af
297 files changed
+2983
-1274
No files matched your search
@@ -14,13 +14,18 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.auth import has_any_permission
|
||||
from app.core.auth import CHAT_VIEW_PERMISSIONS, has_any_permission
|
||||
from app.core.logger import get_logger
|
||||
from app.core.socketio import sio
|
||||
from app.repositories.chat import ChatRepository
|
||||
from app.repositories.session_to_agent import SessionToAgentRepository
|
||||
from app.repositories.user import UserRepository
|
||||
from app.services.chat_notifications import UNASSIGNED_CHAT_VIEWERS
|
||||
|
||||
@@ -28,6 +33,19 @@ from app.services.chat_notifications import UNASSIGNED_CHAT_VIEWERS
|
||||
# updates about any of the org's conversations — including AI-owned ones,
|
||||
# which have no handling agent to address.
|
||||
ORG_INBOX_ROOM_PREFIX = "org_inbox_"
|
||||
ORG_CHAT_UPDATES_PREFIX = "org_chat_updates_"
|
||||
CHAT_WATCH_PREFIX = "chat_watch_"
|
||||
logger = get_logger(__name__)
|
||||
_INBOX_COALESCE_SECONDS = 0.25
|
||||
|
||||
|
||||
@dataclass
|
||||
class _InboxUpdate:
|
||||
dirty: bool = True
|
||||
task: Optional[asyncio.Task] = None
|
||||
|
||||
|
||||
_pending_inbox_updates: dict[str, _InboxUpdate] = {}
|
||||
|
||||
|
||||
# chat_reply `type`s the inbox appends to a conversation
|
||||
@@ -54,6 +72,85 @@ def may_join_org_inbox(db: Session, socket_session: dict, room: str) -> bool:
|
||||
and has_any_permission(user, UNASSIGNED_CHAT_VIEWERS))
|
||||
|
||||
|
||||
def may_join_chat_updates(db: Session, socket_session: dict, room: str) -> bool:
|
||||
"""The organization-wide room carries only a content-free change signal."""
|
||||
organization_id = room.removeprefix(ORG_CHAT_UPDATES_PREFIX)
|
||||
if str(socket_session.get('organization_id')) != organization_id:
|
||||
return False
|
||||
user_id = socket_session.get('user_id')
|
||||
user = UserRepository(db).get_user(user_id) if user_id else None
|
||||
return bool(user and user.is_active and str(user.organization_id) == organization_id
|
||||
and has_any_permission(user, CHAT_VIEW_PERMISSIONS))
|
||||
|
||||
|
||||
async def may_watch_chat(db: Session, socket_session: dict, room: str) -> bool:
|
||||
"""Mirror the detail endpoint's organization and chat-scope checks."""
|
||||
session_id = room.removeprefix(CHAT_WATCH_PREFIX)
|
||||
user_id = socket_session.get('user_id')
|
||||
user = UserRepository(db).get_user(user_id) if user_id else None
|
||||
if not user or not user.is_active or not has_any_permission(user, CHAT_VIEW_PERMISSIONS):
|
||||
return False
|
||||
if str(user.organization_id) != str(socket_session.get('organization_id')):
|
||||
return False
|
||||
try:
|
||||
chat = SessionToAgentRepository(db).get_session(session_id)
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
if not chat or str(chat.organization_id) != str(user.organization_id):
|
||||
return False
|
||||
if has_any_permission(user, ('view_all_chats',)):
|
||||
return True
|
||||
can_view_assigned = has_any_permission(user, ('view_assigned_chats',))
|
||||
return await ChatRepository(db).check_session_access(
|
||||
session_id=session_id,
|
||||
user_id=user.id if can_view_assigned else None,
|
||||
user_groups=[str(group.id) for group in user.groups] if can_view_assigned else [],
|
||||
include_unassigned=has_any_permission(user, ('view_unassigned_chats',)),
|
||||
)
|
||||
|
||||
|
||||
async def notify_conversation_changed(organization_id, session_id) -> None:
|
||||
"""Invalidate the selected thread and inbox after a committed change.
|
||||
|
||||
Never send message text or identifiers to the org-wide room. The thread
|
||||
room also carries no content: every refresh goes through the normal HTTP
|
||||
authorization, including after a chat is reassigned.
|
||||
"""
|
||||
if not organization_id or not session_id:
|
||||
return
|
||||
await _emit_change('chat_changed', f'{CHAT_WATCH_PREFIX}{session_id}')
|
||||
org_key = str(organization_id)
|
||||
state = _pending_inbox_updates.get(org_key)
|
||||
if state is not None:
|
||||
state.dirty = True
|
||||
return
|
||||
state = _InboxUpdate()
|
||||
_pending_inbox_updates[org_key] = state
|
||||
# One task per active organization, not one task per message. Each server
|
||||
# worker coalesces a burst before broadcasting it through the Redis manager.
|
||||
state.task = asyncio.create_task(_flush_inbox_changes(org_key, state))
|
||||
|
||||
|
||||
async def _emit_change(event: str, room: str) -> None:
|
||||
try:
|
||||
await sio.emit(event, {}, room=room, namespace='/agent')
|
||||
except Exception as exc:
|
||||
# A broker outage must not turn an already-committed chat into an error.
|
||||
logger.warning('Failed to notify inbox of a conversation change: %s', type(exc).__name__)
|
||||
|
||||
|
||||
async def _flush_inbox_changes(org_key: str, state: _InboxUpdate) -> None:
|
||||
try:
|
||||
while state.dirty:
|
||||
await asyncio.sleep(_INBOX_COALESCE_SECONDS)
|
||||
# Everything received before this point fits in this signal. An
|
||||
# arrival during publish sets dirty again and gets a trailing one.
|
||||
state.dirty = False
|
||||
await _emit_change('inbox_changed', f'{ORG_CHAT_UPDATES_PREFIX}{org_key}')
|
||||
finally:
|
||||
_pending_inbox_updates.pop(org_key, None)
|
||||
|
||||
|
||||
async def emit_to_session_agents(event: str, payload: dict, session_id,
|
||||
user_id: Optional[object] = None) -> None:
|
||||
"""Push a live update to the dashboard for one conversation.
|
||||
@@ -92,4 +189,5 @@ async def relay_stored_message(session_record, stored, message_type: str) -> Non
|
||||
# Carries e.g. the button-reply marker, so tags show live too
|
||||
'attributes': stored.attributes or {},
|
||||
}
|
||||
await notify_conversation_changed(session_record.organization_id, session_id)
|
||||
await emit_to_session_agents('chat_reply', payload, session_id, session_record.user_id)
|
||||
Reference in new issue
Block a user