feat: align agent preview with learner topics
This commit is contained in:
@@ -3,7 +3,7 @@ import { ElMessage, ElMessageBox } from "element-plus";
|
|||||||
import { computed, nextTick, onMounted, reactive, ref, watch } from "vue";
|
import { computed, nextTick, onMounted, reactive, ref, watch } from "vue";
|
||||||
|
|
||||||
import { api, streamDebugAgent } from "../services/api";
|
import { api, streamDebugAgent } from "../services/api";
|
||||||
import type { AdminUser, AgentRuntimeConfig, KnowledgeItem, ModelItem, PromptDetail, PromptHistoryItem } from "../types/api";
|
import type { AdminUser, AgentRuntimeConfig, KnowledgeItem, ModelItem, PromptDetail, PromptHistoryItem, TopicSessionRecord } from "../types/api";
|
||||||
import AgentGenerationParameters from "./AgentGenerationParameters.vue";
|
import AgentGenerationParameters from "./AgentGenerationParameters.vue";
|
||||||
import AgentResponseDepthControl from "./AgentResponseDepthControl.vue";
|
import AgentResponseDepthControl from "./AgentResponseDepthControl.vue";
|
||||||
import AdminPagination from "./AdminPagination.vue";
|
import AdminPagination from "./AdminPagination.vue";
|
||||||
@@ -27,6 +27,8 @@ const models = ref<ModelItem[]>([]);
|
|||||||
const knowledge = ref<KnowledgeItem[]>([]);
|
const knowledge = ref<KnowledgeItem[]>([]);
|
||||||
const debugUsers = ref<AdminUser[]>([]);
|
const debugUsers = ref<AdminUser[]>([]);
|
||||||
const debugUserLoading = ref(false);
|
const debugUserLoading = ref(false);
|
||||||
|
const debugTopics = ref<TopicSessionRecord[]>([]);
|
||||||
|
const debugTopicLoading = ref(false);
|
||||||
const history = ref<PromptHistoryItem[]>([]);
|
const history = ref<PromptHistoryItem[]>([]);
|
||||||
const historyPager = reactive({ page: 1, pageSize: 10, total: 0 });
|
const historyPager = reactive({ page: 1, pageSize: 10, total: 0 });
|
||||||
const historyDetailOpen = ref(false);
|
const historyDetailOpen = ref(false);
|
||||||
@@ -47,6 +49,7 @@ const agentPreviewMessages = ref<{
|
|||||||
const agentForm = reactive({
|
const agentForm = reactive({
|
||||||
modelId: undefined as number | undefined,
|
modelId: undefined as number | undefined,
|
||||||
userId: undefined as number | undefined,
|
userId: undefined as number | undefined,
|
||||||
|
topicSessionId: undefined as number | undefined,
|
||||||
knowledgeIds: [] as number[],
|
knowledgeIds: [] as number[],
|
||||||
temperature: 0.2 as number | null,
|
temperature: 0.2 as number | null,
|
||||||
topP: null as number | null,
|
topP: null as number | null,
|
||||||
@@ -89,6 +92,11 @@ const selectedDebugUserLabel = computed(() => {
|
|||||||
const user = debugUsers.value.find((item) => item.id === agentForm.userId);
|
const user = debugUsers.value.find((item) => item.id === agentForm.userId);
|
||||||
return user ? `${user.name || user.nickname || user.phone} · #${user.id}` : `学员 #${agentForm.userId}`;
|
return user ? `${user.name || user.nickname || user.phone} · #${user.id}` : `学员 #${agentForm.userId}`;
|
||||||
});
|
});
|
||||||
|
const selectedDebugTopicLabel = computed(() => {
|
||||||
|
if (!agentForm.topicSessionId) return "未指定主题";
|
||||||
|
const topic = debugTopics.value.find((item) => item.id === agentForm.topicSessionId);
|
||||||
|
return topic ? topic.title : `主题 #${agentForm.topicSessionId}`;
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(load);
|
onMounted(load);
|
||||||
|
|
||||||
@@ -135,6 +143,55 @@ async function searchDebugUsers(keyword: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleDebugUserChange(userId?: number) {
|
||||||
|
agentForm.topicSessionId = undefined;
|
||||||
|
debugTopics.value = [];
|
||||||
|
clearAgentPreview("模拟学员已切换,可以选择其中一个主题继续调试。");
|
||||||
|
if (!userId) return;
|
||||||
|
debugTopicLoading.value = true;
|
||||||
|
try {
|
||||||
|
await loadDebugTopics(userId);
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(errorMessage(error, "学员主题加载失败"));
|
||||||
|
} finally {
|
||||||
|
debugTopicLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDebugTopics(userId: number, keyword = "") {
|
||||||
|
const selected = debugTopics.value.find((item) => item.id === agentForm.topicSessionId);
|
||||||
|
const result = await api.userTopics(userId, { keyword, page: 1, pageSize: 100 });
|
||||||
|
debugTopics.value = selected && !result.items.some((item) => item.id === selected.id)
|
||||||
|
? [selected, ...result.items]
|
||||||
|
: result.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchDebugTopics(keyword: string) {
|
||||||
|
if (!agentForm.userId) return;
|
||||||
|
debugTopicLoading.value = true;
|
||||||
|
try {
|
||||||
|
await loadDebugTopics(agentForm.userId, keyword);
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(errorMessage(error, "学员主题搜索失败"));
|
||||||
|
} finally {
|
||||||
|
debugTopicLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDebugTopicChange() {
|
||||||
|
clearAgentPreview(
|
||||||
|
agentForm.topicSessionId
|
||||||
|
? "模拟主题已切换,下一条调试会加载该主题的摘要和最近对话。"
|
||||||
|
: "已恢复通用学员模拟,不加载具体主题对话。",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function debugTopicOptionLabel(topic: TopicSessionRecord) {
|
||||||
|
const statusLabel = topic.status === "active" ? "进行中" : topic.status === "completed" ? "已完成" : topic.status;
|
||||||
|
const summaryLabel = topic.summaryAvailable ? "有摘要" : "无摘要";
|
||||||
|
return `${topic.title} · ${statusLabel} · ${topic.messageCount} 条消息 · ${summaryLabel}`;
|
||||||
|
}
|
||||||
|
|
||||||
function applyRuntimeConfig(value: AgentRuntimeConfig) {
|
function applyRuntimeConfig(value: AgentRuntimeConfig) {
|
||||||
runtimeConfig.value = value;
|
runtimeConfig.value = value;
|
||||||
Object.assign(runtimeForm, {
|
Object.assign(runtimeForm, {
|
||||||
@@ -325,6 +382,7 @@ async function debugAgent() {
|
|||||||
promptContent: promptContent.value,
|
promptContent: promptContent.value,
|
||||||
modelId: agentForm.modelId,
|
modelId: agentForm.modelId,
|
||||||
userId: agentForm.userId || null,
|
userId: agentForm.userId || null,
|
||||||
|
topicSessionId: agentForm.topicSessionId || null,
|
||||||
knowledgeIds: agentForm.knowledgeIds,
|
knowledgeIds: agentForm.knowledgeIds,
|
||||||
question,
|
question,
|
||||||
history: conversationHistory,
|
history: conversationHistory,
|
||||||
@@ -385,8 +443,8 @@ function scrollAgentPreview() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearAgentPreview() {
|
function clearAgentPreview(message = "预览已清空,可以继续发起新的 Agent 调试。") {
|
||||||
agentPreviewMessages.value = [{ role: "assistant", content: "预览已清空,可以继续发起新的 Agent 调试。" }];
|
agentPreviewMessages.value = [{ role: "assistant", content: message }];
|
||||||
agentDebugTrace.value = [];
|
agentDebugTrace.value = [];
|
||||||
lastDebugRoute.value = null;
|
lastDebugRoute.value = null;
|
||||||
}
|
}
|
||||||
@@ -501,6 +559,7 @@ function errorMessage(error: unknown, fallback: string) {
|
|||||||
<el-option v-for="model in models" :key="model.id" :label="model.displayName || model.modelName" :value="model.id" />
|
<el-option v-for="model in models" :key="model.id" :label="model.displayName || model.modelName" :value="model.id" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<div class="agent-debug-context-grid">
|
||||||
<el-form-item label="模拟学员(可选)">
|
<el-form-item label="模拟学员(可选)">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="agentForm.userId"
|
v-model="agentForm.userId"
|
||||||
@@ -509,7 +568,9 @@ function errorMessage(error: unknown, fallback: string) {
|
|||||||
remote
|
remote
|
||||||
:remote-method="searchDebugUsers"
|
:remote-method="searchDebugUsers"
|
||||||
:loading="debugUserLoading"
|
:loading="debugUserLoading"
|
||||||
|
:disabled="agentDebugging"
|
||||||
placeholder="不选择则不注入学员权益和成长档案"
|
placeholder="不选择则不注入学员权益和成长档案"
|
||||||
|
@change="handleDebugUserChange"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="user in debugUsers"
|
v-for="user in debugUsers"
|
||||||
@@ -518,8 +579,30 @@ function errorMessage(error: unknown, fallback: string) {
|
|||||||
:value="user.id"
|
:value="user.id"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
<div class="agent-form-help">用于让后台预览加载该学员的权益、主题额度和长期成长档案;不会消耗额度,也不会影响正式会话。</div>
|
<div class="agent-form-help">加载该学员的权益和长期成长档案;不会消耗额度或改动正式会话。</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="模拟主题(可选)">
|
||||||
|
<el-select
|
||||||
|
v-model="agentForm.topicSessionId"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
remote
|
||||||
|
:remote-method="searchDebugTopics"
|
||||||
|
:loading="debugTopicLoading"
|
||||||
|
:disabled="!agentForm.userId || agentDebugging"
|
||||||
|
placeholder="选择学员后可加载具体主题"
|
||||||
|
@change="handleDebugTopicChange"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="topic in debugTopics"
|
||||||
|
:key="topic.id"
|
||||||
|
:label="debugTopicOptionLabel(topic)"
|
||||||
|
:value="topic.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<div class="agent-form-help">选择后会加载该主题摘要和最近对话,用于验证追问效果。</div>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
<el-form-item label="调试知识库">
|
<el-form-item label="调试知识库">
|
||||||
<el-select v-model="agentForm.knowledgeIds" class="debug-knowledge-select" multiple filterable collapse-tags collapse-tags-tooltip :max-collapse-tags="2" popper-class="debug-knowledge-popper" placeholder="默认使用全部已开放知识库">
|
<el-select v-model="agentForm.knowledgeIds" class="debug-knowledge-select" multiple filterable collapse-tags collapse-tags-tooltip :max-collapse-tags="2" popper-class="debug-knowledge-popper" placeholder="默认使用全部已开放知识库">
|
||||||
<el-option v-for="item in knowledge" :key="item.id" :label="`${item.name}${item.status === 0 ? ' · 已关闭(仅本次预览)' : ''}`" :value="item.id" />
|
<el-option v-for="item in knowledge" :key="item.id" :label="`${item.name}${item.status === 0 ? ' · 已关闭(仅本次预览)' : ''}`" :value="item.id" />
|
||||||
@@ -593,7 +676,7 @@ function errorMessage(error: unknown, fallback: string) {
|
|||||||
|
|
||||||
<aside class="agent-preview-panel">
|
<aside class="agent-preview-panel">
|
||||||
<div class="agent-preview-head">
|
<div class="agent-preview-head">
|
||||||
<div><h3>调试预览</h3><small>{{ selectedKnowledgeSummary }} · {{ selectedDebugUserLabel }}</small></div>
|
<div><h3>调试预览</h3><small>{{ selectedKnowledgeSummary }} · {{ selectedDebugUserLabel }} · {{ selectedDebugTopicLabel }}</small></div>
|
||||||
<div class="agent-preview-head-actions"><span :title="lastDebugRoute?.routeReason || ''">{{ lastDebugRoute?.modelName || selectedModelName }}</span><el-button link :disabled="agentDebugging" @click="clearAgentPreview">清空</el-button></div>
|
<div class="agent-preview-head-actions"><span :title="lastDebugRoute?.routeReason || ''">{{ lastDebugRoute?.modelName || selectedModelName }}</span><el-button link :disabled="agentDebugging" @click="clearAgentPreview">清空</el-button></div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="agentPreviewChat" class="agent-preview-chat">
|
<div ref="agentPreviewChat" class="agent-preview-chat">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
ElAlert,
|
ElAlert,
|
||||||
ElButton,
|
ElButton,
|
||||||
|
ElCheckbox,
|
||||||
ElCollapse,
|
ElCollapse,
|
||||||
ElCollapseItem,
|
ElCollapseItem,
|
||||||
ElDatePicker,
|
ElDatePicker,
|
||||||
@@ -32,6 +33,7 @@ import {
|
|||||||
import "element-plus/theme-chalk/base.css";
|
import "element-plus/theme-chalk/base.css";
|
||||||
import "element-plus/theme-chalk/el-alert.css";
|
import "element-plus/theme-chalk/el-alert.css";
|
||||||
import "element-plus/theme-chalk/el-button.css";
|
import "element-plus/theme-chalk/el-button.css";
|
||||||
|
import "element-plus/theme-chalk/el-checkbox.css";
|
||||||
import "element-plus/theme-chalk/el-collapse.css";
|
import "element-plus/theme-chalk/el-collapse.css";
|
||||||
import "element-plus/theme-chalk/el-date-picker.css";
|
import "element-plus/theme-chalk/el-date-picker.css";
|
||||||
import "element-plus/theme-chalk/el-divider.css";
|
import "element-plus/theme-chalk/el-divider.css";
|
||||||
@@ -70,6 +72,7 @@ app.config.globalProperties.$ELEMENT = { locale: zhCn };
|
|||||||
[
|
[
|
||||||
ElAlert,
|
ElAlert,
|
||||||
ElButton,
|
ElButton,
|
||||||
|
ElCheckbox,
|
||||||
ElCollapse,
|
ElCollapse,
|
||||||
ElCollapseItem,
|
ElCollapseItem,
|
||||||
ElDatePicker,
|
ElDatePicker,
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import type {
|
|||||||
PromptHistoryItem,
|
PromptHistoryItem,
|
||||||
QuestionInsightRefreshResult,
|
QuestionInsightRefreshResult,
|
||||||
QuestionInsightSummary,
|
QuestionInsightSummary,
|
||||||
|
TopicSessionRecord,
|
||||||
} from "../types/api";
|
} from "../types/api";
|
||||||
|
|
||||||
const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "/api";
|
const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "/api";
|
||||||
@@ -129,6 +130,8 @@ export const api = {
|
|||||||
},
|
},
|
||||||
users: (query: { keyword?: string; page?: number; pageSize?: number } = {}) => request<PageResult<AdminUser>>(`/admin/user/list${queryString(query)}`),
|
users: (query: { keyword?: string; page?: number; pageSize?: number } = {}) => request<PageResult<AdminUser>>(`/admin/user/list${queryString(query)}`),
|
||||||
userDetail: (id: number) => request<AdminUserDetail>(`/admin/user/${id}/detail`),
|
userDetail: (id: number) => request<AdminUserDetail>(`/admin/user/${id}/detail`),
|
||||||
|
userTopics: (id: number, query: { keyword?: string; page?: number; pageSize?: number } = {}) =>
|
||||||
|
request<PageResult<TopicSessionRecord>>(`/admin/user/${id}/topics${queryString(query)}`),
|
||||||
userReports: (id: number, limit = 20) => request<PeriodicReportRecord[]>(`/admin/user/${id}/reports${queryString({ limit })}`),
|
userReports: (id: number, limit = 20) => request<PeriodicReportRecord[]>(`/admin/user/${id}/reports${queryString({ limit })}`),
|
||||||
generateUserReport: (id: number, payload: { reportType: string; periodStart?: string | null; periodEnd?: string | null }) =>
|
generateUserReport: (id: number, payload: { reportType: string; periodStart?: string | null; periodEnd?: string | null }) =>
|
||||||
request<PeriodicReportRecord>(`/admin/user/${id}/reports/generate`, { method: "POST", body: JSON.stringify(payload) }),
|
request<PeriodicReportRecord>(`/admin/user/${id}/reports/generate`, { method: "POST", body: JSON.stringify(payload) }),
|
||||||
|
|||||||
@@ -1379,6 +1379,16 @@ textarea {
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.agent-debug-context-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-debug-context-grid .el-form-item {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.debug-knowledge-select { width: 100%; min-width: 0; }
|
.debug-knowledge-select { width: 100%; min-width: 0; }
|
||||||
.debug-knowledge-select .el-select__wrapper { height: 40px; min-height: 40px; overflow: hidden; }
|
.debug-knowledge-select .el-select__wrapper { height: 40px; min-height: 40px; overflow: hidden; }
|
||||||
.debug-knowledge-select .el-select__selection { flex-wrap: nowrap; overflow: hidden; }
|
.debug-knowledge-select .el-select__selection { flex-wrap: nowrap; overflow: hidden; }
|
||||||
@@ -2385,6 +2395,10 @@ textarea {
|
|||||||
.agent-page-head {
|
.agent-page-head {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.agent-debug-context-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 860px) {
|
@media (max-width: 860px) {
|
||||||
|
|||||||
@@ -562,6 +562,7 @@ export interface TopicSessionRecord {
|
|||||||
createdAt?: string | null;
|
createdAt?: string | null;
|
||||||
updatedAt?: string | null;
|
updatedAt?: string | null;
|
||||||
summary?: TopicSummaryRecord | null;
|
summary?: TopicSummaryRecord | null;
|
||||||
|
summaryAvailable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QuestionInsightSummary {
|
export interface QuestionInsightSummary {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from fastapi.responses import StreamingResponse
|
|||||||
from openpyxl import Workbook, load_workbook
|
from openpyxl import Workbook, load_workbook
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
from sqlalchemy import extract, func, select
|
from sqlalchemy import extract, func, or_, select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
@@ -267,11 +267,49 @@ def user_operation_detail(
|
|||||||
"recentTopics": _recent_topics(db, user.id),
|
"recentTopics": _recent_topics(db, user.id),
|
||||||
"recentHelpCards": [help_card_dict(item) for item in _recent_help_cards(db, user.id)],
|
"recentHelpCards": [help_card_dict(item) for item in _recent_help_cards(db, user.id)],
|
||||||
"recentShareDrafts": [share_draft_dict(item) for item in _recent_share_drafts(db, user.id)],
|
"recentShareDrafts": [share_draft_dict(item) for item in _recent_share_drafts(db, user.id)],
|
||||||
"recentReports": [periodic_report_dict(item) for item in PeriodicReportService.list_user_reports(db, user_id=user.id, limit=10)],
|
"recentReports": [
|
||||||
|
periodic_report_dict(item)
|
||||||
|
for item in PeriodicReportService.list_user_reports(db, user_id=user.id, limit=10)
|
||||||
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/user/{user_id}/topics")
|
||||||
|
def user_topic_options(
|
||||||
|
user_id: int,
|
||||||
|
keyword: str = Query(default="", max_length=100),
|
||||||
|
page: int = Query(default=1, ge=1),
|
||||||
|
pageSize: int = Query(default=50, ge=10, le=100),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_admin: Admin = Depends(get_current_admin),
|
||||||
|
) -> dict:
|
||||||
|
"""Lightweight topic selector data for Agent preview; no message bodies are returned."""
|
||||||
|
user = _get_user(db, user_id)
|
||||||
|
conditions = [TopicSession.user_id == user.id]
|
||||||
|
normalized_keyword = keyword.strip()
|
||||||
|
if normalized_keyword:
|
||||||
|
like = f"%{normalized_keyword}%"
|
||||||
|
conditions.append(or_(TopicSession.title.like(like), TopicSession.core_question.like(like)))
|
||||||
|
total = db.scalar(select(func.count(TopicSession.id)).where(*conditions)) or 0
|
||||||
|
rows = db.execute(
|
||||||
|
select(TopicSession, TopicSummary.id)
|
||||||
|
.join(TopicSummary, TopicSummary.topic_session_id == TopicSession.id, isouter=True)
|
||||||
|
.where(*conditions)
|
||||||
|
.order_by(TopicSession.updated_at.desc(), TopicSession.id.desc())
|
||||||
|
.offset((page - 1) * pageSize)
|
||||||
|
.limit(pageSize)
|
||||||
|
).all()
|
||||||
|
items = [
|
||||||
|
{
|
||||||
|
**topic_dict(topic),
|
||||||
|
"summaryAvailable": summary_id is not None,
|
||||||
|
}
|
||||||
|
for topic, summary_id in rows
|
||||||
|
]
|
||||||
|
return api_success(page_result(items, total=total, page=page, page_size=pageSize))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/user/{user_id}/reports")
|
@router.get("/user/{user_id}/reports")
|
||||||
def user_reports(
|
def user_reports(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ class AgentDebugRequest(BaseModel):
|
|||||||
promptContent: str = Field(min_length=1)
|
promptContent: str = Field(min_length=1)
|
||||||
modelId: int = Field(gt=0)
|
modelId: int = Field(gt=0)
|
||||||
userId: int | None = Field(default=None, gt=0)
|
userId: int | None = Field(default=None, gt=0)
|
||||||
|
topicSessionId: int | None = Field(default=None, gt=0)
|
||||||
knowledgeIds: list[int] = Field(default_factory=list)
|
knowledgeIds: list[int] = Field(default_factory=list)
|
||||||
knowledgeVersions: dict[int, int] = Field(default_factory=dict)
|
knowledgeVersions: dict[int, int] = Field(default_factory=dict)
|
||||||
question: str = Field(min_length=1, max_length=2000)
|
question: str = Field(min_length=1, max_length=2000)
|
||||||
|
|||||||
@@ -5,14 +5,18 @@ from collections.abc import AsyncIterator
|
|||||||
from dataclasses import replace
|
from dataclasses import replace
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.models.chat import ChatMessage, ChatSession, TopicSession
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.models.admin import Admin
|
from app.models.admin import Admin
|
||||||
from app.models.ai_config import ModelConfig
|
from app.models.ai_config import ModelConfig
|
||||||
from app.schemas.admin import AgentDebugRequest
|
from app.schemas.admin import AgentDebugRequest
|
||||||
from app.services.admin_service import OperationLogService
|
from app.services.admin_service import OperationLogService
|
||||||
from app.services.entitlement_service import EntitlementService, entitlement_dict
|
from app.services.chat_context_service import ChatContextService
|
||||||
|
from app.services.entitlement_service import EntitlementService, entitlement_dict, entitlement_prompt_context
|
||||||
from app.services.growth_profile_service import GrowthProfileService
|
from app.services.growth_profile_service import GrowthProfileService
|
||||||
from app.services.knowledge_agent_service import KnowledgeAgentService
|
from app.services.knowledge_agent_service import KnowledgeAgentService
|
||||||
from app.services.model_stream_service import ModelStreamService
|
from app.services.model_stream_service import ModelStreamService
|
||||||
@@ -25,17 +29,21 @@ from app.services.topic_session_service import TopicSessionService
|
|||||||
class AgentDebugService:
|
class AgentDebugService:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def build_result(db: Session, payload: AgentDebugRequest) -> RagResult:
|
async def build_result(db: Session, payload: AgentDebugRequest) -> RagResult:
|
||||||
history = [
|
|
||||||
SimpleNamespace(id=index + 1, role=item.role, content=item.content)
|
|
||||||
for index, item in enumerate(payload.history)
|
|
||||||
]
|
|
||||||
preview_knowledge_ids = payload.knowledgeIds or None
|
preview_knowledge_ids = payload.knowledgeIds or None
|
||||||
version_overrides = payload.knowledgeVersions or None
|
version_overrides = payload.knowledgeVersions or None
|
||||||
debug_context = AgentDebugService._debug_user_context(db, payload.userId)
|
debug_context = AgentDebugService._debug_user_context(db, payload.userId, payload.topicSessionId)
|
||||||
|
persisted_history = debug_context["history"]
|
||||||
|
max_history_id = max((int(item.id) for item in persisted_history), default=0)
|
||||||
|
preview_history = [
|
||||||
|
SimpleNamespace(id=max_history_id + index + 1, role=item.role, content=item.content)
|
||||||
|
for index, item in enumerate(payload.history)
|
||||||
|
]
|
||||||
rag_result = await KnowledgeAgentService.build_result(
|
rag_result = await KnowledgeAgentService.build_result(
|
||||||
db,
|
db,
|
||||||
question=payload.question,
|
question=payload.question,
|
||||||
history=history,
|
history=[*persisted_history, *preview_history],
|
||||||
|
session_summary=debug_context["session_summary"],
|
||||||
|
summary_up_to_message_id=debug_context["summary_up_to_message_id"],
|
||||||
version_overrides=version_overrides,
|
version_overrides=version_overrides,
|
||||||
preview_knowledge_ids=preview_knowledge_ids,
|
preview_knowledge_ids=preview_knowledge_ids,
|
||||||
user_id=payload.userId,
|
user_id=payload.userId,
|
||||||
@@ -43,6 +51,8 @@ class AgentDebugService:
|
|||||||
prompt_override=payload.promptContent,
|
prompt_override=payload.promptContent,
|
||||||
response_depth=payload.responseDepth,
|
response_depth=payload.responseDepth,
|
||||||
growth_context=debug_context["growth_context"],
|
growth_context=debug_context["growth_context"],
|
||||||
|
topic_context=debug_context["topic_context"],
|
||||||
|
product_context=debug_context["product_context"],
|
||||||
)
|
)
|
||||||
return RagResult(
|
return RagResult(
|
||||||
question=rag_result.question,
|
question=rag_result.question,
|
||||||
@@ -56,13 +66,30 @@ class AgentDebugService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _debug_user_context(db: Session, user_id: int | None) -> dict:
|
def _debug_user_context(db: Session, user_id: int | None, topic_session_id: int | None) -> dict:
|
||||||
if user_id is None:
|
if user_id is None:
|
||||||
return {"growth_context": None, "trace": []}
|
if topic_session_id is not None:
|
||||||
user = db.get(User, user_id)
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="选择主题前请先选择模拟学员")
|
||||||
if user is None or user.is_deleted:
|
|
||||||
return {
|
return {
|
||||||
"growth_context": None,
|
"growth_context": None,
|
||||||
|
"topic_context": None,
|
||||||
|
"product_context": None,
|
||||||
|
"session_summary": None,
|
||||||
|
"summary_up_to_message_id": None,
|
||||||
|
"history": [],
|
||||||
|
"trace": [],
|
||||||
|
}
|
||||||
|
user = db.get(User, user_id)
|
||||||
|
if user is None or user.is_deleted:
|
||||||
|
if topic_session_id is not None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="模拟学员不存在或已删除")
|
||||||
|
return {
|
||||||
|
"growth_context": None,
|
||||||
|
"topic_context": None,
|
||||||
|
"product_context": None,
|
||||||
|
"session_summary": None,
|
||||||
|
"summary_up_to_message_id": None,
|
||||||
|
"history": [],
|
||||||
"trace": [
|
"trace": [
|
||||||
{
|
{
|
||||||
"tool": "load_debug_user_context",
|
"tool": "load_debug_user_context",
|
||||||
@@ -81,13 +108,55 @@ class AgentDebugService:
|
|||||||
monthly_topic_used=TopicSessionService.monthly_used_count(db, user.id),
|
monthly_topic_used=TopicSessionService.monthly_used_count(db, user.id),
|
||||||
)
|
)
|
||||||
growth_context = GrowthProfileService.prompt_context(db, user) if entitlement.enable_growth_profile else None
|
growth_context = GrowthProfileService.prompt_context(db, user) if entitlement.enable_growth_profile else None
|
||||||
|
product_context = entitlement_prompt_context(entitlement)
|
||||||
|
topic = None
|
||||||
|
topic_context = None
|
||||||
|
topic_summary_used = False
|
||||||
|
session_summary = None
|
||||||
|
summary_up_to_message_id = None
|
||||||
|
history: list[ChatMessage] = []
|
||||||
|
if topic_session_id is not None:
|
||||||
|
topic = db.scalar(
|
||||||
|
select(TopicSession).where(
|
||||||
|
TopicSession.id == topic_session_id,
|
||||||
|
TopicSession.user_id == user.id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if topic is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="所选主题不属于当前模拟学员")
|
||||||
|
history_limit = ChatContextService.message_limit(db)
|
||||||
|
if history_limit > 0:
|
||||||
|
latest_history = list(
|
||||||
|
db.scalars(
|
||||||
|
select(ChatMessage)
|
||||||
|
.where(
|
||||||
|
ChatMessage.topic_session_id == topic.id,
|
||||||
|
ChatMessage.user_id == user.id,
|
||||||
|
ChatMessage.role.in_(("user", "assistant")),
|
||||||
|
)
|
||||||
|
.order_by(ChatMessage.created_at.desc(), ChatMessage.id.desc())
|
||||||
|
.limit(history_limit)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
history = list(reversed(latest_history))
|
||||||
|
if topic.status == "active":
|
||||||
|
chat_session = db.get(ChatSession, topic.chat_session_id)
|
||||||
|
if chat_session is not None and chat_session.user_id == user.id:
|
||||||
|
session_summary = chat_session.summary
|
||||||
|
summary_up_to_message_id = chat_session.summary_up_to_message_id
|
||||||
|
topic_context, topic_summary_used = TopicSessionService.prompt_context(db, topic)
|
||||||
return {
|
return {
|
||||||
"growth_context": growth_context,
|
"growth_context": growth_context,
|
||||||
|
"topic_context": topic_context,
|
||||||
|
"product_context": product_context,
|
||||||
|
"session_summary": session_summary,
|
||||||
|
"summary_up_to_message_id": summary_up_to_message_id,
|
||||||
|
"history": history,
|
||||||
"trace": [
|
"trace": [
|
||||||
{
|
{
|
||||||
"tool": "load_debug_user_context",
|
"tool": "load_debug_user_context",
|
||||||
"order": 1,
|
"order": 1,
|
||||||
"request": {"userId": user.id},
|
"request": {"userId": user.id, "topicSessionId": topic_session_id},
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"durationMs": 0,
|
"durationMs": 0,
|
||||||
"response": {
|
"response": {
|
||||||
@@ -95,6 +164,16 @@ class AgentDebugService:
|
|||||||
"userName": user.name,
|
"userName": user.name,
|
||||||
"entitlement": entitlement_dict(entitlement),
|
"entitlement": entitlement_dict(entitlement),
|
||||||
"growthProfileUsed": bool(growth_context),
|
"growthProfileUsed": bool(growth_context),
|
||||||
|
"productContextUsed": True,
|
||||||
|
"topic": {
|
||||||
|
"id": topic.id,
|
||||||
|
"title": topic.title,
|
||||||
|
"status": topic.status,
|
||||||
|
"messageCount": topic.message_count,
|
||||||
|
"loadedHistoryCount": len(history),
|
||||||
|
"summaryUsed": topic_summary_used,
|
||||||
|
"rollingSummaryUsed": bool(session_summary),
|
||||||
|
} if topic is not None else None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from sqlalchemy.orm import Session
|
|||||||
from app.models.chat import ChatMessage, ChatSession
|
from app.models.chat import ChatMessage, ChatSession
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.services.ai_request_log_service import AiRequestLogService
|
from app.services.ai_request_log_service import AiRequestLogService
|
||||||
from app.services.entitlement_service import EntitlementService
|
from app.services.entitlement_service import EntitlementService, entitlement_prompt_context
|
||||||
from app.services.external_errors import ExternalServiceError
|
from app.services.external_errors import ExternalServiceError
|
||||||
from app.services.growth_profile_service import GrowthProfileService
|
from app.services.growth_profile_service import GrowthProfileService
|
||||||
from app.services.chat_context_service import ChatContextService
|
from app.services.chat_context_service import ChatContextService
|
||||||
@@ -117,6 +117,7 @@ class ChatService:
|
|||||||
select(ChatMessage)
|
select(ChatMessage)
|
||||||
.where(
|
.where(
|
||||||
ChatMessage.session_id == session.id,
|
ChatMessage.session_id == session.id,
|
||||||
|
ChatMessage.topic_session_id == topic.id,
|
||||||
ChatMessage.user_id == user.id,
|
ChatMessage.user_id == user.id,
|
||||||
ChatMessage.id < user_message.id,
|
ChatMessage.id < user_message.id,
|
||||||
)
|
)
|
||||||
@@ -130,6 +131,27 @@ class ChatService:
|
|||||||
if growth_context:
|
if growth_context:
|
||||||
context_trace = list(context_trace or [])
|
context_trace = list(context_trace or [])
|
||||||
context_trace.append({"tool": "load_growth_profile", "status": "success", "count": 1})
|
context_trace.append({"tool": "load_growth_profile", "status": "success", "count": 1})
|
||||||
|
topic_context, topic_summary_used = TopicSessionService.prompt_context(db, topic)
|
||||||
|
product_context = entitlement_prompt_context(entitlement)
|
||||||
|
context_trace = list(context_trace or [])
|
||||||
|
context_trace.extend(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"tool": "load_topic_context",
|
||||||
|
"status": "success",
|
||||||
|
"response": {"topicSessionId": topic.id, "summaryUsed": topic_summary_used},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tool": "load_product_entitlement",
|
||||||
|
"status": "success",
|
||||||
|
"response": {
|
||||||
|
"planId": entitlement.plan_id,
|
||||||
|
"allowHelpCard": entitlement.allow_help_card,
|
||||||
|
"allowShareDraft": entitlement.allow_share_draft,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# 构建 prompt(传入历史 + 摘要)
|
# 构建 prompt(传入历史 + 摘要)
|
||||||
rag_result = RagService.build_result(
|
rag_result = RagService.build_result(
|
||||||
@@ -139,6 +161,8 @@ class ChatService:
|
|||||||
summary_up_to_message_id=session.summary_up_to_message_id,
|
summary_up_to_message_id=session.summary_up_to_message_id,
|
||||||
context_trace=context_trace,
|
context_trace=context_trace,
|
||||||
growth_context=growth_context,
|
growth_context=growth_context,
|
||||||
|
topic_context=topic_context,
|
||||||
|
product_context=product_context,
|
||||||
)
|
)
|
||||||
completion = ModelClientService.complete(db, rag_result)
|
completion = ModelClientService.complete(db, rag_result)
|
||||||
except ExternalServiceError as exc:
|
except ExternalServiceError as exc:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from app.models.user import User
|
|||||||
from app.services.ai_request_log_service import AiRequestLogService
|
from app.services.ai_request_log_service import AiRequestLogService
|
||||||
from app.services.chat_service import ChatService, _title_from_question
|
from app.services.chat_service import ChatService, _title_from_question
|
||||||
from app.services.chat_context_service import ChatContextService
|
from app.services.chat_context_service import ChatContextService
|
||||||
from app.services.entitlement_service import EntitlementService
|
from app.services.entitlement_service import EntitlementService, entitlement_prompt_context
|
||||||
from app.services.external_errors import ExternalServiceError
|
from app.services.external_errors import ExternalServiceError
|
||||||
from app.services.growth_profile_service import GrowthProfileService
|
from app.services.growth_profile_service import GrowthProfileService
|
||||||
from app.services.human_attention_service import HumanAttentionService
|
from app.services.human_attention_service import HumanAttentionService
|
||||||
@@ -67,6 +67,7 @@ class ChatStreamService:
|
|||||||
select(ChatMessage)
|
select(ChatMessage)
|
||||||
.where(
|
.where(
|
||||||
ChatMessage.session_id == session.id,
|
ChatMessage.session_id == session.id,
|
||||||
|
ChatMessage.topic_session_id == topic.id,
|
||||||
ChatMessage.user_id == user.id,
|
ChatMessage.user_id == user.id,
|
||||||
ChatMessage.id < user_message.id,
|
ChatMessage.id < user_message.id,
|
||||||
)
|
)
|
||||||
@@ -79,6 +80,14 @@ class ChatStreamService:
|
|||||||
if growth_context:
|
if growth_context:
|
||||||
context_trace = list(context_trace or [])
|
context_trace = list(context_trace or [])
|
||||||
context_trace.append({"tool": "load_growth_profile", "status": "success", "count": 1})
|
context_trace.append({"tool": "load_growth_profile", "status": "success", "count": 1})
|
||||||
|
topic_context, topic_summary_used = TopicSessionService.prompt_context(db, topic)
|
||||||
|
product_context = entitlement_prompt_context(entitlement)
|
||||||
|
context_trace = _append_runtime_context_trace(
|
||||||
|
context_trace,
|
||||||
|
topic=topic,
|
||||||
|
topic_summary_used=topic_summary_used,
|
||||||
|
entitlement=entitlement,
|
||||||
|
)
|
||||||
|
|
||||||
started_at = perf_counter()
|
started_at = perf_counter()
|
||||||
rag_result = None
|
rag_result = None
|
||||||
@@ -95,6 +104,8 @@ class ChatStreamService:
|
|||||||
getattr(session, "summary_up_to_message_id", None),
|
getattr(session, "summary_up_to_message_id", None),
|
||||||
context_trace,
|
context_trace,
|
||||||
growth_context,
|
growth_context,
|
||||||
|
topic_context,
|
||||||
|
product_context,
|
||||||
)
|
)
|
||||||
model_response = ModelStreamService.stream(db, rag_result)
|
model_response = ModelStreamService.stream(db, rag_result)
|
||||||
for chunk in model_response.chunks:
|
for chunk in model_response.chunks:
|
||||||
@@ -239,6 +250,7 @@ class ChatStreamService:
|
|||||||
select(ChatMessage)
|
select(ChatMessage)
|
||||||
.where(
|
.where(
|
||||||
ChatMessage.session_id == session.id,
|
ChatMessage.session_id == session.id,
|
||||||
|
ChatMessage.topic_session_id == topic.id,
|
||||||
ChatMessage.user_id == user.id,
|
ChatMessage.user_id == user.id,
|
||||||
ChatMessage.id < user_message.id,
|
ChatMessage.id < user_message.id,
|
||||||
)
|
)
|
||||||
@@ -251,6 +263,14 @@ class ChatStreamService:
|
|||||||
if growth_context:
|
if growth_context:
|
||||||
context_trace = list(context_trace or [])
|
context_trace = list(context_trace or [])
|
||||||
context_trace.append({"tool": "load_growth_profile", "status": "success", "count": 1})
|
context_trace.append({"tool": "load_growth_profile", "status": "success", "count": 1})
|
||||||
|
topic_context, topic_summary_used = TopicSessionService.prompt_context(db, topic)
|
||||||
|
product_context = entitlement_prompt_context(entitlement)
|
||||||
|
context_trace = _append_runtime_context_trace(
|
||||||
|
context_trace,
|
||||||
|
topic=topic,
|
||||||
|
topic_summary_used=topic_summary_used,
|
||||||
|
entitlement=entitlement,
|
||||||
|
)
|
||||||
|
|
||||||
started_at = perf_counter()
|
started_at = perf_counter()
|
||||||
rag_result = None
|
rag_result = None
|
||||||
@@ -268,6 +288,8 @@ class ChatStreamService:
|
|||||||
session_id=session.id,
|
session_id=session.id,
|
||||||
context_trace=context_trace,
|
context_trace=context_trace,
|
||||||
growth_context=growth_context,
|
growth_context=growth_context,
|
||||||
|
topic_context=topic_context,
|
||||||
|
product_context=product_context,
|
||||||
)
|
)
|
||||||
model_response = ModelStreamService.stream_async(db, rag_result)
|
model_response = ModelStreamService.stream_async(db, rag_result)
|
||||||
async for chunk in model_response.chunks:
|
async for chunk in model_response.chunks:
|
||||||
@@ -485,6 +507,8 @@ def _build_rag_result(
|
|||||||
summary_up_to_message_id: int | None,
|
summary_up_to_message_id: int | None,
|
||||||
context_trace: list[dict] | None = None,
|
context_trace: list[dict] | None = None,
|
||||||
growth_context: str | None = None,
|
growth_context: str | None = None,
|
||||||
|
topic_context: str | None = None,
|
||||||
|
product_context: str | None = None,
|
||||||
):
|
):
|
||||||
parameters = signature(RagService.build_result).parameters
|
parameters = signature(RagService.build_result).parameters
|
||||||
if "history" in parameters:
|
if "history" in parameters:
|
||||||
@@ -497,5 +521,30 @@ def _build_rag_result(
|
|||||||
summary_up_to_message_id=summary_up_to_message_id,
|
summary_up_to_message_id=summary_up_to_message_id,
|
||||||
context_trace=context_trace,
|
context_trace=context_trace,
|
||||||
growth_context=growth_context,
|
growth_context=growth_context,
|
||||||
|
topic_context=topic_context,
|
||||||
|
product_context=product_context,
|
||||||
)
|
)
|
||||||
return RagService.build_result(db, user, question)
|
return RagService.build_result(db, user, question)
|
||||||
|
|
||||||
|
|
||||||
|
def _append_runtime_context_trace(context_trace, *, topic, topic_summary_used: bool, entitlement) -> list[dict]:
|
||||||
|
result = list(context_trace or [])
|
||||||
|
result.extend(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"tool": "load_topic_context",
|
||||||
|
"status": "success",
|
||||||
|
"response": {"topicSessionId": topic.id, "summaryUsed": topic_summary_used},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tool": "load_product_entitlement",
|
||||||
|
"status": "success",
|
||||||
|
"response": {
|
||||||
|
"planId": entitlement.plan_id,
|
||||||
|
"allowHelpCard": entitlement.allow_help_card,
|
||||||
|
"allowShareDraft": entitlement.allow_share_draft,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|||||||
@@ -214,6 +214,19 @@ def entitlement_dict(view: EntitlementView) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def entitlement_prompt_context(view: EntitlementView) -> str:
|
||||||
|
"""Describe product capabilities without letting the model pretend to execute them."""
|
||||||
|
help_card = "可由学员主动生成" if view.allow_help_card else "当前权益不可生成"
|
||||||
|
share_draft = "可由学员主动生成" if view.allow_share_draft else "当前权益不可生成"
|
||||||
|
return (
|
||||||
|
"[当前产品权益]\n"
|
||||||
|
f"权益名称:{view.name};权益类型:{view.plan_type}。\n"
|
||||||
|
f"老师求助卡:{help_card};班级分享稿:{share_draft}。\n"
|
||||||
|
"这些能力由页面按钮和专用接口真实执行。不得声称已自动转人工、已联系老师、已生成或已发送卡片;"
|
||||||
|
"只有学员明确询问相关能力时,才说明当前是否可以由学员主动生成。"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def view_from_plan(
|
def view_from_plan(
|
||||||
plan: EntitlementPlan,
|
plan: EntitlementPlan,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ class KnowledgeAgentService:
|
|||||||
prompt_override: str | None = None,
|
prompt_override: str | None = None,
|
||||||
response_depth: int | None = None,
|
response_depth: int | None = None,
|
||||||
growth_context: str | None = None,
|
growth_context: str | None = None,
|
||||||
|
topic_context: str | None = None,
|
||||||
|
product_context: str | None = None,
|
||||||
) -> RagResult:
|
) -> RagResult:
|
||||||
started = perf_counter()
|
started = perf_counter()
|
||||||
catalog = cls.get_knowledge_catalog(
|
catalog = cls.get_knowledge_catalog(
|
||||||
@@ -211,6 +213,8 @@ class KnowledgeAgentService:
|
|||||||
prompt_override=prompt_override,
|
prompt_override=prompt_override,
|
||||||
response_depth=response_depth,
|
response_depth=response_depth,
|
||||||
growth_context=growth_context,
|
growth_context=growth_context,
|
||||||
|
topic_context=topic_context,
|
||||||
|
product_context=product_context,
|
||||||
)
|
)
|
||||||
return RagResult(
|
return RagResult(
|
||||||
question=question,
|
question=question,
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ class AsyncRagService:
|
|||||||
session_id: int | None = None,
|
session_id: int | None = None,
|
||||||
context_trace: list[dict] | None = None,
|
context_trace: list[dict] | None = None,
|
||||||
growth_context: str | None = None,
|
growth_context: str | None = None,
|
||||||
|
topic_context: str | None = None,
|
||||||
|
product_context: str | None = None,
|
||||||
) -> RagResult:
|
) -> RagResult:
|
||||||
return await KnowledgeAgentService.build_result(
|
return await KnowledgeAgentService.build_result(
|
||||||
db,
|
db,
|
||||||
@@ -31,4 +33,6 @@ class AsyncRagService:
|
|||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
context_trace=context_trace,
|
context_trace=context_trace,
|
||||||
growth_context=growth_context,
|
growth_context=growth_context,
|
||||||
|
topic_context=topic_context,
|
||||||
|
product_context=product_context,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ class RagService:
|
|||||||
summary_up_to_message_id: int | None = None,
|
summary_up_to_message_id: int | None = None,
|
||||||
context_trace: list[dict] | None = None,
|
context_trace: list[dict] | None = None,
|
||||||
growth_context: str | None = None,
|
growth_context: str | None = None,
|
||||||
|
topic_context: str | None = None,
|
||||||
|
product_context: str | None = None,
|
||||||
) -> RagResult:
|
) -> RagResult:
|
||||||
scopes = KnowledgeAccessService.get_allowed_knowledge(db, user)
|
scopes = KnowledgeAccessService.get_allowed_knowledge(db, user)
|
||||||
chunks = FeishuKnowledgeService.retrieve(question, scopes, db)
|
chunks = FeishuKnowledgeService.retrieve(question, scopes, db)
|
||||||
@@ -74,6 +76,8 @@ class RagService:
|
|||||||
session_summary,
|
session_summary,
|
||||||
summary_up_to_message_id,
|
summary_up_to_message_id,
|
||||||
growth_context=growth_context,
|
growth_context=growth_context,
|
||||||
|
topic_context=topic_context,
|
||||||
|
product_context=product_context,
|
||||||
)
|
)
|
||||||
prompt = PromptService.render_messages(messages)
|
prompt = PromptService.render_messages(messages)
|
||||||
return RagResult(
|
return RagResult(
|
||||||
@@ -101,6 +105,8 @@ class PromptService:
|
|||||||
session_summary: str | None = None,
|
session_summary: str | None = None,
|
||||||
summary_up_to_message_id: int | None = None,
|
summary_up_to_message_id: int | None = None,
|
||||||
growth_context: str | None = None,
|
growth_context: str | None = None,
|
||||||
|
topic_context: str | None = None,
|
||||||
|
product_context: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
return cls.render_messages(
|
return cls.render_messages(
|
||||||
cls.build_messages(
|
cls.build_messages(
|
||||||
@@ -111,6 +117,8 @@ class PromptService:
|
|||||||
session_summary,
|
session_summary,
|
||||||
summary_up_to_message_id,
|
summary_up_to_message_id,
|
||||||
growth_context=growth_context,
|
growth_context=growth_context,
|
||||||
|
topic_context=topic_context,
|
||||||
|
product_context=product_context,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -126,6 +134,8 @@ class PromptService:
|
|||||||
prompt_override: str | None = None,
|
prompt_override: str | None = None,
|
||||||
response_depth: int | None = None,
|
response_depth: int | None = None,
|
||||||
growth_context: str | None = None,
|
growth_context: str | None = None,
|
||||||
|
topic_context: str | None = None,
|
||||||
|
product_context: str | None = None,
|
||||||
) -> list[dict[str, str]]:
|
) -> list[dict[str, str]]:
|
||||||
prompt = prompt_override.strip() if prompt_override and prompt_override.strip() else cls._load_active_prompt(db)
|
prompt = prompt_override.strip() if prompt_override and prompt_override.strip() else cls._load_active_prompt(db)
|
||||||
|
|
||||||
@@ -157,6 +167,10 @@ class PromptService:
|
|||||||
messages.append({"role": "system", "content": f"[历史对话摘要]\n{visible_summary}"})
|
messages.append({"role": "system", "content": f"[历史对话摘要]\n{visible_summary}"})
|
||||||
if growth_context and growth_context.strip():
|
if growth_context and growth_context.strip():
|
||||||
messages.append({"role": "system", "content": growth_context.strip()})
|
messages.append({"role": "system", "content": growth_context.strip()})
|
||||||
|
if topic_context and topic_context.strip():
|
||||||
|
messages.append({"role": "system", "content": topic_context.strip()})
|
||||||
|
if product_context and product_context.strip():
|
||||||
|
messages.append({"role": "system", "content": product_context.strip()})
|
||||||
|
|
||||||
for message in recent_history:
|
for message in recent_history:
|
||||||
content = cls._clean_history_content(message.content)
|
content = cls._clean_history_content(message.content)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from sqlalchemy import extract, func, select
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.models.chat import ChatMessage, ChatSession, TopicSession
|
from app.models.chat import ChatMessage, ChatSession, TopicSession
|
||||||
|
from app.models.growth import TopicSummary
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
|
|
||||||
|
|
||||||
@@ -50,6 +51,12 @@ class TopicSessionService:
|
|||||||
topic = TopicSessionService.active_for_session(db, user=user, session=session)
|
topic = TopicSessionService.active_for_session(db, user=user, session=session)
|
||||||
if topic is not None:
|
if topic is not None:
|
||||||
return topic
|
return topic
|
||||||
|
# A finished topic is a real memory boundary. Its durable summary lives in
|
||||||
|
# TopicSummary / growth profile; the rolling ChatSession summary must start
|
||||||
|
# clean for the next topic in the same chat window.
|
||||||
|
session.summary = None
|
||||||
|
session.summary_up_to_message_id = None
|
||||||
|
db.add(session)
|
||||||
topic = TopicSession(
|
topic = TopicSession(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
chat_session_id=session.id,
|
chat_session_id=session.id,
|
||||||
@@ -104,6 +111,43 @@ class TopicSessionService:
|
|||||||
"updatedAt": topic.updated_at,
|
"updatedAt": topic.updated_at,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def prompt_context(db: Session, topic: TopicSession) -> tuple[str, bool]:
|
||||||
|
"""Build bounded topic context shared by formal chat and admin preview."""
|
||||||
|
summary = db.scalar(
|
||||||
|
select(TopicSummary)
|
||||||
|
.where(TopicSummary.topic_session_id == topic.id)
|
||||||
|
.order_by(TopicSummary.generated_at.desc(), TopicSummary.id.desc())
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
lines = [
|
||||||
|
"[当前对话主题]",
|
||||||
|
"以下信息用于延续当前主题,不能替代本轮可靠知识,也不得据此臆造课程内容。",
|
||||||
|
f"主题:{_limit(topic.title, 240)}",
|
||||||
|
f"核心问题:{_limit(topic.core_question, 1200)}",
|
||||||
|
f"主题状态:{topic.status}",
|
||||||
|
]
|
||||||
|
if topic.recommended_homework:
|
||||||
|
lines.append(f"已记录的建议功课:{_limit(topic.recommended_homework, 1000)}")
|
||||||
|
if summary is not None and summary.status in {"success", "fallback"}:
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
"\n[所选主题摘要]",
|
||||||
|
_limit(summary.summary, 2400),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
details = [
|
||||||
|
("主要事件", summary.main_events),
|
||||||
|
("情绪", summary.emotions),
|
||||||
|
("身体感受", summary.body_feelings),
|
||||||
|
("信念", summary.beliefs),
|
||||||
|
("建议功课", summary.recommended_homework),
|
||||||
|
("下一步观察", summary.next_observation),
|
||||||
|
]
|
||||||
|
lines.extend(f"{label}:{_limit(value, 800)}" for label, value in details if value)
|
||||||
|
return "\n".join(lines), True
|
||||||
|
return "\n".join(lines), False
|
||||||
|
|
||||||
|
|
||||||
def _now() -> datetime:
|
def _now() -> datetime:
|
||||||
return datetime.now(UTC).replace(tzinfo=None)
|
return datetime.now(UTC).replace(tzinfo=None)
|
||||||
@@ -112,3 +156,8 @@ def _now() -> datetime:
|
|||||||
def _title_from_question(question: str) -> str:
|
def _title_from_question(question: str) -> str:
|
||||||
title = question.strip().replace("\n", " ")
|
title = question.strip().replace("\n", " ")
|
||||||
return title[:40] if title else "新主题"
|
return title[:40] if title else "新主题"
|
||||||
|
|
||||||
|
|
||||||
|
def _limit(value: str, limit: int) -> str:
|
||||||
|
text = value.strip()
|
||||||
|
return text if len(text) <= limit else text[:limit].rstrip() + "…"
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ from types import SimpleNamespace
|
|||||||
|
|
||||||
from app.api.admin_agent_records import attention_list, retrieval_logs
|
from app.api.admin_agent_records import attention_list, retrieval_logs
|
||||||
from app.api.admin_records import ai_logs, chat_detail, chat_messages, question_insights, refresh_question_insights
|
from app.api.admin_records import ai_logs, chat_detail, chat_messages, question_insights, refresh_question_insights
|
||||||
from app.api.admin_users import list_users
|
from app.api.admin_users import list_users, user_operation_detail, user_topic_options
|
||||||
from app.models import Base
|
from app.models import Base
|
||||||
from app.models.chat import ChatMessage, ChatSession
|
from app.models.chat import ChatMessage, ChatSession, TopicSession
|
||||||
|
from app.models.growth import TopicSummary
|
||||||
from app.models.insight import QuestionInsightCleanedQuestion
|
from app.models.insight import QuestionInsightCleanedQuestion
|
||||||
from app.models.knowledge import HumanAttentionRecord, KnowledgeRetrievalLog
|
from app.models.knowledge import HumanAttentionRecord, KnowledgeRetrievalLog
|
||||||
from app.models.logs import AiRequestLog
|
from app.models.logs import AiRequestLog
|
||||||
@@ -34,6 +35,44 @@ def test_user_list_uses_database_pagination():
|
|||||||
assert len(response["data"]["items"]) == 10
|
assert len(response["data"]["items"]) == 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_agent_preview_topic_options_are_lightweight_and_paginated():
|
||||||
|
with _database() as db:
|
||||||
|
user = User(id=1, phone="13800000000", name="学员", daily_chat_limit=10)
|
||||||
|
db.add(user)
|
||||||
|
topics = [
|
||||||
|
TopicSession(
|
||||||
|
id=index + 1,
|
||||||
|
user_id=1,
|
||||||
|
chat_session_id=100 + index,
|
||||||
|
title=f"主题{index + 1}",
|
||||||
|
core_question=f"核心问题{index + 1}",
|
||||||
|
message_count=index,
|
||||||
|
)
|
||||||
|
for index in range(12)
|
||||||
|
]
|
||||||
|
db.add_all(topics)
|
||||||
|
db.flush()
|
||||||
|
db.add(TopicSummary(topic_session_id=topics[-1].id, user_id=1, summary="只用于判断有无摘要", status="success"))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
response = user_topic_options(1, keyword="", page=2, pageSize=10, db=db, current_admin=object())
|
||||||
|
|
||||||
|
data = response["data"]
|
||||||
|
assert data["total"] == 12
|
||||||
|
assert data["page"] == 2
|
||||||
|
assert len(data["items"]) == 2
|
||||||
|
assert all("summary" not in item for item in data["items"])
|
||||||
|
assert all("summaryAvailable" in item for item in data["items"])
|
||||||
|
|
||||||
|
detail = user_operation_detail(1, db=db, current_admin=object())["data"]
|
||||||
|
assert detail["user"]["id"] == 1
|
||||||
|
assert detail["metrics"]["totalTopics"] == 12
|
||||||
|
|
||||||
|
searched = user_topic_options(1, keyword="核心问题12", page=1, pageSize=10, db=db, current_admin=object())["data"]
|
||||||
|
assert searched["total"] == 1
|
||||||
|
assert searched["items"][0]["title"] == "主题12"
|
||||||
|
|
||||||
|
|
||||||
def test_ai_log_page_does_not_load_large_detail_fields():
|
def test_ai_log_page_does_not_load_large_detail_fields():
|
||||||
with _database() as db:
|
with _database() as db:
|
||||||
db.add_all([
|
db.add_all([
|
||||||
|
|||||||
@@ -2,13 +2,16 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi import HTTPException
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy.pool import StaticPool
|
from sqlalchemy.pool import StaticPool
|
||||||
|
|
||||||
from app.models import Base
|
from app.models import Base
|
||||||
|
from app.models.chat import ChatMessage, ChatSession, TopicSession
|
||||||
from app.models.entitlement import EntitlementPlan
|
from app.models.entitlement import EntitlementPlan
|
||||||
from app.models.growth import UserGrowthProfile
|
from app.models.growth import TopicSummary, UserGrowthProfile
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.schemas.admin import AgentDebugRequest
|
from app.schemas.admin import AgentDebugRequest
|
||||||
from app.services.agent_debug_service import AgentDebugService
|
from app.services.agent_debug_service import AgentDebugService
|
||||||
@@ -54,4 +57,118 @@ def test_agent_debug_can_simulate_user_growth_profile_context():
|
|||||||
assert "表达障碍" in rendered
|
assert "表达障碍" in rendered
|
||||||
assert result.tool_trace[0]["tool"] == "load_debug_user_context"
|
assert result.tool_trace[0]["tool"] == "load_debug_user_context"
|
||||||
assert result.tool_trace[0]["response"]["growthProfileUsed"] is True
|
assert result.tool_trace[0]["response"]["growthProfileUsed"] is True
|
||||||
|
assert "[当前产品权益]" in rendered
|
||||||
|
|
||||||
|
|
||||||
|
def test_agent_debug_loads_selected_topic_history_summary_and_permissions():
|
||||||
|
with _db() as db:
|
||||||
|
user = User(id=1, phone="13800000001", name="测试学员", daily_chat_limit=100, daily_chat_used=0)
|
||||||
|
session = ChatSession(id=20, user_id=1, title="表达障碍", message_count=2, is_deleted=0)
|
||||||
|
topic = TopicSession(
|
||||||
|
id=30,
|
||||||
|
user_id=1,
|
||||||
|
chat_session_id=20,
|
||||||
|
title="表达障碍练习",
|
||||||
|
core_question="我在面对领导时不敢表达",
|
||||||
|
status="completed",
|
||||||
|
message_count=2,
|
||||||
|
)
|
||||||
|
db.add_all(
|
||||||
|
[
|
||||||
|
user,
|
||||||
|
session,
|
||||||
|
topic,
|
||||||
|
EntitlementPlan(
|
||||||
|
id=10,
|
||||||
|
name="深度陪伴版",
|
||||||
|
plan_type="deep",
|
||||||
|
monthly_topic_limit=90,
|
||||||
|
enable_growth_profile=1,
|
||||||
|
allow_help_card=1,
|
||||||
|
allow_share_draft=0,
|
||||||
|
status=1,
|
||||||
|
),
|
||||||
|
UserGrowthProfile(user_id=1, profile_text="用户在表达时容易身体紧绷。"),
|
||||||
|
ChatMessage(
|
||||||
|
id=100,
|
||||||
|
session_id=20,
|
||||||
|
topic_session_id=30,
|
||||||
|
user_id=1,
|
||||||
|
role="user",
|
||||||
|
content="我又不敢说话了",
|
||||||
|
),
|
||||||
|
ChatMessage(
|
||||||
|
id=101,
|
||||||
|
session_id=20,
|
||||||
|
topic_session_id=30,
|
||||||
|
user_id=1,
|
||||||
|
role="assistant",
|
||||||
|
content="先观察当下的身体感受。",
|
||||||
|
),
|
||||||
|
TopicSummary(
|
||||||
|
topic_session_id=30,
|
||||||
|
user_id=1,
|
||||||
|
summary="学员正在观察面对权威时的紧绷。",
|
||||||
|
emotions="害怕",
|
||||||
|
body_feelings="心口紧",
|
||||||
|
status="success",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
result = asyncio.run(
|
||||||
|
AgentDebugService.build_result(
|
||||||
|
db,
|
||||||
|
AgentDebugRequest(
|
||||||
|
promptContent="你是测试 Agent",
|
||||||
|
modelId=1,
|
||||||
|
userId=1,
|
||||||
|
topicSessionId=30,
|
||||||
|
question="那我现在怎么继续?",
|
||||||
|
history=[{"role": "user", "content": "我想继续刚才的主题"}],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
rendered = "\n".join(item["content"] for item in result.messages)
|
||||||
|
assert "[当前对话主题]" in rendered
|
||||||
|
assert "[所选主题摘要]" in rendered
|
||||||
|
assert "我又不敢说话了" in rendered
|
||||||
|
assert "我想继续刚才的主题" in rendered
|
||||||
|
assert "老师求助卡:可由学员主动生成" in rendered
|
||||||
|
assert "班级分享稿:当前权益不可生成" in rendered
|
||||||
|
context_response = result.tool_trace[0]["response"]
|
||||||
|
assert context_response["topic"]["id"] == 30
|
||||||
|
assert context_response["topic"]["loadedHistoryCount"] == 2
|
||||||
|
assert context_response["topic"]["summaryUsed"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_agent_debug_rejects_topic_from_another_user():
|
||||||
|
with _db() as db:
|
||||||
|
db.add_all(
|
||||||
|
[
|
||||||
|
User(id=1, phone="13800000001", name="学员A", daily_chat_limit=100, daily_chat_used=0),
|
||||||
|
User(id=2, phone="13800000002", name="学员B", daily_chat_limit=100, daily_chat_used=0),
|
||||||
|
ChatSession(id=20, user_id=2, title="B的会话", message_count=0, is_deleted=0),
|
||||||
|
TopicSession(id=30, user_id=2, chat_session_id=20, title="B的主题", core_question="B的问题"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException) as exc:
|
||||||
|
asyncio.run(
|
||||||
|
AgentDebugService.build_result(
|
||||||
|
db,
|
||||||
|
AgentDebugRequest(
|
||||||
|
promptContent="你是测试 Agent",
|
||||||
|
modelId=1,
|
||||||
|
userId=1,
|
||||||
|
topicSessionId=30,
|
||||||
|
question="继续这个主题",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert exc.value.status_code == 400
|
||||||
|
assert exc.value.detail == "所选主题不属于当前模拟学员"
|
||||||
|
|||||||
@@ -4,16 +4,17 @@ from datetime import UTC, datetime
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine, event
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy.pool import StaticPool
|
from sqlalchemy.pool import StaticPool
|
||||||
|
|
||||||
from app.models import Base
|
from app.models import Base
|
||||||
from app.models.chat import ChatSession, TopicSession
|
from app.models.chat import ChatMessage, ChatSession, TopicSession
|
||||||
from app.models.entitlement import EntitlementPlan
|
from app.models.entitlement import EntitlementPlan
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.services.chat_service import ChatService
|
from app.services.chat_service import ChatService
|
||||||
from app.services.entitlement_service import EntitlementService
|
from app.services.entitlement_service import EntitlementService
|
||||||
|
from app.services.rag_service import RagResult, RagService
|
||||||
from app.services.topic_session_service import TopicSessionService
|
from app.services.topic_session_service import TopicSessionService
|
||||||
|
|
||||||
|
|
||||||
@@ -142,3 +143,79 @@ def test_monthly_topic_quota_blocks_new_topic_but_allows_existing_topic():
|
|||||||
db.flush()
|
db.flush()
|
||||||
|
|
||||||
ChatService._ensure_topic_quota(db, user, session, entitlement)
|
ChatService._ensure_topic_quota(db, user, session, entitlement)
|
||||||
|
|
||||||
|
|
||||||
|
def test_formal_chat_passes_topic_and_product_context_to_rag(monkeypatch):
|
||||||
|
with _db() as db:
|
||||||
|
message_sequence = iter(range(1000, 1010))
|
||||||
|
|
||||||
|
def assign_sqlite_message_id(_session, _flush_context, _instances):
|
||||||
|
for instance in _session.new:
|
||||||
|
if isinstance(instance, ChatMessage) and instance.id is None:
|
||||||
|
instance.id = next(message_sequence)
|
||||||
|
|
||||||
|
event.listen(db, "before_flush", assign_sqlite_message_id)
|
||||||
|
user, session = _seed_user_session(db)
|
||||||
|
db.add(
|
||||||
|
EntitlementPlan(
|
||||||
|
id=10,
|
||||||
|
name="基础陪伴版",
|
||||||
|
plan_type="basic",
|
||||||
|
monthly_topic_limit=30,
|
||||||
|
allow_help_card=1,
|
||||||
|
allow_share_draft=0,
|
||||||
|
status=1,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def fake_build_result(_db, _user, question, **kwargs):
|
||||||
|
captured.update(kwargs)
|
||||||
|
return RagResult(
|
||||||
|
question=question,
|
||||||
|
knowledge_scopes=[],
|
||||||
|
chunks=[],
|
||||||
|
prompt="测试 prompt",
|
||||||
|
allow_general_knowledge=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr(RagService, "build_result", fake_build_result)
|
||||||
|
|
||||||
|
ChatService.create_answer(db, user, session.id, "我想继续这个问题")
|
||||||
|
|
||||||
|
assert "[当前对话主题]" in captured["topic_context"]
|
||||||
|
assert "我想继续这个问题" in captured["topic_context"]
|
||||||
|
assert "[当前产品权益]" in captured["product_context"]
|
||||||
|
assert "班级分享稿:当前权益不可生成" in captured["product_context"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_new_topic_resets_previous_topic_rolling_summary():
|
||||||
|
with _db() as db:
|
||||||
|
user, session = _seed_user_session(db)
|
||||||
|
session.summary = "上一个主题的滚动摘要"
|
||||||
|
session.summary_up_to_message_id = 88
|
||||||
|
db.add(
|
||||||
|
TopicSession(
|
||||||
|
id=90,
|
||||||
|
user_id=user.id,
|
||||||
|
chat_session_id=session.id,
|
||||||
|
title="已完成主题",
|
||||||
|
core_question="上一个问题",
|
||||||
|
status="completed",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
topic = TopicSessionService.get_or_create_active(
|
||||||
|
db,
|
||||||
|
user=user,
|
||||||
|
session=session,
|
||||||
|
question="这是一个新主题",
|
||||||
|
deduct_quota=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert topic.id != 90
|
||||||
|
assert topic.status == "active"
|
||||||
|
assert session.summary is None
|
||||||
|
assert session.summary_up_to_message_id is None
|
||||||
|
|||||||
Reference in New Issue
Block a user