feat: 异步沉淀主题摘要和成长档案
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -10,11 +10,12 @@ from app.models import Base
|
||||
from app.models.ai_config import SystemConfig
|
||||
from app.models.chat import ChatMessage, ChatSession, TopicSession
|
||||
from app.models.entitlement import EntitlementPlan
|
||||
from app.models.growth import GrowthProfileRevision, UserGrowthProfile
|
||||
from app.models.growth import GrowthProfileRevision, TopicSummary, UserGrowthProfile
|
||||
from app.models.user import User
|
||||
from app.services.entitlement_service import EntitlementService
|
||||
from app.services.growth_profile_service import GrowthProfileService
|
||||
from app.services.growth_profile_service import GrowthProfileService, _parse_summary_json
|
||||
from app.services.rag_service import PromptService
|
||||
from app.services.topic_settlement_worker import TopicSettlementWorker
|
||||
|
||||
|
||||
def _db() -> Session:
|
||||
@@ -27,7 +28,7 @@ def _now() -> datetime:
|
||||
return datetime.now(UTC).replace(tzinfo=None)
|
||||
|
||||
|
||||
def test_finish_topic_generates_summary_and_updates_growth_profile_for_enabled_plan():
|
||||
def test_finish_topic_enqueues_summary_and_worker_updates_growth_profile_for_enabled_plan():
|
||||
with _db() as db:
|
||||
db.add(SystemConfig(config_key="mock_model_enabled", config_value="true"))
|
||||
db.add(EntitlementPlan(id=10, name="深度陪伴版", plan_type="deep", monthly_topic_limit=90, enable_growth_profile=1, status=1))
|
||||
@@ -58,13 +59,114 @@ def test_finish_topic_generates_summary_and_updates_growth_profile_for_enabled_p
|
||||
result = GrowthProfileService.finish_active_topic(db, user=user, session=session)
|
||||
|
||||
assert result["topic"]["status"] == "completed"
|
||||
assert result["summary"]["summary"]
|
||||
assert result["profile"] is not None
|
||||
assert "最近主题" in result["profile"]["profileText"]
|
||||
assert result["settlementStatus"] == "pending"
|
||||
assert result["summary"]["summary"] == ""
|
||||
assert result["profile"] is None
|
||||
assert db.query(UserGrowthProfile).filter_by(user_id=1).count() == 0
|
||||
|
||||
summary_id = TopicSettlementWorker.claim_next(db, worker_id="test-worker", now=_now())
|
||||
assert summary_id == result["summary"]["id"]
|
||||
completed = TopicSettlementWorker.execute_claimed(db, summary_id=summary_id, worker_id="test-worker")
|
||||
|
||||
assert completed is not None
|
||||
assert completed.status == "success"
|
||||
assert completed.summary
|
||||
profile = db.query(UserGrowthProfile).filter_by(user_id=1).one()
|
||||
assert "最近主题" in profile.profile_text
|
||||
assert db.query(UserGrowthProfile).filter_by(user_id=1).count() == 1
|
||||
assert db.query(GrowthProfileRevision).filter_by(user_id=1).count() == 1
|
||||
|
||||
|
||||
def test_finish_topic_is_idempotent_and_does_not_call_model_in_request(monkeypatch):
|
||||
with _db() as db:
|
||||
now = _now()
|
||||
user = User(id=1, phone="13800000001", name="学员", daily_chat_limit=100, daily_chat_used=0)
|
||||
session = ChatSession(id=1, user_id=1, title="测试", message_count=1, last_message_at=now, is_deleted=0)
|
||||
topic = TopicSession(
|
||||
id=1,
|
||||
user_id=1,
|
||||
chat_session_id=1,
|
||||
title="测试主题",
|
||||
core_question="我想沉淀一下",
|
||||
status="active",
|
||||
message_count=1,
|
||||
started_at=now,
|
||||
)
|
||||
message = ChatMessage(id=1, session_id=1, topic_session_id=1, user_id=1, role="user", content="我想沉淀一下", created_at=now)
|
||||
db.add_all([user, session, topic, message])
|
||||
db.commit()
|
||||
monkeypatch.setattr(
|
||||
"app.services.tracked_generation_service.TrackedGenerationService.generate",
|
||||
lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("结束主题接口不应调用模型")),
|
||||
)
|
||||
|
||||
first = GrowthProfileService.finish_active_topic(db, user=user, session=session)
|
||||
second = GrowthProfileService.finish_active_topic(db, user=user, session=session)
|
||||
|
||||
assert first["summary"]["id"] == second["summary"]["id"]
|
||||
assert first["settlementStatus"] == "pending"
|
||||
assert second["settlementStatus"] == "pending"
|
||||
assert db.query(TopicSummary).count() == 1
|
||||
|
||||
|
||||
def test_failed_topic_settlement_is_retried(monkeypatch):
|
||||
with _db() as db:
|
||||
now = _now()
|
||||
user = User(id=1, phone="13800000001", name="学员", daily_chat_limit=100, daily_chat_used=0)
|
||||
session = ChatSession(id=1, user_id=1, title="测试", message_count=1, last_message_at=now, is_deleted=0)
|
||||
topic = TopicSession(id=1, user_id=1, chat_session_id=1, title="测试", core_question="测试", status="active", started_at=now)
|
||||
message = ChatMessage(id=1, session_id=1, topic_session_id=1, user_id=1, role="user", content="测试", created_at=now)
|
||||
db.add_all([user, session, topic, message])
|
||||
db.commit()
|
||||
result = GrowthProfileService.finish_active_topic(db, user=user, session=session)
|
||||
monkeypatch.setattr(
|
||||
GrowthProfileService,
|
||||
"process_topic_settlement",
|
||||
staticmethod(lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("模型暂时不可用"))),
|
||||
)
|
||||
|
||||
summary_id = TopicSettlementWorker.claim_next(db, worker_id="retry-worker", now=_now())
|
||||
retried = TopicSettlementWorker.execute_claimed(db, summary_id=summary_id, worker_id="retry-worker")
|
||||
|
||||
assert summary_id == result["summary"]["id"]
|
||||
assert retried is not None
|
||||
assert retried.status == "pending"
|
||||
assert retried.attempt_count == 1
|
||||
assert retried.next_run_at is not None
|
||||
assert "模型暂时不可用" in (retried.error_message or "")
|
||||
|
||||
|
||||
def test_stale_topic_settlement_is_recovered_after_restart():
|
||||
with _db() as db:
|
||||
now = _now()
|
||||
user = User(id=1, phone="13800000001", name="学员", daily_chat_limit=100, daily_chat_used=0)
|
||||
session = ChatSession(id=1, user_id=1, title="测试", message_count=1, last_message_at=now, is_deleted=0)
|
||||
topic = TopicSession(id=1, user_id=1, chat_session_id=1, title="测试", core_question="测试", status="completed", started_at=now, ended_at=now)
|
||||
summary = TopicSummary(
|
||||
id=1,
|
||||
topic_session_id=1,
|
||||
user_id=1,
|
||||
summary="",
|
||||
status="running",
|
||||
attempt_count=1,
|
||||
max_attempts=3,
|
||||
locked_by="stopped-worker",
|
||||
locked_at=now - timedelta(minutes=31),
|
||||
)
|
||||
db.add_all([user, session, topic, summary])
|
||||
db.commit()
|
||||
|
||||
recovered = TopicSettlementWorker.recover_stale_jobs(db, now=now)
|
||||
db.commit()
|
||||
db.refresh(summary)
|
||||
|
||||
assert recovered == 1
|
||||
assert summary.status == "pending"
|
||||
assert summary.locked_by is None
|
||||
assert summary.next_run_at == now
|
||||
assert "自动恢复" in (summary.error_message or "")
|
||||
|
||||
|
||||
def test_prompt_can_include_growth_profile_context_without_replacing_knowledge_context():
|
||||
with _db() as db:
|
||||
user = User(id=1, phone="13800000001", name="学员", daily_chat_limit=100, daily_chat_used=0)
|
||||
@@ -84,3 +186,27 @@ def test_prompt_can_include_growth_profile_context_without_replacing_knowledge_c
|
||||
assert "[长期成长档案]" in content
|
||||
assert "表达障碍" in content
|
||||
assert "[本轮可靠知识上下文]" in content
|
||||
|
||||
|
||||
def test_summary_json_parser_ignores_model_reasoning_and_uses_final_json():
|
||||
raw = """
|
||||
<think>
|
||||
先分析一下,并给出一个未完成草稿:
|
||||
```json
|
||||
{"profileText": "不应采用的草稿"}
|
||||
```
|
||||
</think>
|
||||
```json
|
||||
{
|
||||
"profileText": "只保留最终成长档案",
|
||||
"commonEmotions": ["紧张"],
|
||||
"recentProgress": ["开始观察身体感受"]
|
||||
}
|
||||
```
|
||||
"""
|
||||
|
||||
parsed = _parse_summary_json(raw)
|
||||
|
||||
assert parsed is not None
|
||||
assert parsed["profileText"] == "只保留最终成长档案"
|
||||
assert "think" not in str(parsed).lower()
|
||||
|
||||
Reference in New Issue
Block a user