初始化项目,完整业务代码提交

This commit is contained in:
qinyong@9artedu.com 2026-06-15 09:27:28 +08:00
commit 16d736e3aa
64 changed files with 47090 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
# 忽略Maven编译目录
.trae
.venv

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.12 (sales-assistant-py)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (sales-assistant-py)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/sales-assistant-py.iml" filepath="$PROJECT_DIR$/.idea/sales-assistant-py.iml" />
</modules>
</component>
</project>

10
.idea/sales-assistant-py.iml generated Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.12 (sales-assistant-py)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

0
app/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

1
app/aa.md Normal file
View File

@ -0,0 +1 @@
【日期】 2026年6月11日 \n【沟通对象】 客户Janice \n** 核心诉求 (User Intent)** \n咨询线上角色建模相关课程销售进一步了解其年级与专业背景。 \n** 处理结果 (Resolution)** \n初步确认客户对角色建模有兴趣正在收集基本信息。 \n** 待办事项 (Action Items)** \n \n** 关键标签**#角色建模#初步咨询

0
app/client/__init__.py Normal file
View File

Binary file not shown.

View File

@ -0,0 +1,70 @@
import asyncio
import time
import aiohttp
import requests
from app.conf.app_config import EmbeddingConfig, app_config
class EmbeddingClientManager:
def __init__(self, config: EmbeddingConfig):
self.config = config
self.client = None
def _get_url(self):
return f"http://{self.config.host}:{self.config.port}"
def init(self, wait_for_ready: bool = True):
self.client = aiohttp.ClientSession()
async def close(self):
if self.client:
await self.client.close()
self.client = None
async def aembed_documents(self, texts: list) -> list:
if not self.client:
self.init(wait_for_ready=False)
url = f"{self._get_url()}/embed"
payload = {
"inputs": texts,
"parameters": {"truncate": True}
}
async with self.client.post(url, json=payload) as response:
result = await response.json()
if isinstance(result, list):
return result
return result.get("embeddings", [])
async def aembed_query(self, text: str) -> list:
embeddings = await self.aembed_documents([text])
return embeddings[0] if embeddings else []
embedding_client = EmbeddingClientManager(app_config.embedding)
if __name__ == "__main__":
print("Testing EmbeddingClientManager...")
async def test():
try:
embedding_client.init(wait_for_ready=True)
print("Initialization successful")
text = "What is Deep Learning?"
print(f"Test text: {text}")
result = await embedding_client.aembed_query(text)
print(f"Embedding successful")
print(f"Vector length: {len(result)}")
print(f"First 10 values: {result[:10]}")
await embedding_client.close()
except Exception as e:
print(f"Test failed: {str(e)}")
asyncio.run(test())

View File

@ -0,0 +1,59 @@
from typing import Optional
from pymilvus import MilvusClient as PyMilvusClient
from app.conf.app_config import MilvusConfig, app_config
class MilvusClientWrapper:
"""Milvus 客户端单例包装器"""
_instance = None
_initialized = False
def __new__(cls, milvus_config: MilvusConfig = None):
if cls._instance is None:
cls._instance = super(MilvusClientWrapper, cls).__new__(cls)
return cls._instance
def __init__(self, milvus_config: MilvusConfig = None):
if self._initialized:
return
if milvus_config is None:
milvus_config = app_config.milvus
self.milvus_config = milvus_config
self.client: Optional[PyMilvusClient] = None
self._initialized = True
def init(self):
if self.client is not None:
return
uri = f"http://{self.milvus_config.host}:{self.milvus_config.port}"
self.client = PyMilvusClient(
uri=uri,
user=self.milvus_config.user,
password=self.milvus_config.password
)
def close(self):
if self.client is not None:
self.client.close()
self.client = None
def has_collection(self, collection_name: str) -> bool:
self._ensure_connection()
return self.client.has_collection(collection_name)
def drop_collection(self, collection_name: str):
self._ensure_connection()
if self.client.has_collection(collection_name):
self.client.drop_collection(collection_name)
def _ensure_connection(self):
if self.client is None:
self.init()
milvus_client = MilvusClientWrapper()

View File

@ -0,0 +1,45 @@
import asyncio
from typing import Optional
from sqlalchemy import text
from sqlalchemy.ext.asyncio import create_async_engine, AsyncEngine, async_sessionmaker
from app.conf.app_config import DBConfig, app_config
class MysqlClientManager:
def __init__(self, db_config: DBConfig):
self.db_config = db_config
self.engine: Optional[AsyncEngine] = None
self.session_factory = None
def _get_url(self):
return f"mysql+asyncmy://{self.db_config.user}:{self.db_config.password}@{self.db_config.host}:{self.db_config.port}/{self.db_config.database}?charset=utf8mb4"
def init(self):
self.engine = create_async_engine(url=self._get_url(),
pool_size=10,
pool_pre_ping=True)
self.session_factory = async_sessionmaker(db_assistant_mysql_client_manager.engine,
autoflush=True,
expire_on_commit=False)
async def close(self):
await self.engine.dispose()
db_assistant_mysql_client_manager = MysqlClientManager(app_config.db_assistant)
if __name__ == '__main__':
db_assistant_mysql_client_manager.init()
async def test():
async with db_assistant_mysql_client_manager.session_factory() as session:
result = await session.execute(text("select * from archive_messages limit 10"))
rows = result.mappings().fetchall()
print(type(rows[0]))
print(rows)
asyncio.run(test())

0
app/conf/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

72
app/conf/app_config.py Normal file
View File

@ -0,0 +1,72 @@
from dataclasses import dataclass
from pathlib import Path
from omegaconf import OmegaConf
# 日志配置
@dataclass
class File:
enable: bool
level: str
path: str
rotation: str
retention: str
@dataclass
class Console:
enable: bool
level: str
@dataclass
class LoggingConfig:
file: File
console: Console
# 数据库配置
@dataclass
class DBConfig:
host: str
port: int
user: str
password: str
database: str
@dataclass
class EmbeddingConfig:
host: str
port: int
model: str
@dataclass
class MilvusConfig:
host: str
port: int
user: str
password: str
embedding_size: int
@dataclass
class LLMConfig:
model_name: str
api_key: str
base_url: str
@dataclass
class AppConfig:
logging: LoggingConfig
db_assistant: DBConfig
embedding: EmbeddingConfig
llm: LLMConfig
milvus: MilvusConfig
config_file = Path(__file__).parents[2] / 'conf' / 'app_config.yaml'
context = OmegaConf.load(config_file)
schema = OmegaConf.structured(AppConfig)
app_config: AppConfig = OmegaConf.to_object(OmegaConf.merge(schema, context))
if __name__ == '__main__':
print(app_config.db_assistant.host)

0
app/dto/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

9
app/dto/summary_dto.py Normal file
View File

@ -0,0 +1,9 @@
from dataclasses import dataclass
@dataclass
class SummaryDto:
msg_date: str
from_user: str
to_user: str
summary: str

5
app/llm.py Normal file
View File

@ -0,0 +1,5 @@
from langchain.chat_models import init_chat_model
from app.conf.app_config import app_config
llm = init_chat_model(model=app_config.llm.model_name, api_key=app_config.llm.api_key, base_url=app_config.llm.base_url ,temperature=1,extra_body={"thinking": {"type": "disabled"}},)

0
app/models/__init__.py Normal file
View File

Binary file not shown.

View File

@ -0,0 +1,3 @@
from .archive_messages import ArchiveMessages
__all__ = ["ArchiveMessages"]

Binary file not shown.

View File

@ -0,0 +1,64 @@
import json
from datetime import datetime
from sqlalchemy import BigInteger, String, Text, Integer, TIMESTAMP
from sqlalchemy.orm import DeclarativeBase, mapped_column
class Base(DeclarativeBase):
pass
class ArchiveMessages(Base):
__tablename__ = 'archive_messages'
__table_args__ = {
'comment': '企微会话存档消息表'
}
id = mapped_column(BigInteger, primary_key=True, autoincrement=True, comment='自增主键')
msgid = mapped_column(String(255), nullable=False, comment='企微消息唯一ID')
seq = mapped_column(BigInteger, nullable=False, comment='存档序列号,用于增量拉取')
corp_id = mapped_column(String(64), nullable=False, comment='企业ID')
action = mapped_column(String(32), default='send', comment='send/recall/agree/disagree')
from_user = mapped_column(String(128), nullable=False, comment='发送者userid')
from_role = mapped_column(String(32), nullable=False, comment='发送者角色: INTERNAL-企业内部成员 EXTERNAL-外部联系人 SYSTEM-系统')
to_user = mapped_column(String(128), nullable=True, comment='接收者userid单聊')
tolist = mapped_column(Text, nullable=True, comment='接收者列表群聊JSON数组字符串')
roomid = mapped_column(String(255), nullable=True, comment='群聊ID群聊时有值')
msgtype = mapped_column(String(64), nullable=False, comment='text/image/voice/video/file/link/location/weapp/chatrecord/voip')
msgtime = mapped_column(BigInteger, nullable=False, comment='消息发送时间UTC毫秒时间戳')
content = mapped_column(Text, nullable=True, comment='文本消息内容长文本使用LONGTEXT')
media_data = mapped_column(Text, nullable=True, comment='媒体消息元数据sdkfileid/文件大小/时长等JSON字符串')
session_id = mapped_column(String(128), nullable=True, comment='关联的会话ID业务生成')
decrypt_status = mapped_column(Integer, default=1, comment='解密状态: 1成功 2失败')
decrypt_error = mapped_column(String(512), nullable=True, comment='解密失败原因')
created_at = mapped_column(TIMESTAMP, nullable=False, default=datetime.now, comment='创建时间')
updated_at = mapped_column(TIMESTAMP, nullable=False, default=datetime.now, onupdate=datetime.now, comment='更新时间')
def to_dict(self):
return {
'id': self.id,
'msgid': self.msgid,
'seq': self.seq,
'corp_id': self.corp_id,
'action': self.action,
'from_user': self.from_user,
'from_role': self.from_role,
'to_user': self.to_user,
'tolist': self.tolist,
'roomid': self.roomid,
'msgtype': self.msgtype,
'msgtime': self.msgtime,
'content': self.content,
'media_data': self.media_data,
'session_id': self.session_id,
'decrypt_status': self.decrypt_status,
'decrypt_error': self.decrypt_error,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None
}
def __repr__(self):
return json.dumps(self.to_dict())

View File

Binary file not shown.

View File

@ -0,0 +1,72 @@
from typing import List
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.mysql import ArchiveMessages
class ArchiveMessagesRepository:
def __init__(self, session: AsyncSession):
self.session = session
async def get_message_statistics_by_date(self, start_date: str = None, end_date: str = None) -> List[ArchiveMessages]:
"""
根据日期范围获取消息记录
Args:
start_date: 开始日期 (格式: YYYY-MM-DD)
end_date: 结束日期 (格式: YYYY-MM-DD)
Returns:
ArchiveMessages 对象列表
"""
base_query = """
SELECT DATE(created_at) AS created_at, from_user
FROM archive_messages
"""
conditions = []
params = {}
if start_date:
conditions.append(" and DATE(created_at) >= :start_date")
params["start_date"] = start_date
if end_date:
conditions.append("DATE(created_at) <= :end_date")
params["end_date"] = end_date
base_query += " WHERE 1=1 and roomid='' and msgtype='text' "
#base_query += " WHERE (from_user='wmI1AkDQAA3h0jxrRpeaHZhMKeHExA4w' )or( from_user='LiHeYi' and to_user='[\"wmI1AkDQAA3h0jxrRpeaHZhMKeHExA4w\"]') and roomid='' and msgtype='text' "
if conditions:
base_query += " AND ".join(conditions)+" "
base_query += " GROUP BY DATE(created_at), from_user ORDER BY created_at DESC, from_user;"
query = text(base_query)
result = await self.session.execute(query, params)
return [ArchiveMessages(**dict(row)) for row in result.mappings().fetchall()]
async def get_message_statistics_by_date_user(self, day: str = None, from_user: str = None, to_user: str = None) -> List[ArchiveMessages]:
"""
根据日期+发送人+接收人查询记录
Returns:
ArchiveMessages 对象列表
"""
if to_user:
sql = """
SELECT * FROM archive_messages
WHERE DATE(created_at) = :day
AND from_user = :from_user
AND to_user = :to_user and msgtype='text'
ORDER BY created_at DESC
"""
result = await self.session.execute(text(sql), {"day": day, "from_user": from_user, "to_user": to_user})
else :
sql = """
SELECT from_user,to_user FROM archive_messages
WHERE DATE(created_at) = :day
AND from_user = :from_user and msgtype='text'
group by to_user
"""
result = await self.session.execute(text(sql), {"day": day, "from_user": from_user})
return [ArchiveMessages(**dict(row)) for row in result.mappings().fetchall()]

View File

View File

@ -0,0 +1,156 @@
from typing import Optional, List, Dict, Any
from pymilvus import MilvusClient, DataType
from app.client.milvus_client_manager import milvus_client
from app.conf.app_config import app_config
class SummaryRepository:
collection_name: str = 'message_summary'
def __init__(self):
self.dim = app_config.milvus.embedding_size
self._ensure_connection()
def _ensure_connection(self):
if milvus_client.client is None:
milvus_client.init()
def create_collection(self, dim: int = None) -> bool:
"""
创建消息摘要集合
Args:
dim: 向量维度默认从配置读取
Returns:
是否成功创建已存在返回False
"""
if dim is not None:
self.dim = dim
if milvus_client.has_collection(self.collection_name):
return False
schema = milvus_client.client.create_schema(
auto_id=True,
enable_dynamic_field=True,
description="消息摘要向量集合"
)
schema.add_field(field_name="id", datatype=DataType.INT64, is_primary=True, auto_id=True)
schema.add_field(field_name="msg_time", datatype=DataType.VARCHAR, max_length=100)
schema.add_field(field_name="msg_type", datatype=DataType.INT8)
schema.add_field(field_name="from_user", datatype=DataType.VARCHAR, max_length=150)
schema.add_field(field_name="to_user", datatype=DataType.VARCHAR, max_length=150)
schema.add_field(field_name="room_id", datatype=DataType.VARCHAR, max_length=150)
schema.add_field(field_name="message_context", datatype=DataType.VARCHAR, max_length=65535)
schema.add_field(field_name="summary", datatype=DataType.VARCHAR, max_length=65535)
schema.add_field(field_name="message_dense_vector", datatype=DataType.FLOAT_VECTOR, dim=self.dim)
schema.add_field(field_name="summary_dense_vector", datatype=DataType.FLOAT_VECTOR, dim=self.dim)
schema.add_field(field_name="created_at", datatype=DataType.INT64)
index_params = milvus_client.client.prepare_index_params()
index_params.add_index(
field_name="message_dense_vector",
index_type="HNSW",
metric_type="COSINE",
params={"M": 32, "efConstruction": 300}
)
index_params.add_index(
field_name="summary_dense_vector",
index_type="HNSW",
metric_type="COSINE",
params={"M": 32, "efConstruction": 300}
)
milvus_client.client.create_collection(
collection_name=self.collection_name,
schema=schema,
index_params=index_params
)
return True
def insert(self, msg_time: List[str], msg_type: List[int], from_user: List[str],
to_user: List[str], room_id: List[str],message_context: List[str], summary: List[str], message_dense_vector: List[List[float]],
summary_dense_vector: List[List[float]]) -> List[int]:
"""
插入消息摘要数据
Args:
msg_time: 消息时间列表
msg_type: 消息类型列表
from_user: 发送者列表
to_user: 接收者列表
room_id: 群聊ID列表
summary: 消息摘要列表
dense_vector: 向量列表
Returns:
插入的数据ID列表
"""
self._ensure_connection()
self.create_collection()
import time
created_at = [int(time.time() * 1000)] * len(msg_time)
data = []
for i in range(len(msg_time)):
data.append({
"msg_time": msg_time[i],
"msg_type": msg_type[i],
"from_user": from_user[i],
"to_user": to_user[i],
"room_id": room_id[i],
"message_context": message_context[i],
"summary": summary[i],
"message_dense_vector": message_dense_vector[i],
"summary_dense_vector": summary_dense_vector[i],
"created_at": created_at[i]
})
result = milvus_client.client.insert(
collection_name=self.collection_name,
data=data
)
milvus_client.client.flush(self.collection_name)
return result.get("ids", [])
def delete(self, expr: str):
"""
删除数据
Args:
expr: 删除条件表达式
"""
self._ensure_connection()
milvus_client.client.delete(
collection_name=self.collection_name,
filter=expr
)
milvus_client.client.flush(self.collection_name)
def drop_collection(self):
"""删除集合"""
milvus_client.drop_collection(self.collection_name)
def get_collection_stats(self) -> Dict[str, Any]:
"""获取集合统计信息"""
self._ensure_connection()
try:
result = milvus_client.client.get_collection_stats(collection_name=self.collection_name)
return {
"num_entities": result.get("row_count", 0),
"collection_name": self.collection_name
}
except Exception as e:
print(f"获取统计信息失败: {e}")
return {"num_entities": 0, "collection_name": self.collection_name}
def close(self):
"""关闭连接"""
milvus_client.close()

4
app/summary/__init__.py Normal file
View File

@ -0,0 +1,4 @@
from .router import router as summary_router
from .service import SummaryService
__all__ = ["summary_router", "SummaryService"]

26
app/summary/router.py Normal file
View File

@ -0,0 +1,26 @@
from typing import List, Dict, Any, Optional
from fastapi import APIRouter, Query
from app.summary.service import SummaryService
router = APIRouter(prefix="/summary", tags=["summary"])
@router.get("/message-statistics", response_model=List[Dict[str, Any]])
async def get_message_statistics(
start_date: Optional[str] = Query(None, description="开始日期 (格式: YYYY-MM-DD)"),
end_date: Optional[str] = Query(None, description="结束日期 (格式: YYYY-MM-DD)")
):
"""
获取按日期和发送者分组的消息统计数据
SQL逻辑:
SELECT DATE(created_at) AS created_at, from_user
FROM archive_messages
GROUP BY DATE(created_at), from_user
ORDER BY created_at DESC;
"""
if start_date or end_date:
return await SummaryService.get_message_statistics_by_date(start_date, end_date)
return await SummaryService.get_message_statistics()

180
app/summary/service.py Normal file
View File

@ -0,0 +1,180 @@
import asyncio
import time
from typing import List
from langchain_core.prompts import PromptTemplate
from app.client.embedding_client_manager import EmbeddingClientManager, embedding_client
from app.client.milvus_client_manager import milvus_client
from app.client.mysql_client_manager import db_assistant_mysql_client_manager
from app.dto.summary_dto import SummaryDto
from app.llm import llm
from app.models.mysql import ArchiveMessages
from app.repository.archive_messages_repository import ArchiveMessagesRepository
from app.repository.milvus.summary_repository import SummaryRepository
class SummaryService:
def __init__(self, archive_messages_repository: ArchiveMessagesRepository,
embedding_client: EmbeddingClientManager,
summary_repository: SummaryRepository):
self.archive_messages_repository = archive_messages_repository
self.embedding_client = embedding_client
self.summary_repository = summary_repository
async def get_message_summary(self, date: str = None):
stime = time.time()
if date:
day_ArchiveMessages = await self.archive_messages_repository.get_message_statistics_by_date(start_date=date,
end_date=date)
print(f"{date}共产生了{len(day_ArchiveMessages)}条会话")
else:
day_ArchiveMessages = await self.archive_messages_repository.get_message_statistics_by_date()
# 改成map
date_user_message_a_set = set()
for idx, day_message in enumerate(day_ArchiveMessages, 1): #打印计数
print(f"处理第{idx}会话")
day_date = str(day_message.created_at)
user_a = day_message.from_user
# 找出发送人相关的接收人信息
date_user_message_a = await self.archive_messages_repository.get_message_statistics_by_date_user(day_date,
user_a)
coro_list = []
# 找出发送人与接收人 相关的所有会话信息
for date_a in date_user_message_a:
date_user_b = date_a.to_user
# if date_user_b != "[\"wmI1AkDQAA3h0jxrRpeaHZhMKeHExA4w\"]":
# continue
date_user_a = date_a.from_user
date_user_b_clean = date_user_b.replace("[\"", "").replace("\"]", "")
date_user_a_append = "[\"" + date_user_a + "\"]"
# 判断只要存在就跳过
if date_user_a + date_user_b in date_user_message_a_set or date_user_b_clean + date_user_a_append in date_user_message_a_set:
continue
print(f"查询{date_user_a}{date_user_b}对话开始")
list1 = await self.archive_messages_repository.get_message_statistics_by_date_user(day_date,
date_user_a,
date_user_b)
list2 = await self.archive_messages_repository.get_message_statistics_by_date_user(day_date,
date_user_b_clean,
date_user_a_append)
# 查询过的聊天账号,后续不再查询
date_user_message_a_set.add(date_user_a + date_user_b)
date_user_message_a_set.add(date_user_b_clean + date_user_a_append)
coro_list = list1 + list2
coro_list.sort(key=lambda x: x.created_at, reverse=False)
print(f"查询{date_user_a}{date_user_b}对话共:{len(coro_list)}")
# 保留语义骨架,去除闲聊填充
filter_gossip_message_res = await self.filter_gossip_message(coro_list)
filter_gossip_message_text = str(filter_gossip_message_res.content) if hasattr(
filter_gossip_message_res, 'content') else str(filter_gossip_message_res)
print(f"查询{date_user_a}{date_user_b}对话进行保留语义骨架,去除闲聊填充")
# 摘要
summary_res = await self.date_message_summary(coro_list)
summary_text = str(summary_res.content) if hasattr(summary_res, 'content') else str(summary_res)
print(f"查询{date_user_a}{date_user_b}对话进行摘要")
# 摘要向量
batch_embeddings = await self.embedding_client.aembed_documents(
[summary_text, filter_gossip_message_text])
#
try:
self.summary_repository.insert(
msg_time=[day_date],
msg_type=[0],
from_user=[date_a.from_user],
to_user=[date_user_b_clean],
room_id=[date_a.roomid or ""],
message_context=[filter_gossip_message_text],
summary=[summary_text],
message_dense_vector=[batch_embeddings[1]],
summary_dense_vector=[batch_embeddings[0]]
)
print(f"查询{date_user_a}{date_user_b}对话成功入 库")
except Exception as e:
print(e)
print(f"运行结束:{(time.time()-stime)}")
async def filter_gossip_message(self, coro_list: List[ArchiveMessages]) -> str:
prompt = """
保留语义骨架去除闲聊填充
# 历史对话
{message_str}
其中INTERNAL表示销售EXTERNAL表示用户
# 例如
原文
用户在吗
销售在的您好请问有什么可以帮您
用户我想问一下那个我的订单怎么还没发货啊
销售好的请问您的订单号是多少呢
用户订单号是 12345我昨天就下单了
销售我查一下稍等... 您的订单正在打包中预计今天发出
结构化提取后
[用户] 咨询订单发货状态订单号 12345昨日下单
[销售] 查询后告知正在打包预计今日发出
"""
lines = [f"{msg.created_at} {msg.from_role}{msg.content}" for msg in coro_list]
message_str = "\n".join(lines)
prompt_template = PromptTemplate(template=prompt, input_variables=["message_str"])
chain = prompt_template | llm
result = await chain.ainvoke({"message_str": message_str})
print(result)
return result
async def date_message_summary(self, coro_list: List[ArchiveMessages]) -> str:
prompt = """
# 聊天历史轻量化摘要任务
任务生成极简轮次摘要用于替换原始对话节省上下文窗口
# 历史对话
{message_str}
## 说明
其中INTERNAL表示销售EXTERNAL表示客户
输出格式示例无多余文字
日期 202X年X月X日
** 核心诉求**
用户反馈订单 #12345 物流停滞超7天未更新。
询问是否支持跨店满减叠加优惠券
** 处理结果**
已联系快递网点核实包裹因暴雨滞留预计明日送达已安抚情绪并补偿10元无门槛券
明确告知当前活动规则不支持叠加引导领取店铺专属券
** 待办事项 **
[明日 10:00] 跟进物流签收状态并回访用户
** 关键标签**#物流异常#优惠规则咨询#情绪安抚
"""
lines = [f"{msg.created_at} {msg.from_role}{msg.content}" for msg in coro_list]
message_str = "\n".join(lines)
prompt_template = PromptTemplate(template=prompt, input_variables=["message_str"])
chain = prompt_template | llm
result = await chain.ainvoke({"message_str": message_str})
print(result)
return result
async def build():
db_assistant_mysql_client_manager.init()
embedding_client.init()
milvus_client.init()
try:
async with db_assistant_mysql_client_manager.session_factory() as db_assistant:
archive_messages_repository = ArchiveMessagesRepository(db_assistant)
summary_repository = SummaryRepository()
service = SummaryService(archive_messages_repository, embedding_client, summary_repository)
await service.get_message_summary("2026-6-11")
finally:
await embedding_client.close()
await db_assistant_mysql_client_manager.close()
milvus_client.close()
if __name__ == '__main__':
asyncio.run(build())

88
app/summary/task.py Normal file
View File

@ -0,0 +1,88 @@
import asyncio
import logging
from datetime import datetime, timedelta
from typing import Optional
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from app.client.mysql_client_manager import db_assistant_mysql_client_manager
from app.repository.archive_messages_repository import ArchiveMessagesRepository
from app.summary.service import SummaryService
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[logging.FileHandler('summary_task.log'), logging.StreamHandler()]
)
logger = logging.getLogger(__name__)
async def generate_daily_summary(date: Optional[str] = None):
"""
生成指定日期的消息摘要如果不传日期则生成前一天的摘要
Args:
date: 日期字符串格式 YYYY-MM-DD不传则默认前一天
"""
if not date:
yesterday = datetime.now() - timedelta(days=1)
date = yesterday.strftime("%Y-%m-%d")
logger.info(f"开始生成 {date} 的消息摘要...")
try:
db_assistant_mysql_client_manager.init()
async with db_assistant_mysql_client_manager.session_factory() as db_assistant:
archive_messages_repository = ArchiveMessagesRepository(db_assistant)
service = SummaryService(archive_messages_repository)
summaries = await service.get_message_summary(date)
logger.info(f"生成完成,共生成 {len(summaries)} 条摘要")
for summary in summaries:
logger.info(f"日期: {summary.date}, 发送人: {summary.from_user}, 接收人: {summary.to_user}")
return summaries
except Exception as e:
logger.error(f"生成摘要失败: {str(e)}", exc_info=True)
raise
def start_scheduler():
"""
启动定时任务调度器每天凌晨2点执行
"""
scheduler = AsyncIOScheduler(timezone='Asia/Shanghai')
scheduler.add_job(
generate_daily_summary,
trigger='cron',
hour=2,
minute=0,
second=0,
id='daily_summary_task',
name='每日消息摘要任务',
replace_existing=True
)
logger.info("定时任务调度器已启动每天凌晨2:00执行消息摘要任务")
scheduler.start()
try:
asyncio.get_event_loop().run_forever()
except (KeyboardInterrupt, SystemExit):
logger.info("正在关闭定时任务调度器...")
scheduler.shutdown()
if __name__ == '__main__':
import sys
if len(sys.argv) > 1:
# 如果传入了日期参数,直接生成指定日期的摘要
target_date = sys.argv[1]
asyncio.run(generate_daily_summary(target_date))
else:
# 否则启动定时任务调度器
start_scheduler()

35
conf/app_config.yaml Normal file
View File

@ -0,0 +1,35 @@
logging:
file:
enable: true
level: INFO
path: logs
rotation: "10 MB"
retention: "7 days"
console:
enable: true
level: INFO
db_assistant:
host: sh-cdb-6fzlwnms.sql.tencentcdb.com
port: 63912
user: assistant_prod
password: U%$4Tu_C3+4
database: ai_assistant
embedding:
host: localhost
port: 8081
model: BAAI/bge-large-zh-v1.5
milvus:
host: localhost
port: 19530
user: root
password: Milvus
embedding_size: 1024
llm:
model_name: deepseek-v4-flash
api_key: sk-8edc9f25d59643b3b08efea5d81c55a9
base_url: https://api.deepseek.com

0
conf/sql.sql Normal file
View File

80
docker/docker-compose.yml Normal file
View File

@ -0,0 +1,80 @@
services:
etcd:
container_name: milvus-etcd
image: quay.io/coreos/etcd:v3.5.18
environment:
- ETCD_AUTO_COMPACTION_MODE=revision
- ETCD_AUTO_COMPACTION_RETENTION=1000
- ETCD_QUOTA_BACKEND_BYTES=4294967296
- ETCD_SNAPSHOT_COUNT=50000
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd
# 核心修改127.0.0.1 → etcd
command: etcd -advertise-client-urls=http://etcd:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd
healthcheck:
test: ["CMD", "etcdctl", "endpoint", "health"]
interval: 30s
timeout: 20s
retries: 3
minio:
container_name: milvus-minio
image: minio/minio:RELEASE.2023-03-20T20-16-18Z
environment:
MINIO_ACCESS_KEY: minioadmin
MINIO_SECRET_KEY: minioadmin
ports:
- "9001:9001"
- "9000:9000"
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data
command: minio server /minio_data --console-address ":9001"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 3
standalone:
container_name: milvus-standalone
# 修正镜像tag删除多余v
image: milvusdb/milvus:v2.5.5
command: ["milvus", "run", "standalone"]
security_opt:
- seccomp:unconfined
environment:
ETCD_ENDPOINTS: etcd:2379
MINIO_ADDRESS: minio:9000
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9091/healthz"]
interval: 30s
start_period: 90s
timeout: 20s
retries: 3
ports:
- "19530:19530"
- "9091:9091"
depends_on:
- "etcd"
- "minio"
# Hugging Face TEI 引擎 用 Rust + C 张量库 推理
embedding:
image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.8
container_name: embedding
restart: unless-stopped
ports:
- "8081:80"
environment:
MODEL_ID: /models/bge-large-zh-v1.5
MAX_CONCURRENT_REQUESTS: "16"
MAX_BATCH_TOKENS: "16384"
volumes:
- ./embedding/bge-large-zh-v1.5:/models/bge-large-zh-v1.5
networks:
default:
name: milvus

View File

@ -0,0 +1,35 @@
*.7z filter=lfs diff=lfs merge=lfs -text
*.arrow filter=lfs diff=lfs merge=lfs -text
*.bin filter=lfs diff=lfs merge=lfs -text
*.bz2 filter=lfs diff=lfs merge=lfs -text
*.ckpt filter=lfs diff=lfs merge=lfs -text
*.ftz filter=lfs diff=lfs merge=lfs -text
*.gz filter=lfs diff=lfs merge=lfs -text
*.h5 filter=lfs diff=lfs merge=lfs -text
*.joblib filter=lfs diff=lfs merge=lfs -text
*.lfs.* filter=lfs diff=lfs merge=lfs -text
*.mlmodel filter=lfs diff=lfs merge=lfs -text
*.model filter=lfs diff=lfs merge=lfs -text
*.msgpack filter=lfs diff=lfs merge=lfs -text
*.npy filter=lfs diff=lfs merge=lfs -text
*.npz filter=lfs diff=lfs merge=lfs -text
*.onnx filter=lfs diff=lfs merge=lfs -text
*.ot filter=lfs diff=lfs merge=lfs -text
*.parquet filter=lfs diff=lfs merge=lfs -text
*.pb filter=lfs diff=lfs merge=lfs -text
*.pickle filter=lfs diff=lfs merge=lfs -text
*.pkl filter=lfs diff=lfs merge=lfs -text
*.pt filter=lfs diff=lfs merge=lfs -text
*.pth filter=lfs diff=lfs merge=lfs -text
*.rar filter=lfs diff=lfs merge=lfs -text
*.safetensors filter=lfs diff=lfs merge=lfs -text
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
*.tar.* filter=lfs diff=lfs merge=lfs -text
*.tar filter=lfs diff=lfs merge=lfs -text
*.tflite filter=lfs diff=lfs merge=lfs -text
*.tgz filter=lfs diff=lfs merge=lfs -text
*.wasm filter=lfs diff=lfs merge=lfs -text
*.xz filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
*.zst filter=lfs diff=lfs merge=lfs -text
*tfevents* filter=lfs diff=lfs merge=lfs -text

View File

@ -0,0 +1,7 @@
{
"word_embedding_dimension": 1024,
"pooling_mode_cls_token": true,
"pooling_mode_mean_tokens": false,
"pooling_mode_max_tokens": false,
"pooling_mode_mean_sqrt_len_tokens": false
}

View File

@ -0,0 +1,427 @@
---
license: mit
language:
- zh
tags:
- sentence-transformers
- feature-extraction
- sentence-similarity
- transformers
---
<h1 align="center">FlagEmbedding</h1>
<h4 align="center">
<p>
<a href=#model-list>Model List</a> |
<a href=#frequently-asked-questions>FAQ</a> |
<a href=#usage>Usage</a> |
<a href="#evaluation">Evaluation</a> |
<a href="#train">Train</a> |
<a href="#contact">Contact</a> |
<a href="#citation">Citation</a> |
<a href="#license">License</a>
<p>
</h4>
For more details please refer to our Github: [FlagEmbedding](https://github.com/FlagOpen/FlagEmbedding).
If you are looking for a model that supports more languages, longer texts, and other retrieval methods, you can try using [bge-m3](https://huggingface.co/BAAI/bge-m3).
[English](README.md) | [中文](https://github.com/FlagOpen/FlagEmbedding/blob/master/README_zh.md)
FlagEmbedding focuses on retrieval-augmented LLMs, consisting of the following projects currently:
- **Long-Context LLM**: [Activation Beacon](https://github.com/FlagOpen/FlagEmbedding/tree/master/Long_LLM/activation_beacon)
- **Fine-tuning of LM** : [LM-Cocktail](https://github.com/FlagOpen/FlagEmbedding/tree/master/LM_Cocktail)
- **Dense Retrieval**: [BGE-M3](https://github.com/FlagOpen/FlagEmbedding/tree/master/FlagEmbedding/BGE_M3), [LLM Embedder](https://github.com/FlagOpen/FlagEmbedding/tree/master/FlagEmbedding/llm_embedder), [BGE Embedding](https://github.com/FlagOpen/FlagEmbedding/tree/master/FlagEmbedding/baai_general_embedding)
- **Reranker Model**: [BGE Reranker](https://github.com/FlagOpen/FlagEmbedding/tree/master/FlagEmbedding/reranker)
- **Benchmark**: [C-MTEB](https://github.com/FlagOpen/FlagEmbedding/tree/master/C_MTEB)
## News
- 1/30/2024: Release **BGE-M3**, a new member to BGE model series! M3 stands for **M**ulti-linguality (100+ languages), **M**ulti-granularities (input length up to 8192), **M**ulti-Functionality (unification of dense, lexical, multi-vec/colbert retrieval).
It is the first embedding model which supports all three retrieval methods, achieving new SOTA on multi-lingual (MIRACL) and cross-lingual (MKQA) benchmarks.
[Technical Report](https://github.com/FlagOpen/FlagEmbedding/blob/master/FlagEmbedding/BGE_M3/BGE_M3.pdf) and [Code](https://github.com/FlagOpen/FlagEmbedding/tree/master/FlagEmbedding/BGE_M3). :fire:
- 1/9/2024: Release [Activation-Beacon](https://github.com/FlagOpen/FlagEmbedding/tree/master/Long_LLM/activation_beacon), an effective, efficient, compatible, and low-cost (training) method to extend the context length of LLM. [Technical Report](https://arxiv.org/abs/2401.03462) :fire:
- 12/24/2023: Release **LLaRA**, a LLaMA-7B based dense retriever, leading to state-of-the-art performances on MS MARCO and BEIR. Model and code will be open-sourced. Please stay tuned. [Technical Report](https://arxiv.org/abs/2312.15503) :fire:
- 11/23/2023: Release [LM-Cocktail](https://github.com/FlagOpen/FlagEmbedding/tree/master/LM_Cocktail), a method to maintain general capabilities during fine-tuning by merging multiple language models. [Technical Report](https://arxiv.org/abs/2311.13534) :fire:
- 10/12/2023: Release [LLM-Embedder](https://github.com/FlagOpen/FlagEmbedding/tree/master/FlagEmbedding/llm_embedder), a unified embedding model to support diverse retrieval augmentation needs for LLMs. [Technical Report](https://arxiv.org/pdf/2310.07554.pdf)
- 09/15/2023: The [technical report](https://arxiv.org/pdf/2309.07597.pdf) and [massive training data](https://data.baai.ac.cn/details/BAAI-MTP) of BGE has been released
- 09/12/2023: New models:
- **New reranker model**: release cross-encoder models `BAAI/bge-reranker-base` and `BAAI/bge-reranker-large`, which are more powerful than embedding model. We recommend to use/fine-tune them to re-rank top-k documents returned by embedding models.
- **update embedding model**: release `bge-*-v1.5` embedding model to alleviate the issue of the similarity distribution, and enhance its retrieval ability without instruction.
<details>
<summary>More</summary>
<!-- ### More -->
- 09/07/2023: Update [fine-tune code](https://github.com/FlagOpen/FlagEmbedding/blob/master/FlagEmbedding/baai_general_embedding/README.md): Add script to mine hard negatives and support adding instruction during fine-tuning.
- 08/09/2023: BGE Models are integrated into **Langchain**, you can use it like [this](#using-langchain); C-MTEB **leaderboard** is [available](https://huggingface.co/spaces/mteb/leaderboard).
- 08/05/2023: Release base-scale and small-scale models, **best performance among the models of the same size 🤗**
- 08/02/2023: Release `bge-large-*`(short for BAAI General Embedding) Models, **rank 1st on MTEB and C-MTEB benchmark!** :tada: :tada:
- 08/01/2023: We release the [Chinese Massive Text Embedding Benchmark](https://github.com/FlagOpen/FlagEmbedding/blob/master/C_MTEB) (**C-MTEB**), consisting of 31 test dataset.
</details>
## Model List
`bge` is short for `BAAI general embedding`.
| Model | Language | | Description | query instruction for retrieval [1] |
|:-------------------------------|:--------:| :--------:| :--------:|:--------:|
| [BAAI/bge-m3](https://huggingface.co/BAAI/bge-m3) | Multilingual | [Inference](https://github.com/FlagOpen/FlagEmbedding/tree/master/FlagEmbedding/BGE_M3#usage) [Fine-tune](https://github.com/FlagOpen/FlagEmbedding/tree/master/FlagEmbedding/BGE_M3) | Multi-Functionality(dense retrieval, sparse retrieval, multi-vector(colbert)), Multi-Linguality, and Multi-Granularity(8192 tokens) | |
| [BAAI/llm-embedder](https://huggingface.co/BAAI/llm-embedder) | English | [Inference](./FlagEmbedding/llm_embedder/README.md) [Fine-tune](./FlagEmbedding/llm_embedder/README.md) | a unified embedding model to support diverse retrieval augmentation needs for LLMs | See [README](./FlagEmbedding/llm_embedder/README.md) |
| [BAAI/bge-reranker-large](https://huggingface.co/BAAI/bge-reranker-large) | Chinese and English | [Inference](#usage-for-reranker) [Fine-tune](https://github.com/FlagOpen/FlagEmbedding/tree/master/examples/reranker) | a cross-encoder model which is more accurate but less efficient [2] | |
| [BAAI/bge-reranker-base](https://huggingface.co/BAAI/bge-reranker-base) | Chinese and English | [Inference](#usage-for-reranker) [Fine-tune](https://github.com/FlagOpen/FlagEmbedding/tree/master/examples/reranker) | a cross-encoder model which is more accurate but less efficient [2] | |
| [BAAI/bge-large-en-v1.5](https://huggingface.co/BAAI/bge-large-en-v1.5) | English | [Inference](#usage-for-embedding-model) [Fine-tune](https://github.com/FlagOpen/FlagEmbedding/tree/master/examples/finetune) | version 1.5 with more reasonable similarity distribution | `Represent this sentence for searching relevant passages: ` |
| [BAAI/bge-base-en-v1.5](https://huggingface.co/BAAI/bge-base-en-v1.5) | English | [Inference](#usage-for-embedding-model) [Fine-tune](https://github.com/FlagOpen/FlagEmbedding/tree/master/examples/finetune) | version 1.5 with more reasonable similarity distribution | `Represent this sentence for searching relevant passages: ` |
| [BAAI/bge-small-en-v1.5](https://huggingface.co/BAAI/bge-small-en-v1.5) | English | [Inference](#usage-for-embedding-model) [Fine-tune](https://github.com/FlagOpen/FlagEmbedding/tree/master/examples/finetune) | version 1.5 with more reasonable similarity distribution | `Represent this sentence for searching relevant passages: ` |
| [BAAI/bge-large-zh-v1.5](https://huggingface.co/BAAI/bge-large-zh-v1.5) | Chinese | [Inference](#usage-for-embedding-model) [Fine-tune](https://github.com/FlagOpen/FlagEmbedding/tree/master/examples/finetune) | version 1.5 with more reasonable similarity distribution | `为这个句子生成表示以用于检索相关文章:` |
| [BAAI/bge-base-zh-v1.5](https://huggingface.co/BAAI/bge-base-zh-v1.5) | Chinese | [Inference](#usage-for-embedding-model) [Fine-tune](https://github.com/FlagOpen/FlagEmbedding/tree/master/examples/finetune) | version 1.5 with more reasonable similarity distribution | `为这个句子生成表示以用于检索相关文章:` |
| [BAAI/bge-small-zh-v1.5](https://huggingface.co/BAAI/bge-small-zh-v1.5) | Chinese | [Inference](#usage-for-embedding-model) [Fine-tune](https://github.com/FlagOpen/FlagEmbedding/tree/master/examples/finetune) | version 1.5 with more reasonable similarity distribution | `为这个句子生成表示以用于检索相关文章:` |
| [BAAI/bge-large-en](https://huggingface.co/BAAI/bge-large-en) | English | [Inference](#usage-for-embedding-model) [Fine-tune](https://github.com/FlagOpen/FlagEmbedding/tree/master/examples/finetune) | :trophy: rank **1st** in [MTEB](https://huggingface.co/spaces/mteb/leaderboard) leaderboard | `Represent this sentence for searching relevant passages: ` |
| [BAAI/bge-base-en](https://huggingface.co/BAAI/bge-base-en) | English | [Inference](#usage-for-embedding-model) [Fine-tune](https://github.com/FlagOpen/FlagEmbedding/tree/master/examples/finetune) | a base-scale model but with similar ability to `bge-large-en` | `Represent this sentence for searching relevant passages: ` |
| [BAAI/bge-small-en](https://huggingface.co/BAAI/bge-small-en) | English | [Inference](#usage-for-embedding-model) [Fine-tune](https://github.com/FlagOpen/FlagEmbedding/tree/master/examples/finetune) |a small-scale model but with competitive performance | `Represent this sentence for searching relevant passages: ` |
| [BAAI/bge-large-zh](https://huggingface.co/BAAI/bge-large-zh) | Chinese | [Inference](#usage-for-embedding-model) [Fine-tune](https://github.com/FlagOpen/FlagEmbedding/tree/master/examples/finetune) | :trophy: rank **1st** in [C-MTEB](https://github.com/FlagOpen/FlagEmbedding/tree/master/C_MTEB) benchmark | `为这个句子生成表示以用于检索相关文章:` |
| [BAAI/bge-base-zh](https://huggingface.co/BAAI/bge-base-zh) | Chinese | [Inference](#usage-for-embedding-model) [Fine-tune](https://github.com/FlagOpen/FlagEmbedding/tree/master/examples/finetune) | a base-scale model but with similar ability to `bge-large-zh` | `为这个句子生成表示以用于检索相关文章:` |
| [BAAI/bge-small-zh](https://huggingface.co/BAAI/bge-small-zh) | Chinese | [Inference](#usage-for-embedding-model) [Fine-tune](https://github.com/FlagOpen/FlagEmbedding/tree/master/examples/finetune) | a small-scale model but with competitive performance | `为这个句子生成表示以用于检索相关文章:` |
[1\]: If you need to search the relevant passages to a query, we suggest to add the instruction to the query; in other cases, no instruction is needed, just use the original query directly. In all cases, **no instruction** needs to be added to passages.
[2\]: Different from embedding model, reranker uses question and document as input and directly output similarity instead of embedding. To balance the accuracy and time cost, cross-encoder is widely used to re-rank top-k documents retrieved by other simple models.
For examples, use bge embedding model to retrieve top 100 relevant documents, and then use bge reranker to re-rank the top 100 document to get the final top-3 results.
All models have been uploaded to Huggingface Hub, and you can see them at https://huggingface.co/BAAI.
If you cannot open the Huggingface Hub, you also can download the models at https://model.baai.ac.cn/models .
## Frequently asked questions
<details>
<summary>1. How to fine-tune bge embedding model?</summary>
<!-- ### How to fine-tune bge embedding model? -->
Following this [example](https://github.com/FlagOpen/FlagEmbedding/tree/master/examples/finetune) to prepare data and fine-tune your model.
Some suggestions:
- Mine hard negatives following this [example](https://github.com/FlagOpen/FlagEmbedding/tree/master/examples/finetune#hard-negatives), which can improve the retrieval performance.
- If you pre-train bge on your data, the pre-trained model cannot be directly used to calculate similarity, and it must be fine-tuned with contrastive learning before computing similarity.
- If the accuracy of the fine-tuned model is still not high, it is recommended to use/fine-tune the cross-encoder model (bge-reranker) to re-rank top-k results. Hard negatives also are needed to fine-tune reranker.
</details>
<details>
<summary>2. The similarity score between two dissimilar sentences is higher than 0.5</summary>
<!-- ### The similarity score between two dissimilar sentences is higher than 0.5 -->
**Suggest to use bge v1.5, which alleviates the issue of the similarity distribution.**
Since we finetune the models by contrastive learning with a temperature of 0.01,
the similarity distribution of the current BGE model is about in the interval \[0.6, 1\].
So a similarity score greater than 0.5 does not indicate that the two sentences are similar.
For downstream tasks, such as passage retrieval or semantic similarity,
**what matters is the relative order of the scores, not the absolute value.**
If you need to filter similar sentences based on a similarity threshold,
please select an appropriate similarity threshold based on the similarity distribution on your data (such as 0.8, 0.85, or even 0.9).
</details>
<details>
<summary>3. When does the query instruction need to be used</summary>
<!-- ### When does the query instruction need to be used -->
For the `bge-*-v1.5`, we improve its retrieval ability when not using instruction.
No instruction only has a slight degradation in retrieval performance compared with using instruction.
So you can generate embedding without instruction in all cases for convenience.
For a retrieval task that uses short queries to find long related documents,
it is recommended to add instructions for these short queries.
**The best method to decide whether to add instructions for queries is choosing the setting that achieves better performance on your task.**
In all cases, the documents/passages do not need to add the instruction.
</details>
## Usage
### Usage for Embedding Model
Here are some examples for using `bge` models with
[FlagEmbedding](#using-flagembedding), [Sentence-Transformers](#using-sentence-transformers), [Langchain](#using-langchain), or [Huggingface Transformers](#using-huggingface-transformers).
#### Using FlagEmbedding
```
pip install -U FlagEmbedding
```
If it doesn't work for you, you can see [FlagEmbedding](https://github.com/FlagOpen/FlagEmbedding/blob/master/FlagEmbedding/baai_general_embedding/README.md) for more methods to install FlagEmbedding.
```python
from FlagEmbedding import FlagModel
sentences_1 = ["样例数据-1", "样例数据-2"]
sentences_2 = ["样例数据-3", "样例数据-4"]
model = FlagModel('BAAI/bge-large-zh-v1.5',
query_instruction_for_retrieval="为这个句子生成表示以用于检索相关文章:",
use_fp16=True) # Setting use_fp16 to True speeds up computation with a slight performance degradation
embeddings_1 = model.encode(sentences_1)
embeddings_2 = model.encode(sentences_2)
similarity = embeddings_1 @ embeddings_2.T
print(similarity)
# for s2p(short query to long passage) retrieval task, suggest to use encode_queries() which will automatically add the instruction to each query
# corpus in retrieval task can still use encode() or encode_corpus(), since they don't need instruction
queries = ['query_1', 'query_2']
passages = ["样例文档-1", "样例文档-2"]
q_embeddings = model.encode_queries(queries)
p_embeddings = model.encode(passages)
scores = q_embeddings @ p_embeddings.T
```
For the value of the argument `query_instruction_for_retrieval`, see [Model List](https://github.com/FlagOpen/FlagEmbedding/tree/master#model-list).
By default, FlagModel will use all available GPUs when encoding. Please set `os.environ["CUDA_VISIBLE_DEVICES"]` to select specific GPUs.
You also can set `os.environ["CUDA_VISIBLE_DEVICES"]=""` to make all GPUs unavailable.
#### Using Sentence-Transformers
You can also use the `bge` models with [sentence-transformers](https://www.SBERT.net):
```
pip install -U sentence-transformers
```
```python
from sentence_transformers import SentenceTransformer
sentences_1 = ["样例数据-1", "样例数据-2"]
sentences_2 = ["样例数据-3", "样例数据-4"]
model = SentenceTransformer('BAAI/bge-large-zh-v1.5')
embeddings_1 = model.encode(sentences_1, normalize_embeddings=True)
embeddings_2 = model.encode(sentences_2, normalize_embeddings=True)
similarity = embeddings_1 @ embeddings_2.T
print(similarity)
```
For s2p(short query to long passage) retrieval task,
each short query should start with an instruction (instructions see [Model List](https://github.com/FlagOpen/FlagEmbedding/tree/master#model-list)).
But the instruction is not needed for passages.
```python
from sentence_transformers import SentenceTransformer
queries = ['query_1', 'query_2']
passages = ["样例文档-1", "样例文档-2"]
instruction = "为这个句子生成表示以用于检索相关文章:"
model = SentenceTransformer('BAAI/bge-large-zh-v1.5')
q_embeddings = model.encode([instruction+q for q in queries], normalize_embeddings=True)
p_embeddings = model.encode(passages, normalize_embeddings=True)
scores = q_embeddings @ p_embeddings.T
```
#### Using Langchain
You can use `bge` in langchain like this:
```python
from langchain.embeddings import HuggingFaceBgeEmbeddings
model_name = "BAAI/bge-large-en-v1.5"
model_kwargs = {'device': 'cuda'}
encode_kwargs = {'normalize_embeddings': True} # set True to compute cosine similarity
model = HuggingFaceBgeEmbeddings(
model_name=model_name,
model_kwargs=model_kwargs,
encode_kwargs=encode_kwargs,
query_instruction="为这个句子生成表示以用于检索相关文章:"
)
model.query_instruction = "为这个句子生成表示以用于检索相关文章:"
```
#### Using HuggingFace Transformers
With the transformers package, you can use the model like this: First, you pass your input through the transformer model, then you select the last hidden state of the first token (i.e., [CLS]) as the sentence embedding.
```python
from transformers import AutoTokenizer, AutoModel
import torch
# Sentences we want sentence embeddings for
sentences = ["样例数据-1", "样例数据-2"]
# Load model from HuggingFace Hub
tokenizer = AutoTokenizer.from_pretrained('BAAI/bge-large-zh-v1.5')
model = AutoModel.from_pretrained('BAAI/bge-large-zh-v1.5')
model.eval()
# Tokenize sentences
encoded_input = tokenizer(sentences, padding=True, truncation=True, return_tensors='pt')
# for s2p(short query to long passage) retrieval task, add an instruction to query (not add instruction for passages)
# encoded_input = tokenizer([instruction + q for q in queries], padding=True, truncation=True, return_tensors='pt')
# Compute token embeddings
with torch.no_grad():
model_output = model(**encoded_input)
# Perform pooling. In this case, cls pooling.
sentence_embeddings = model_output[0][:, 0]
# normalize embeddings
sentence_embeddings = torch.nn.functional.normalize(sentence_embeddings, p=2, dim=1)
print("Sentence embeddings:", sentence_embeddings)
```
### Usage for Reranker
Different from embedding model, reranker uses question and document as input and directly output similarity instead of embedding.
You can get a relevance score by inputting query and passage to the reranker.
The reranker is optimized based cross-entropy loss, so the relevance score is not bounded to a specific range.
#### Using FlagEmbedding
```
pip install -U FlagEmbedding
```
Get relevance scores (higher scores indicate more relevance):
```python
from FlagEmbedding import FlagReranker
reranker = FlagReranker('BAAI/bge-reranker-large', use_fp16=True) # Setting use_fp16 to True speeds up computation with a slight performance degradation
score = reranker.compute_score(['query', 'passage'])
print(score)
scores = reranker.compute_score([['what is panda?', 'hi'], ['what is panda?', 'The giant panda (Ailuropoda melanoleuca), sometimes called a panda bear or simply panda, is a bear species endemic to China.']])
print(scores)
```
#### Using Huggingface transformers
```python
import torch
from transformers import AutoModelForSequenceClassification, AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained('BAAI/bge-reranker-large')
model = AutoModelForSequenceClassification.from_pretrained('BAAI/bge-reranker-large')
model.eval()
pairs = [['what is panda?', 'hi'], ['what is panda?', 'The giant panda (Ailuropoda melanoleuca), sometimes called a panda bear or simply panda, is a bear species endemic to China.']]
with torch.no_grad():
inputs = tokenizer(pairs, padding=True, truncation=True, return_tensors='pt', max_length=512)
scores = model(**inputs, return_dict=True).logits.view(-1, ).float()
print(scores)
```
## Evaluation
`baai-general-embedding` models achieve **state-of-the-art performance on both MTEB and C-MTEB leaderboard!**
For more details and evaluation tools see our [scripts](https://github.com/FlagOpen/FlagEmbedding/blob/master/C_MTEB/README.md).
- **MTEB**:
| Model Name | Dimension | Sequence Length | Average (56) | Retrieval (15) |Clustering (11) | Pair Classification (3) | Reranking (4) | STS (10) | Summarization (1) | Classification (12) |
|:----:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| [BAAI/bge-large-en-v1.5](https://huggingface.co/BAAI/bge-large-en-v1.5) | 1024 | 512 | **64.23** | **54.29** | 46.08 | 87.12 | 60.03 | 83.11 | 31.61 | 75.97 |
| [BAAI/bge-base-en-v1.5](https://huggingface.co/BAAI/bge-base-en-v1.5) | 768 | 512 | 63.55 | 53.25 | 45.77 | 86.55 | 58.86 | 82.4 | 31.07 | 75.53 |
| [BAAI/bge-small-en-v1.5](https://huggingface.co/BAAI/bge-small-en-v1.5) | 384 | 512 | 62.17 |51.68 | 43.82 | 84.92 | 58.36 | 81.59 | 30.12 | 74.14 |
| [bge-large-en](https://huggingface.co/BAAI/bge-large-en) | 1024 | 512 | 63.98 | 53.9 | 46.98 | 85.8 | 59.48 | 81.56 | 32.06 | 76.21 |
| [bge-base-en](https://huggingface.co/BAAI/bge-base-en) | 768 | 512 | 63.36 | 53.0 | 46.32 | 85.86 | 58.7 | 81.84 | 29.27 | 75.27 |
| [gte-large](https://huggingface.co/thenlper/gte-large) | 1024 | 512 | 63.13 | 52.22 | 46.84 | 85.00 | 59.13 | 83.35 | 31.66 | 73.33 |
| [gte-base](https://huggingface.co/thenlper/gte-base) | 768 | 512 | 62.39 | 51.14 | 46.2 | 84.57 | 58.61 | 82.3 | 31.17 | 73.01 |
| [e5-large-v2](https://huggingface.co/intfloat/e5-large-v2) | 1024| 512 | 62.25 | 50.56 | 44.49 | 86.03 | 56.61 | 82.05 | 30.19 | 75.24 |
| [bge-small-en](https://huggingface.co/BAAI/bge-small-en) | 384 | 512 | 62.11 | 51.82 | 44.31 | 83.78 | 57.97 | 80.72 | 30.53 | 74.37 |
| [instructor-xl](https://huggingface.co/hkunlp/instructor-xl) | 768 | 512 | 61.79 | 49.26 | 44.74 | 86.62 | 57.29 | 83.06 | 32.32 | 61.79 |
| [e5-base-v2](https://huggingface.co/intfloat/e5-base-v2) | 768 | 512 | 61.5 | 50.29 | 43.80 | 85.73 | 55.91 | 81.05 | 30.28 | 73.84 |
| [gte-small](https://huggingface.co/thenlper/gte-small) | 384 | 512 | 61.36 | 49.46 | 44.89 | 83.54 | 57.7 | 82.07 | 30.42 | 72.31 |
| [text-embedding-ada-002](https://platform.openai.com/docs/guides/embeddings) | 1536 | 8192 | 60.99 | 49.25 | 45.9 | 84.89 | 56.32 | 80.97 | 30.8 | 70.93 |
| [e5-small-v2](https://huggingface.co/intfloat/e5-base-v2) | 384 | 512 | 59.93 | 49.04 | 39.92 | 84.67 | 54.32 | 80.39 | 31.16 | 72.94 |
| [sentence-t5-xxl](https://huggingface.co/sentence-transformers/sentence-t5-xxl) | 768 | 512 | 59.51 | 42.24 | 43.72 | 85.06 | 56.42 | 82.63 | 30.08 | 73.42 |
| [all-mpnet-base-v2](https://huggingface.co/sentence-transformers/all-mpnet-base-v2) | 768 | 514 | 57.78 | 43.81 | 43.69 | 83.04 | 59.36 | 80.28 | 27.49 | 65.07 |
| [sgpt-bloom-7b1-msmarco](https://huggingface.co/bigscience/sgpt-bloom-7b1-msmarco) | 4096 | 2048 | 57.59 | 48.22 | 38.93 | 81.9 | 55.65 | 77.74 | 33.6 | 66.19 |
- **C-MTEB**:
We create the benchmark C-MTEB for Chinese text embedding which consists of 31 datasets from 6 tasks.
Please refer to [C_MTEB](https://github.com/FlagOpen/FlagEmbedding/blob/master/C_MTEB/README.md) for a detailed introduction.
| Model | Embedding dimension | Avg | Retrieval | STS | PairClassification | Classification | Reranking | Clustering |
|:-------------------------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|
| [**BAAI/bge-large-zh-v1.5**](https://huggingface.co/BAAI/bge-large-zh-v1.5) | 1024 | **64.53** | 70.46 | 56.25 | 81.6 | 69.13 | 65.84 | 48.99 |
| [BAAI/bge-base-zh-v1.5](https://huggingface.co/BAAI/bge-base-zh-v1.5) | 768 | 63.13 | 69.49 | 53.72 | 79.75 | 68.07 | 65.39 | 47.53 |
| [BAAI/bge-small-zh-v1.5](https://huggingface.co/BAAI/bge-small-zh-v1.5) | 512 | 57.82 | 61.77 | 49.11 | 70.41 | 63.96 | 60.92 | 44.18 |
| [BAAI/bge-large-zh](https://huggingface.co/BAAI/bge-large-zh) | 1024 | 64.20 | 71.53 | 54.98 | 78.94 | 68.32 | 65.11 | 48.39 |
| [bge-large-zh-noinstruct](https://huggingface.co/BAAI/bge-large-zh-noinstruct) | 1024 | 63.53 | 70.55 | 53 | 76.77 | 68.58 | 64.91 | 50.01 |
| [BAAI/bge-base-zh](https://huggingface.co/BAAI/bge-base-zh) | 768 | 62.96 | 69.53 | 54.12 | 77.5 | 67.07 | 64.91 | 47.63 |
| [multilingual-e5-large](https://huggingface.co/intfloat/multilingual-e5-large) | 1024 | 58.79 | 63.66 | 48.44 | 69.89 | 67.34 | 56.00 | 48.23 |
| [BAAI/bge-small-zh](https://huggingface.co/BAAI/bge-small-zh) | 512 | 58.27 | 63.07 | 49.45 | 70.35 | 63.64 | 61.48 | 45.09 |
| [m3e-base](https://huggingface.co/moka-ai/m3e-base) | 768 | 57.10 | 56.91 | 50.47 | 63.99 | 67.52 | 59.34 | 47.68 |
| [m3e-large](https://huggingface.co/moka-ai/m3e-large) | 1024 | 57.05 | 54.75 | 50.42 | 64.3 | 68.2 | 59.66 | 48.88 |
| [multilingual-e5-base](https://huggingface.co/intfloat/multilingual-e5-base) | 768 | 55.48 | 61.63 | 46.49 | 67.07 | 65.35 | 54.35 | 40.68 |
| [multilingual-e5-small](https://huggingface.co/intfloat/multilingual-e5-small) | 384 | 55.38 | 59.95 | 45.27 | 66.45 | 65.85 | 53.86 | 45.26 |
| [text-embedding-ada-002(OpenAI)](https://platform.openai.com/docs/guides/embeddings/what-are-embeddings) | 1536 | 53.02 | 52.0 | 43.35 | 69.56 | 64.31 | 54.28 | 45.68 |
| [luotuo](https://huggingface.co/silk-road/luotuo-bert-medium) | 1024 | 49.37 | 44.4 | 42.78 | 66.62 | 61 | 49.25 | 44.39 |
| [text2vec-base](https://huggingface.co/shibing624/text2vec-base-chinese) | 768 | 47.63 | 38.79 | 43.41 | 67.41 | 62.19 | 49.45 | 37.66 |
| [text2vec-large](https://huggingface.co/GanymedeNil/text2vec-large-chinese) | 1024 | 47.36 | 41.94 | 44.97 | 70.86 | 60.66 | 49.16 | 30.02 |
- **Reranking**:
See [C_MTEB](https://github.com/FlagOpen/FlagEmbedding/blob/master/C_MTEB/) for evaluation script.
| Model | T2Reranking | T2RerankingZh2En\* | T2RerankingEn2Zh\* | MMarcoReranking | CMedQAv1 | CMedQAv2 | Avg |
|:-------------------------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|
| text2vec-base-multilingual | 64.66 | 62.94 | 62.51 | 14.37 | 48.46 | 48.6 | 50.26 |
| multilingual-e5-small | 65.62 | 60.94 | 56.41 | 29.91 | 67.26 | 66.54 | 57.78 |
| multilingual-e5-large | 64.55 | 61.61 | 54.28 | 28.6 | 67.42 | 67.92 | 57.4 |
| multilingual-e5-base | 64.21 | 62.13 | 54.68 | 29.5 | 66.23 | 66.98 | 57.29 |
| m3e-base | 66.03 | 62.74 | 56.07 | 17.51 | 77.05 | 76.76 | 59.36 |
| m3e-large | 66.13 | 62.72 | 56.1 | 16.46 | 77.76 | 78.27 | 59.57 |
| bge-base-zh-v1.5 | 66.49 | 63.25 | 57.02 | 29.74 | 80.47 | 84.88 | 63.64 |
| bge-large-zh-v1.5 | 65.74 | 63.39 | 57.03 | 28.74 | 83.45 | 85.44 | 63.97 |
| [BAAI/bge-reranker-base](https://huggingface.co/BAAI/bge-reranker-base) | 67.28 | 63.95 | 60.45 | 35.46 | 81.26 | 84.1 | 65.42 |
| [BAAI/bge-reranker-large](https://huggingface.co/BAAI/bge-reranker-large) | 67.6 | 64.03 | 61.44 | 37.16 | 82.15 | 84.18 | 66.09 |
\* : T2RerankingZh2En and T2RerankingEn2Zh are cross-language retrieval tasks
## Train
### BAAI Embedding
We pre-train the models using [retromae](https://github.com/staoxiao/RetroMAE) and train them on large-scale pairs data using contrastive learning.
**You can fine-tune the embedding model on your data following our [examples](https://github.com/FlagOpen/FlagEmbedding/tree/master/examples/finetune).**
We also provide a [pre-train example](https://github.com/FlagOpen/FlagEmbedding/tree/master/examples/pretrain).
Note that the goal of pre-training is to reconstruct the text, and the pre-trained model cannot be used for similarity calculation directly, it needs to be fine-tuned.
More training details for bge see [baai_general_embedding](https://github.com/FlagOpen/FlagEmbedding/blob/master/FlagEmbedding/baai_general_embedding/README.md).
### BGE Reranker
Cross-encoder will perform full-attention over the input pair,
which is more accurate than embedding model (i.e., bi-encoder) but more time-consuming than embedding model.
Therefore, it can be used to re-rank the top-k documents returned by embedding model.
We train the cross-encoder on a multilingual pair data,
The data format is the same as embedding model, so you can fine-tune it easily following our [example](https://github.com/FlagOpen/FlagEmbedding/tree/master/examples/reranker).
More details please refer to [./FlagEmbedding/reranker/README.md](https://github.com/FlagOpen/FlagEmbedding/tree/master/FlagEmbedding/reranker)
## Contact
If you have any question or suggestion related to this project, feel free to open an issue or pull request.
You also can email Shitao Xiao(stxiao@baai.ac.cn) and Zheng Liu(liuzheng@baai.ac.cn).
## Citation
If you find this repository useful, please consider giving a star :star: and citation
```
@misc{bge_embedding,
title={C-Pack: Packaged Resources To Advance General Chinese Embedding},
author={Shitao Xiao and Zheng Liu and Peitian Zhang and Niklas Muennighoff},
year={2023},
eprint={2309.07597},
archivePrefix={arXiv},
primaryClass={cs.CL}
}
```
## License
FlagEmbedding is licensed under the [MIT License](https://github.com/FlagOpen/FlagEmbedding/blob/master/LICENSE). The released models can be used for commercial purposes free of charge.

View File

@ -0,0 +1,40 @@
{
"_name_or_path": "/root/.cache/torch/sentence_transformers/BAAI_bge-large-zh/",
"architectures": [
"BertModel"
],
"attention_probs_dropout_prob": 0.1,
"bos_token_id": 0,
"classifier_dropout": null,
"directionality": "bidi",
"eos_token_id": 2,
"hidden_act": "gelu",
"hidden_dropout_prob": 0.1,
"hidden_size": 1024,
"id2label": {
"0": "LABEL_0"
},
"initializer_range": 0.02,
"intermediate_size": 4096,
"label2id": {
"LABEL_0": 0
},
"layer_norm_eps": 1e-12,
"max_position_embeddings": 512,
"model_type": "bert",
"num_attention_heads": 16,
"num_hidden_layers": 24,
"output_past": true,
"pad_token_id": 0,
"pooler_fc_size": 768,
"pooler_num_attention_heads": 12,
"pooler_num_fc_layers": 3,
"pooler_size_per_head": 128,
"pooler_type": "first_token_transform",
"position_embedding_type": "absolute",
"torch_dtype": "float32",
"transformers_version": "4.30.0",
"type_vocab_size": 2,
"use_cache": true,
"vocab_size": 21128
}

View File

@ -0,0 +1,7 @@
{
"__version__": {
"sentence_transformers": "2.2.2",
"transformers": "4.28.1",
"pytorch": "1.13.0+cu117"
}
}

View File

@ -0,0 +1,20 @@
[
{
"idx": 0,
"name": "0",
"path": "",
"type": "sentence_transformers.models.Transformer"
},
{
"idx": 1,
"name": "1",
"path": "1_Pooling",
"type": "sentence_transformers.models.Pooling"
},
{
"idx": 2,
"name": "2",
"path": "2_Normalize",
"type": "sentence_transformers.models.Normalize"
}
]

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bf84a56fb045c24e090495195584a3922c3e4204107f0bba8b79c11f67a207f2
size 1302220525

View File

@ -0,0 +1,4 @@
{
"max_seq_length": 512,
"do_lower_case": true
}

View File

@ -0,0 +1,7 @@
{
"cls_token": "[CLS]",
"mask_token": "[MASK]",
"pad_token": "[PAD]",
"sep_token": "[SEP]",
"unk_token": "[UNK]"
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
{
"clean_up_tokenization_spaces": true,
"cls_token": "[CLS]",
"do_basic_tokenize": true,
"do_lower_case": true,
"mask_token": "[MASK]",
"model_max_length": 1000000000000000019884624838656,
"never_split": null,
"pad_token": "[PAD]",
"sep_token": "[SEP]",
"strip_accents": null,
"tokenize_chinese_chars": true,
"tokenizer_class": "BertTokenizer",
"unk_token": "[UNK]"
}

File diff suppressed because it is too large Load Diff

28
main.py Normal file
View File

@ -0,0 +1,28 @@
from fastapi import FastAPI
from app.client.mysql_client_manager import db_assistant_mysql_client_manager
from app.summary import summary_router
app = FastAPI(title="Sales Assistant API", version="1.0.0")
app.include_router(summary_router)
@app.on_event("startup")
async def startup():
db_assistant_mysql_client_manager.init()
@app.on_event("shutdown")
async def shutdown():
await db_assistant_mysql_client_manager.close()
@app.get("/")
async def root():
return {"message": "Sales Assistant API is running"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

23
pyproject.toml Normal file
View File

@ -0,0 +1,23 @@
[project]
name = "sales-assistant-py"
version = "0.1.0"
description = "Add your description here"
requires-python = ">=3.12"
dependencies = [
"aiohttp>=3.14.1",
"apscheduler>=3.11.2",
"asyncmy>=0.2.11",
"cryptography>=48.0.1",
"fastapi[standard]>=0.136.3",
"huggingface-hub>=1.18.0",
"jieba>=0.42.1",
"langchain>=1.3.7",
"langchain-deepseek>=1.1.0",
"langchain-huggingface>=1.2.2",
"langgraph>=1.2.4",
"loguru>=0.7.3",
"omegaconf>=2.3.0",
"pymilvus>=3.0.0",
"pyyaml>=6.0.3",
"sqlalchemy>=2.0.50",
]

3051
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff