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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user