Skip to content

Commit e279ae4

Browse files
committed
feat: Q&A Chat
1 parent b50ee7e commit e279ae4

File tree

13 files changed

+670
-453
lines changed

13 files changed

+670
-453
lines changed

backend/alembic/env.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
# target_metadata = None
2626

2727
# from apps.system.models.user import SQLModel # noqa
28-
from apps.settings.models.setting_models import SQLModel
28+
# from apps.settings.models.setting_models import SQLModel
29+
from apps.chat.models.chat_model import SQLModel
2930
from common.core.config import settings # noqa
3031

3132
target_metadata = SQLModel.metadata

backend/apps/chat/api/chat.py

Lines changed: 69 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,68 @@
11
from fastapi import APIRouter, HTTPException
22
from fastapi.responses import StreamingResponse
33
from sqlmodel import select
4+
5+
from apps.chat.curd.chat import list_chats, get_chat_with_records, create_chat, save_question, save_answer
6+
from apps.chat.models.chat_model import CreateChat, ChatRecord
47
from apps.chat.schemas.chat_base_schema import LLMConfig
58
from apps.chat.schemas.chat_schema import ChatQuestion
69
from apps.chat.schemas.llm import AgentService
710
from apps.datasource.models.datasource import CoreDatasource
811
from apps.system.models.system_model import AiModelDetail
9-
from common.core.deps import SessionDep
12+
from common.core.deps import SessionDep, CurrentUser
1013
import json
1114
import asyncio
1215

1316
router = APIRouter(tags=["Data Q&A"], prefix="/chat")
1417

1518

19+
@router.get("/list")
20+
async def chats(session: SessionDep, current_user: CurrentUser):
21+
return list_chats(session, current_user)
22+
23+
24+
@router.get("/get/{chart_id}")
25+
async def list_chat(session: SessionDep, current_user: CurrentUser, chart_id: int):
26+
try:
27+
return get_chat_with_records(chart_id=chart_id, session=session, current_user=current_user)
28+
except Exception as e:
29+
raise HTTPException(
30+
status_code=500,
31+
detail=str(e)
32+
)
33+
34+
35+
@router.post("/start")
36+
async def start_chat(session: SessionDep, current_user: CurrentUser, create_chat_obj: CreateChat):
37+
try:
38+
return create_chat(session, current_user, create_chat_obj)
39+
except Exception as e:
40+
raise HTTPException(
41+
status_code=400,
42+
detail=str(e)
43+
)
44+
45+
1646
@router.post("/question")
17-
async def stream_sql(session: SessionDep, requestQuestion: ChatQuestion):
47+
async def stream_sql(session: SessionDep, current_user: CurrentUser, request_question: ChatQuestion):
1848
"""Stream SQL analysis results
1949
2050
Args:
2151
session: Database session
22-
requestQuestion: User question model
52+
current_user: CurrentUser
53+
request_question: User question model
2354
2455
Returns:
2556
Streaming response with analysis results
2657
"""
27-
question = requestQuestion.question
28-
58+
question = request_question.question
59+
2960
# Get available AI model
3061
aimodel = session.exec(select(AiModelDetail).where(
31-
AiModelDetail.status == True,
62+
AiModelDetail.status == True,
3263
AiModelDetail.api_key.is_not(None)
3364
)).first()
34-
65+
3566
# Get available datasource
3667
ds = session.exec(select(CoreDatasource).where(
3768
CoreDatasource.status == 'Success'
@@ -42,13 +73,22 @@ async def stream_sql(session: SessionDep, requestQuestion: ChatQuestion):
4273
status_code=400,
4374
detail="No available AI model configuration found"
4475
)
45-
76+
4677
if not ds:
4778
raise HTTPException(
4879
status_code=400,
4980
detail="No available datasource configuration found"
5081
)
51-
82+
83+
record: ChatRecord
84+
try:
85+
record = save_question(session=session, current_user=current_user, question=request_question)
86+
except Exception as e1:
87+
raise HTTPException(
88+
status_code=400,
89+
detail=str(e1)
90+
)
91+
5292
# Use Tongyi Qianwen
5393
tongyi_config = LLMConfig(
5494
model_type="openai",
@@ -72,25 +112,37 @@ async def stream_sql(session: SessionDep, requestQuestion: ChatQuestion):
72112
vllm_service = LLMService(vllm_config) """
73113
""" result = llm_service.generate_sql(question)
74114
return result """
75-
115+
76116
async def event_generator():
117+
all_text = ''
77118
try:
78119
async for chunk in llm_service.async_generate(question):
79120
data = json.loads(chunk.replace('data: ', ''))
80-
121+
81122
if data['type'] in ['final', 'tool_result']:
82123
content = data['content']
124+
print('-- ' + content)
125+
all_text += content
83126
for char in content:
84127
yield f"data: {json.dumps({'type': 'char', 'content': char})}\n\n"
85-
await asyncio.sleep(0.05)
86-
128+
await asyncio.sleep(0.05)
129+
87130
if 'html' in data:
88131
yield f"data: {json.dumps({'type': 'html', 'content': data['html']})}\n\n"
89132
else:
90133
yield chunk
91-
134+
92135
except Exception as e:
136+
all_text = 'Exception:' + str(e)
93137
yield f"data: {json.dumps({'type': 'error', 'content': str(e)})}\n\n"
94-
95-
#return EventSourceResponse(event_generator(), headers={"Content-Type": "text/event-stream"})
96-
return StreamingResponse(event_generator(), media_type="text/event-stream")
138+
139+
try:
140+
save_answer(session=session, id=record.id, answer=all_text)
141+
except Exception as e:
142+
raise HTTPException(
143+
status_code=500,
144+
detail=str(e)
145+
)
146+
147+
# return EventSourceResponse(event_generator(), headers={"Content-Type": "text/event-stream"})
148+
return StreamingResponse(event_generator(), media_type="text/event-stream")

backend/apps/chat/curd/__init__.py

Whitespace-only changes.

backend/apps/chat/curd/chat.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import datetime
2+
import json
3+
from typing import List
4+
5+
from sqlalchemy import text, and_
6+
from sqlmodel import select
7+
8+
from apps.chat.models.chat_model import Chat, ChatRecord, CreateChat, ChatInfo
9+
from apps.chat.schemas.chat_schema import ChatQuestion
10+
from apps.datasource.models.datasource import CoreDatasource
11+
from common.core.deps import SessionDep, CurrentUser
12+
13+
14+
def list_chats(session: SessionDep, current_user: CurrentUser) -> List[Chat]:
15+
chart_list = session.query(Chat).filter(Chat.create_by == current_user.id).order_by(
16+
Chat.create_time.desc()).all()
17+
return chart_list
18+
19+
20+
def get_chat_with_records(session: SessionDep, chart_id: int, current_user: CurrentUser) -> ChatInfo:
21+
chat = session.query(Chat).filter(Chat.id == chart_id).first()
22+
if not chat:
23+
raise Exception(f"Chat with id {chart_id} not found")
24+
25+
chat_info = ChatInfo(**chat.model_dump())
26+
27+
ds = session.query(CoreDatasource).filter(CoreDatasource.id == chat.datasource).first()
28+
if not ds:
29+
chat_info.datasource_exists = False
30+
chat_info.datasource_name = 'Datasource not exist'
31+
else:
32+
chat_info.datasource_exists = True
33+
chat_info.datasource_name = ds.name
34+
35+
record_list = session.query(ChatRecord).filter(
36+
and_(Chat.create_by == current_user.id, ChatRecord.id == chart_id)).order_by(ChatRecord.create_time).all()
37+
38+
chat_info.records = record_list
39+
40+
return chat_info
41+
42+
43+
def create_chat(session: SessionDep, current_user: CurrentUser, create_chat_obj: CreateChat) -> ChatInfo:
44+
if not create_chat_obj.datasource:
45+
raise Exception("Datasource cannot be None")
46+
47+
if not create_chat_obj.question or create_chat_obj.question.strip() == '':
48+
raise Exception("Question cannot be Empty")
49+
50+
chat = Chat(create_time=datetime.datetime.now(),
51+
create_by=current_user.id,
52+
brief=create_chat_obj.question.strip()[:20],
53+
datasource=create_chat_obj.datasource)
54+
55+
ds = session.query(CoreDatasource).filter(CoreDatasource.id == create_chat_obj.datasource).first()
56+
57+
if not ds:
58+
raise Exception(f"Datasource with id {create_chat_obj.datasource} not found")
59+
60+
chat.engine_type = ds.type_name
61+
62+
chat_info = ChatInfo(**chat.model_dump())
63+
64+
session.add(chat)
65+
session.flush()
66+
session.refresh(chat)
67+
chat_info.id = chat.id
68+
session.commit()
69+
70+
return chat_info
71+
72+
73+
def save_question(session: SessionDep, current_user: CurrentUser, question: ChatQuestion) -> ChatRecord:
74+
if not question.chat_id:
75+
raise Exception("ChatId cannot be None")
76+
if not question.question or question.question.strip() == '':
77+
raise Exception("Question cannot be Empty")
78+
79+
chat = session.query(Chat).filter(Chat.id == question.chat_id).first()
80+
if not chat:
81+
raise Exception(f"Chat with id {question.chat_id} not found")
82+
83+
record = ChatRecord()
84+
record.question = question.question
85+
record.chat_id = chat.id
86+
record.create_time = datetime.datetime.now()
87+
record.create_by = current_user.id
88+
record.datasource = chat.datasource
89+
record.engine_type = chat.engine_type
90+
91+
result = ChatRecord(**record.model_dump())
92+
93+
session.add(record)
94+
session.flush()
95+
session.refresh(record)
96+
result.id = record.id
97+
session.commit()
98+
99+
return result
100+
101+
def save_full_question(session: SessionDep, id: int, full_question: str) -> ChatRecord:
102+
if not id:
103+
raise Exception("Record id cannot be None")
104+
record = session.query(ChatRecord).filter(Chat.id == id).first()
105+
record.full_question = full_question
106+
107+
result = ChatRecord(**record.model_dump())
108+
109+
session.add(record)
110+
session.flush()
111+
session.refresh(record)
112+
113+
session.commit()
114+
115+
return result
116+
117+
def save_answer(session: SessionDep, id: int, answer: str) -> ChatRecord:
118+
if not id:
119+
raise Exception("Record id cannot be None")
120+
121+
record = session.query(ChatRecord).filter(Chat.id == id).first()
122+
record.answer = answer
123+
124+
result = ChatRecord(**record.model_dump())
125+
126+
session.add(record)
127+
session.flush()
128+
session.refresh(record)
129+
130+
session.commit()
131+
132+
return result

backend/apps/chat/models/__init__.py

Whitespace-only changes.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from sqlmodel import SQLModel, Field
2+
from sqlalchemy import Column, Text, BigInteger, DateTime, Integer, Identity
3+
from datetime import datetime
4+
from pydantic import BaseModel
5+
from typing import List, Optional
6+
7+
8+
class Chat(SQLModel, table=True):
9+
__tablename__ = "chat"
10+
id: Optional[int] = Field(sa_column=Column(Integer, Identity(always=True), primary_key=True))
11+
create_time: datetime = Field(sa_column=Column(DateTime(timezone=True), nullable=True))
12+
create_by: int = Field(sa_column=Column(BigInteger, nullable=True))
13+
brief: str = Field(max_length=64, nullable=True)
14+
chat_type: str = Field(max_length=20, default="chat") # chat, datasource
15+
datasource: int = Field(sa_column=Column(Integer, nullable=False))
16+
engine_type: str = Field(max_length=64)
17+
18+
19+
class ChatRecord(SQLModel, table=True):
20+
__tablename__ = "chat_record"
21+
id: Optional[int] = Field(sa_column=Column(Integer, Identity(always=True), primary_key=True))
22+
chat_id: int = Field(sa_column=Column(Integer))
23+
create_time: datetime = Field(sa_column=Column(DateTime(timezone=True), nullable=True))
24+
create_by: int = Field(sa_column=Column(BigInteger, nullable=True))
25+
datasource: int = Field(sa_column=Column(Integer, nullable=False))
26+
engine_type: str = Field(max_length=64)
27+
question: str = Field(sa_column=Column(Text, nullable=True))
28+
full_question: str = Field(sa_column=Column(Text, nullable=True))
29+
answer: str = Field(sa_column=Column(Text, nullable=True))
30+
run_time: float = Field(default=0)
31+
32+
33+
class CreateChat(BaseModel):
34+
id: int = None
35+
question: str = ''
36+
datasource: int = None
37+
38+
39+
class ChatInfo(BaseModel):
40+
id: Optional[int] = None
41+
create_time: datetime = None
42+
create_by: int = None
43+
brief: str = ''
44+
chat_type: str = "chat"
45+
datasource: int = None
46+
engine_type: str = ''
47+
datasource_name: str = ''
48+
datasource_exists: bool = True
49+
records: List[ChatRecord] = []

backend/apps/chat/schemas/chat_schema.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33

44

55
class ChatQuestion(BaseModel):
6-
question: str
6+
question: str
7+
chat_id: int

backend/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ dependencies = [
3636
"oracledb (>=3.1.1,<4.0.0)"
3737
]
3838
[[tool.uv.index]]
39-
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
39+
url = "https://mirrors.aliyun.com/pypi/simple"
4040
default = true
4141

4242
[tool.uv]

frontend/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"snowflake-id": "^1.1.0",
1414
"vue": "^3.5.13",
1515
"vue-router": "^4.5.0",
16-
"web-storage-cache": "^1.1.1"
16+
"web-storage-cache": "^1.1.1",
17+
"dayjs": "^1.11.13"
1718
},
1819
"devDependencies": {
1920
"@element-plus/icons-vue": "^2.3.1",

0 commit comments

Comments
 (0)