feat: align agent preview with learner topics

This commit is contained in:
2026-07-31 18:21:36 +08:00
parent ab2c945f0b
commit b6aff78aac
18 changed files with 656 additions and 44 deletions

View File

@@ -6,9 +6,10 @@ from types import SimpleNamespace
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_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.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.knowledge import HumanAttentionRecord, KnowledgeRetrievalLog
from app.models.logs import AiRequestLog
@@ -34,6 +35,44 @@ def test_user_list_uses_database_pagination():
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():
with _database() as db:
db.add_all([

View File

@@ -2,13 +2,16 @@ from __future__ import annotations
import asyncio
import pytest
from fastapi import HTTPException
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool
from app.models import Base
from app.models.chat import ChatMessage, ChatSession, TopicSession
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.schemas.admin import AgentDebugRequest
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 result.tool_trace[0]["tool"] == "load_debug_user_context"
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 == "所选主题不属于当前模拟学员"

View File

@@ -4,16 +4,17 @@ from datetime import UTC, datetime
import pytest
from fastapi import HTTPException
from sqlalchemy import create_engine
from sqlalchemy import create_engine, event
from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool
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.user import User
from app.services.chat_service import ChatService
from app.services.entitlement_service import EntitlementService
from app.services.rag_service import RagResult, RagService
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()
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