Merge branch 'develop' of http://47.106.207.27:3000/AI260626/Mutual_Fund into develop_feature_customer
# Conflicts: # .gitignore # api/deps.py # api/router.py # main.py # tests/test_customer_agent_router.py # tests/test_knowledge_router.py
This commit is contained in:
+67
@@ -62,6 +62,73 @@ data/files/
|
||||
*.csv
|
||||
*.xlsx
|
||||
*.parquet
|
||||
# Python 缓存与编译文件
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# 虚拟环境(非常重要,防止把本地依赖包提交上去)
|
||||
venv/
|
||||
env/
|
||||
.idea
|
||||
.venv/
|
||||
ENV/
|
||||
|
||||
# 日志文件
|
||||
*.log
|
||||
|
||||
# 操作系统文件
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# pytest 测试产物
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
coverage.xml
|
||||
htmlcov/
|
||||
junit.xml
|
||||
|
||||
# mypy 类型检查缓存
|
||||
.mypy_cache/
|
||||
# 向量库、本地数据库、模型文件(大文件不要提交git)
|
||||
*.db
|
||||
*.sqlite
|
||||
*.milvus
|
||||
*.neo4j
|
||||
*.bin
|
||||
*.pth
|
||||
*.ckpt
|
||||
*.onnx
|
||||
|
||||
# 本地导出数据、临时csv/excel
|
||||
data/tmp/
|
||||
data/output/
|
||||
*.csv
|
||||
*.xlsx
|
||||
*.parquet
|
||||
|
||||
# 本地临时需求&开发计划文档
|
||||
DK2_客服Agent模块完整开发计划(v1.1).md
|
||||
DK4_客服Agent需求文档(修复完善版v1.4).md
|
||||
客服Agent模块 · TODO List(最终修复版v1.2).md
|
||||
|
||||
|
||||
DK2_客服Agent模块完整开发计划(v1.1).md
|
||||
DK4_客服Agent需求文档(修复完善版v1.4).md
|
||||
|
||||
Generated
-10
@@ -1,10 +0,0 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 已忽略包含查询文件的默认文件夹
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
Generated
-1
@@ -1 +0,0 @@
|
||||
Mutual_Fund
|
||||
Generated
-7
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="com.codeverse.userSettings.MarscodeWorkspaceAppSettingsState">
|
||||
<option name="chatAppRouterInfo" value="chat-session" />
|
||||
<option name="progress" value="1.0" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
-20
@@ -1,20 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ClaudeCodeTabState">
|
||||
<option name="tabSessions">
|
||||
<map>
|
||||
<entry key="0">
|
||||
<value>
|
||||
<TabSessionState>
|
||||
<option name="provider" value="claude" />
|
||||
<option name="sessionId" value="ed440cf1-c034-44b5-b5db-bc5afb824005" />
|
||||
<option name="cwd" value="D:\Projects\py\Mutual_Fund" />
|
||||
<option name="model" value="claude-sonnet-5[1m]" />
|
||||
<option name="permissionMode" value="default" />
|
||||
</TabSessionState>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
Generated
-8
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/Mutual_Fund.iml" filepath="$PROJECT_DIR$/.idea/Mutual_Fund.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
Generated
-6
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Anonymous customer-service Agent HTTP endpoints."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
|
||||
from service.customer_agent.chat import QueryTooLongError
|
||||
from agent.customer_agent.session import SessionOwnershipError
|
||||
from utils.request_id import get_request_id, new_request_id
|
||||
from utils.response import fail, success
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _runtime(request: Request):
|
||||
runtime = getattr(request.app.state, "customer_agent_runtime", None)
|
||||
if runtime is None:
|
||||
raise RuntimeError("customer agent runtime is not configured")
|
||||
return runtime
|
||||
|
||||
|
||||
@router.post("/session/create")
|
||||
async def create_session(request: Request):
|
||||
runtime = _runtime(request)
|
||||
session_id = await runtime.session_service.create_session()
|
||||
return success({"session_id": session_id, "customer_id": None})
|
||||
|
||||
|
||||
@router.get("/chat")
|
||||
async def chat(request: Request, session_id: str, query: str):
|
||||
runtime = _runtime(request)
|
||||
trace_id = request.headers.get("X-Trace-Id") or get_request_id() or new_request_id()
|
||||
try:
|
||||
await runtime.session_service.verify_session_ownership(session_id)
|
||||
retry_after = await runtime.session_service.consume_chat_quota(session_id)
|
||||
if retry_after is not None:
|
||||
response = fail(429, "请求过于频繁", {"retry_after": retry_after})
|
||||
return JSONResponse(
|
||||
status_code=429,
|
||||
headers={"Retry-After": str(retry_after)},
|
||||
content=response.model_dump(),
|
||||
)
|
||||
result = await runtime.agent.handle(session_id, query, trace_id=trace_id)
|
||||
except SessionOwnershipError as exc:
|
||||
return JSONResponse(
|
||||
status_code=exc.code,
|
||||
content=fail(exc.code, exc.message).model_dump(),
|
||||
)
|
||||
except QueryTooLongError as exc:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content=fail(400, str(exc)).model_dump(),
|
||||
)
|
||||
|
||||
async def events():
|
||||
yield f"data: {json.dumps(result, ensure_ascii=False)}\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
events(), media_type="text/event-stream", headers={"X-Trace-Id": trace_id}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/session/end")
|
||||
async def end_session(request: Request, body: dict):
|
||||
runtime = _runtime(request)
|
||||
session_id = body.get("session_id", "")
|
||||
try:
|
||||
await runtime.session_service.verify_session_ownership(session_id)
|
||||
except SessionOwnershipError as exc:
|
||||
return JSONResponse(
|
||||
status_code=exc.code,
|
||||
content=fail(exc.code, exc.message).model_dump(),
|
||||
)
|
||||
if hasattr(runtime.redis, "delete"):
|
||||
await runtime.redis.delete(
|
||||
f"session:{session_id}", f"session:{session_id}:messages",
|
||||
f"rate:limit:anon:{session_id}:chat",
|
||||
)
|
||||
return success({"session_id": session_id, "archived": False})
|
||||
@@ -0,0 +1,108 @@
|
||||
"""Knowledge-base upload, preview, and confirmation endpoints."""
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from service.knowledge_base.upload import KnowledgeUploadService, UploadValidationError
|
||||
from api.deps import require_knowledge_operator
|
||||
from model.sys_user import SysUser
|
||||
from utils.response import fail, success
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _service(request: Request) -> KnowledgeUploadService:
|
||||
service = getattr(request.app.state, "knowledge_upload_service", None)
|
||||
if service is None:
|
||||
raise RuntimeError("knowledge upload service is not configured")
|
||||
return service
|
||||
|
||||
|
||||
def _optional_int(value):
|
||||
return None if value in (None, "") else int(value)
|
||||
|
||||
|
||||
@router.get("/documents")
|
||||
async def list_documents(
|
||||
request: Request,
|
||||
_: SysUser = Depends(require_knowledge_operator),
|
||||
):
|
||||
return success(await _service(request).list_documents())
|
||||
|
||||
|
||||
@router.get("/documents/{doc_id}")
|
||||
async def get_document(
|
||||
request: Request,
|
||||
doc_id: str,
|
||||
_: SysUser = Depends(require_knowledge_operator),
|
||||
):
|
||||
try:
|
||||
result = await _service(request).get_document(doc_id)
|
||||
if result is None:
|
||||
response = fail(404, "文档不存在")
|
||||
return JSONResponse(status_code=404, content=response.model_dump())
|
||||
return success(result)
|
||||
except (UploadValidationError, ValueError) as exc:
|
||||
response = fail(400, str(exc))
|
||||
return JSONResponse(status_code=400, content=response.model_dump())
|
||||
|
||||
|
||||
@router.post("/documents/preview")
|
||||
async def preview_document_upload(
|
||||
request: Request,
|
||||
_: SysUser = Depends(require_knowledge_operator),
|
||||
):
|
||||
try:
|
||||
form = await request.form()
|
||||
upload = form.get("file")
|
||||
if upload is None or not hasattr(upload, "read"):
|
||||
raise UploadValidationError("缺少上传文件")
|
||||
result = await _service(request).preview(
|
||||
upload.filename,
|
||||
await upload.read(),
|
||||
strategy=form.get("strategy", ""),
|
||||
chunk_size=_optional_int(form.get("chunk_size")),
|
||||
chunk_overlap=_optional_int(form.get("chunk_overlap")),
|
||||
)
|
||||
return success(result)
|
||||
except (UploadValidationError, ValueError) as exc:
|
||||
response = fail(400, str(exc))
|
||||
return JSONResponse(status_code=400, content=response.model_dump())
|
||||
|
||||
|
||||
@router.post("/documents/confirm")
|
||||
async def confirm_document_upload(
|
||||
request: Request,
|
||||
_: SysUser = Depends(require_knowledge_operator),
|
||||
):
|
||||
try:
|
||||
body = await request.json()
|
||||
result = await _service(request).confirm(
|
||||
upload_id=body.get("upload_id", ""),
|
||||
title=body.get("title", ""),
|
||||
doc_id=body.get("doc_id", ""),
|
||||
collection_name=body.get("collection_name", ""),
|
||||
strategy=body.get("strategy", ""),
|
||||
chunk_size=body.get("chunk_size"),
|
||||
chunk_overlap=body.get("chunk_overlap"),
|
||||
)
|
||||
return success(result)
|
||||
except (UploadValidationError, ValueError) as exc:
|
||||
response = fail(400, str(exc))
|
||||
return JSONResponse(status_code=400, content=response.model_dump())
|
||||
|
||||
|
||||
@router.delete("/documents/{doc_id}")
|
||||
async def delete_document(
|
||||
request: Request,
|
||||
doc_id: str,
|
||||
_: SysUser = Depends(require_knowledge_operator),
|
||||
):
|
||||
try:
|
||||
result = await _service(request).delete_document(doc_id)
|
||||
return success(result)
|
||||
except (UploadValidationError, ValueError) as exc:
|
||||
response = fail(400, str(exc))
|
||||
return JSONResponse(status_code=400, content=response.model_dump())
|
||||
+10
@@ -30,6 +30,16 @@ async def get_current_user(
|
||||
return user
|
||||
|
||||
|
||||
async def require_knowledge_operator(user: SysUser = Depends(get_current_user)) -> SysUser:
|
||||
"""Allow only administrators or explicitly assigned knowledge operators."""
|
||||
if user.user_type == "ADMIN":
|
||||
return user
|
||||
if user.user_type != "EMPLOYEE" or user.employee_role not in KNOWLEDGE_OPERATOR_ROLES:
|
||||
raise ForbiddenError("仅运营人员可以管理知识库")
|
||||
return user
|
||||
|
||||
|
||||
|
||||
async def require_knowledge_operator(
|
||||
user: SysUser = Depends(get_current_user),
|
||||
) -> SysUser:
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
"""
|
||||
from fastapi import APIRouter
|
||||
|
||||
from api.chat import customer_agent, knowledge
|
||||
from api.routers import product, questionnaire
|
||||
from api.routers import account, auth, holdings, purchase, redeem
|
||||
from api.routers import auth, customer_agent, knowledge, product, questionnaire
|
||||
from api.routers import account, holdings, purchase, redeem
|
||||
|
||||
|
||||
@@ -34,6 +34,16 @@ def client() -> AsyncMilvusClient:
|
||||
return _client
|
||||
|
||||
|
||||
async def ensure_database(milvus_client: AsyncMilvusClient | None = None) -> None:
|
||||
"""Create the configured project database only when it does not exist."""
|
||||
target = milvus_client or client()
|
||||
database_name = settings.milvus.db_name
|
||||
if not database_name:
|
||||
raise RuntimeError("MILVUS_DB must be configured")
|
||||
if database_name not in await target.list_databases():
|
||||
await target.create_database(database_name)
|
||||
|
||||
|
||||
async def init_db() -> None:
|
||||
client() # 急切建连;失败由注册表重试(config/database/__init__.py)
|
||||
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
fastapi
|
||||
python-multipart
|
||||
uvicorn
|
||||
httpx
|
||||
pyjwt
|
||||
pydantic
|
||||
pydantic-settings
|
||||
sqlalchemy
|
||||
aiomysql
|
||||
redis
|
||||
pymilvus
|
||||
neo4j
|
||||
python-dotenv
|
||||
pypdf
|
||||
|
||||
fastapi~=0.141.1
|
||||
sqlalchemy~=2.0.52
|
||||
httpx~=0.28.1
|
||||
|
||||
+4
-28
@@ -265,6 +265,7 @@ CREATE TABLE IF NOT EXISTS conversation_archive (
|
||||
role VARCHAR(16) NOT NULL COMMENT 'user/assistant/system',
|
||||
content MEDIUMTEXT NULL COMMENT '对话内容',
|
||||
tool_calls JSON NULL COMMENT '工具调用记录 [{"tool":"nl2sql",...}]',
|
||||
trace_id VARCHAR(64) NULL COMMENT '请求链路追踪ID',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
trace_id VARCHAR(64) NULL COMMENT '请求链路追踪ID',
|
||||
KEY idx_session (session_id),
|
||||
@@ -341,7 +342,7 @@ CREATE TABLE IF NOT EXISTS audit_log (
|
||||
target VARCHAR(128) NULL COMMENT '操作对象(单号/ID)',
|
||||
detail TEXT NULL COMMENT '详情(JSON 字符串)',
|
||||
ip VARCHAR(64) NULL,
|
||||
trace_id VARCHAR(64) NULL COMMENT '链路号',
|
||||
trace_id VARCHAR(32) NULL COMMENT '链路号',
|
||||
status VARCHAR(8) NOT NULL DEFAULT '成功' COMMENT '成功/失败',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
KEY idx_user (user_id),
|
||||
@@ -410,16 +411,7 @@ CREATE TABLE IF NOT EXISTS memory_unit (
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT 'active/demoted/archived/deleted',
|
||||
valid_until DATE NULL COMMENT '有效期',
|
||||
KEY idx_customer_status (customer_id, status),
|
||||
KEY idx_tag (tag),
|
||||
session_id VARCHAR(64) NULL COMMENT '记忆来源会话',
|
||||
agent_run_id VARCHAR(64) NULL COMMENT '产生该记忆的Agent运行ID',
|
||||
evidence_ref VARCHAR(128) NULL COMMENT '证据引用',
|
||||
milvus_id VARCHAR(128) NULL COMMENT 'Milvus向量ID',
|
||||
graph_node_id VARCHAR(128) NULL COMMENT 'Neo4j节点或关系ID',
|
||||
valid_from DATETIME NULL COMMENT '记忆生效时间',
|
||||
last_verified_at DATETIME NULL COMMENT '最近确认时间',
|
||||
KEY idx_customer_tag_status (customer_id, tag, status),
|
||||
KEY idx_agent_run (agent_run_id);
|
||||
KEY idx_tag (tag)
|
||||
) COMMENT='记忆单元表(三层记忆中期主体,向量镜像在 Milvus customer_memory)';
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
@@ -450,20 +442,4 @@ CREATE TABLE IF NOT EXISTS portfolio_benchmark (
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_risk_level (risk_level)
|
||||
) COMMENT='组合基准配置表(投顾Agent 再平衡参照,运营可调整)';
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 25 客户资金账户表(现金账户,一人一户)
|
||||
-- ---------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS fin_account (
|
||||
customer_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID,一对一关联 sys_user.id',
|
||||
balance DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '账户总余额(元),含冻结部分;可用余额=balance-frozen_amount',
|
||||
frozen_amount DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '冻结金额(元),申购在途/待确认订单占用',
|
||||
currency VARCHAR(8) NOT NULL DEFAULT 'CNY' COMMENT '币种,当前仅 CNY',
|
||||
status VARCHAR(16) NOT NULL DEFAULT '正常' COMMENT '账户状态:正常/冻结/销户',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,扣款/入账 CAS 用',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (customer_id),
|
||||
KEY idx_status (status)
|
||||
) COMMENT='客户资金账户表(现金余额,申购扣款/赎回入账的账务载体)';
|
||||
) COMMENT='组合基准配置表(投顾Agent 再平衡参照,运营可调整)';
|
||||
@@ -5,6 +5,7 @@ from types import SimpleNamespace
|
||||
import httpx
|
||||
from fastapi import FastAPI
|
||||
|
||||
from api.chat.customer_agent import router
|
||||
from api.routers.customer_agent import router
|
||||
from rag.intent import Intent
|
||||
from service.customer_agent.chat import AnonymousCustomerAgent
|
||||
|
||||
@@ -3,6 +3,7 @@ import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from api.chat.knowledge import (
|
||||
from api.routers.knowledge import (
|
||||
confirm_document_upload,
|
||||
delete_document,
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ class Code:
|
||||
SERVER_ERROR = 500
|
||||
LLM_FAIL = 1001 # LLM 调用失败
|
||||
KB_NO_RESULT = 1002 # 知识库检索无结果
|
||||
SQL_GEN_FAIL = 1003 # NL2SQL 生成失败
|
||||
SQL_GEN_FAIL = 1003 # nl2sql 生成失败
|
||||
RISK_TRIGGERED = 1004 # 风控规则触发 / 交易被拦截
|
||||
NOT_SUITABLE = 1005 # 适当性不匹配
|
||||
|
||||
|
||||
Reference in New Issue
Block a user