feat: align agent preview with learner topics
This commit is contained in:
@@ -9,7 +9,7 @@ from fastapi.responses import StreamingResponse
|
||||
from openpyxl import Workbook, load_workbook
|
||||
from pydantic import BaseModel
|
||||
from pydantic import ValidationError
|
||||
from sqlalchemy import extract, func, select
|
||||
from sqlalchemy import extract, func, or_, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
@@ -267,11 +267,49 @@ def user_operation_detail(
|
||||
"recentTopics": _recent_topics(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)],
|
||||
"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")
|
||||
def user_reports(
|
||||
user_id: int,
|
||||
|
||||
@@ -126,6 +126,7 @@ class AgentDebugRequest(BaseModel):
|
||||
promptContent: str = Field(min_length=1)
|
||||
modelId: int = Field(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)
|
||||
knowledgeVersions: dict[int, int] = Field(default_factory=dict)
|
||||
question: str = Field(min_length=1, max_length=2000)
|
||||
|
||||
@@ -5,14 +5,18 @@ from collections.abc import AsyncIterator
|
||||
from dataclasses import replace
|
||||
from types import SimpleNamespace
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.chat import ChatMessage, ChatSession, TopicSession
|
||||
from app.models.user import User
|
||||
from app.models.admin import Admin
|
||||
from app.models.ai_config import ModelConfig
|
||||
from app.schemas.admin import AgentDebugRequest
|
||||
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.knowledge_agent_service import KnowledgeAgentService
|
||||
from app.services.model_stream_service import ModelStreamService
|
||||
@@ -25,17 +29,21 @@ from app.services.topic_session_service import TopicSessionService
|
||||
class AgentDebugService:
|
||||
@staticmethod
|
||||
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
|
||||
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(
|
||||
db,
|
||||
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,
|
||||
preview_knowledge_ids=preview_knowledge_ids,
|
||||
user_id=payload.userId,
|
||||
@@ -43,6 +51,8 @@ class AgentDebugService:
|
||||
prompt_override=payload.promptContent,
|
||||
response_depth=payload.responseDepth,
|
||||
growth_context=debug_context["growth_context"],
|
||||
topic_context=debug_context["topic_context"],
|
||||
product_context=debug_context["product_context"],
|
||||
)
|
||||
return RagResult(
|
||||
question=rag_result.question,
|
||||
@@ -56,13 +66,30 @@ class AgentDebugService:
|
||||
)
|
||||
|
||||
@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:
|
||||
return {"growth_context": None, "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": [],
|
||||
}
|
||||
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": [
|
||||
{
|
||||
"tool": "load_debug_user_context",
|
||||
@@ -81,13 +108,55 @@ class AgentDebugService:
|
||||
monthly_topic_used=TopicSessionService.monthly_used_count(db, user.id),
|
||||
)
|
||||
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 {
|
||||
"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": [
|
||||
{
|
||||
"tool": "load_debug_user_context",
|
||||
"order": 1,
|
||||
"request": {"userId": user.id},
|
||||
"request": {"userId": user.id, "topicSessionId": topic_session_id},
|
||||
"status": "success",
|
||||
"durationMs": 0,
|
||||
"response": {
|
||||
@@ -95,6 +164,16 @@ class AgentDebugService:
|
||||
"userName": user.name,
|
||||
"entitlement": entitlement_dict(entitlement),
|
||||
"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.user import User
|
||||
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.growth_profile_service import GrowthProfileService
|
||||
from app.services.chat_context_service import ChatContextService
|
||||
@@ -117,6 +117,7 @@ class ChatService:
|
||||
select(ChatMessage)
|
||||
.where(
|
||||
ChatMessage.session_id == session.id,
|
||||
ChatMessage.topic_session_id == topic.id,
|
||||
ChatMessage.user_id == user.id,
|
||||
ChatMessage.id < user_message.id,
|
||||
)
|
||||
@@ -130,6 +131,27 @@ class ChatService:
|
||||
if growth_context:
|
||||
context_trace = list(context_trace or [])
|
||||
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(传入历史 + 摘要)
|
||||
rag_result = RagService.build_result(
|
||||
@@ -139,6 +161,8 @@ class ChatService:
|
||||
summary_up_to_message_id=session.summary_up_to_message_id,
|
||||
context_trace=context_trace,
|
||||
growth_context=growth_context,
|
||||
topic_context=topic_context,
|
||||
product_context=product_context,
|
||||
)
|
||||
completion = ModelClientService.complete(db, rag_result)
|
||||
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.chat_service import ChatService, _title_from_question
|
||||
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.growth_profile_service import GrowthProfileService
|
||||
from app.services.human_attention_service import HumanAttentionService
|
||||
@@ -67,6 +67,7 @@ class ChatStreamService:
|
||||
select(ChatMessage)
|
||||
.where(
|
||||
ChatMessage.session_id == session.id,
|
||||
ChatMessage.topic_session_id == topic.id,
|
||||
ChatMessage.user_id == user.id,
|
||||
ChatMessage.id < user_message.id,
|
||||
)
|
||||
@@ -79,6 +80,14 @@ class ChatStreamService:
|
||||
if growth_context:
|
||||
context_trace = list(context_trace or [])
|
||||
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()
|
||||
rag_result = None
|
||||
@@ -95,6 +104,8 @@ class ChatStreamService:
|
||||
getattr(session, "summary_up_to_message_id", None),
|
||||
context_trace,
|
||||
growth_context,
|
||||
topic_context,
|
||||
product_context,
|
||||
)
|
||||
model_response = ModelStreamService.stream(db, rag_result)
|
||||
for chunk in model_response.chunks:
|
||||
@@ -239,6 +250,7 @@ class ChatStreamService:
|
||||
select(ChatMessage)
|
||||
.where(
|
||||
ChatMessage.session_id == session.id,
|
||||
ChatMessage.topic_session_id == topic.id,
|
||||
ChatMessage.user_id == user.id,
|
||||
ChatMessage.id < user_message.id,
|
||||
)
|
||||
@@ -251,6 +263,14 @@ class ChatStreamService:
|
||||
if growth_context:
|
||||
context_trace = list(context_trace or [])
|
||||
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()
|
||||
rag_result = None
|
||||
@@ -268,6 +288,8 @@ class ChatStreamService:
|
||||
session_id=session.id,
|
||||
context_trace=context_trace,
|
||||
growth_context=growth_context,
|
||||
topic_context=topic_context,
|
||||
product_context=product_context,
|
||||
)
|
||||
model_response = ModelStreamService.stream_async(db, rag_result)
|
||||
async for chunk in model_response.chunks:
|
||||
@@ -485,6 +507,8 @@ def _build_rag_result(
|
||||
summary_up_to_message_id: int | None,
|
||||
context_trace: list[dict] | None = None,
|
||||
growth_context: str | None = None,
|
||||
topic_context: str | None = None,
|
||||
product_context: str | None = None,
|
||||
):
|
||||
parameters = signature(RagService.build_result).parameters
|
||||
if "history" in parameters:
|
||||
@@ -497,5 +521,30 @@ def _build_rag_result(
|
||||
summary_up_to_message_id=summary_up_to_message_id,
|
||||
context_trace=context_trace,
|
||||
growth_context=growth_context,
|
||||
topic_context=topic_context,
|
||||
product_context=product_context,
|
||||
)
|
||||
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(
|
||||
plan: EntitlementPlan,
|
||||
*,
|
||||
|
||||
@@ -86,6 +86,8 @@ class KnowledgeAgentService:
|
||||
prompt_override: str | None = None,
|
||||
response_depth: int | None = None,
|
||||
growth_context: str | None = None,
|
||||
topic_context: str | None = None,
|
||||
product_context: str | None = None,
|
||||
) -> RagResult:
|
||||
started = perf_counter()
|
||||
catalog = cls.get_knowledge_catalog(
|
||||
@@ -211,6 +213,8 @@ class KnowledgeAgentService:
|
||||
prompt_override=prompt_override,
|
||||
response_depth=response_depth,
|
||||
growth_context=growth_context,
|
||||
topic_context=topic_context,
|
||||
product_context=product_context,
|
||||
)
|
||||
return RagResult(
|
||||
question=question,
|
||||
|
||||
@@ -20,6 +20,8 @@ class AsyncRagService:
|
||||
session_id: int | None = None,
|
||||
context_trace: list[dict] | None = None,
|
||||
growth_context: str | None = None,
|
||||
topic_context: str | None = None,
|
||||
product_context: str | None = None,
|
||||
) -> RagResult:
|
||||
return await KnowledgeAgentService.build_result(
|
||||
db,
|
||||
@@ -31,4 +33,6 @@ class AsyncRagService:
|
||||
user_id=user.id,
|
||||
context_trace=context_trace,
|
||||
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,
|
||||
context_trace: list[dict] | None = None,
|
||||
growth_context: str | None = None,
|
||||
topic_context: str | None = None,
|
||||
product_context: str | None = None,
|
||||
) -> RagResult:
|
||||
scopes = KnowledgeAccessService.get_allowed_knowledge(db, user)
|
||||
chunks = FeishuKnowledgeService.retrieve(question, scopes, db)
|
||||
@@ -74,6 +76,8 @@ class RagService:
|
||||
session_summary,
|
||||
summary_up_to_message_id,
|
||||
growth_context=growth_context,
|
||||
topic_context=topic_context,
|
||||
product_context=product_context,
|
||||
)
|
||||
prompt = PromptService.render_messages(messages)
|
||||
return RagResult(
|
||||
@@ -101,6 +105,8 @@ class PromptService:
|
||||
session_summary: str | None = None,
|
||||
summary_up_to_message_id: int | None = None,
|
||||
growth_context: str | None = None,
|
||||
topic_context: str | None = None,
|
||||
product_context: str | None = None,
|
||||
) -> str:
|
||||
return cls.render_messages(
|
||||
cls.build_messages(
|
||||
@@ -111,6 +117,8 @@ class PromptService:
|
||||
session_summary,
|
||||
summary_up_to_message_id,
|
||||
growth_context=growth_context,
|
||||
topic_context=topic_context,
|
||||
product_context=product_context,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -126,6 +134,8 @@ class PromptService:
|
||||
prompt_override: str | None = None,
|
||||
response_depth: int | None = None,
|
||||
growth_context: str | None = None,
|
||||
topic_context: str | None = None,
|
||||
product_context: str | None = None,
|
||||
) -> list[dict[str, str]]:
|
||||
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}"})
|
||||
if growth_context and 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:
|
||||
content = cls._clean_history_content(message.content)
|
||||
|
||||
@@ -6,6 +6,7 @@ from sqlalchemy import extract, func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.chat import ChatMessage, ChatSession, TopicSession
|
||||
from app.models.growth import TopicSummary
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
@@ -50,6 +51,12 @@ class TopicSessionService:
|
||||
topic = TopicSessionService.active_for_session(db, user=user, session=session)
|
||||
if topic is not None:
|
||||
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(
|
||||
user_id=user.id,
|
||||
chat_session_id=session.id,
|
||||
@@ -104,6 +111,43 @@ class TopicSessionService:
|
||||
"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:
|
||||
return datetime.now(UTC).replace(tzinfo=None)
|
||||
@@ -112,3 +156,8 @@ def _now() -> datetime:
|
||||
def _title_from_question(question: str) -> str:
|
||||
title = question.strip().replace("\n", " ")
|
||||
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() + "…"
|
||||
|
||||
Reference in New Issue
Block a user