fix: improve maintenance and synchronize live conversations

This commit is contained in:
WangShen007 committed 2026-10-29 09:46:21 +08:00
1 parent bb541bd212
commit ef86a5d3af
297 files changed
+2983 -1274

No files matched your search

+3 -4
View File
@@ -16,13 +16,12 @@ limitations under the License.
import os
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Request
from typing import List, Union
from typing import List
from app.core.logger import get_logger
from app.database import get_db
from app.models.user import User, UserGroup
from app.core.auth import (
INBOX_PERMISSIONS,
get_current_user,
require_any_permission,
require_any_unified_permission,
require_permissions,
@@ -30,7 +29,7 @@ from app.core.auth import (
)
from app.repositories.agent import AgentRepository
from app.repositories.knowledge import KnowledgeRepository
from app.models.schemas.agent import AgentUpdate, AgentResponse, AgentCreate, AgentRosterItem, AgentWithCustomizationResponse
from app.models.schemas.agent import AgentUpdate, AgentCreate, AgentRosterItem, AgentWithCustomizationResponse
from sqlalchemy.orm import Session
from app.models.agent import Agent, AgentCustomization
from app.models.organization import Organization
@@ -718,7 +717,7 @@ async def generate_instructions(
# Check if enterprise module is available
try:
from app.enterprise.repositories.subscription import SubscriptionRepository
from app.enterprise.repositories.subscription import SubscriptionRepository # noqa: F401 - presence check preserves the optional enterprise contract
HAS_ENTERPRISE = True
except ImportError:
HAS_ENTERPRISE = False
+1 -2
View File
@@ -16,11 +16,10 @@ limitations under the License.
import traceback
from fastapi import APIRouter, Depends, HTTPException, status
from app.core import config
from app.core.logger import get_logger
from app.database import get_db
from app.models.user import User
from app.core.auth import get_current_user, require_permissions
from app.core.auth import require_permissions
from app.repositories.ai_config import AIConfigRepository
from app.agents.chat_agent import ChatAgent
from app.core.security import decrypt_api_key
+1 -2
View File
@@ -26,8 +26,7 @@ from app.models.rating import Rating
from app.models.session_to_agent import SessionToAgent, SessionStatus
from app.models.chat_history import ChatHistory
from datetime import datetime, timedelta, timezone
from sqlalchemy import func, and_, or_, desc, distinct, case
from typing import Optional, List
from sqlalchemy import func, and_, desc, case
from app.core.logger import get_logger
from uuid import UUID
-1
View File
@@ -25,7 +25,6 @@ from app.core.auth import (
check_permissions,
get_current_organization,
require_any_permission,
require_permissions,
)
from app.services.meta_own_app import AUTO_WEBHOOK_SETTING, is_own_app, own_app_webhook_url
from app.core.config import webhooks_base_url
+2 -5
View File
@@ -14,14 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
"""
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from typing import List, Optional
from datetime import datetime
from app.models.schemas.chat import ChatOverviewResponse, ChatDetailResponse
from app.core.auth import get_current_user, require_permissions, get_unified_chat_auth
from app.services.shopify_session import require_shopify_or_jwt_auth
from app.models.user import User
from app.core.auth import get_unified_chat_auth
from app.database import get_db
from app.repositories.chat import ChatRepository
from app.models.session_to_agent import SessionStatus
@@ -29,7 +27,6 @@ from app.core.params import MAX_DB_INT
from app.core.logger import get_logger
from uuid import UUID
from fastapi import status
router = APIRouter()
+2 -5
View File
@@ -14,20 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
"""
from fastapi import APIRouter, UploadFile, File, HTTPException, Depends, status, Header, Request, Query
from fastapi.responses import FileResponse, JSONResponse
from fastapi import APIRouter, HTTPException, Depends, status, Header, Request, Query
from fastapi.responses import FileResponse
from typing import Optional, Tuple
import uuid
import os
from datetime import datetime
from urllib.parse import quote
from sqlalchemy.orm import Session
from app.core.config import settings
from app.core.s3 import upload_file_to_s3, get_s3_signed_url
from app.core.local_file_urls import verify_local_signature
from app.core.logger import get_logger
from app.core.security import verify_conversation_token
from app.models import User
from app.core.auth import get_current_user
from app.database import get_db
-5
View File
@@ -45,10 +45,6 @@ from app.services import knowledge_vector_links
from sqlalchemy.orm import Session
from app.core.s3 import upload_file_to_s3, get_s3_signed_url
from app.core.file_validation import read_validated, safe_filename, PDF_MAGIC
from app.repositories.user import UserRepository
from app.models.notification import Notification, NotificationType
from app.services.user import send_fcm_notification
from app.workers.knowledge_processor import process_queue_item, run_processor
# Try to import enterprise modules
try:
@@ -1099,7 +1095,6 @@ async def get_queue_status(
):
"""Get status of a queued knowledge item"""
try:
queue_repo = KnowledgeQueueRepository(db)
item = db.query(KnowledgeQueue).filter(
KnowledgeQueue.id == queue_id).first()
+1 -1
View File
@@ -19,7 +19,7 @@ from typing import List
from app.core.logger import get_logger
from app.database import get_db
from app.models.user import User
from app.core.auth import get_current_user, require_permissions
from app.core.auth import require_permissions
from app.repositories.mcp_tool import MCPToolRepository
from app.repositories.agent import AgentRepository
-1
View File
@@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
"""
from typing import List
from urllib.parse import quote, urlparse
from fastapi import APIRouter, Depends, HTTPException, status, Response
from sqlalchemy.orm import Session
-2
View File
@@ -20,7 +20,6 @@ from sqlalchemy.exc import IntegrityError
from app.database import get_db
from app.models.user import User
from app.core.auth import (
check_permissions,
require_any_permission,
require_grantable,
require_permissions,
@@ -35,7 +34,6 @@ from app.models.schemas.role import (
PermissionResponse
)
from typing import List
from uuid import UUID
router = APIRouter()
+4
View File
@@ -29,6 +29,7 @@ from app.core.auth import CHAT_MANAGE_PERMISSIONS, get_current_user, has_any_per
from app.database import get_db
from app.services.chat_notifications import notify_chat_assigned
from app.services.human_routing import notify_customer, route_session_to_human
from app.services.agent_rooms import notify_conversation_changed
from app.models.schemas.chat import ChatDetailResponse, TransferReasonType
@@ -131,6 +132,7 @@ async def takeover_chat(
# Let the customer know a human joined (external channels only), before
# the detail is read back so the notice is part of the returned thread.
await _notify_customer_of_handover(db, session, current_user)
await notify_conversation_changed(current_user.organization_id, session_id)
# Get updated chat details
chat = await chat_repo.get_chat_detail(
@@ -187,6 +189,7 @@ async def route_chat_to_human(
if not routed:
raise HTTPException(
status_code=400, detail="This chat is already waiting for a human")
await notify_conversation_changed(current_user.organization_id, session_id)
chat = await ChatRepository(db).get_chat_detail(
session_id=session_id,
@@ -249,6 +252,7 @@ async def reassign_chat(
success = session_repo.reassign_session(session_id=session_id, to_user_id=to_user_id)
if not success:
raise HTTPException(status_code=400, detail="Failed to reassign chat")
await notify_conversation_changed(current_user.organization_id, session_id)
# Fetch updated chat detail
chat_repo = ChatRepository(db)
+1 -9
View File
@@ -15,8 +15,7 @@ limitations under the License.
"""
import traceback
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response
from fastapi.responses import RedirectResponse, HTMLResponse
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from sqlalchemy.orm import Session
from app.core.config import settings
from app.core.logger import get_logger
@@ -25,19 +24,12 @@ from app.models.schemas.shopify import ShopifyShopCreate, ShopifyShop, ShopifySh
from app.models.schemas.shopify import AgentShopifyConfigBase, AgentShopifyConfig, AgentShopifyConfigCreate, AgentShopifyConfigUpdate
from app.repositories.shopify_shop_repository import ShopifyShopRepository
from app.repositories.agent_shopify_config_repository import AgentShopifyConfigRepository
from typing import Optional
import requests
import hmac
import hashlib
import base64
import json
from urllib.parse import urlencode, quote
from app.core.auth import get_current_user, require_permissions, get_current_organization, check_permissions
from app.models.user import User
from app.models.organization import Organization
from app.repositories.widget import WidgetRepository
from app.services.shopify import ShopifyService
from app.services.shopify_auth_service import ShopifyAuthService
from app.services.shopify_helper_service import ShopifyHelperService
from app.services.shopify_session import require_shopify_session
from app.services.shopify_urls import shopify_admin_url, shopify_oauth_token_url
+1 -1
View File
@@ -15,7 +15,7 @@ limitations under the License.
"""
from datetime import datetime, timezone
from typing import List, Optional
from typing import Optional
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, status
-2
View File
@@ -18,13 +18,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status, Request
from sqlalchemy.orm import Session
from datetime import datetime, timezone
import logging
import time
from typing import Optional, Dict, Any
from app.database import get_db
from app.models.widget import Widget
from app.models.customer import Customer
from app.core.config import settings
from app.core.security import (
verify_conversation_token,
get_existing_valid_token_jti,
+1 -1
View File
@@ -15,7 +15,7 @@ limitations under the License.
"""
from fastapi import APIRouter, Depends, HTTPException, Query, status, Response, Request, Body, File, UploadFile
from fastapi.responses import JSONResponse, RedirectResponse
from fastapi.responses import RedirectResponse
from fastapi.security import HTTPBearer, OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from typing import List, Optional
-1
View File
@@ -27,7 +27,6 @@ from urllib.parse import urlparse
from app.models.agent import Agent
from app.models.customer import Customer
from app.models.widget import Widget
from app.models.user import User
from app.core.auth import get_current_user
from app.database import get_db
+44 -15
View File
@@ -14,10 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
"""
import asyncio
import datetime
import json
import os
import re
from types import SimpleNamespace
from fastapi import APIRouter
@@ -50,7 +47,11 @@ from app.services.file_storage import resolve_public_url
from app.models.session_to_agent import ACTIVE_STATUSES, SessionStatus
from app.agents.transfer_agent import get_agent_availability_response
from app.repositories.agent import AgentRepository
from app.services.agent_rooms import is_org_inbox_room, may_join_org_inbox
from app.services.agent_rooms import (
CHAT_WATCH_PREFIX, ORG_CHAT_UPDATES_PREFIX, is_org_inbox_room,
may_join_chat_updates, may_join_org_inbox, may_watch_chat,
notify_conversation_changed,
)
from app.services.chat_notifications import notify_new_chat
from app.services.contact_capture import retain_unstored_email
from app.services.human_routing import (
@@ -637,6 +638,7 @@ async def handle_widget_chat(sid, data):
)
db.add(file_attachment)
db.commit()
await notify_conversation_changed(org_id, session_id)
chat_history = []
chat_history = await chat_repo.get_session_history(session_id)
jira_repo = JiraRepository(db)
@@ -691,6 +693,7 @@ async def handle_widget_chat(sid, data):
"shopify_output": response_content.shopify_output
}
})
await notify_conversation_changed(org_id, session_id)
response = response_content
elif active_session.workflow_id and active_session.user_id is not None:
# Workflow session but human agent has taken over - handle like regular human takeover
@@ -720,6 +723,7 @@ async def handle_widget_chat(sid, data):
)
db.add(file_attachment)
db.commit()
await notify_conversation_changed(org_id, session_id)
# Get session data to find assigned user
session_data = session_repo.get_session(session_id)
user_id = str(session_data.user_id) if session_data and session_data.user_id else None
@@ -779,6 +783,7 @@ async def handle_widget_chat(sid, data):
)
db.add(file_attachment)
db.commit()
await notify_conversation_changed(org_id, session_id)
# Get session data to find assigned user
session_data = session_repo.get_session(session_id)
user_id = str(session_data.user_id) if session_data and session_data.user_id else None
@@ -1291,6 +1296,7 @@ async def handle_agent_message(sid, data):
logger.info(f"Agent ended chat session {session_id}")
# Update session status to closed
session_repo.update_session_status(session_id, "CLOSED")
await notify_conversation_changed(session.get('organization_id'), session_id)
# Emit to widget clients
@@ -1396,6 +1402,22 @@ async def handle_join_room(sid, data):
await sio.enter_room(sid, session_id, namespace='/agent')
return
# These rooms carry invalidations only, never message content. A
# watcher must satisfy the same scope as the chat-detail endpoint.
if session_id.startswith(ORG_CHAT_UPDATES_PREFIX):
db = next(get_db())
if not may_join_chat_updates(db, session, session_id):
raise ValueError("Unauthorized to join chat updates room")
await sio.enter_room(sid, session_id, namespace='/agent')
return
if session_id.startswith(CHAT_WATCH_PREFIX):
db = next(get_db())
if not await may_watch_chat(db, session, session_id):
raise ValueError("Unauthorized to watch chat")
await sio.enter_room(sid, session_id, namespace='/agent')
return
# Verify this agent has permission to join this room
db = next(get_db())
session_repo = SessionToAgentRepository(db)
@@ -1451,7 +1473,9 @@ async def handle_leave_room(sid, data):
# Notify a conversation room of the leave — not the org inbox room,
# where the whole team would hear every chat switch
if not is_org_inbox_room(session_id):
if not (is_org_inbox_room(session_id) or session_id.startswith(
(ORG_CHAT_UPDATES_PREFIX, CHAT_WATCH_PREFIX)
)):
await sio.emit('room_event', {
'type': 'leave',
'user_id': str(session.get('user_id')),
@@ -1641,6 +1665,7 @@ async def handle_get_workflow_state(sid):
"initial_execution": True
}
})
await notify_conversation_changed(org_id, session_id)
# Emit intermediate message to client immediately
await sio.emit('chat_response', {
@@ -1686,6 +1711,7 @@ async def handle_get_workflow_state(sid):
"initial_message": True
}
})
await notify_conversation_changed(org_id, session_id)
# The widget does not render a workflow_state message, so
# send the text as a chat_response like the other handlers.
@@ -1934,6 +1960,7 @@ async def handle_proceed_workflow(sid, data):
"message_node": True
}
})
await notify_conversation_changed(org_id, session_id)
# Emit intermediate message to client immediately
await sio.emit('chat_response', {
@@ -1971,6 +1998,7 @@ async def handle_proceed_workflow(sid, data):
"request_rating": workflow_result.request_rating,
}
})
await notify_conversation_changed(org_id, session_id)
# Emit chat response
await sio.emit('chat_response', {
@@ -2109,6 +2137,7 @@ async def handle_form_submission(sid, data):
# Validate form data
form_data = data.get('form_data', {})
if not form_data:
logger.warning(f"Form submission rejected for sid {sid}: no form data")
raise ValueError("No form data provided")
db = next(get_db())
@@ -2117,6 +2146,7 @@ async def handle_form_submission(sid, data):
active_session = _own_session(session_repo, session)
if not active_session or not active_session.workflow_id:
logger.warning(f"Form submission rejected for sid {sid}: no active workflow session")
raise ValueError("No active workflow session found")
# Get the current form configuration for validation
@@ -2125,6 +2155,7 @@ async def handle_form_submission(sid, data):
workflow = workflow_repo.get_workflow_with_nodes_and_connections(active_session.workflow_id)
if not workflow:
logger.warning(f"Form submission rejected for sid {sid}: workflow not found")
raise ValueError("Workflow not found")
# Find the current form node to get field configurations
@@ -2144,7 +2175,7 @@ async def handle_form_submission(sid, data):
if validation_errors:
error_message = "; ".join(validation_errors)
logger.error(f"Form validation failed: {error_message}")
logger.error(f"Form validation failed for sid {sid}: {error_message}")
await sio.emit('error', {
'error': error_message,
'type': 'validation_error'
@@ -2184,14 +2215,13 @@ async def handle_form_submission(sid, data):
existing_customer = customer_repo.get_customer_by_email(submitted_email, org_id)
if not existing_customer:
# Update customer email
old_email = customer.email
customer.email = submitted_email
db.commit()
logger.info(f"Updated customer {customer_id} email from {old_email} to {submitted_email}")
logger.info(f"Updated email for customer {customer_id} in session {session_id}")
else:
logger.warning(f"Email {submitted_email} already exists for another customer, skipping update")
logger.warning(f"Email already belongs to customer {existing_customer.id}; skipping update for customer {customer_id}")
except Exception as e:
logger.error(f"Failed to update customer email: {str(e)}")
logger.error(f"Failed to update email for customer {customer_id}: {type(e).__name__}")
db.rollback()
# Store form submission in chat history
@@ -2201,8 +2231,6 @@ async def handle_form_submission(sid, data):
if workflow_result.intermediate_messages:
logger.info(f"Found {len(workflow_result.intermediate_messages)} intermediate messages from MESSAGE nodes")
for intermediate_message in workflow_result.intermediate_messages:
logger.debug(f"Emitting intermediate message: {intermediate_message}")
# Store intermediate message in chat history
chat_repo.create_message({
"message": intermediate_message,
@@ -2218,6 +2246,7 @@ async def handle_form_submission(sid, data):
"message_node": True
}
})
await notify_conversation_changed(org_id, session_id)
# Emit intermediate message to client immediately
await sio.emit('chat_response', {
@@ -2248,6 +2277,7 @@ async def handle_form_submission(sid, data):
"request_rating": workflow_result.request_rating,
}
})
await notify_conversation_changed(org_id, session_id)
# Emit chat response
await sio.emit('chat_response', {
@@ -2273,14 +2303,13 @@ async def handle_form_submission(sid, data):
}, to=sid, namespace='/widget')
except ValueError as e:
logger.error(f"Form submission error for sid {sid}: {str(e)}")
logger.error(f"Form submission error for sid {sid}: {type(e).__name__}")
await sio.emit('error', {
'error': str(e),
'type': 'form_error'
}, to=sid, namespace='/widget')
except Exception as e:
logger.error(f"Form submission error for sid {sid}: {str(e)}")
logger.error(traceback.format_exc())
logger.error(f"Form submission error for sid {sid}: {type(e).__name__}")
await sio.emit('error', {
'error': 'Failed to submit form',
'type': 'form_error'
+2 -2
View File
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
"""
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List, Dict, Any, Optional
from typing import List, Dict, Any
from uuid import UUID
from pydantic import BaseModel
from app.core.logger import get_logger