mirror of
https://github.com/dataelement/bisheng.git
synced 2026-08-31 02:05:43 +08:00
feat(feedback): unify task-mode 点赞/点踩 on the task-result ChatMessage
The linsight task result is already persisted as a category="task" ChatMessage
in the unified daily conversation, so rate it through the shared chatmessage
feedback (/liked + /chat/comment) instead of a linsight_session_version-specific
column/endpoint. All four AI Q&A surfaces now share one storage + rollup path.
Backend:
- chat_helpers: agent history formatter returns liked + remark (fixes daily +
in-conversation task-turn highlight on reload)
- workstation_schema: WorkstationMessage carries liked + remark (channel surface)
- linsight utils: get_task_feedback_by_version maps session_version -> its task
ChatMessage {message_id, liked}
- linsight endpoint: session-version-list enriches each version with message_id +
liked for the standalone linsight page
Frontend:
- ResultPanel: rate by messageId via likeChatApi / disLikeCommentApi
- TaskTurnPanel forwards messageId + liked; AiMessageBubble passes the task row's
message.messageId (hidden on the read-only share view); ExecutionFlow reads them
from the enriched session-version store field
- drop the now-unused likeLinsightVersion / commentLinsightVersion
This commit is contained in:
@@ -537,7 +537,18 @@ async def get_linsight_session_version_list(
|
||||
model for model in linsight_session_version_models if model.id == shared_version_id
|
||||
]
|
||||
|
||||
return resp_200([model.model_dump() for model in linsight_session_version_models])
|
||||
# Unified like/dislike via ChatMessage: the task result is itself a
|
||||
# category="task" ChatMessage and the verdict lives on that row. Attach each
|
||||
# version's linked task message_id + liked so the standalone linsight page can
|
||||
# rate through the shared /liked endpoint and re-highlight on reload (same as
|
||||
# the in-conversation task turn).
|
||||
version_dumps = [model.model_dump() for model in linsight_session_version_models]
|
||||
feedback_map = await linsight_execute_utils.get_task_feedback_by_version(session_id)
|
||||
for dump in version_dumps:
|
||||
info = feedback_map.get(dump.get("id"))
|
||||
dump["message_id"] = info["message_id"] if info else None
|
||||
dump["liked"] = info["liked"] if info else 0
|
||||
return resp_200(version_dumps)
|
||||
|
||||
|
||||
# Get task execution details
|
||||
|
||||
@@ -387,6 +387,32 @@ async def persist_task_turn_message(session_model: LinsightSessionVersion) -> Ch
|
||||
)
|
||||
|
||||
|
||||
async def get_task_feedback_by_version(session_id: str) -> dict[str, dict]:
|
||||
"""Map each linsight session_version id -> its task ChatMessage feedback.
|
||||
|
||||
The task result is a bot ``ChatMessage`` (``category="task"``) in the
|
||||
conversation ``session_id`` carrying ``extra.linsight_session_version_id``.
|
||||
The like/dislike verdict is stored on that ChatMessage row (unified with
|
||||
daily / knowledge / channel), so the standalone linsight page rates and
|
||||
echoes the highlight via the shared chatmessage feedback instead of a
|
||||
linsight-specific column.
|
||||
|
||||
Returns ``{session_version_id: {"message_id": int, "liked": int}}``.
|
||||
"""
|
||||
rows = await ChatMessageDao.aget_messages_by_chat_id(chat_id=session_id, category_list=["task"], limit=1000)
|
||||
result: dict[str, dict] = {}
|
||||
for row in rows:
|
||||
if not row.is_bot:
|
||||
continue
|
||||
try:
|
||||
svid = json.loads(row.extra or "{}").get("linsight_session_version_id")
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
svid = None
|
||||
if svid:
|
||||
result[svid] = {"message_id": row.id, "liked": row.liked or 0}
|
||||
return result
|
||||
|
||||
|
||||
async def persist_task_user_turn(chat_id: str, user_id: int, question: str, files: list | None = None) -> ChatMessage:
|
||||
"""F035 Track J (TJ-3): persist the task user turn into the unified conversation.
|
||||
|
||||
|
||||
@@ -31,6 +31,10 @@ class WorkstationMessage(BaseModel):
|
||||
# to the execution detail for lazy-loading. Both absent on normal turns.
|
||||
category: str | None = None
|
||||
linsightSessionVersionId: str | None = None
|
||||
# like/dislike echo: 0 none / 1 up / 2 down, plus the dislike reason. The
|
||||
# frontend re-highlights the rated state on reload.
|
||||
liked: int | None = None
|
||||
remark: str | None = None
|
||||
|
||||
@field_validator("messageId", mode="before")
|
||||
@classmethod
|
||||
@@ -70,6 +74,8 @@ class WorkstationMessage(BaseModel):
|
||||
source=message.source,
|
||||
category=message.category,
|
||||
linsightSessionVersionId=extra.get("linsight_session_version_id"),
|
||||
liked=message.liked,
|
||||
remark=message.remark,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -219,6 +219,11 @@ def _message_base_fields(msg: ChatMessage) -> dict:
|
||||
"flow_id": msg.flow_id,
|
||||
"source": msg.source,
|
||||
"sender": msg.sender,
|
||||
# like/dislike echo: 0 none / 1 up / 2 down, plus the dislike reason. The
|
||||
# frontend re-highlights the rated state on reload. Daily answers and task
|
||||
# results are both ChatMessage rows, so they share this one path.
|
||||
"liked": msg.liked,
|
||||
"remark": msg.remark,
|
||||
"create_time": msg.create_time.isoformat() if msg.create_time else None,
|
||||
}
|
||||
|
||||
|
||||
@@ -48,23 +48,6 @@ export function startLinsight(versionId: string): Promise<any> {
|
||||
});
|
||||
}
|
||||
|
||||
// 灵思任务结果点赞/点踩(liked: 0 未评 / 1 赞 / 2 踩)。
|
||||
// 落库到 linsight_session_version.liked,并汇总进 message_session(后端待接入,见 PRD)。
|
||||
export function likeLinsightVersion(versionId: string, liked: number): Promise<any> {
|
||||
return request.post('/api/v1/linsight/workbench/feedback', {
|
||||
session_version_id: versionId,
|
||||
liked
|
||||
});
|
||||
}
|
||||
|
||||
// 灵思点踩原因,落库到 linsight_session_version.execute_feedback(后端待接入)。
|
||||
export function commentLinsightVersion(versionId: string, comment: string): Promise<any> {
|
||||
return request.post('/api/v1/linsight/workbench/feedback', {
|
||||
session_version_id: versionId,
|
||||
comment
|
||||
});
|
||||
}
|
||||
|
||||
// F035 多轮对话:在已完成的同一会话里追加新一轮(复用同一 session_version + agent thread,保留上下文)
|
||||
export function continueLinsight(session_version_id: string, question: string): Promise<any> {
|
||||
return request.post('/api/v1/linsight/workbench/continue', {
|
||||
|
||||
@@ -603,6 +603,8 @@ function AssistantBubble({
|
||||
<div className={cn("min-w-0", knowledgeChatLayout ? "w-full max-w-none" : "max-w-[80%]")}>
|
||||
<TaskTurnPanel
|
||||
versionId={message.linsightSessionVersionId || ""}
|
||||
messageId={allowFeedback ? message.messageId : undefined}
|
||||
liked={message.liked}
|
||||
conversationId={message.conversationId}
|
||||
answer={message.text}
|
||||
onPreviewFile={onPreviewFile}
|
||||
|
||||
@@ -231,7 +231,7 @@ export function ExecutionFlow({ versionId, conversationId, isSharePage = false,
|
||||
card — lifted into the terminal ResultPanel (peak-end). ── */}
|
||||
{completed && (
|
||||
<ResultPanel
|
||||
versionId={versionId}
|
||||
messageId={linsight?.message_id ?? undefined}
|
||||
liked={linsight?.liked ?? undefined}
|
||||
allowFeedback={!readOnly && !isSharePage}
|
||||
>
|
||||
|
||||
@@ -12,21 +12,23 @@ import { Outlined } from 'bisheng-icons';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { MessageFeedbackButtons } from '~/components/Chat/MessageFeedbackButtons';
|
||||
import { likeLinsightVersion, commentLinsightVersion } from '~/api/linsight';
|
||||
import { likeChatApi, disLikeCommentApi } from '~/api/apps';
|
||||
import { INK } from './execTokens';
|
||||
|
||||
interface ResultPanelProps {
|
||||
/** the terminal deliverable (typically <ResultSection />) */
|
||||
children: ReactNode;
|
||||
/** linsight session_version id — the feedback target */
|
||||
versionId?: string;
|
||||
/** persisted 点赞/点踩 verdict: 0 none / 1 up / 2 down */
|
||||
/** task-result ChatMessage id — the like/dislike target. The task result is a
|
||||
category="task" ChatMessage, so feedback reuses the shared /liked +
|
||||
/chat/comment endpoints (same as daily / knowledge / channel). */
|
||||
messageId?: string;
|
||||
/** persisted like/dislike verdict: 0 none / 1 up / 2 down */
|
||||
liked?: number;
|
||||
/** show 点赞/点踩 (off for read-only / share view) */
|
||||
/** show like/dislike (off for read-only / share view) */
|
||||
allowFeedback?: boolean;
|
||||
}
|
||||
|
||||
export function ResultPanel({ children, versionId, liked, allowFeedback }: ResultPanelProps) {
|
||||
export function ResultPanel({ children, messageId, liked, allowFeedback }: ResultPanelProps) {
|
||||
const localize = useLocalize();
|
||||
// peak-end (§2.6): a DoubleCheck Ink "task completed" header marks the
|
||||
// terminal state and lifts the deliverable out of the homogeneous flow; body
|
||||
@@ -45,15 +47,15 @@ export function ResultPanel({ children, versionId, liked, allowFeedback }: Resul
|
||||
</span>
|
||||
</div>
|
||||
{children}
|
||||
{/* 点赞/点踩 — the task result lives in linsight_session_version, so
|
||||
persist via the linsight feedback endpoint keyed by version id
|
||||
(best-effort: optimistic UI, backend wiring per PRD). */}
|
||||
{allowFeedback && versionId && (
|
||||
{/* like/dislike — the task result is a category="task" ChatMessage, so
|
||||
rate it through the shared chatmessage feedback endpoints keyed by
|
||||
message id (rollup to message_session is maintained backend-side). */}
|
||||
{allowFeedback && messageId && (
|
||||
<div className="mt-3">
|
||||
<MessageFeedbackButtons
|
||||
liked={liked}
|
||||
onLike={(l) => { void likeLinsightVersion(versionId, l).catch(() => {}); }}
|
||||
onDislikeComment={(c) => { void commentLinsightVersion(versionId, c).catch(() => {}); }}
|
||||
onLike={(l) => likeChatApi(messageId, l)}
|
||||
onDislikeComment={(c) => disLikeCommentApi(messageId, c)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -47,6 +47,10 @@ import { findPendingUserInput, hasRenderableTimeline, isTaskRunning, isTaskStart
|
||||
interface TaskTurnPanelProps {
|
||||
/** linsight session_version id holding this turn's execution detail */
|
||||
versionId: string;
|
||||
/** task-result ChatMessage id — the like/dislike target (this turn's bot row) */
|
||||
messageId?: string;
|
||||
/** persisted like/dislike verdict on the task result: 0 none / 1 up / 2 down */
|
||||
liked?: number;
|
||||
/** chat id of the hosting conversation (for the history lazy-load) */
|
||||
conversationId?: string;
|
||||
/** final answer text (fallback shown before the panel hydrates) */
|
||||
@@ -58,7 +62,7 @@ interface TaskTurnPanelProps {
|
||||
onPreviewFile?: (file: ArtifactFile) => void;
|
||||
}
|
||||
|
||||
export function TaskTurnPanel({ versionId, conversationId, answer, readOnly = false, onPreviewFile }: TaskTurnPanelProps) {
|
||||
export function TaskTurnPanel({ versionId, messageId, liked, conversationId, answer, readOnly = false, onPreviewFile }: TaskTurnPanelProps) {
|
||||
const localize = useLocalize();
|
||||
const { getLinsight, switchAndUpdateLinsight } = useLinsightManager();
|
||||
// WS pump — self-guards on status===Running, so mounting it for a completed
|
||||
@@ -250,7 +254,7 @@ export function TaskTurnPanel({ versionId, conversationId, answer, readOnly = fa
|
||||
document link opens it directly in ChatView's inline workspace
|
||||
panel (preview), replacing the legacy right-side drawer. */}
|
||||
{completed && (
|
||||
<ResultPanel versionId={versionId} liked={linsight?.liked ?? undefined} allowFeedback={!readOnly}>
|
||||
<ResultPanel messageId={messageId} liked={liked} allowFeedback={!readOnly}>
|
||||
<ResultSection
|
||||
answer={linsight.output_result?.answer}
|
||||
files={fileList}
|
||||
|
||||
@@ -52,7 +52,11 @@ export type LinsightInfo = {
|
||||
}[];
|
||||
output_result: null | any;
|
||||
score: null | number;
|
||||
// 点赞/点踩 verdict on the task result: 0 none / 1 up / 2 down.
|
||||
// like/dislike on the task result. The result is a category="task" ChatMessage;
|
||||
// `message_id` is that row's id (the feedback target) and `liked` its verdict
|
||||
// (0 none / 1 up / 2 down). Both come enriched on the session-version list
|
||||
// (snake_case so they pass through the raw `...version` spread into the store).
|
||||
message_id?: string | null;
|
||||
liked?: null | number;
|
||||
has_reexecute: boolean;
|
||||
id: string;
|
||||
|
||||
Reference in New Issue
Block a user