feat(exploration): report drawer exploration findings section with session drill-down
This commit is contained in:
parent
51980016a4
commit
a12eb09da3
@ -201,6 +201,28 @@ async def create_session(
|
|||||||
return repo.create(session_obj).model_dump(mode="json")
|
return repo.create(session_obj).model_dump(mode="json")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/campaigns/{campaign_id}/sessions")
|
||||||
|
async def list_campaign_sessions(
|
||||||
|
campaign_id: str,
|
||||||
|
session: Session = Depends(get_db),
|
||||||
|
) -> dict:
|
||||||
|
if not CampaignRepository(session).get(campaign_id):
|
||||||
|
raise HTTPException(status_code=404, detail="campaign not found")
|
||||||
|
sessions = ExplorationSessionRepository(session).list_by_campaign(campaign_id)
|
||||||
|
return {"sessions": [s.model_dump(mode="json") for s in sessions]}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sessions/{session_id}/messages")
|
||||||
|
async def list_session_messages(
|
||||||
|
session_id: str,
|
||||||
|
session: Session = Depends(get_db),
|
||||||
|
) -> dict:
|
||||||
|
if not ExplorationSessionRepository(session).get(session_id):
|
||||||
|
raise HTTPException(status_code=404, detail="exploration session not found")
|
||||||
|
messages = ExplorationMessageRepository(session).list_by_session(session_id)
|
||||||
|
return {"messages": [m.model_dump(mode="json") for m in messages]}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/sessions/{session_id}/messages")
|
@router.post("/sessions/{session_id}/messages")
|
||||||
async def send_session_message(
|
async def send_session_message(
|
||||||
session_id: str,
|
session_id: str,
|
||||||
|
|||||||
@ -324,6 +324,69 @@ export interface ExplorationBudgetConfig {
|
|||||||
min_interval_seconds?: number | null
|
min_interval_seconds?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExplorationIssueCount {
|
||||||
|
issue: string
|
||||||
|
count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExplorationJudgeReview {
|
||||||
|
reviewed_sessions: number
|
||||||
|
findings: { dimension: string; rating: string; comment: string }[]
|
||||||
|
summaries: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExplorationSummary {
|
||||||
|
session_count: number
|
||||||
|
sessions_with_experience: number
|
||||||
|
goal_achieved_count: number
|
||||||
|
goal_achievement_rate: number | null
|
||||||
|
issues: ExplorationIssueCount[]
|
||||||
|
misled: ExplorationIssueCount[]
|
||||||
|
judge_review: ExplorationJudgeReview | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExplorationExperience {
|
||||||
|
goal_achieved: boolean
|
||||||
|
blockers: string[]
|
||||||
|
misled: string[]
|
||||||
|
emotion: string
|
||||||
|
notes: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExplorationSession {
|
||||||
|
id: string
|
||||||
|
campaign_id: string
|
||||||
|
target_id: string
|
||||||
|
persona: Record<string, unknown>
|
||||||
|
goal: string
|
||||||
|
seed_ref: Record<string, unknown> | null
|
||||||
|
status: string
|
||||||
|
triggered_by: string
|
||||||
|
experience: ExplorationExperience | null
|
||||||
|
judge_review: Record<string, unknown> | null
|
||||||
|
turn_count: number
|
||||||
|
error: string | null
|
||||||
|
created_at: string | null
|
||||||
|
closed_at: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExplorationMessage {
|
||||||
|
id: string
|
||||||
|
session_id: string
|
||||||
|
round_index: number
|
||||||
|
role: string
|
||||||
|
content: string
|
||||||
|
latency_ms: number | null
|
||||||
|
created_at: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const explorationApi = {
|
||||||
|
listSessions: (campaignId: string) =>
|
||||||
|
api.get<{ sessions: ExplorationSession[] }>(`/exploration/campaigns/${campaignId}/sessions`),
|
||||||
|
listMessages: (sessionId: string) =>
|
||||||
|
api.get<{ messages: ExplorationMessage[] }>(`/exploration/sessions/${sessionId}/messages`),
|
||||||
|
}
|
||||||
|
|
||||||
export interface CampaignProgress {
|
export interface CampaignProgress {
|
||||||
current_offset_seconds: number
|
current_offset_seconds: number
|
||||||
spawned_runs: number
|
spawned_runs: number
|
||||||
@ -394,6 +457,7 @@ export interface CampaignReport {
|
|||||||
}
|
}
|
||||||
time_trend: CampaignTrendBucket[]
|
time_trend: CampaignTrendBucket[]
|
||||||
capability_summary: CampaignCapability[]
|
capability_summary: CampaignCapability[]
|
||||||
|
exploration?: ExplorationSummary | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CampaignTimelineEntry {
|
export interface CampaignTimelineEntry {
|
||||||
|
|||||||
237
frontend/web/src/components/ExplorationSection.tsx
Normal file
237
frontend/web/src/components/ExplorationSection.tsx
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { Collapse, Empty, Space, Spin, Tag } from 'antd'
|
||||||
|
import { UserOutlined, RobotOutlined } from '@ant-design/icons'
|
||||||
|
import {
|
||||||
|
explorationApi,
|
||||||
|
type ExplorationMessage,
|
||||||
|
type ExplorationSession,
|
||||||
|
type ExplorationSummary,
|
||||||
|
} from '../api'
|
||||||
|
import { colors } from '../tokens'
|
||||||
|
import { shortDateTime } from '../utils/date'
|
||||||
|
|
||||||
|
const SESSION_STATUS: Record<string, { label: string; color: string }> = {
|
||||||
|
running: { label: '进行中', color: 'processing' },
|
||||||
|
completed: { label: '已完成', color: 'success' },
|
||||||
|
failed: { label: '失败', color: 'error' },
|
||||||
|
expired: { label: '已过期', color: 'default' },
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMOTION_LABELS: Record<string, string> = {
|
||||||
|
positive: '满意',
|
||||||
|
neutral: '平静',
|
||||||
|
confused: '困惑',
|
||||||
|
frustrated: '沮丧',
|
||||||
|
}
|
||||||
|
|
||||||
|
const DIMENSION_LABELS: Record<string, string> = {
|
||||||
|
attitude: '服务态度',
|
||||||
|
professionalism: '专业度',
|
||||||
|
hallucination: '幻觉',
|
||||||
|
}
|
||||||
|
|
||||||
|
function pct(rate: number | null): string {
|
||||||
|
return rate == null ? '—' : `${(rate * 100).toFixed(1)}%`
|
||||||
|
}
|
||||||
|
|
||||||
|
function IssueList({ label, items, misled }: {
|
||||||
|
label: string
|
||||||
|
items: { issue: string; count: number }[]
|
||||||
|
misled?: boolean
|
||||||
|
}) {
|
||||||
|
if (items.length === 0) return null
|
||||||
|
return (
|
||||||
|
<div style={{ marginBottom: 10 }}>
|
||||||
|
<div style={{ fontSize: 12, color: colors.textSecondary, marginBottom: 6 }}>{label}</div>
|
||||||
|
<Space size={8} wrap>
|
||||||
|
{items.map((item, i) => (
|
||||||
|
<Tag key={i} color={misled ? 'orange' : 'red'}>
|
||||||
|
{misled ? `(被误导)${item.issue}` : item.issue} ×{item.count}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SessionDetail({ session, messages }: {
|
||||||
|
session: ExplorationSession
|
||||||
|
messages: ExplorationMessage[] | null
|
||||||
|
}) {
|
||||||
|
const experience = session.experience
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 12, color: colors.textSecondary, marginBottom: 10 }}>
|
||||||
|
目标:{session.goal}
|
||||||
|
{session.created_at && <> · 开始于 {shortDateTime(session.created_at)}</>}
|
||||||
|
{session.closed_at && <> · 结束于 {shortDateTime(session.closed_at)}</>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!messages && <Spin size="small" />}
|
||||||
|
{messages && messages.length === 0 && (
|
||||||
|
<Empty description="暂无对话记录" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
|
)}
|
||||||
|
{messages && messages.map((m) => (
|
||||||
|
<div
|
||||||
|
key={m.id}
|
||||||
|
style={{ display: 'flex', justifyContent: m.role === 'user' ? 'flex-start' : 'flex-end', marginBottom: 8 }}
|
||||||
|
>
|
||||||
|
{m.role === 'user' ? (
|
||||||
|
<div style={{
|
||||||
|
background: colors.chatUser, padding: '8px 14px',
|
||||||
|
borderRadius: '12px 12px 12px 2px', maxWidth: '70%',
|
||||||
|
fontSize: 13, lineHeight: 1.6,
|
||||||
|
}}>
|
||||||
|
<UserOutlined style={{ marginRight: 6, color: '#1677ff' }} />
|
||||||
|
{m.content}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{
|
||||||
|
background: colors.chatAgent, padding: '8px 14px',
|
||||||
|
borderRadius: '12px 12px 2px 12px', maxWidth: '70%',
|
||||||
|
fontSize: 13, lineHeight: 1.6,
|
||||||
|
}}>
|
||||||
|
<RobotOutlined style={{ marginRight: 6, color: '#52c41a' }} />
|
||||||
|
{m.content}
|
||||||
|
{m.latency_ms != null && (
|
||||||
|
<span style={{ color: colors.textMuted, fontSize: 11, marginLeft: 8 }}>{m.latency_ms}ms</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{experience && (
|
||||||
|
<div style={{
|
||||||
|
marginTop: 8, border: `1px solid ${colors.border}`, borderRadius: 8,
|
||||||
|
background: colors.bgSubtle, padding: '10px 12px', fontSize: 13, lineHeight: 1.8,
|
||||||
|
}}>
|
||||||
|
<div style={{ fontWeight: 500, marginBottom: 4 }}>体验记录</div>
|
||||||
|
<Space size={8} wrap>
|
||||||
|
{experience.goal_achieved
|
||||||
|
? <Tag color="success">目标达成</Tag>
|
||||||
|
: <Tag color="error">目标未达成</Tag>}
|
||||||
|
<Tag>{EMOTION_LABELS[experience.emotion] ?? experience.emotion}</Tag>
|
||||||
|
</Space>
|
||||||
|
{experience.blockers.length > 0 && (
|
||||||
|
<div style={{ color: colors.textSecondary }}>障碍:{experience.blockers.join(';')}</div>
|
||||||
|
)}
|
||||||
|
{experience.misled.length > 0 && (
|
||||||
|
<div style={{ color: colors.warning }}>被误导:{experience.misled.join(';')}</div>
|
||||||
|
)}
|
||||||
|
{experience.notes && (
|
||||||
|
<div style={{ color: colors.textSecondary }}>备注:{experience.notes}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{session.error && (
|
||||||
|
<div style={{ marginTop: 8, fontSize: 12, color: colors.textMuted }}>会话异常:{session.error}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExplorationSection({ campaignId, summary }: {
|
||||||
|
campaignId: string
|
||||||
|
summary: ExplorationSummary
|
||||||
|
}) {
|
||||||
|
const [sessions, setSessions] = useState<ExplorationSession[] | null>(null)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [messageCache, setMessageCache] = useState<Record<string, ExplorationMessage[]>>({})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
setLoading(true)
|
||||||
|
explorationApi.listSessions(campaignId)
|
||||||
|
.then((res) => { if (!cancelled) setSessions(res.data.sessions) })
|
||||||
|
.catch(() => undefined) // 拦截器已弹错;避免未处理 rejection
|
||||||
|
.finally(() => { if (!cancelled) setLoading(false) })
|
||||||
|
return () => { cancelled = true }
|
||||||
|
}, [campaignId])
|
||||||
|
|
||||||
|
const onExpand = (keys: string | string[]) => {
|
||||||
|
for (const key of Array.isArray(keys) ? keys : [keys]) {
|
||||||
|
if (key in messageCache) continue
|
||||||
|
void explorationApi.listMessages(key)
|
||||||
|
.then((res) => { setMessageCache((prev) => ({ ...prev, [key]: res.data.messages })) })
|
||||||
|
.catch(() => undefined) // 拦截器已弹错;收起再展开可重试
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const judge = summary.judge_review
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
border: `1px solid ${colors.border}`, borderRadius: 8,
|
||||||
|
padding: '12px 14px', marginBottom: 16,
|
||||||
|
}}>
|
||||||
|
<div style={{ fontSize: 12, color: colors.textSecondary, marginBottom: 10 }}>
|
||||||
|
探索会话 {summary.session_count} 个 · 有体验记录 {summary.sessions_with_experience} 个
|
||||||
|
{' · '}目标达成率 {pct(summary.goal_achievement_rate)}({summary.goal_achieved_count}/{summary.sessions_with_experience})
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<IssueList label="问题清单(体验记录)" items={summary.issues} />
|
||||||
|
<IssueList label="被误导清单(体验记录)" items={summary.misled} misled />
|
||||||
|
{summary.issues.length === 0 && summary.misled.length === 0 && (
|
||||||
|
<div style={{ fontSize: 12, color: colors.textMuted, marginBottom: 10 }}>体验记录未报告问题</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{judge && (
|
||||||
|
<div style={{ marginBottom: 10 }}>
|
||||||
|
<div style={{ fontSize: 12, color: colors.textSecondary, marginBottom: 6 }}>
|
||||||
|
judge 复核({judge.reviewed_sessions} 个会话抽样)
|
||||||
|
</div>
|
||||||
|
{judge.findings.length === 0
|
||||||
|
? <div style={{ fontSize: 13, color: colors.textSecondary }}>未发现问题</div>
|
||||||
|
: (
|
||||||
|
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||||
|
{judge.findings.map((f, i) => (
|
||||||
|
<div key={i} style={{ fontSize: 13 }}>
|
||||||
|
<Tag color="red" style={{ margin: 0 }}>
|
||||||
|
{DIMENSION_LABELS[f.dimension] ?? f.dimension}
|
||||||
|
</Tag>
|
||||||
|
{' '}{f.comment}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
{judge.summaries.map((s, i) => (
|
||||||
|
<div key={i} style={{ fontSize: 13, color: colors.textSecondary, marginTop: 4 }}>
|
||||||
|
复核结论:{s}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loading && <Spin size="small" />}
|
||||||
|
{!loading && sessions && sessions.length === 0 && (
|
||||||
|
<Empty description="暂无探索会话" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
|
)}
|
||||||
|
{sessions && sessions.length > 0 && (
|
||||||
|
<Collapse
|
||||||
|
size="small"
|
||||||
|
onChange={onExpand}
|
||||||
|
items={sessions.map((s) => {
|
||||||
|
const meta = SESSION_STATUS[s.status] ?? SESSION_STATUS.running
|
||||||
|
const personaName = typeof s.persona?.name === 'string' ? s.persona.name : s.id.slice(0, 8)
|
||||||
|
return {
|
||||||
|
key: s.id,
|
||||||
|
label: (
|
||||||
|
<Space size={8} wrap>
|
||||||
|
<span style={{ fontWeight: 500 }}>{personaName}</span>
|
||||||
|
<Tag color={meta.color}>{meta.label}</Tag>
|
||||||
|
<Tag>{s.turn_count} 轮</Tag>
|
||||||
|
{s.experience?.goal_achieved != null && (
|
||||||
|
s.experience.goal_achieved
|
||||||
|
? <Tag color="success">目标达成</Tag>
|
||||||
|
: <Tag color="error">目标未达成</Tag>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
children: <SessionDetail session={s} messages={messageCache[s.id] ?? null} />,
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -8,7 +8,7 @@ import {
|
|||||||
PlusOutlined, ReloadOutlined, StopOutlined, BarChartOutlined,
|
PlusOutlined, ReloadOutlined, StopOutlined, BarChartOutlined,
|
||||||
FileMarkdownOutlined, MinusCircleOutlined, QuestionCircleOutlined,
|
FileMarkdownOutlined, MinusCircleOutlined, QuestionCircleOutlined,
|
||||||
RocketOutlined, CheckCircleOutlined, SafetyOutlined, ClockCircleOutlined,
|
RocketOutlined, CheckCircleOutlined, SafetyOutlined, ClockCircleOutlined,
|
||||||
RobotOutlined, SwapOutlined,
|
RobotOutlined, SwapOutlined, CompassOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { Line, Bar } from '@ant-design/charts'
|
import { Line, Bar } from '@ant-design/charts'
|
||||||
import PageWrapper from '../components/PageWrapper'
|
import PageWrapper from '../components/PageWrapper'
|
||||||
@ -25,6 +25,7 @@ import { deriveTimeScale, acceleratedDuration, formatScale } from '../utils/camp
|
|||||||
import WindowTimeline, { type TimelineMarker } from '../components/WindowTimeline'
|
import WindowTimeline, { type TimelineMarker } from '../components/WindowTimeline'
|
||||||
import CampaignRunTimeline from '../components/CampaignRunTimeline'
|
import CampaignRunTimeline from '../components/CampaignRunTimeline'
|
||||||
import PeriodComparisonSection from '../components/PeriodComparisonSection'
|
import PeriodComparisonSection from '../components/PeriodComparisonSection'
|
||||||
|
import ExplorationSection from '../components/ExplorationSection'
|
||||||
import { useResource } from '../hooks/useResource'
|
import { useResource } from '../hooks/useResource'
|
||||||
import { usePolling } from '../hooks/usePolling'
|
import { usePolling } from '../hooks/usePolling'
|
||||||
import { useTabStore } from '../stores/tabStore'
|
import { useTabStore } from '../stores/tabStore'
|
||||||
@ -1055,6 +1056,17 @@ export default function CampaignsPage() {
|
|||||||
onGenerate={generateComparison}
|
onGenerate={generateComparison}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{report.exploration && (
|
||||||
|
<>
|
||||||
|
<SectionTitle><CompassOutlined /> 探索发现</SectionTitle>
|
||||||
|
<ExplorationSection
|
||||||
|
key={report.campaign_id}
|
||||||
|
campaignId={report.campaign_id}
|
||||||
|
summary={report.exploration}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<SectionTitle>过程时间轴</SectionTitle>
|
<SectionTitle>过程时间轴</SectionTitle>
|
||||||
<div style={{
|
<div style={{
|
||||||
border: `1px solid ${colors.border}`, borderRadius: 8,
|
border: `1px solid ${colors.border}`, borderRadius: 8,
|
||||||
|
|||||||
@ -349,6 +349,66 @@ async def test_close_triggers_judge_review(seeded_db, mock_channel, client, monk
|
|||||||
assert started == [session_id]
|
assert started == [session_id]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- read endpoints (ticket 06)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_list_campaign_sessions_returns_lifecycle_fields(seeded_db, mock_channel, client):
|
||||||
|
resp = await _create_session(client)
|
||||||
|
assert resp.status_code == 200, resp.text
|
||||||
|
session_id = resp.json()["id"]
|
||||||
|
|
||||||
|
await client.post(f"/api/exploration/sessions/{session_id}/messages", json={"content": "查账单"})
|
||||||
|
resp = await client.post(
|
||||||
|
f"/api/exploration/sessions/{session_id}/close",
|
||||||
|
json={"experience": {"goal_achieved": True, "blockers": [], "misled": ["跳转误导"], "notes": "绕了三圈"}},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200, resp.text
|
||||||
|
|
||||||
|
resp = await client.get("/api/exploration/campaigns/c-1/sessions")
|
||||||
|
assert resp.status_code == 200, resp.text
|
||||||
|
sessions = resp.json()["sessions"]
|
||||||
|
assert len(sessions) == 1
|
||||||
|
entry = sessions[0]
|
||||||
|
assert entry["id"] == session_id
|
||||||
|
assert entry["status"] == "completed"
|
||||||
|
assert entry["turn_count"] == 1
|
||||||
|
assert entry["goal"] == "查询本月账单并完成缴费"
|
||||||
|
assert entry["persona"]["name"] == "急性子用户"
|
||||||
|
assert entry["experience"]["goal_achieved"] is True
|
||||||
|
assert entry["experience"]["misled"] == ["跳转误导"]
|
||||||
|
assert entry["closed_at"] is not None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_list_sessions_empty_campaign(seeded_db, client):
|
||||||
|
resp = await client.get("/api/exploration/campaigns/c-1/sessions")
|
||||||
|
assert resp.status_code == 200, resp.text
|
||||||
|
assert resp.json() == {"sessions": []}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_list_session_messages_returns_conversation(seeded_db, mock_channel, client):
|
||||||
|
session_id = (await _create_session(client)).json()["id"]
|
||||||
|
await client.post(f"/api/exploration/sessions/{session_id}/messages", json={"content": "我要查账单"})
|
||||||
|
|
||||||
|
resp = await client.get(f"/api/exploration/sessions/{session_id}/messages")
|
||||||
|
assert resp.status_code == 200, resp.text
|
||||||
|
messages = resp.json()["messages"]
|
||||||
|
assert len(messages) == 2
|
||||||
|
user_msg, agent_msg = messages
|
||||||
|
assert user_msg["role"] == "user"
|
||||||
|
assert user_msg["content"] == "我要查账单"
|
||||||
|
assert user_msg["round_index"] == 1
|
||||||
|
assert agent_msg["role"] == "assistant"
|
||||||
|
assert agent_msg["content"] == "您好,请问有什么可以帮您?"
|
||||||
|
assert agent_msg["round_index"] == 1
|
||||||
|
assert isinstance(agent_msg["latency_ms"], int)
|
||||||
|
assert user_msg["created_at"] is not None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_list_messages_unknown_session_returns_404(seeded_db, client):
|
||||||
|
resp = await client.get("/api/exploration/sessions/nope/messages")
|
||||||
|
assert resp.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- migration
|
# ---------------------------------------------------------------- migration
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user