Skip to content

Commit a46d396

Browse files
committed
feat: completion logs view
1 parent 4f5d2f6 commit a46d396

File tree

20 files changed

+698
-204
lines changed

20 files changed

+698
-204
lines changed

backend/api/models/doc/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ class AskLogDocument(Document):
281281
time: datetime.datetime = Field(default_factory=lambda: datetime.datetime.utcnow())
282282
meta: Union[OpenaiWebAskLogMeta, OpenaiApiAskLogMeta] = Field(discriminator='source')
283283
user_id: int
284+
conversation_id: Optional[uuid.UUID] = None
284285
queueing_time: Optional[float]
285286
ask_time: Optional[float]
286287

backend/api/routers/chat.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ async def reply(response: AskResponse):
325325
# stream 传输
326326
async for data in manager.complete(text_content=ask_request.text_content,
327327
conversation_id=ask_request.conversation_id,
328-
parent_id=ask_request.parent,
328+
parent_message_id=ask_request.parent,
329329
model=model,
330330
plugin_ids=ask_request.openai_web_plugin_ids if ask_request.new_conversation else None,
331331
attachments=ask_request.openai_web_attachments,
@@ -476,7 +476,7 @@ async def reply(response: AskResponse):
476476
str(ask_message.id): ask_message,
477477
str(message.id): message
478478
},
479-
current_node=str(message.id),
479+
current_node=message.id,
480480
current_model=message.model
481481
)
482482

@@ -572,6 +572,7 @@ async def reply(response: AskResponse):
572572
user_id=user.id,
573573
queueing_time=queueing_time,
574574
ask_time=ask_time,
575+
conversation_id=conversation_id,
575576
).create()
576577

577578
websocket.scope["ask_websocket_close_code"] = websocket_code

backend/api/routers/logs.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from datetime import datetime
2+
from datetime import datetime
3+
4+
from fastapi import APIRouter, Depends
5+
6+
import api.globals as g
7+
from api.conf import Config, Credentials
8+
from api.models.db import User
9+
from api.models.doc import AskLogDocument
10+
from api.schemas import LogFilterOptions
11+
from api.users import current_super_user
12+
from utils.logger import get_logger
13+
14+
logger = get_logger(__name__)
15+
config = Config()
16+
credentials = Credentials()
17+
18+
router = APIRouter()
19+
20+
21+
def read_last_n_lines(file_path, n, exclude_key_words=None):
22+
if exclude_key_words is None:
23+
exclude_key_words = []
24+
try:
25+
with open(file_path, "r") as f:
26+
lines = f.readlines()[::-1]
27+
except FileNotFoundError:
28+
return [f"File not found: {file_path}"]
29+
last_n_lines = []
30+
for line in lines:
31+
if len(last_n_lines) >= n:
32+
break
33+
if any([line.find(key_word) != -1 for key_word in exclude_key_words]):
34+
continue
35+
last_n_lines.append(line)
36+
return last_n_lines[::-1]
37+
38+
39+
@router.post("/logs/server", tags=["logs"])
40+
async def get_server_logs(_user: User = Depends(current_super_user), options: LogFilterOptions = LogFilterOptions()):
41+
lines = read_last_n_lines(
42+
g.server_log_filename,
43+
options.max_lines,
44+
options.exclude_keywords
45+
)
46+
return lines
47+
48+
49+
@router.get("/logs/completions", tags=["logs"], response_model=list[AskLogDocument])
50+
async def get_completion_logs(start_time: datetime = None, end_time: datetime = None, max_results: int = 100,
51+
_user: User = Depends(current_super_user)):
52+
criteria = []
53+
if start_time:
54+
criteria.append(AskLogDocument.time >= start_time)
55+
if end_time:
56+
criteria.append(AskLogDocument.time <= end_time)
57+
if not criteria:
58+
logs = await AskLogDocument.find_all().sort(-AskLogDocument.time).limit(max_results).to_list()
59+
else:
60+
logs = await AskLogDocument.find(*criteria).sort(-AskLogDocument.time).limit(max_results).to_list()
61+
return logs

backend/api/routers/system.py

Lines changed: 1 addition & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
import csv
21
import random
32
from datetime import datetime, timedelta, timezone
4-
from typing import Optional
5-
63
import httpx
7-
from fastapi import APIRouter, Depends, UploadFile, File
4+
from fastapi import APIRouter, Depends
85
from fastapi_cache.decorator import cache
96
from sqlalchemy import select
107

@@ -78,36 +75,6 @@ async def get_system_info(_user: User = Depends(current_super_user)):
7875
return result
7976

8077

81-
FAKE_REQ_START_TIMESTAMP = 1672502400 # 2023-01-01 00:00:00
82-
83-
84-
def make_fake_requests_count(total=100, max=500):
85-
result = {}
86-
start_stage = FAKE_REQ_START_TIMESTAMP // config.stats.request_counts_interval
87-
for i in range(total):
88-
result[start_stage + i] = [random.randint(0, max), [1]]
89-
return result
90-
91-
92-
def make_fake_ask_records(total=100, days=2):
93-
result = []
94-
model_names = list(api.enums.models.OpenaiWebChatModels)
95-
for i in range(total):
96-
ask_time = random.random() * 60 + 1
97-
total_time = ask_time + random.random() * 30
98-
result.append([
99-
[
100-
# random.randint(1, 10), # user_id
101-
1,
102-
model_names[random.randint(0, len(model_names) - 1)], # model_name
103-
ask_time,
104-
total_time
105-
],
106-
FAKE_REQ_START_TIMESTAMP + random.random() * 60 * 60 * 24 * days, # ask_time
107-
])
108-
return result
109-
110-
11178
@router.get("/system/stats/request", tags=["system"], response_model=list[RequestLogAggregation])
11279
@cache(expire=30)
11380
async def get_request_statistics(
@@ -208,34 +175,6 @@ async def get_ask_statistics(
208175
return result
209176

210177

211-
def read_last_n_lines(file_path, n, exclude_key_words=None):
212-
if exclude_key_words is None:
213-
exclude_key_words = []
214-
try:
215-
with open(file_path, "r") as f:
216-
lines = f.readlines()[::-1]
217-
except FileNotFoundError:
218-
return [f"File not found: {file_path}"]
219-
last_n_lines = []
220-
for line in lines:
221-
if len(last_n_lines) >= n:
222-
break
223-
if any([line.find(key_word) != -1 for key_word in exclude_key_words]):
224-
continue
225-
last_n_lines.append(line)
226-
return last_n_lines[::-1]
227-
228-
229-
@router.post("/system/logs/server", tags=["system"])
230-
async def get_server_logs(_user: User = Depends(current_super_user), options: LogFilterOptions = LogFilterOptions()):
231-
lines = read_last_n_lines(
232-
g.server_log_filename,
233-
options.max_lines,
234-
options.exclude_keywords
235-
)
236-
return lines
237-
238-
239178
@router.get("/system/config", tags=["system"], response_model=ConfigModel)
240179
async def get_config(_user: User = Depends(current_super_user)):
241180
return config.model()
@@ -276,54 +215,3 @@ async def sync_openai_web_conversations(_user: User = Depends(current_super_user
276215
else:
277216
raise exception
278217
return None
279-
280-
281-
# @router.post("/system/import-users", tags=["system"])
282-
async def import_users(file: UploadFile = File(...), _user: User = Depends(current_super_user)):
283-
"""
284-
解析csv文件,导入用户
285-
csv字段:
286-
"""
287-
raise NotImplementedError()
288-
289-
headers = ["id", "username", "nickname", "email", "active_time", "chat_status", "can_use_paid", "max_conv_count",
290-
"available_ask_count", "is_superuser", "is_active", "is_verified", "hashed_password", "can_use_gpt4",
291-
"available_gpt4_ask_count"]
292-
content = await file.read()
293-
content = content.decode("utf-8")
294-
reader = csv.DictReader(content.splitlines())
295-
# check headers
296-
for field in headers:
297-
if field not in reader.fieldnames:
298-
raise InvalidParamsException(f"Invalid csv file, missing field: {field}")
299-
async with get_async_session_context() as session:
300-
async with get_user_db_context(session) as user_db:
301-
async with get_user_manager_context(user_db) as user_manager:
302-
for row in reader:
303-
user_create = UserCreate(
304-
username=row["username"],
305-
nickname=row["nickname"],
306-
email=f"{row['username']}@example.com",
307-
password="12345678",
308-
remark=row["email"]
309-
)
310-
await user_manager._check_username_unique(user_create.username)
311-
user_dict = user_create.dict()
312-
313-
del user_dict["password"]
314-
user_dict["hashed_password"] = row["hashed_password"]
315-
316-
user_setting = UserSettingSchema(
317-
credits=0,
318-
openai_web=OpenaiWebSourceSettingSchema.default(),
319-
openai_api=OpenaiApiSourceSettingSchema.default(),
320-
)
321-
user_setting.openai_web.available_models = ["gpt_3_5", "gpt_4"]
322-
if not row["can_use_gpt4"]:
323-
user_setting.openai_web.available_models = ["gpt_3_5"]
324-
user_setting.openai_web.per_model_ask_count.gpt_3_5 = max(
325-
int(row["available_ask_count"]) - int(row["available_gpt4_ask_count"]), 0)
326-
user_setting.openai_web.per_model_ask_count.gpt_4 = int(row["available_gpt4_ask_count"])
327-
user_setting.openai_web.max_conv_count = int(row["max_conv_count"])
328-
329-
await user_manager.create_with_user_dict(user_dict, user_setting)

backend/api/sources/openai_api.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def reset_session(self):
6161
self.session = make_session()
6262

6363
async def complete(self, text_content: str, conversation_id: uuid.UUID = None,
64-
parent_id: uuid.UUID = None, model: OpenaiApiChatModels = None,
64+
parent_message_id: uuid.UUID = None, model: OpenaiApiChatModels = None,
6565
context_message_count: int = -1, extra_args: Optional[dict] = None, **_kwargs):
6666

6767
assert config.openai_api.enabled, "openai_api is not enabled"
@@ -73,7 +73,7 @@ async def complete(self, text_content: str, conversation_id: uuid.UUID = None,
7373
id=message_id,
7474
role="user",
7575
create_time=now_time,
76-
parent=parent_id,
76+
parent=parent_message_id,
7777
children=[],
7878
content=OpenaiApiChatMessageTextContent(content_type="text", text=text_content),
7979
metadata=OpenaiApiChatMessageMetadata(
@@ -84,16 +84,16 @@ async def complete(self, text_content: str, conversation_id: uuid.UUID = None,
8484
messages = []
8585

8686
if not conversation_id:
87-
assert parent_id is None, "parent_id must be None when conversation_id is None"
87+
assert parent_message_id is None, "parent_id must be None when conversation_id is None"
8888
messages = [new_message]
8989
else:
9090
conv_history = await OpenaiApiConversationHistoryDocument.get(conversation_id)
9191
if not conv_history:
9292
raise ValueError("conversation_id not found")
9393
if conv_history.source != ChatSourceTypes.openai_api:
9494
raise ValueError(f"{conversation_id} is not api conversation")
95-
if not conv_history.mapping.get(str(parent_id)):
96-
raise ValueError(f"{parent_id} is not a valid parent of {conversation_id}")
95+
if not conv_history.mapping.get(str(parent_message_id)):
96+
raise ValueError(f"{parent_message_id} is not a valid parent of {conversation_id}")
9797

9898
# 从 current_node 开始往前找 context_message_count 个 message
9999
if not conv_history.current_node:

backend/main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from api.middlewares import AccessLoggerMiddleware, StatisticsMiddleware
2424
from api.models.db import User
2525
from api.response import CustomJSONResponse, handle_exception_response
26-
from api.routers import users, conv, chat, system, status, files
26+
from api.routers import users, conv, chat, system, status, files, logs
2727
from api.schemas import UserCreate, UserSettingSchema
2828
from api.sources import OpenaiWebChatManager
2929
from api.users import get_user_manager_context
@@ -121,6 +121,7 @@ async def lifespan(app: FastAPI):
121121
app.include_router(conv.router)
122122
app.include_router(chat.router)
123123
app.include_router(system.router)
124+
app.include_router(logs.router)
124125
app.include_router(status.router)
125126
app.include_router(files.router)
126127

frontend/src/api/logs.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import axios from 'axios';
2+
3+
import {
4+
AskLogAggregation,
5+
AskLogDocument,
6+
ConfigModel,
7+
CredentialsModel,
8+
LogFilterOptions,
9+
RequestLogAggregation,
10+
SystemInfo,
11+
} from '@/types/schema';
12+
13+
import ApiUrl from './url';
14+
15+
export function getServerLogsApi(options: LogFilterOptions | null) {
16+
return axios.post(ApiUrl.ServerLogs, options);
17+
}
18+
19+
export function getCompletionLogsApi(start_time?: string, end_time?: string, limit = 100) {
20+
return axios.get<AskLogDocument[]>(ApiUrl.CompletionLogs, { params: { start_time, end_time, limit } });
21+
}

frontend/src/api/system.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,6 @@ export function getAskStatisticsApi(granularity: number) {
2727
});
2828
}
2929

30-
export function getServerLogsApi(options: LogFilterOptions | null) {
31-
return axios.post(ApiUrl.ServerLogs, options);
32-
}
33-
3430
export function getSystemConfig() {
3531
return axios.get<ConfigModel>(ApiUrl.SystemConfig);
3632
}

frontend/src/api/url.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ enum ApiUrl {
1717
SystemInfo = '/system/info',
1818
SystemRequestStatistics = '/system/stats/request',
1919
SystemAskStatistics = '/system/stats/ask',
20-
ServerLogs = '/system/logs/server',
2120
SystemActionSyncOpenaiWebConversations = '/system/action/sync-openai-web-conv',
2221

22+
ServerLogs = '/logs/server',
23+
CompletionLogs = '/logs/completions',
24+
2325
SystemConfig = '/system/config',
2426
SystemCredentials = '/system/credentials',
2527

frontend/src/locales/en-US.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,9 @@
184184
"readFileResult": "Result",
185185
"updateTime": "Update Time",
186186
"executeCode": "Execute Code",
187-
"siteTitleFull": "ChatGPT Web Share"
187+
"siteTitleFull": "ChatGPT Web Share",
188+
"completionLogs": "Completion Logs",
189+
"time": "Time"
188190
},
189191
"errors": {
190192
"403": "403 error: access denied",
@@ -343,7 +345,8 @@
343345
"enabled_models": "Enabled Models",
344346
"model_code_mapping": "Model Code Mapping",
345347
"file_upload_strategy": "File Upload Strategy"
346-
}
348+
},
349+
"conversation_id": "Conversatin ID"
347350
},
348351
"models": {
349352
"gpt_3_5": "GPT-3.5",

0 commit comments

Comments
 (0)