Spaces:
Sleeping
Sleeping
update
Browse files- components/dbo/alembic/versions/d6124aee7cce_add_chat_id_to_log.py +32 -0
- components/dbo/models/log.py +4 -4
- components/llm/common.py +3 -2
- components/services/log.py +14 -1
- requirements.txt +1 -0
- routes/llm.py +13 -10
- routes/log.py +53 -0
- schemas/chat.py +30 -0
- schemas/log.py +17 -3
components/dbo/alembic/versions/d6124aee7cce_add_chat_id_to_log.py
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Add chat_id to Log
|
2 |
+
|
3 |
+
Revision ID: d6124aee7cce
|
4 |
+
Revises: 12bb1ebae3ff
|
5 |
+
Create Date: 2025-04-18 16:02:17.245022
|
6 |
+
|
7 |
+
"""
|
8 |
+
from typing import Sequence, Union
|
9 |
+
|
10 |
+
from alembic import op
|
11 |
+
import sqlalchemy as sa
|
12 |
+
|
13 |
+
|
14 |
+
# revision identifiers, used by Alembic.
|
15 |
+
revision: str = 'd6124aee7cce'
|
16 |
+
down_revision: Union[str, None] = '12bb1ebae3ff'
|
17 |
+
branch_labels: Union[str, Sequence[str], None] = None
|
18 |
+
depends_on: Union[str, Sequence[str], None] = None
|
19 |
+
|
20 |
+
|
21 |
+
def upgrade() -> None:
|
22 |
+
"""Upgrade schema."""
|
23 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
24 |
+
op.add_column('log', sa.Column('chat_id', sa.String(), nullable=True))
|
25 |
+
# ### end Alembic commands ###
|
26 |
+
|
27 |
+
|
28 |
+
def downgrade() -> None:
|
29 |
+
"""Downgrade schema."""
|
30 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
31 |
+
op.drop_column('log', 'chat_id')
|
32 |
+
# ### end Alembic commands ###
|
components/dbo/models/log.py
CHANGED
@@ -1,8 +1,7 @@
|
|
1 |
from sqlalchemy import (
|
2 |
-
|
3 |
-
String,
|
4 |
)
|
5 |
-
from sqlalchemy.orm import
|
6 |
from components.dbo.models.base import Base
|
7 |
|
8 |
|
@@ -15,4 +14,5 @@ class Log(Base):
|
|
15 |
llm_result = mapped_column(String)
|
16 |
llm_settings = mapped_column(String)
|
17 |
user_name = mapped_column(String)
|
18 |
-
error = mapped_column(String)
|
|
|
|
1 |
from sqlalchemy import (
|
2 |
+
String
|
|
|
3 |
)
|
4 |
+
from sqlalchemy.orm import mapped_column
|
5 |
from components.dbo.models.base import Base
|
6 |
|
7 |
|
|
|
14 |
llm_result = mapped_column(String)
|
15 |
llm_settings = mapped_column(String)
|
16 |
user_name = mapped_column(String)
|
17 |
+
error = mapped_column(String)
|
18 |
+
chat_id = mapped_column(String)
|
components/llm/common.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
from pydantic import BaseModel, Field
|
2 |
from typing import Optional, List, Protocol
|
3 |
|
4 |
class LlmPredictParams(BaseModel):
|
@@ -77,4 +77,5 @@ class Message(BaseModel):
|
|
77 |
reasoning: Optional[str] = ''
|
78 |
|
79 |
class ChatRequest(BaseModel):
|
80 |
-
history: List[Message]
|
|
|
|
1 |
+
from pydantic import UUID4, BaseModel, Field
|
2 |
from typing import Optional, List, Protocol
|
3 |
|
4 |
class LlmPredictParams(BaseModel):
|
|
|
77 |
reasoning: Optional[str] = ''
|
78 |
|
79 |
class ChatRequest(BaseModel):
|
80 |
+
history: List[Message]
|
81 |
+
chat_id: Optional[str] = None
|
components/services/log.py
CHANGED
@@ -34,11 +34,15 @@ class LogService:
|
|
34 |
def get_list(self, filters: LogFilterSchema) -> PaginatedLogResponse:
|
35 |
logger.info(f"Fetching logs with filters: {filters.model_dump(exclude_none=True)}")
|
36 |
with self.db() as session:
|
37 |
-
query = session.query(LogSQL)
|
38 |
|
39 |
# Применение фильтра по user_name
|
40 |
if filters.user_name:
|
41 |
query = query.filter(LogSQL.user_name == filters.user_name)
|
|
|
|
|
|
|
|
|
42 |
|
43 |
# Применение фильтра по диапазону date_created
|
44 |
if filters.date_from:
|
@@ -46,6 +50,15 @@ class LogService:
|
|
46 |
if filters.date_to:
|
47 |
query = query.filter(LogSQL.date_created <= filters.date_to)
|
48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
49 |
total = query.count()
|
50 |
|
51 |
# Применение пагинации
|
|
|
34 |
def get_list(self, filters: LogFilterSchema) -> PaginatedLogResponse:
|
35 |
logger.info(f"Fetching logs with filters: {filters.model_dump(exclude_none=True)}")
|
36 |
with self.db() as session:
|
37 |
+
query = session.query(LogSQL).order_by(LogSQL.date_created.desc())
|
38 |
|
39 |
# Применение фильтра по user_name
|
40 |
if filters.user_name:
|
41 |
query = query.filter(LogSQL.user_name == filters.user_name)
|
42 |
+
|
43 |
+
# Применение фильтра по chat_id (contains)
|
44 |
+
if filters.chat_id:
|
45 |
+
query = query.filter(LogSQL.chat_id.startswith(filters.chat_id))
|
46 |
|
47 |
# Применение фильтра по диапазону date_created
|
48 |
if filters.date_from:
|
|
|
50 |
if filters.date_to:
|
51 |
query = query.filter(LogSQL.date_created <= filters.date_to)
|
52 |
|
53 |
+
# Сортировка
|
54 |
+
if filters.sort:
|
55 |
+
for sort_param in filters.sort:
|
56 |
+
if sort_param.field == "date_created":
|
57 |
+
if sort_param.direction == "asc":
|
58 |
+
query = query.order_by(LogSQL.date_created.asc())
|
59 |
+
elif sort_param.direction == "desc":
|
60 |
+
query = query.order_by(LogSQL.date_created.desc())
|
61 |
+
|
62 |
total = query.count()
|
63 |
|
64 |
# Применение пагинации
|
requirements.txt
CHANGED
@@ -3,6 +3,7 @@ fastapi==0.113.0
|
|
3 |
unicorn==2.0.1.post1
|
4 |
transformers==4.42.4
|
5 |
pandas==2.2.2
|
|
|
6 |
numpy==1.26.4
|
7 |
tqdm==4.66.5
|
8 |
nltk==3.8.1
|
|
|
3 |
unicorn==2.0.1.post1
|
4 |
transformers==4.42.4
|
5 |
pandas==2.2.2
|
6 |
+
openpyxl==3.1.5
|
7 |
numpy==1.26.4
|
8 |
tqdm==4.66.5
|
9 |
nltk==3.8.1
|
routes/llm.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1 |
import json
|
2 |
import logging
|
3 |
import os
|
4 |
-
from typing import Annotated, AsyncGenerator, Optional
|
5 |
|
6 |
from fastapi import APIRouter, Depends, HTTPException
|
7 |
from fastapi.responses import StreamingResponse
|
@@ -87,7 +87,7 @@ def try_insert_reasoning(
|
|
87 |
if msg.role == "user":
|
88 |
msg.reasoning = reasoning
|
89 |
|
90 |
-
def collapse_history_to_first_message(
|
91 |
"""
|
92 |
Сворачивает историю в первое сообщение и возвращает новый объект ChatRequest.
|
93 |
Формат:
|
@@ -118,18 +118,18 @@ def collapse_history_to_first_message(chat_request: ChatRequest) -> ChatRequest:
|
|
118 |
</last-request>
|
119 |
assistant:
|
120 |
"""
|
121 |
-
if not
|
122 |
-
return
|
123 |
|
124 |
-
last_user_message =
|
125 |
-
if
|
126 |
logger.warning("Last message is not user message")
|
127 |
|
128 |
|
129 |
# Собираем историю в одну строку
|
130 |
collapsed_content = []
|
131 |
collapsed_content.append("<INPUT><history>\n")
|
132 |
-
for msg in
|
133 |
if msg.content.strip():
|
134 |
tabulated_content = msg.content.strip().replace("\n", "\n\t\t")
|
135 |
collapsed_content.append(f"\t<{msg.role.strip()}>\n\t\t{tabulated_content}\n\t</{msg.role.strip()}>\n")
|
@@ -160,7 +160,7 @@ def collapse_history_to_first_message(chat_request: ChatRequest) -> ChatRequest:
|
|
160 |
content=new_content,
|
161 |
searchResults=''
|
162 |
)
|
163 |
-
return
|
164 |
|
165 |
async def sse_generator(request: ChatRequest, llm_api: DeepInfraApi, system_prompt: str,
|
166 |
predict_params: LlmPredictParams,
|
@@ -173,7 +173,7 @@ async def sse_generator(request: ChatRequest, llm_api: DeepInfraApi, system_prom
|
|
173 |
Генератор для стриминга ответа LLM через SSE.
|
174 |
"""
|
175 |
# Создаем экземпляр "сквозного" лога через весь процесс
|
176 |
-
log = LogCreateSchema(user_name=current_user.username)
|
177 |
|
178 |
try:
|
179 |
old_history = request.history
|
@@ -259,7 +259,10 @@ async def sse_generator(request: ChatRequest, llm_api: DeepInfraApi, system_prom
|
|
259 |
log_error = None
|
260 |
try:
|
261 |
# Сворачиваем историю в первое сообщение
|
262 |
-
collapsed_request =
|
|
|
|
|
|
|
263 |
|
264 |
log.llm_result = ''
|
265 |
|
|
|
1 |
import json
|
2 |
import logging
|
3 |
import os
|
4 |
+
from typing import Annotated, AsyncGenerator, List, Optional
|
5 |
|
6 |
from fastapi import APIRouter, Depends, HTTPException
|
7 |
from fastapi.responses import StreamingResponse
|
|
|
87 |
if msg.role == "user":
|
88 |
msg.reasoning = reasoning
|
89 |
|
90 |
+
def collapse_history_to_first_message(chat_history: List[Message]) -> List[Message]:
|
91 |
"""
|
92 |
Сворачивает историю в первое сообщение и возвращает новый объект ChatRequest.
|
93 |
Формат:
|
|
|
118 |
</last-request>
|
119 |
assistant:
|
120 |
"""
|
121 |
+
if not chat_history:
|
122 |
+
return []
|
123 |
|
124 |
+
last_user_message = chat_history[-1]
|
125 |
+
if chat_history[-1].role != "user":
|
126 |
logger.warning("Last message is not user message")
|
127 |
|
128 |
|
129 |
# Собираем историю в одну строку
|
130 |
collapsed_content = []
|
131 |
collapsed_content.append("<INPUT><history>\n")
|
132 |
+
for msg in chat_history[:-1]:
|
133 |
if msg.content.strip():
|
134 |
tabulated_content = msg.content.strip().replace("\n", "\n\t\t")
|
135 |
collapsed_content.append(f"\t<{msg.role.strip()}>\n\t\t{tabulated_content}\n\t</{msg.role.strip()}>\n")
|
|
|
160 |
content=new_content,
|
161 |
searchResults=''
|
162 |
)
|
163 |
+
return [new_message]
|
164 |
|
165 |
async def sse_generator(request: ChatRequest, llm_api: DeepInfraApi, system_prompt: str,
|
166 |
predict_params: LlmPredictParams,
|
|
|
173 |
Генератор для стриминга ответа LLM через SSE.
|
174 |
"""
|
175 |
# Создаем экземпляр "сквозного" лога через весь процесс
|
176 |
+
log = LogCreateSchema(user_name=current_user.username, chat_id=request.chat_id)
|
177 |
|
178 |
try:
|
179 |
old_history = request.history
|
|
|
259 |
log_error = None
|
260 |
try:
|
261 |
# Сворачиваем историю в первое сообщение
|
262 |
+
collapsed_request = ChatRequest(
|
263 |
+
history=collapse_history_to_first_message(request.history),
|
264 |
+
chat_id = request.chat_id
|
265 |
+
)
|
266 |
|
267 |
log.llm_result = ''
|
268 |
|
routes/log.py
CHANGED
@@ -1,8 +1,11 @@
|
|
|
|
1 |
import logging
|
2 |
from datetime import datetime
|
|
|
3 |
from typing import Annotated, List, Optional
|
4 |
|
5 |
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
|
6 |
from pydantic import BaseModel
|
7 |
|
8 |
from common import auth
|
@@ -10,6 +13,7 @@ from common.common import configure_logging
|
|
10 |
from components.services.log import LogService
|
11 |
from schemas.log import LogCreateSchema, LogFilterSchema, LogSchema, PaginatedLogResponse
|
12 |
import common.dependencies as DI
|
|
|
13 |
|
14 |
router = APIRouter(tags=['Logs'])
|
15 |
|
@@ -22,6 +26,7 @@ async def get_all_logs(
|
|
22 |
log_service: Annotated[LogService, Depends(DI.get_log_service)],
|
23 |
current_user: Annotated[any, Depends(auth.get_current_user)]
|
24 |
):
|
|
|
25 |
logger.info(f'GET /logs')
|
26 |
|
27 |
try:
|
@@ -29,4 +34,52 @@ async def get_all_logs(
|
|
29 |
except HTTPException as e:
|
30 |
raise e
|
31 |
except Exception as e:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
raise HTTPException(status_code=500, detail=str(e))
|
|
|
1 |
+
from io import BytesIO
|
2 |
import logging
|
3 |
from datetime import datetime
|
4 |
+
import sys
|
5 |
from typing import Annotated, List, Optional
|
6 |
|
7 |
from fastapi import APIRouter, Depends, HTTPException, Query
|
8 |
+
from fastapi.responses import StreamingResponse
|
9 |
from pydantic import BaseModel
|
10 |
|
11 |
from common import auth
|
|
|
13 |
from components.services.log import LogService
|
14 |
from schemas.log import LogCreateSchema, LogFilterSchema, LogSchema, PaginatedLogResponse
|
15 |
import common.dependencies as DI
|
16 |
+
import pandas as pd
|
17 |
|
18 |
router = APIRouter(tags=['Logs'])
|
19 |
|
|
|
26 |
log_service: Annotated[LogService, Depends(DI.get_log_service)],
|
27 |
current_user: Annotated[any, Depends(auth.get_current_user)]
|
28 |
):
|
29 |
+
logger.info(f"Fetching logsываыва with filters: {filters.model_dump(exclude_none=True)}")
|
30 |
logger.info(f'GET /logs')
|
31 |
|
32 |
try:
|
|
|
34 |
except HTTPException as e:
|
35 |
raise e
|
36 |
except Exception as e:
|
37 |
+
raise HTTPException(status_code=500, detail=str(e))
|
38 |
+
|
39 |
+
@router.get('/logs/excel')
|
40 |
+
async def get_all_logs_excel(
|
41 |
+
filters: LogFilterSchema = Depends(),
|
42 |
+
log_service: LogService = Depends(DI.get_log_service),
|
43 |
+
current_user: any = Depends(auth.get_current_user)
|
44 |
+
):
|
45 |
+
logger.info(f'GET /logs/excel with filters: {filters.model_dump(exclude_none=True)}')
|
46 |
+
|
47 |
+
try:
|
48 |
+
# Получаем логи без пагинации (все записи по фильтру)
|
49 |
+
filters.page = 1
|
50 |
+
filters.page_size = sys.maxsize
|
51 |
+
logs_response = log_service.get_list(filters)
|
52 |
+
|
53 |
+
logs_data = [
|
54 |
+
{
|
55 |
+
'ID': log.id,
|
56 |
+
'Date Created': log.date_created,
|
57 |
+
'User Name': log.user_name or '',
|
58 |
+
'Chat ID': log.chat_id or '',
|
59 |
+
'User Request': log.user_request or '',
|
60 |
+
'QE Result': log.qe_result or '',
|
61 |
+
'Search Result': log.search_result or '',
|
62 |
+
'LLM Result': log.llm_result or ''
|
63 |
+
}
|
64 |
+
for log in logs_response.data
|
65 |
+
]
|
66 |
+
|
67 |
+
df = pd.DataFrame(logs_data)
|
68 |
+
|
69 |
+
output = BytesIO()
|
70 |
+
with pd.ExcelWriter(output, engine='openpyxl') as writer:
|
71 |
+
df.to_excel(writer, index=False, sheet_name='Logs')
|
72 |
+
|
73 |
+
headers = {
|
74 |
+
'Content-Disposition': 'attachment; filename="logs.xlsx"',
|
75 |
+
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
76 |
+
}
|
77 |
+
|
78 |
+
output.seek(0)
|
79 |
+
return StreamingResponse(output, headers=headers, media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
80 |
+
|
81 |
+
except HTTPException as e:
|
82 |
+
raise e
|
83 |
+
except Exception as e:
|
84 |
+
logger.error(f'Error generating Excel: {str(e)}')
|
85 |
raise HTTPException(status_code=500, detail=str(e))
|
schemas/chat.py
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import List, Optional
|
2 |
+
from pydantic import BaseModel
|
3 |
+
|
4 |
+
from components.llm.common import ChatRequest, Message
|
5 |
+
|
6 |
+
|
7 |
+
class MessageSchema(BaseModel):
|
8 |
+
role: str
|
9 |
+
content: str
|
10 |
+
searchResults: Optional[str] = ''
|
11 |
+
searchEntities: Optional[List[str]] = []
|
12 |
+
reasoning: Optional[str] = ''
|
13 |
+
|
14 |
+
def to_bl(self) -> Message:
|
15 |
+
return ChatRequest.model_validate(self.model_dump())
|
16 |
+
|
17 |
+
|
18 |
+
class ChatRequestSchema(BaseModel):
|
19 |
+
history: List[MessageSchema]
|
20 |
+
chat_id: Optional[str]
|
21 |
+
|
22 |
+
def to_bl(self) -> ChatRequest:
|
23 |
+
return ChatRequest.model_validate(self.model_dump(exclude={"chat_id"}))
|
24 |
+
|
25 |
+
@classmethod
|
26 |
+
def from_bl(cls, bl: ChatRequest, chat_id: Optional[str] = None) -> "ChatRequestSchema":
|
27 |
+
return cls.model_validate({
|
28 |
+
"history": [msg.model_dump() for msg in bl.history],
|
29 |
+
"chat_id": chat_id
|
30 |
+
})
|
schemas/log.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1 |
from datetime import datetime
|
2 |
from typing import List, Optional
|
3 |
|
4 |
-
from pydantic import BaseModel
|
5 |
|
6 |
|
7 |
class LogSchema(BaseModel):
|
@@ -13,6 +13,7 @@ class LogSchema(BaseModel):
|
|
13 |
llm_result: Optional[str] = None
|
14 |
llm_settings: Optional[str] = None
|
15 |
user_name: Optional[str] = None
|
|
|
16 |
error: Optional[str] = None
|
17 |
|
18 |
class LogCreateSchema(BaseModel):
|
@@ -23,14 +24,21 @@ class LogCreateSchema(BaseModel):
|
|
23 |
llm_settings: Optional[str] = None
|
24 |
user_name: Optional[str] = None
|
25 |
error: Optional[str] = None
|
|
|
26 |
|
|
|
|
|
|
|
|
|
27 |
class LogFilterSchema(BaseModel):
|
28 |
user_name: Optional[str] = None
|
|
|
29 |
date_from: Optional[datetime] = None
|
30 |
date_to: Optional[datetime] = None
|
31 |
|
32 |
page: int = 1 # Номер страницы, по умолчанию 1
|
33 |
page_size: int = 50 # Размер страницы, по умолчанию 50
|
|
|
34 |
|
35 |
class Config:
|
36 |
json_schema_extra = {
|
@@ -39,7 +47,10 @@ class LogFilterSchema(BaseModel):
|
|
39 |
"date_from": "2024-01-01T00:00:00",
|
40 |
"date_to": "2026-12-31T23:59:59",
|
41 |
"page": 1,
|
42 |
-
"page_size": 50
|
|
|
|
|
|
|
43 |
}
|
44 |
}
|
45 |
|
@@ -48,4 +59,7 @@ class PaginatedLogResponse(BaseModel):
|
|
48 |
total: int
|
49 |
page: int
|
50 |
page_size: int
|
51 |
-
total_pages: int
|
|
|
|
|
|
|
|
1 |
from datetime import datetime
|
2 |
from typing import List, Optional
|
3 |
|
4 |
+
from pydantic import UUID4, BaseModel
|
5 |
|
6 |
|
7 |
class LogSchema(BaseModel):
|
|
|
13 |
llm_result: Optional[str] = None
|
14 |
llm_settings: Optional[str] = None
|
15 |
user_name: Optional[str] = None
|
16 |
+
chat_id: Optional[str] = None
|
17 |
error: Optional[str] = None
|
18 |
|
19 |
class LogCreateSchema(BaseModel):
|
|
|
24 |
llm_settings: Optional[str] = None
|
25 |
user_name: Optional[str] = None
|
26 |
error: Optional[str] = None
|
27 |
+
chat_id: Optional[str] = None
|
28 |
|
29 |
+
class SortParam(BaseModel):
|
30 |
+
field: str
|
31 |
+
direction: str # "asc" | "desc"
|
32 |
+
|
33 |
class LogFilterSchema(BaseModel):
|
34 |
user_name: Optional[str] = None
|
35 |
+
chat_id: Optional[str] = None
|
36 |
date_from: Optional[datetime] = None
|
37 |
date_to: Optional[datetime] = None
|
38 |
|
39 |
page: int = 1 # Номер страницы, по умолчанию 1
|
40 |
page_size: int = 50 # Размер страницы, по умолчанию 50
|
41 |
+
sort: Optional[List[SortParam]] = None # Список параметров сортировки
|
42 |
|
43 |
class Config:
|
44 |
json_schema_extra = {
|
|
|
47 |
"date_from": "2024-01-01T00:00:00",
|
48 |
"date_to": "2026-12-31T23:59:59",
|
49 |
"page": 1,
|
50 |
+
"page_size": 50,
|
51 |
+
"sort": [
|
52 |
+
{"field": "date_created", "direction": "desc"}
|
53 |
+
]
|
54 |
}
|
55 |
}
|
56 |
|
|
|
59 |
total: int
|
60 |
page: int
|
61 |
page_size: int
|
62 |
+
total_pages: int
|
63 |
+
|
64 |
+
|
65 |
+
|