feat: align agent preview user context
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 { AgentRuntimeConfig, KnowledgeItem, ModelItem, PromptDetail, PromptHistoryItem } from "../types/api";
|
import type { AdminUser, AgentRuntimeConfig, KnowledgeItem, ModelItem, PromptDetail, PromptHistoryItem } 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";
|
||||||
@@ -25,6 +25,8 @@ const promptContent = ref("");
|
|||||||
const savedPromptContent = ref("");
|
const savedPromptContent = ref("");
|
||||||
const models = ref<ModelItem[]>([]);
|
const models = ref<ModelItem[]>([]);
|
||||||
const knowledge = ref<KnowledgeItem[]>([]);
|
const knowledge = ref<KnowledgeItem[]>([]);
|
||||||
|
const debugUsers = ref<AdminUser[]>([]);
|
||||||
|
const debugUserLoading = 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);
|
||||||
@@ -43,6 +45,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,
|
||||||
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,
|
||||||
@@ -80,6 +83,11 @@ const selectedKnowledgeSummary = computed(() => {
|
|||||||
}
|
}
|
||||||
return `${agentForm.knowledgeIds.length} 个知识库`;
|
return `${agentForm.knowledgeIds.length} 个知识库`;
|
||||||
});
|
});
|
||||||
|
const selectedDebugUserLabel = computed(() => {
|
||||||
|
if (!agentForm.userId) return "未模拟学员";
|
||||||
|
const user = debugUsers.value.find((item) => item.id === agentForm.userId);
|
||||||
|
return user ? `${user.name || user.nickname || user.phone} · #${user.id}` : `学员 #${agentForm.userId}`;
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(load);
|
onMounted(load);
|
||||||
|
|
||||||
@@ -103,6 +111,7 @@ async function load() {
|
|||||||
.filter((item) => item.status === 1 && item.lifecycleStatus === "active")
|
.filter((item) => item.status === 1 && item.lifecycleStatus === "active")
|
||||||
.map((item) => item.id);
|
.map((item) => item.id);
|
||||||
if (props.previewKnowledgeId) applyPreviewKnowledge(props.previewKnowledgeId);
|
if (props.previewKnowledgeId) applyPreviewKnowledge(props.previewKnowledgeId);
|
||||||
|
void searchDebugUsers("");
|
||||||
await loadHistory();
|
await loadHistory();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(errorMessage(error, "Agent 配置加载失败"));
|
ElMessage.error(errorMessage(error, "Agent 配置加载失败"));
|
||||||
@@ -111,6 +120,18 @@ async function load() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function searchDebugUsers(keyword: string) {
|
||||||
|
debugUserLoading.value = true;
|
||||||
|
try {
|
||||||
|
const result = await api.users({ keyword, page: 1, pageSize: 20 });
|
||||||
|
debugUsers.value = result.items;
|
||||||
|
} catch {
|
||||||
|
debugUsers.value = [];
|
||||||
|
} finally {
|
||||||
|
debugUserLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function applyRuntimeConfig(value: AgentRuntimeConfig) {
|
function applyRuntimeConfig(value: AgentRuntimeConfig) {
|
||||||
runtimeConfig.value = value;
|
runtimeConfig.value = value;
|
||||||
Object.assign(runtimeForm, {
|
Object.assign(runtimeForm, {
|
||||||
@@ -299,6 +320,7 @@ async function debugAgent() {
|
|||||||
{
|
{
|
||||||
promptContent: promptContent.value,
|
promptContent: promptContent.value,
|
||||||
modelId: agentForm.modelId,
|
modelId: agentForm.modelId,
|
||||||
|
userId: agentForm.userId || null,
|
||||||
knowledgeIds: agentForm.knowledgeIds,
|
knowledgeIds: agentForm.knowledgeIds,
|
||||||
question,
|
question,
|
||||||
history: conversationHistory,
|
history: conversationHistory,
|
||||||
@@ -473,6 +495,25 @@ 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>
|
||||||
|
<el-form-item label="模拟学员(可选)">
|
||||||
|
<el-select
|
||||||
|
v-model="agentForm.userId"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
remote
|
||||||
|
:remote-method="searchDebugUsers"
|
||||||
|
:loading="debugUserLoading"
|
||||||
|
placeholder="不选择则不注入学员权益和成长档案"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="user in debugUsers"
|
||||||
|
:key="user.id"
|
||||||
|
:label="`${user.name || user.nickname || user.phone} · ${user.phone} · #${user.id}`"
|
||||||
|
:value="user.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<div class="agent-form-help">用于让后台预览加载该学员的权益、主题额度和长期成长档案;不会消耗额度,也不会影响正式会话。</div>
|
||||||
|
</el-form-item>
|
||||||
<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" />
|
||||||
@@ -546,7 +587,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 }}</small></div>
|
<div><h3>调试预览</h3><small>{{ selectedKnowledgeSummary }} · {{ selectedDebugUserLabel }}</small></div>
|
||||||
<div class="agent-preview-head-actions"><span>{{ selectedModelName }}</span><el-button link :disabled="agentDebugging" @click="clearAgentPreview">清空</el-button></div>
|
<div class="agent-preview-head-actions"><span>{{ 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">
|
||||||
|
|||||||
@@ -1368,6 +1368,13 @@ textarea {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.agent-form-help {
|
||||||
|
margin-top: 6px;
|
||||||
|
color: #7b8d86;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
.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; }
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ class AgentDebugHistoryMessage(BaseModel):
|
|||||||
class AgentDebugRequest(BaseModel):
|
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)
|
||||||
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)
|
||||||
|
|||||||
@@ -6,14 +6,18 @@ from types import SimpleNamespace
|
|||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
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.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
|
||||||
from app.services.reasoning_policy_service import ReasoningPolicyService
|
from app.services.reasoning_policy_service import ReasoningPolicyService
|
||||||
from app.services.rag_service import RagResult
|
from app.services.rag_service import RagResult
|
||||||
|
from app.services.topic_session_service import TopicSessionService
|
||||||
|
|
||||||
|
|
||||||
class AgentDebugService:
|
class AgentDebugService:
|
||||||
@@ -25,14 +29,18 @@ class AgentDebugService:
|
|||||||
]
|
]
|
||||||
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)
|
||||||
rag_result = await KnowledgeAgentService.build_result(
|
rag_result = await KnowledgeAgentService.build_result(
|
||||||
db,
|
db,
|
||||||
question=payload.question,
|
question=payload.question,
|
||||||
history=history,
|
history=history,
|
||||||
version_overrides=version_overrides,
|
version_overrides=version_overrides,
|
||||||
preview_knowledge_ids=preview_knowledge_ids,
|
preview_knowledge_ids=preview_knowledge_ids,
|
||||||
|
user_id=payload.userId,
|
||||||
|
context_trace=debug_context["trace"],
|
||||||
prompt_override=payload.promptContent,
|
prompt_override=payload.promptContent,
|
||||||
response_depth=payload.responseDepth,
|
response_depth=payload.responseDepth,
|
||||||
|
growth_context=debug_context["growth_context"],
|
||||||
)
|
)
|
||||||
return RagResult(
|
return RagResult(
|
||||||
question=rag_result.question,
|
question=rag_result.question,
|
||||||
@@ -45,6 +53,51 @@ class AgentDebugService:
|
|||||||
messages=rag_result.messages,
|
messages=rag_result.messages,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _debug_user_context(db: Session, user_id: int | None) -> dict:
|
||||||
|
if user_id is None:
|
||||||
|
return {"growth_context": None, "trace": []}
|
||||||
|
user = db.get(User, user_id)
|
||||||
|
if user is None or user.is_deleted:
|
||||||
|
return {
|
||||||
|
"growth_context": None,
|
||||||
|
"trace": [
|
||||||
|
{
|
||||||
|
"tool": "load_debug_user_context",
|
||||||
|
"order": 1,
|
||||||
|
"request": {"userId": user_id},
|
||||||
|
"status": "failed",
|
||||||
|
"durationMs": 0,
|
||||||
|
"response": None,
|
||||||
|
"error": "模拟学员不存在或已删除",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
entitlement = EntitlementService.active_entitlement(
|
||||||
|
db,
|
||||||
|
user,
|
||||||
|
monthly_topic_used=TopicSessionService.monthly_used_count(db, user.id),
|
||||||
|
)
|
||||||
|
growth_context = GrowthProfileService.prompt_context(db, user) if entitlement.enable_growth_profile else None
|
||||||
|
return {
|
||||||
|
"growth_context": growth_context,
|
||||||
|
"trace": [
|
||||||
|
{
|
||||||
|
"tool": "load_debug_user_context",
|
||||||
|
"order": 1,
|
||||||
|
"request": {"userId": user.id},
|
||||||
|
"status": "success",
|
||||||
|
"durationMs": 0,
|
||||||
|
"response": {
|
||||||
|
"userId": user.id,
|
||||||
|
"userName": user.name,
|
||||||
|
"entitlement": entitlement_dict(entitlement),
|
||||||
|
"growthProfileUsed": bool(growth_context),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def overrides(payload: AgentDebugRequest) -> dict:
|
def overrides(payload: AgentDebugRequest) -> dict:
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy.pool import StaticPool
|
||||||
|
|
||||||
|
from app.models import Base
|
||||||
|
from app.models.entitlement import EntitlementPlan
|
||||||
|
from app.models.growth import UserGrowthProfile
|
||||||
|
from app.models.user import User
|
||||||
|
from app.schemas.admin import AgentDebugRequest
|
||||||
|
from app.services.agent_debug_service import AgentDebugService
|
||||||
|
|
||||||
|
|
||||||
|
def _db() -> Session:
|
||||||
|
engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False}, poolclass=StaticPool)
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
return Session(engine)
|
||||||
|
|
||||||
|
|
||||||
|
def test_agent_debug_can_simulate_user_growth_profile_context():
|
||||||
|
with _db() as db:
|
||||||
|
user = User(id=1, phone="13800000001", name="测试学员", daily_chat_limit=100, daily_chat_used=0)
|
||||||
|
db.add(user)
|
||||||
|
db.add(
|
||||||
|
EntitlementPlan(
|
||||||
|
id=10,
|
||||||
|
name="深度陪伴版",
|
||||||
|
plan_type="deep",
|
||||||
|
monthly_topic_limit=90,
|
||||||
|
enable_growth_profile=1,
|
||||||
|
status=1,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
db.add(UserGrowthProfile(user_id=1, profile_text="用户在表达障碍主题上反复出现身体紧绷。"))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
result = asyncio.run(
|
||||||
|
AgentDebugService.build_result(
|
||||||
|
db,
|
||||||
|
AgentDebugRequest(
|
||||||
|
promptContent="你是测试 Agent",
|
||||||
|
modelId=1,
|
||||||
|
userId=1,
|
||||||
|
question="我又表达不出来了怎么办",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
rendered = "\n".join(item["content"] for item in result.messages)
|
||||||
|
assert "[长期成长档案]" in rendered
|
||||||
|
assert "表达障碍" in rendered
|
||||||
|
assert result.tool_trace[0]["tool"] == "load_debug_user_context"
|
||||||
|
assert result.tool_trace[0]["response"]["growthProfileUsed"] is True
|
||||||
|
|
||||||
@@ -320,6 +320,8 @@ Agent 已经可以:
|
|||||||
- 使用上下文摘要;
|
- 使用上下文摘要;
|
||||||
- 记录检索日志;
|
- 记录检索日志;
|
||||||
- 支持后台预览。
|
- 支持后台预览。
|
||||||
|
- 用户端正式聊天已按权益决定是否注入成长档案;
|
||||||
|
- 后台 Agent 预览已支持选择“模拟学员”,用于注入该学员权益和成长档案上下文。
|
||||||
|
|
||||||
但后续要让 Agent 区分:
|
但后续要让 Agent 区分:
|
||||||
|
|
||||||
@@ -353,6 +355,11 @@ Agent 已经可以:
|
|||||||
- 基础版不会调用长期成长档案;
|
- 基础版不会调用长期成长档案;
|
||||||
- 生成日志能看出本次用了哪些上下文。
|
- 生成日志能看出本次用了哪些上下文。
|
||||||
|
|
||||||
|
#### 开发进度
|
||||||
|
|
||||||
|
- 2026-07-31:已补齐后台预览的“模拟学员”入口;选择学员后,预览链路会加载该学员权益、主题额度使用量和长期成长档案,并在检索追踪中记录 `load_debug_user_context`。默认不选择学员时,保持原后台调试行为。
|
||||||
|
- 待继续:后台预览模拟具体主题上下文、求助卡/分享稿权限注入。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 05. 老师求助卡
|
### 05. 老师求助卡
|
||||||
@@ -905,4 +912,3 @@ AI 日志增加:
|
|||||||
1. 先只建权益版本和用户权益,不立刻改完整扣费;
|
1. 先只建权益版本和用户权益,不立刻改完整扣费;
|
||||||
2. 再建主题会话,只做记录不做复杂自动判断;
|
2. 再建主题会话,只做记录不做复杂自动判断;
|
||||||
3. 最后把主题会话接入权益扣减和成长档案。
|
3. 最后把主题会话接入权益扣减和成长档案。
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user