瀏覽代碼

增加外呼评级功能

boweniac 2 月之前
父節點
當前提交
252698c01c
共有 40 個文件被更改,包括 1237 次插入46 次删除
  1. 0 0
      app/admin/__init__.py
  2. 9 0
      app/admin/api/__init__.py
  3. 19 0
      app/admin/api/auth.py
  4. 74 0
      app/admin/api/intent_org.py
  5. 0 0
      app/admin/crud/__init__.py
  6. 82 0
      app/admin/crud/crud_intent_org.py
  7. 0 0
      app/admin/schema/__init__.py
  8. 10 0
      app/admin/schema/auth.py
  9. 33 0
      app/admin/schema/intent_org.py
  10. 24 0
      app/admin/schema/token.py
  11. 0 0
      app/admin/service/__init__.py
  12. 22 0
      app/admin/service/auth_service.py
  13. 50 0
      app/admin/service/intent_org_service.py
  14. 0 0
      app/call_center/__init__.py
  15. 0 0
      app/call_center/api/__init__.py
  16. 7 0
      app/call_center/api/intent/__init__.py
  17. 96 0
      app/call_center/api/intent/evaluate.py
  18. 0 0
      app/call_center/crud/__init__.py
  19. 120 0
      app/call_center/crud/crud_intent_records.py
  20. 0 0
      app/call_center/schema/__init__.py
  21. 55 0
      app/call_center/schema/intent_records.py
  22. 0 0
      app/call_center/service/__init__.py
  23. 62 0
      app/call_center/service/intent_records_service.py
  24. 5 6
      app/gpt/api/route.py
  25. 0 1
      app/gpt/service/ali_filetrans.py
  26. 8 3
      app/router.py
  27. 0 0
      batch_task/__init__.py
  28. 35 0
      batch_task/batch_task.py
  29. 189 0
      batch_task/update_llm_intent.py
  30. 18 0
      common/dataclasses.py
  31. 6 2
      common/model.py
  32. 116 0
      common/security/jwt_call_center.py
  33. 3 0
      core/conf.py
  34. 74 12
      core/registrar.py
  35. 7 20
      main.py
  36. 55 0
      middleware/jwt_call_center_auth_middleware.py
  37. 27 0
      model/intent_org.py
  38. 30 0
      model/intent_records.py
  39. 1 2
      model/records.py
  40. 二進制
      requirements.txt

+ 0 - 0
app/admin/__init__.py


+ 9 - 0
app/admin/api/__init__.py

@@ -0,0 +1,9 @@
+from fastapi import APIRouter
+
+from app.admin.api.intent_org import router as intent_org_router
+from app.admin.api.auth import router as auth_router
+
+v1 = APIRouter(prefix='/admin', tags=['admin'])
+
+v1.include_router(intent_org_router, prefix='/intent_org', tags=['组织'])
+v1.include_router(auth_router, prefix='/auth', tags=['鉴权'])

+ 19 - 0
app/admin/api/auth.py

@@ -0,0 +1,19 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+from fastapi import APIRouter
+from app.admin.schema.auth import AuthGenTokenParam
+from app.admin.service.auth_service import auth_service
+from common.response.response_schema import ResponseModel, response_base
+
+router = APIRouter()
+
+@router.post(
+    '/gen_token',
+    summary='获取 token',
+    description=''
+)
+async def get_token(
+    obj: AuthGenTokenParam
+) -> ResponseModel:
+    data = await auth_service.gen_token(obj=obj)
+    return response_base.success(data=data)

+ 74 - 0
app/admin/api/intent_org.py

@@ -0,0 +1,74 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+from typing import Annotated
+
+from app.admin.schema.intent_org import CreateIntentOrgParam, GetIntentOrgListDetails, UpdateIntentOrgParam
+from app.admin.service.intent_org_service import intent_org_service
+from common.pagination import DependsPagination, paging_data
+from common.response.response_schema import ResponseModel, response_base
+from common.security.jwt_call_center import DependsJwtAuth
+from common.security.permission import RequestPermission
+from database.db_mysql import CurrentSession
+from fastapi import APIRouter, Depends, Path, Query
+
+router = APIRouter()
+
+
+@router.get('/{pk}', summary='获取 intent_org 详情', dependencies=[DependsJwtAuth])
+async def get_intent_org(pk: Annotated[int, Path(...)]) -> ResponseModel:
+    intent_org = await intent_org_service.get(pk=pk)
+    return response_base.success(data=intent_org)
+
+
+@router.get(
+    '',
+    summary='(模糊条件)分页获取所有 intent_org',
+    dependencies=[
+        DependsJwtAuth,
+        DependsPagination,
+    ],
+)
+async def get_pagination_intent_org(db: CurrentSession) -> ResponseModel:
+    intent_org_select = await intent_org_service.get_select()
+    page_data = await paging_data(db, intent_org_select, GetIntentOrgListDetails)
+    return response_base.success(data=page_data)
+
+
+@router.post(
+    '',
+    summary='创建 intent_org',
+    dependencies=[
+        Depends(RequestPermission('intent_org:add')),
+    ],
+)
+async def create_intent_org(obj: CreateIntentOrgParam) -> ResponseModel:
+    await intent_org_service.create(obj=obj)
+    return response_base.success()
+
+
+@router.put(
+    '/{pk}',
+    summary='更新 intent_org',
+    dependencies=[
+        Depends(RequestPermission('intent_org:edit')),
+    ],
+)
+async def update_intent_org(pk: Annotated[int, Path(...)], obj: UpdateIntentOrgParam) -> ResponseModel:
+    count = await intent_org_service.update(pk=pk, obj=obj)
+    if count > 0:
+        return response_base.success()
+    return response_base.fail()
+
+
+@router.delete(
+    '',
+    summary='(批量)删除 intent_org',
+    dependencies=[
+        Depends(RequestPermission('intent_org:del')),
+    ],
+)
+async def delete_intent_org(pk: Annotated[list[int], Query(...)]) -> ResponseModel:
+    count = await intent_org_service.delete(pk=pk)
+    if count > 0:
+        return response_base.success()
+    return response_base.fail()

+ 0 - 0
app/admin/crud/__init__.py


+ 82 - 0
app/admin/crud/crud_intent_org.py

@@ -0,0 +1,82 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+from typing import Sequence
+
+from model.intent_org import IntentOrg
+from app.admin.schema.intent_org import CreateIntentOrgParam, UpdateIntentOrgParam
+from sqlalchemy import Select
+from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy_crud_plus import CRUDPlus
+
+
+class CRUDIntentOrg(CRUDPlus[IntentOrg]):
+    async def get(self, db: AsyncSession, pk: int) -> IntentOrg | None:
+        """
+        获取IntentOrg
+
+        :param db:
+        :param pk:
+        :return:
+        """
+        return await self.select_model(db, pk)
+
+    async def get_by_token(self, db: AsyncSession, token: str) -> IntentOrg | None:
+        """
+        获取IntentOrg
+
+        :param db:
+        :param token:
+        :return:
+        """
+        return await self.select_model_by_column(db, api_key=token)
+
+    async def get_list(self) -> Select:
+        """
+        获取IntentOrg列表
+
+        :return:
+        """
+        return await self.select_order('created_at', 'desc')
+
+    async def get_all(self, db: AsyncSession) -> Sequence[IntentOrg]:
+        """
+        获取所有IntentOrg
+
+        :param db:
+        :return:
+        """
+        return await self.select_models(db)
+
+    async def create(self, db: AsyncSession, obj_in: CreateIntentOrgParam) -> None:
+        """
+        创建IntentOrg
+
+        :param db:
+        :param obj_in:
+        :return:
+        """
+        await self.create_model(db, obj_in)
+
+    async def update(self, db: AsyncSession, pk: int, obj_in: UpdateIntentOrgParam) -> int:
+        """
+        更新IntentOrg
+
+        :param db:
+        :param pk:
+        :param obj_in:
+        :return:
+        """
+        return await self.update_model(db, pk, obj_in)
+
+    async def delete(self, db: AsyncSession, pk: list[int]) -> int:
+        """
+        删除IntentOrg
+
+        :param db:
+        :param pk:
+        :return:
+        """
+        return  await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
+
+
+intent_org_dao: CRUDIntentOrg = CRUDIntentOrg(IntentOrg)

+ 0 - 0
app/admin/schema/__init__.py


+ 10 - 0
app/admin/schema/auth.py

@@ -0,0 +1,10 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+from common.schema import SchemaBase
+
+
+class AuthSchemaBase(SchemaBase):
+    id: int
+
+class AuthGenTokenParam(AuthSchemaBase):
+    pass

+ 33 - 0
app/admin/schema/intent_org.py

@@ -0,0 +1,33 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+from datetime import datetime
+
+from pydantic import ConfigDict
+
+from common.schema import SchemaBase
+
+
+class IntentOrgSchemaBase(SchemaBase):
+    name: str
+    api_key: str
+    openai_base: str
+    openai_key: str
+    intent_callback: str
+    status: int
+    deleted_time: datetime | None = None
+
+
+class CreateIntentOrgParam(IntentOrgSchemaBase):
+    pass
+
+
+class UpdateIntentOrgParam(IntentOrgSchemaBase):
+    pass
+
+
+class GetIntentOrgListDetails(IntentOrgSchemaBase):
+    model_config = ConfigDict(from_attributes=True)
+
+class CurrentIntentOrgIns(GetIntentOrgListDetails):
+    id: int
+    model_config = ConfigDict(from_attributes=True)

+ 24 - 0
app/admin/schema/token.py

@@ -0,0 +1,24 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+from datetime import datetime
+
+from common.schema import SchemaBase
+
+
+class GetSwaggerToken(SchemaBase):
+    access_token: str
+    token_type: str = 'Bearer'
+
+
+class AccessTokenBase(SchemaBase):
+    access_token: str
+    access_token_type: str = 'Bearer'
+    access_token_expire_time: datetime
+
+
+class GetNewToken(AccessTokenBase):
+    pass
+
+
+class GetLoginToken(AccessTokenBase):
+    pass

+ 0 - 0
app/admin/service/__init__.py


+ 22 - 0
app/admin/service/auth_service.py

@@ -0,0 +1,22 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+from app.admin.schema.token import GetLoginToken
+from app.admin.schema.auth import AuthGenTokenParam
+from common.security.jwt_call_center import create_access_token
+
+
+class AuthService:
+    async def gen_token(
+        self, *, obj: AuthGenTokenParam
+    ) -> GetLoginToken:
+        user_id = obj.id
+        a_token = await create_access_token(str(user_id))
+
+        data = GetLoginToken(
+            access_token=a_token.access_token,
+            access_token_expire_time=a_token.access_token_expire_time,
+        )
+        return data
+
+
+auth_service: AuthService = AuthService()

+ 50 - 0
app/admin/service/intent_org_service.py

@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+from typing import Sequence
+
+from app.admin.crud.crud_intent_org import intent_org_dao
+from model.intent_org import IntentOrg
+from app.admin.schema.intent_org import CreateIntentOrgParam, UpdateIntentOrgParam
+from common.exception import errors
+from database.db_mysql import async_db_session
+from sqlalchemy import Select
+
+
+class IntentOrgService:
+    @staticmethod
+    async def get(*, pk: int) -> IntentOrg:
+        async with async_db_session() as db:
+            intent_org = await intent_org_dao.get(db, pk)
+            if not intent_org:
+                raise errors.NotFoundError(msg='intent_org 不存在')
+            return intent_org
+
+    @staticmethod
+    async def get_select() -> Select:
+        return await intent_org_dao.get_list()
+
+    @staticmethod
+    async def get_all() -> Sequence[IntentOrg]:
+        async with async_db_session() as db:
+            intent_org = await intent_org_dao.get_all(db)
+            return intent_org
+
+    @staticmethod
+    async def create(*, obj: CreateIntentOrgParam) -> None:
+        async with async_db_session.begin() as db:
+            await intent_org_dao.create(db, obj)
+
+    @staticmethod
+    async def update(*, pk: int, obj: UpdateIntentOrgParam) -> int:
+        async with async_db_session.begin() as db:
+            count = await intent_org_dao.update(db, pk, obj)
+            return count
+
+    @staticmethod
+    async def delete(*, pk: list[int]) -> int:
+        async with async_db_session.begin() as db:
+            count = await intent_org_dao.delete(db, pk)
+            return count
+
+
+intent_org_service: IntentOrgService = IntentOrgService()

+ 0 - 0
app/call_center/__init__.py


+ 0 - 0
app/call_center/api/__init__.py


+ 7 - 0
app/call_center/api/intent/__init__.py

@@ -0,0 +1,7 @@
+from fastapi import APIRouter
+
+from app.call_center.api.intent.evaluate import router
+
+v1 = APIRouter(prefix='/intent', tags=['意向度'])
+
+v1.include_router(router, prefix='/evaluate', tags=['评级'])

+ 96 - 0
app/call_center/api/intent/evaluate.py

@@ -0,0 +1,96 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+import uuid
+from typing import Annotated
+
+from app.call_center.schema.intent_records import CreateIntentRecordsParam, GetIntentRecordsListDetails, \
+    UpdateIntentRecordsParam, GetIntentRecordsByIdParam, intent_status_map
+from app.call_center.service.intent_records_service import intent_records_service
+from common.pagination import paging_data
+from common.response.response_schema import ResponseModel, response_base
+from common.security.jwt_call_center import DependsJwtAuth
+from common.security.permission import RequestPermission
+from database.db_mysql import CurrentSession
+from fastapi import APIRouter, Depends, Query, Request
+
+router = APIRouter()
+
+@router.get(
+    '/result',
+    summary='获取 record',
+    dependencies=[
+        DependsJwtAuth,
+    ],
+)
+async def get_intent_record(obj: GetIntentRecordsByIdParam, request: Request) -> ResponseModel:
+    obj.org_id = request.user.id
+    intent_record_select = await intent_records_service.get_select_by_id(obj=obj)
+    data = None
+    if intent_record_select:
+        intent = None
+        if intent_record_select.llm_intent:
+            intent = intent_status_map.get(intent_record_select.llm_intent, "其他")
+        data = {
+            "status": intent_record_select.status,
+            "internal_id": intent_record_select.id,
+            "external_id": intent_record_select.external_id,
+            "score": intent_record_select.llm_intent,
+            "intent": intent
+        }
+    return response_base.success(data=data)
+
+@router.get(
+    '',
+    summary='(模糊条件)分页获取所有record',
+    dependencies=[
+        DependsJwtAuth,
+    ],
+)
+async def get_pagination_intent_record(db: CurrentSession) -> ResponseModel:
+    intent_record_select = await intent_records_service.get_select()
+    page_data = await paging_data(db, intent_record_select, GetIntentRecordsListDetails)
+    return response_base.success(data=page_data)
+
+
+@router.post(
+    '',
+    summary='创建record',
+    dependencies=[
+        DependsJwtAuth
+    ],
+)
+async def create_intent_record(obj: CreateIntentRecordsParam, request: Request) -> ResponseModel:
+    id_str = str(uuid.uuid4())
+    obj.id = id_str
+    obj.org_id = request.user.id
+    await intent_records_service.create(obj=obj)
+    return response_base.success()
+
+
+@router.post(
+    '/update',
+    summary='更新record',
+    dependencies=[
+        DependsJwtAuth
+    ],
+)
+async def update_intent_record(obj: UpdateIntentRecordsParam, request: Request) -> ResponseModel:
+    obj.org_id = request.user.id
+    count = await intent_records_service.update_manual_intent(obj=obj)
+    if count > 0:
+        return response_base.success()
+    return response_base.fail()
+
+
+@router.delete(
+    '',
+    summary='(批量)删除record',
+    dependencies=[
+        Depends(RequestPermission('call:record:del')),
+    ],
+)
+async def delete_intent_record(pk: Annotated[list[int], Query(...)]) -> ResponseModel:
+    count = await intent_records_service.delete(pk=pk)
+    if count > 0:
+        return response_base.success()
+    return response_base.fail()

+ 0 - 0
app/call_center/crud/__init__.py


+ 120 - 0
app/call_center/crud/crud_intent_records.py

@@ -0,0 +1,120 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+from typing import Sequence
+
+from model.intent_records import IntentRecords
+from app.call_center.schema.intent_records import CreateIntentRecordsParam, UpdateIntentRecordsParam, \
+    GetIntentRecordsByIdParam
+from sqlalchemy import Select, select
+from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy_crud_plus import CRUDPlus
+
+
+class CRUDIntentRecords(CRUDPlus[IntentRecords]):
+    async def get(self, db: AsyncSession, obj_in: GetIntentRecordsByIdParam) -> IntentRecords | None:
+        """
+        获取call record
+
+        :param db:
+        :param obj_in:
+        :return:
+        """
+        intent_records = None
+        if obj_in.internal_id:
+            intent_records = await self.select_model_by_column(db, id=obj_in.internal_id, org_id=obj_in.org_id)
+        elif obj_in.external_id:
+            intent_records = await self.select_model_by_column(db, external_id=obj_in.external_id, org_id=obj_in.org_id)
+        return intent_records
+
+    async def get_earliest_record(self, db: AsyncSession) -> IntentRecords | None:
+        """
+        获取最早的 llm_intent 不为空的记录
+
+        :param db:
+        :return:
+        """
+        stmt = select(self.model).where(IntentRecords.llm_intent == None).order_by(IntentRecords.created_at.asc())
+        query = await db.execute(stmt)
+        return query.scalars().first()
+
+    async def get_list(self) -> Select:
+        """
+        获取call record列表
+
+        :return:
+        """
+        return await self.select_order('created_at', 'desc')
+
+    async def get_all(self, db: AsyncSession) -> Sequence[IntentRecords]:
+        """
+        获取所有call record
+
+        :param db:
+        :return:
+        """
+        return await self.select_models(db)
+
+    async def create(self, db: AsyncSession, obj_in: CreateIntentRecordsParam) -> None:
+        """
+        创建call record
+
+        :param db:
+        :param obj_in:
+        :return:
+        """
+        await self.create_model(db, obj_in)
+
+    async def update(self, db: AsyncSession, pk: int, obj_in: UpdateIntentRecordsParam) -> int:
+        """
+        更新call record
+
+        :param db:
+        :param pk:
+        :param obj_in:
+        :return:
+        """
+        return await self.update_model(db, pk, obj_in)
+
+    async def update_manual_intent(self, db: AsyncSession, obj_in: UpdateIntentRecordsParam) -> int:
+        """
+        更新call record
+
+        :param db:
+        :param pk:
+        :param obj_in:
+        :return:
+        """
+        if obj_in.internal_id:
+            return await self.update_model_by_column(db, {'manual_intent': obj_in.manual_intent}, id=obj_in.internal_id)
+        else:
+            return await self.update_model_by_column(db, {'manual_intent': obj_in.manual_intent}, external_id=obj_in.external_id)
+
+    async def update_llm_intent(self, db: AsyncSession, internal_id: str, llm_intent: int, request_data: dict, response_data: dict, status: int) -> int:
+        """
+        更新call record
+
+        :param db:
+        :param pk:
+        :param internal_id:
+        :param llm_intent:
+        :param request_data:
+        :param response_data:
+        :param status:
+        :return:
+        """
+
+        return await self.update_model_by_column(db, {'llm_intent': llm_intent, 'request_data': request_data, 'response_data': response_data, 'status': status}, id=internal_id)
+
+
+    async def delete(self, db: AsyncSession, pk: list[int]) -> int:
+        """
+        删除call record
+
+        :param db:
+        :param pk:
+        :return:
+        """
+        return  await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
+
+
+intent_records_dao: CRUDIntentRecords = CRUDIntentRecords(IntentRecords)

+ 0 - 0
app/call_center/schema/__init__.py


+ 55 - 0
app/call_center/schema/intent_records.py

@@ -0,0 +1,55 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+from datetime import datetime
+
+from pydantic import ConfigDict
+
+from common.schema import SchemaBase
+
+intent_status_map = {
+    1: "有意向",
+    2: "无意向",
+    3: "其他"
+}
+
+
+class IntentRecordsSchemaBase(SchemaBase):
+    pass
+
+class GetIntentRecordsByIdParam(IntentRecordsSchemaBase):
+    internal_id: str | None = None
+    external_id: str | None = None
+    org_id: int | None = 0
+
+class CreateIntentRecordsParam(IntentRecordsSchemaBase):
+    id: str | None = 0
+    external_id: str
+    industry_type: int | None = 0
+    chat_history: str
+    manual_intent: int | None = None
+    org_id: int | None = 0
+    created_at: datetime | None = None
+    updated_at: datetime | None = None
+
+
+class UpdateIntentRecordsParam(IntentRecordsSchemaBase):
+    internal_id: str | None = None
+    external_id: str | None = None
+    manual_intent: int
+    org_id: int | None = 0
+
+
+class GetIntentRecordsListDetails(IntentRecordsSchemaBase):
+    model_config = ConfigDict(from_attributes=True)
+
+    id: str
+    created_at: datetime
+    updated_at: datetime | None = None
+
+class GetIntentRecordsDetails(IntentRecordsSchemaBase):
+    model_config = ConfigDict(from_attributes=True)
+
+    id: str
+    external_id: str
+    chat_history: str
+    org_id: int | None = 0

+ 0 - 0
app/call_center/service/__init__.py


+ 62 - 0
app/call_center/service/intent_records_service.py

@@ -0,0 +1,62 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+from typing import Sequence
+
+from app.call_center.crud.crud_intent_records import intent_records_dao
+from model.intent_records import IntentRecords
+from app.call_center.schema.intent_records import CreateIntentRecordsParam, UpdateIntentRecordsParam, \
+    GetIntentRecordsByIdParam
+from common.exception import errors
+from database.db_mysql import async_db_session
+from sqlalchemy import Select
+
+
+class IntentRecordsService:
+    @staticmethod
+    async def get(*, pk: str) -> IntentRecords:
+        async with async_db_session() as db:
+            intent_records = await intent_records_dao.get(db, pk)
+            if not intent_records:
+                raise errors.NotFoundError(msg='record不存在')
+            return intent_records
+
+    @staticmethod
+    async def get_select_by_id(*, obj: GetIntentRecordsByIdParam) -> IntentRecords | None:
+        async with async_db_session.begin() as db:
+            return await intent_records_dao.get(db, obj)
+
+    @staticmethod
+    async def get_select() -> Select:
+        return await intent_records_dao.get_list()
+
+    @staticmethod
+    async def get_all() -> Sequence[IntentRecords]:
+        async with async_db_session() as db:
+            intent_records = await intent_records_dao.get_all(db)
+            return intent_records
+
+    @staticmethod
+    async def create(*, obj: CreateIntentRecordsParam) -> None:
+        async with async_db_session.begin() as db:
+            await intent_records_dao.create(db, obj)
+
+    @staticmethod
+    async def update(*, pk: int, obj: UpdateIntentRecordsParam) -> int:
+        async with async_db_session.begin() as db:
+            count = await intent_records_dao.update(db, pk, obj)
+            return count
+
+    @staticmethod
+    async def update_manual_intent(*, obj: UpdateIntentRecordsParam) -> int:
+        async with async_db_session.begin() as db:
+            count = await intent_records_dao.update_manual_intent(db, obj)
+            return count
+
+    @staticmethod
+    async def delete(*, pk: list[int]) -> int:
+        async with async_db_session.begin() as db:
+            count = await intent_records_dao.delete(db, pk)
+            return count
+
+
+intent_records_service: IntentRecordsService = IntentRecordsService()

+ 5 - 6
app/gpt/api/route.py

@@ -1,14 +1,13 @@
 import json
 import os
 import tempfile
-from sre_constants import error
 from fastapi import Request
 
 import requests
 from fastapi import APIRouter
 from pydantic import BaseModel
 from sqlalchemy import text
-from starlette.responses import FileResponse, StreamingResponse, HTMLResponse
+from starlette.responses import FileResponse, HTMLResponse
 
 from common.log import log
 
@@ -28,7 +27,7 @@ class Response(BaseModel):
     result: Result = Result(error=0, msg="")
 
 
-@router.get("/gpt/recordlist", response_class=HTMLResponse)
+@router.get("/recordlist", response_class=HTMLResponse)
 async def record_list(request: Request):
     async with async_db_session() as db:
         query = text("select * from records order by id desc")
@@ -39,7 +38,7 @@ async def record_list(request: Request):
     )
 
 
-@router.get("/gpt/recordfile", summary="录音文件转发")
+@router.get("/recordfile", summary="录音文件转发")
 async def record_file(filepath: str):
     url = "http://xycc.ascrm.cn/recordFile/download?file=" + filepath
 
@@ -61,14 +60,14 @@ async def record_file(filepath: str):
     return FileResponse(file_path, media_type="application/octet-stream", filename=file_name)
 
 
-@router.post("/gpt/ali_trans_callback", summary="阿里云录音文件识别回调地址")
+@router.post("/ali_trans_callback", summary="阿里云录音文件识别回调地址")
 async def ali_trans_callback(body: dict):
     log.info(json.dumps(body, indent=4))
     await ali_trans_success(body)
     return Response()
 
 
-@router.post("/gpt/intent", summary="用户意图判断")
+@router.post("/intent", summary="用户意图判断")
 async def user_intent(body: dict):
     log.info(json.dumps(body, indent=4))
 

+ 0 - 1
app/gpt/service/ali_filetrans.py

@@ -1,6 +1,5 @@
 import json
 import os
-import time
 from aliyunsdkcore.acs_exception.exceptions import ClientException
 from aliyunsdkcore.acs_exception.exceptions import ServerException
 from aliyunsdkcore.client import AcsClient

+ 8 - 3
app/router.py

@@ -3,8 +3,13 @@
 from fastapi import APIRouter
 
 from app.gpt.api import v1 as gpt_v1
-from core.conf import settings
+from app.admin.api import v1 as admin_v1
+from app.call_center.api.intent import v1 as intent_v1
 
-route = APIRouter(prefix=settings.API_V1_STR)
+route = APIRouter()
 
-route.include_router(gpt_v1)
+route.include_router(gpt_v1)
+route.include_router(admin_v1)
+
+call_center_route = APIRouter()
+call_center_route.include_router(intent_v1)

+ 0 - 0
batch_task/__init__.py


+ 35 - 0
batch_task/batch_task.py

@@ -0,0 +1,35 @@
+import asyncio
+import threading
+from concurrent.futures import ThreadPoolExecutor
+
+from batch_task.update_llm_intent import update_llm_intent
+from common.log import log
+
+batch_task_event = threading.Event()
+
+async def periodically_execute(interval, func, *args, **kwargs):
+    while True:
+        await func(*args, **kwargs)
+        await asyncio.sleep(interval)
+
+async def execute_task():
+    batch_task_event.set()
+    while batch_task_event.is_set():
+        await update_llm_intent()
+        log.info("Task executed. Sleeping for 10 seconds...")
+        await asyncio.sleep(10)
+
+def start_batch_task():
+    global batch_task_event
+    if not batch_task_event.is_set():
+        log.info("Starting task.")
+        batch_task_event.set()
+        asyncio.run(execute_task())
+        # asyncio.create_task(execute_task())
+
+
+def stop_batch_task():
+    global batch_task_event
+    # 清除事件
+    batch_task_event.clear()
+    log.info("Stop task.")

+ 189 - 0
batch_task/update_llm_intent.py

@@ -0,0 +1,189 @@
+import json
+from time import sleep
+from typing import Dict
+
+import requests
+from openai import OpenAI
+import asyncio
+import aiohttp
+from app.admin.schema.intent_org import CurrentIntentOrgIns
+from app.call_center.crud.crud_intent_records import intent_records_dao
+from app.call_center.schema.intent_records import GetIntentRecordsDetails
+from common.log import log
+from core.conf import settings
+from database.db_mysql import async_db_session
+from database.db_redis import redis_client
+from utils.serializers import select_as_dict
+
+
+async def update_llm_intent():
+    async with async_db_session.begin() as db:
+        record = await intent_records_dao.get_earliest_record(db)
+        if not record:
+            return None
+        record_data = GetIntentRecordsDetails(**select_as_dict(record))
+
+        # 机构 id
+        org_id = record_data.org_id
+
+        # 从缓存中获取机构信息
+        key = f'{settings.TOKEN_CALL_REDIS_PREFIX}:{org_id}'
+        org_json = await redis_client.get(key)
+        if not org_json:
+            # 缓存中没有,从数据库中获取
+            from app.admin.crud.crud_intent_org import intent_org_dao
+            org = await intent_org_dao.get(db, org_id)
+            if not org and org.status is not 1:
+                log.error(f"意向评级时,机构不存在 org_id: {org_id}")
+                return None
+            org_data = CurrentIntentOrgIns(**select_as_dict(org))
+            # 将数据放进缓存
+            await redis_client.setex(
+                key,
+                settings.JWT_USER_REDIS_EXPIRE_SECONDS,
+                org_data.model_dump_json(),
+            )
+        else:
+            org_data = CurrentIntentOrgIns(**json.loads(org_json))
+
+        # 开始评级
+        intent_schema = {
+            "name": "intent_schema",
+            "schema": {  # 添加 schema 字段
+                "type": "object",
+                "description": "为通话记录进行评级",
+                "properties": {
+                    "score": {"type": "string", "description": "评分结果:有意向、待进一步分析、暂时无法沟通、其他"},
+                    "confidence_score": {"type": "number", "description": "置信度分数,范围从0.0到1.0"},
+                    "scoring_criteria": {"type": "string", "description": "请逐步介绍为何评为这个结果"}
+                },
+                "required": ["score", "confidence_score", "scoring_criteria"]
+            }
+        }
+
+        rules = [
+            {
+                'rule': "用户表现出对产品有积极回应,询问有关产品的问题,或愿意进行后续操作,如确认微信号、询问发货地址或表达接受产品(例:'行'、'好'、'可以'、'是的')。",
+                'score': '有意向'},
+            {
+                'rule': "用户虽然未积极回应或表示不需要,但却有相关需求、或着符合某项购买条件、又或着询问了机器人问题,即便后期疲软或持否定态度,可移交销售顾问进行进一步的需求分析,以便改善产品或定制符合客户要求的产品",
+                'score': '待进一步分析'},
+            {
+                'rule': "用户表示自己在忙、在开会等,现在不太方便沟通。又或者对电话的真实性存疑,怀疑是诈骗电话,可后续让专业客服继续跟进",
+                'score': '暂时无法沟通'},
+            {
+                'rule': "除上述规则外的其他用户,请注意客户资源珍贵,请尽量归类为'有意向'或'待进一步分析',后续再由销售专家定性。",
+                'score': '其他'}
+        ]
+
+        score_map = {
+            '有意向': 1,
+            '待进一步分析': 2,
+            '暂时无法沟通': 3,
+            '其他': 4
+        }
+
+        messages = [
+            {"role": "system", "content": f"""# 任务
+1. 首先,判断用户的第一句话是否说了:“你好,(任意内容)通话”,如果说了,则不用理会评级规则,直接强制分配为"语音助手"
+2. 如果不属于“语音助手”,请根据评级规则,对聊天记录给出评级、置信度、评分依据(逐项分析不要遗漏)
+
+# 细节说明
+置信度从0到1,0为置信度最低,1为置信度最高。"""
+             },
+            {
+                "role": "user",
+                "content": f"""# 评级规则:
+            {rules}
+
+            # 聊天记录
+            {record_data.chat_history}
+            """
+            }
+        ]
+        response_data = generate_json(org_data.api_key, org_data.openai_base, messages, intent_schema)
+        if response_data and isinstance(response_data.choices, list) and len(response_data.choices) > 0:
+            first_choice = response_data.choices[0]
+            if first_choice and first_choice.message:
+                response_json = first_choice.message.content
+                if response_json:
+                    intent = json.loads(response_json)
+
+                    score = intent.get('score', "未知")
+                    llm_intent = score_map.get(score, 0)
+                    # confidence_score = intent.get('confidence_score', 0)
+                    # scoring_criteria = intent.get('scoring_criteria', "未知")
+                    log.info(f"response_data.to_dict(): {response_data.to_dict()}")
+
+                    status = 2
+
+                    # 推送
+                    url = org_data.intent_callback
+                    if url:
+                        headers = {
+                            "Content-Type": "application/json"
+                        }
+                        data = {
+                            "internal_id": record_data.id,
+                            "external_id": record_data.external_id,
+                            "score": llm_intent,
+                            "intent": score
+                        }
+
+                    is_success = await send_request_with_retry(url, data, headers, max_retries=3, delay_between_retries=2)
+                    if is_success:
+                        status = 3
+
+                    # 存储结果
+                    status = 2
+                    async with async_db_session.begin() as db:
+                        try:
+                            await intent_records_dao.update_llm_intent(db, record_data.id, llm_intent,
+                                                                       {"messages": messages},
+                                                                       response_data.to_dict(),
+                                                                       status)
+                        except Exception as e:
+                            log.error(f"更新意图记录时发生异常:{e}")
+
+
+
+def generate_json(api_key: str, openai_base: str, messages: list[dict], json_schema: dict):
+    try:
+        client_args = {}
+        if api_key:
+            client_args["api_key"] = api_key
+        if openai_base:
+            client_args["base_url"] = openai_base
+
+        oai_client = OpenAI(**client_args)
+
+        completion = oai_client.chat.completions.create(
+            model="gpt-4o",
+            messages=messages,
+            response_format={
+                "type": "json_schema",
+                "json_schema": json_schema
+            }
+        )
+        if completion and isinstance(completion.choices, list) and len(completion.choices) > 0:
+            first_choice = completion.choices[0]
+            if first_choice and first_choice.message:
+                # return first_choice.message.content
+                return completion
+    except Exception as e:
+        log.error(f"[oai] generate_json failed: {e}")
+
+async def send_request_with_retry(url: str, data: Dict, headers: Dict[str, str], max_retries: int, delay_between_retries: int) -> bool:
+    for attempt in range(max_retries):
+        try:
+            async with session.ClientSession() as session:
+                async with session.post(url, json=data, headers=headers, timeout=10) as response:
+                    if response.status == 200:
+                        return True
+        except (aiohttp.ClientError, asyncio.TimeoutError) as e:
+            log.error(f"请求异常:{e}")
+
+        if attempt < max_retries - 1:
+            print("重试中...")
+            await asyncio.sleep(delay_between_retries)
+    return False

+ 18 - 0
common/dataclasses.py

@@ -40,3 +40,21 @@ class NewTokenReturn:
     new_refresh_token: str
     new_access_token_expire_time: datetime
     new_refresh_token_expire_time: datetime
+
+@dataclasses.dataclass
+class NewToken:
+    new_access_token: str
+    new_access_token_expire_time: datetime
+    new_refresh_token: str
+    new_refresh_token_expire_time: datetime
+
+@dataclasses.dataclass
+class AccessToken:
+    access_token: str
+    access_token_expire_time: datetime
+
+
+@dataclasses.dataclass
+class RefreshToken:
+    refresh_token: str
+    refresh_token_expire_time: datetime

+ 6 - 2
common/model.py

@@ -14,6 +14,10 @@ id_key = Annotated[
     int, mapped_column(primary_key=True, index=True, autoincrement=True, sort_order=-999, comment='主键id')
 ]
 
+id_key_str = Annotated[
+    str, mapped_column(primary_key=True, index=True, sort_order=-999, comment='主键id')
+]
+
 
 # Mixin: 一种面向对象编程概念, 使结构变得更加清晰, `Wiki <https://en.wikipedia.org/wiki/Mixin/>`__
 class UserMixin(MappedAsDataclass):
@@ -26,10 +30,10 @@ class UserMixin(MappedAsDataclass):
 class DateTimeMixin(MappedAsDataclass):
     """日期时间 Mixin 数据类"""
 
-    created_time: Mapped[datetime] = mapped_column(
+    created_at: Mapped[datetime] = mapped_column(
         init=False, default_factory=timezone.now, sort_order=999, comment='创建时间'
     )
-    updated_time: Mapped[datetime | None] = mapped_column(
+    updated_at: Mapped[datetime | None] = mapped_column(
         init=False, onupdate=timezone.now, sort_order=999, comment='更新时间'
     )
 

+ 116 - 0
common/security/jwt_call_center.py

@@ -0,0 +1,116 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+from datetime import timedelta
+
+from app.admin.schema.intent_org import CurrentIntentOrgIns
+from fastapi import Depends, Request
+from fastapi.security import HTTPBearer
+from fastapi.security.utils import get_authorization_scheme_param
+from jose import ExpiredSignatureError, JWTError, jwt
+from pydantic_core import from_json
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from common.log import log
+from model.intent_org import IntentOrg
+from common.dataclasses import AccessToken, NewToken, RefreshToken
+from common.exception.errors import AuthorizationError, TokenError
+from core.conf import settings
+from database.db_mysql import async_db_session
+from database.db_redis import redis_client
+from utils.serializers import select_as_dict
+from utils.timezone import timezone
+
+# JWT authorizes dependency injection
+DependsJwtAuth = Depends(HTTPBearer())
+
+
+async def create_access_token(sub: str) -> AccessToken:
+    """
+    Generate encryption token
+
+    :param sub: The subject/userid of the JWT
+    :param multi_login: multipoint login for user
+    :return:
+    """
+    expire = timezone.now() + timedelta(seconds=settings.TOKEN_EXPIRE_SECONDS)
+
+    to_encode = {'exp': expire, 'sub': sub}
+    access_token = jwt.encode(to_encode, settings.TOKEN_SECRET_KEY, settings.TOKEN_ALGORITHM)
+
+    return AccessToken(access_token=access_token, access_token_expire_time=expire)
+
+
+def get_token(request: Request) -> str:
+    """
+    Get token for request header
+
+    :return:
+    """
+    authorization = request.headers.get('Authorization')
+    scheme, token = get_authorization_scheme_param(authorization)
+    if not authorization or scheme.lower() != 'bearer':
+        raise TokenError(msg='Token 无效')
+    return token
+
+
+def jwt_decode(token: str) -> int:
+    """
+    Decode token
+
+    :param token:
+    :return:
+    """
+    try:
+        payload = jwt.decode(token, settings.TOKEN_SECRET_KEY, algorithms=[settings.TOKEN_ALGORITHM])
+        user_id = int(payload.get('sub'))
+        if not user_id:
+            raise TokenError(msg='Token 无效')
+    except ExpiredSignatureError:
+        raise TokenError(msg='Token 已过期')
+    except (JWTError, Exception):
+        raise TokenError(msg='Token 无效')
+    return user_id
+
+
+async def get_current_organization(db: AsyncSession, pk: str) -> IntentOrg:
+    """
+    Get the current user through token
+
+    :param db:
+    :param pk:
+    :return:
+    """
+    from app.admin.crud.crud_intent_org import intent_org_dao
+    org = await intent_org_dao.get_by_token(db, pk)
+    if not org:
+        raise TokenError(msg='Token 无效')
+    if org.status is not 1:
+        raise AuthorizationError(msg='用户已被锁定,请联系系统管理员')
+
+    return org
+
+
+async def jwt_call_center_authentication(token: str) -> CurrentIntentOrgIns:
+    """
+    JWT authentication
+
+    :param token:
+    :return:
+    """
+    org_id = jwt_decode(token)
+    key = f'{settings.TOKEN_CALL_REDIS_PREFIX}:{org_id}'
+    token_verify = await redis_client.get(key)
+    if not token_verify:
+        async with async_db_session() as db:
+            current_org = await get_current_organization(db, token)
+            org = CurrentIntentOrgIns(**select_as_dict(current_org))
+            await redis_client.setex(
+                key,
+                settings.JWT_USER_REDIS_EXPIRE_SECONDS,
+                org.model_dump_json(),
+            )
+    else:
+        # TODO: 在恰当的时机,应替换为使用 model_validate_json
+        # https://docs.pydantic.dev/latest/concepts/json/#partial-json-parsing
+        org = CurrentIntentOrgIns.model_validate(from_json(token_verify, allow_partial=True))
+    return org

+ 3 - 0
core/conf.py

@@ -87,11 +87,14 @@ class Settings(BaseSettings):
     TOKEN_EXPIRE_SECONDS: int = 60 * 60 * 24 * 1  # 过期时间,单位:秒
     TOKEN_REFRESH_EXPIRE_SECONDS: int = 60 * 60 * 24 * 7  # 刷新过期时间,单位:秒
     TOKEN_REDIS_PREFIX: str = 'fba:token'
+    TOKEN_CALL_REDIS_PREFIX: str = 'fba:call_token'
     TOKEN_REFRESH_REDIS_PREFIX: str = 'fba:token:refresh'
     TOKEN_EXCLUDE: list[str] = [  # JWT / RBAC 白名单
         f'{API_V1_STR}/auth/login',
     ]
 
+    JWT_USER_REDIS_EXPIRE_SECONDS: int = 60 * 60 * 24 * 7
+
     # Sys User
     USER_REDIS_PREFIX: str = 'fba:user'
     USER_REDIS_EXPIRE_SECONDS: int = 60 * 60 * 24 * 7

+ 74 - 12
core/registrar.py

@@ -1,19 +1,24 @@
 #!/usr/bin/env python3
 # -*- coding: utf-8 -*-
+import asyncio
+import threading
 from contextlib import asynccontextmanager
 
-from fastapi import Depends, FastAPI
+from fastapi import Depends, FastAPI, APIRouter
 from fastapi_limiter import FastAPILimiter
 from fastapi_pagination import add_pagination
 from starlette.middleware.authentication import AuthenticationMiddleware
 
-from app.router import route
+from app.router import route, call_center_route
+from batch_task.batch_task import start_batch_task, stop_batch_task, execute_task, \
+    periodically_execute
+from batch_task.update_llm_intent import update_llm_intent
 from common.exception.exception_handler import register_exception
-from common.log import set_customize_logfile, setup_logging
+from common.log import set_customize_logfile, setup_logging, log
 from core.conf import settings
 from core.path_conf import STATIC_DIR
-from database.db_mysql import create_table
 from database.db_redis import redis_client
+from middleware.jwt_call_center_auth_middleware import JwtCallCenterAuthMiddleware
 
 from middleware.opera_log_middleware import OperaLogMiddleware
 from utils.demo_site import demo_site
@@ -21,7 +26,6 @@ from utils.health_check import ensure_unique_route_names, http_limit_callback
 from utils.openapi import simplify_operation_ids
 from utils.serializers import MsgSpecJSONResponse
 
-
 @asynccontextmanager
 async def register_init(app: FastAPI):
     """
@@ -35,7 +39,6 @@ async def register_init(app: FastAPI):
     await redis_client.open()
     # 初始化 limiter
     await FastAPILimiter.init(redis_client, prefix=settings.LIMITER_REDIS_PREFIX, http_callback=http_limit_callback)
-
     yield
 
     # 关闭 redis 连接
@@ -57,23 +60,52 @@ def register_app():
         lifespan=register_init,
     )
 
+    admin_app = FastAPI(
+        title=settings.TITLE,
+        version=settings.VERSION,
+        description=settings.DESCRIPTION,
+        default_response_class=MsgSpecJSONResponse,
+    )
+
+    call_center_app = FastAPI(
+        title="Call Center",  # 子应用的标题
+        version="0.1.0",  # 子应用的版本
+        default_response_class=MsgSpecJSONResponse,
+    )
+
+    # 中间件
+    register_call_center_middleware(call_center_app)
+
+    # 路由
+    register_router(call_center_app, call_center_route)
+
+    # 分页
+    register_page(call_center_app)
+
+    # 全局异常处理
+    register_exception(call_center_app)
+
+    app.mount(f"/call_center", call_center_app)
+
     # 日志
     register_logger()
 
     # 静态文件
-    register_static_file(app)
+    register_static_file(admin_app)
 
     # 中间件
-    register_middleware(app)
+    register_middleware(admin_app)
 
     # 路由
-    register_router(app)
+    register_router(admin_app, route)
 
     # 分页
-    register_page(app)
+    register_page(admin_app)
 
     # 全局异常处理
-    register_exception(app)
+    register_exception(admin_app)
+
+    app.mount(f"{settings.API_V1_STR}/gpt", admin_app)
 
     return app
 
@@ -135,12 +167,42 @@ def register_middleware(app: FastAPI):
             allow_headers=['*'],
         )
 
+def register_call_center_middleware(app: FastAPI):
+    """
+    中间件,执行顺序从下往上
+
+    :param app:
+    :return:
+    """
+    # Opera log (required)
+    app.add_middleware(OperaLogMiddleware)
+    # JWT auth (required)
+    app.add_middleware(
+        AuthenticationMiddleware, backend=JwtCallCenterAuthMiddleware(), on_error=JwtCallCenterAuthMiddleware.auth_exception_handler
+    )
+    # Access log
+    if settings.MIDDLEWARE_ACCESS:
+        from middleware.access_middleware import AccessMiddleware
+
+        app.add_middleware(AccessMiddleware)
+    # CORS: Always at the end
+    if settings.MIDDLEWARE_CORS:
+        from fastapi.middleware.cors import CORSMiddleware
+
+        app.add_middleware(
+            CORSMiddleware,
+            allow_origins=['*'],
+            allow_credentials=True,
+            allow_methods=['*'],
+            allow_headers=['*'],
+        )
 
-def register_router(app: FastAPI):
+def register_router(app: FastAPI, route: APIRouter):
     """
     路由
 
     :param app: FastAPI
+    :param route: APIRouter
     :return:
     """
     dependencies = [Depends(demo_site)] if settings.DEMO_MODE else None

+ 7 - 20
main.py

@@ -1,21 +1,6 @@
-from contextlib import asynccontextmanager
-from pathlib import Path
-
-from fastapi import FastAPI
-import uvicorn
-
-# app = FastAPI()
-
-
-# @app.get("/")
-# async def root():
-#     return {"message": "Hello World"}
-#
-#
-# @app.get("/hello/{name}")
-# async def say_hello(name: str):
-#     return {"message": f"Hello {name}"}
+import atexit
 
+from batch_task.batch_task import start_batch_task, stop_batch_task
 
 from core.registrar import register_app
 
@@ -26,10 +11,12 @@ app = register_app()
 if __name__ == '__main__':
     # 如果你喜欢在 IDE 中进行 DEBUG,main 启动方法会很有帮助
     # 如果你喜欢通过 print 方式进行调试,建议使用 fastapi cli 方式启动服务
+    atexit.register(stop_batch_task)
     try:
-        config = uvicorn.Config(app=f'{Path(__file__).stem}:app', reload=False)
-        server = uvicorn.Server(config)
-        server.run()
+        start_batch_task()
+        # config = uvicorn.Config(app=f'{Path(__file__).stem}:app', reload=False, workers=15)
+        # server = uvicorn.Server(config)
+        # server.run()
 
         # uvicorn.run(app, host='127.0.0.1', port=8000)
     except Exception as e:

+ 55 - 0
middleware/jwt_call_center_auth_middleware.py

@@ -0,0 +1,55 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+from typing import Any
+
+from fastapi import Request, Response
+from fastapi.security.utils import get_authorization_scheme_param
+from starlette.authentication import AuthCredentials, AuthenticationBackend, AuthenticationError
+from starlette.requests import HTTPConnection
+
+from app.admin.schema.intent_org import CurrentIntentOrgIns
+from common.exception.errors import TokenError
+from common.log import log
+from common.security.jwt_call_center import jwt_call_center_authentication
+from core.conf import settings
+from utils.serializers import MsgSpecJSONResponse
+
+
+class _AuthenticationError(AuthenticationError):
+    """重写内部认证错误类"""
+
+    def __init__(self, *, code: int = None, msg: str = None, headers: dict[str, Any] | None = None):
+        self.code = code
+        self.msg = msg
+        self.headers = headers
+
+
+class JwtCallCenterAuthMiddleware(AuthenticationBackend):
+    """JWT 认证中间件"""
+
+    @staticmethod
+    def auth_exception_handler(conn: HTTPConnection, exc: _AuthenticationError) -> Response:
+        """覆盖内部认证错误处理"""
+        return MsgSpecJSONResponse(content={'code': exc.code, 'msg': exc.msg, 'data': None}, status_code=exc.code)
+
+    async def authenticate(self, request: Request) -> tuple[AuthCredentials, CurrentIntentOrgIns] | None:
+        token = request.headers.get('Authorization')
+        if not token:
+            return
+
+        if request.url.path in settings.TOKEN_EXCLUDE:
+            return
+        scheme, token = get_authorization_scheme_param(token)
+        if scheme.lower() != "bearer":
+            return
+        try:
+            org = await jwt_call_center_authentication(token)
+        except TokenError as exc:
+            raise _AuthenticationError(code=exc.code, msg=exc.detail, headers=exc.headers)
+        except Exception as e:
+            log.error(f'JWT 授权异常:{e}')
+            raise _AuthenticationError(code=getattr(e, 'code', 500), msg=getattr(e, 'msg', 'Internal Server Error'))
+
+        # 请注意,此返回使用非标准模式,所以在认证通过时,将丢失某些标准特性
+        # 标准返回模式请查看:https://www.starlette.io/authentication/
+        return AuthCredentials(['authenticated']), org

+ 27 - 0
model/intent_org.py

@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+from datetime import datetime
+
+import sqlalchemy as sa
+from sqlalchemy import BigInteger, TIMESTAMP, text
+
+from common.model import Base, id_key
+from sqlalchemy.dialects import mysql
+from sqlalchemy.orm import Mapped, mapped_column
+
+
+class IntentOrg(Base):
+    """organization"""
+
+    __tablename__ = 'intent_org'
+
+    id: Mapped[id_key] = mapped_column(BigInteger, primary_key=True)
+    created_at: Mapped[datetime | None] = mapped_column(TIMESTAMP, nullable=False, server_default=text('CURRENT_TIMESTAMP'), comment='Create Time | 创建日期')
+    updated_at: Mapped[datetime | None] = mapped_column(TIMESTAMP, nullable=False, server_default=text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), comment='Update Time | 修改日期')
+    name: Mapped[str | None] = mapped_column(sa.String(255), default=None, sort_order=2, comment='机构名称')
+    api_key: Mapped[str | None] = mapped_column(sa.String(255), default=None, sort_order=3, comment='')
+    openai_base: Mapped[str | None] = mapped_column(sa.String(255), default=None, sort_order=4, comment='')
+    openai_key: Mapped[str | None] = mapped_column(sa.String(255), default=None, sort_order=5, comment='')
+    intent_callback: Mapped[str | None] = mapped_column(sa.String(255), default=None, sort_order=6, comment='意向度结果推送地址')
+    status: Mapped[int] = mapped_column(mysql.TINYINT(), default=0, sort_order=9, comment='状态 1 正常 2 禁用')
+    deleted_at: Mapped[datetime | None] = mapped_column(TIMESTAMP, default=None, sort_order=10, comment='Delete Time | 删除日期')

+ 30 - 0
model/intent_records.py

@@ -0,0 +1,30 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+from datetime import datetime
+from uuid import UUID
+
+import sqlalchemy as sa
+from sqlalchemy import TIMESTAMP, text
+
+from common.model import Base, id_key_str
+from sqlalchemy.dialects import mysql
+from sqlalchemy.orm import Mapped, mapped_column
+
+
+class IntentRecords(Base):
+    """call record"""
+
+    __tablename__ = 'intent_records'
+
+    id: Mapped[id_key_str] = mapped_column()
+    created_at: Mapped[datetime | None] = mapped_column(TIMESTAMP, nullable=False, server_default=text('CURRENT_TIMESTAMP'), comment='Create Time | 创建日期')
+    updated_at: Mapped[datetime | None] = mapped_column(TIMESTAMP, nullable=False, server_default=text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), comment='Update Time | 修改日期')
+    external_id: Mapped[str] = mapped_column(sa.String(255), default='', sort_order=2, comment='外部id')
+    industry_type: Mapped[int] = mapped_column(sa.Integer(), default=0, sort_order=3, comment='评分规则代码 0 通用 1 教育')
+    chat_history: Mapped[str] = mapped_column(sa.TEXT(), default='', sort_order=4, comment='通话记录')
+    manual_intent: Mapped[int | None] = mapped_column(mysql.TINYINT(), default=None, sort_order=5, comment='人工意向度 1 有意向 2 无意向 3 其他')
+    llm_intent: Mapped[int | None] = mapped_column(mysql.TINYINT(), default=None, sort_order=6, comment='大模型意向度 1 有意向 2 无意向 3 不确定')
+    org_id: Mapped[int] = mapped_column(sa.BIGINT(), default=0, sort_order=7, comment='机构 ID')
+    status: Mapped[int] = mapped_column(mysql.TINYINT(), default=0, sort_order=10, comment='状态 0 入库 1 已判断 2 已回调')
+    request_data: Mapped[dict | None] = mapped_column(sa.JSON(), default=None, sort_order=12, comment='')
+    response_data: Mapped[dict | None] = mapped_column(sa.JSON(), default=None, sort_order=13, comment='')

+ 1 - 2
model/records.py

@@ -1,7 +1,6 @@
 from sqlalchemy import BigInteger, Column, JSON, String, TIMESTAMP, Text, text
 from sqlalchemy.dialects.mysql import TINYINT, VARCHAR
-from sqlalchemy.orm import Mapped, declarative_base, mapped_column
-from sqlalchemy.orm.base import Mapped
+from sqlalchemy.orm import declarative_base, mapped_column
 
 Base = declarative_base()
 

二進制
requirements.txt