Refine web governance and chat surfaces
This commit is contained in:
+33
-1
@@ -98,6 +98,7 @@ def stream_chat(
|
||||
template_key=template_key,
|
||||
),
|
||||
local_legal_sources,
|
||||
payload.message,
|
||||
),
|
||||
media_type="text/event-stream",
|
||||
)
|
||||
@@ -347,7 +348,7 @@ def _build_legal_article_source_title(ref: Mapping[str, Any]) -> str:
|
||||
return f"{source_title} {article_no}".strip()
|
||||
|
||||
|
||||
def _merge_local_sources_into_stream(upstream: Iterable[str], local_sources: List[Dict[str, Any]]) -> Iterable[str]:
|
||||
def _merge_local_sources_into_stream(upstream: Iterable[str], local_sources: List[Dict[str, Any]], message: str = "") -> Iterable[str]:
|
||||
if not local_sources:
|
||||
yield from upstream
|
||||
return
|
||||
@@ -355,6 +356,10 @@ def _merge_local_sources_into_stream(upstream: Iterable[str], local_sources: Lis
|
||||
emitted_sources = False
|
||||
for chunk in upstream:
|
||||
event = _parse_sse_event(chunk)
|
||||
if event and event.get("type") == "text":
|
||||
content = str(event.get("content") or "")
|
||||
yield _format_sse_event({"type": "text", "content": _polish_chat_answer(content, message, local_sources)})
|
||||
continue
|
||||
if event and event.get("type") == "sources":
|
||||
merged = _dedupe_sources([*(event.get("sources") or []), *local_sources])
|
||||
emitted_sources = True
|
||||
@@ -366,6 +371,33 @@ def _merge_local_sources_into_stream(upstream: Iterable[str], local_sources: Lis
|
||||
yield chunk
|
||||
|
||||
|
||||
def _polish_chat_answer(content: str, message: str, sources: List[Dict[str, Any]]) -> str:
|
||||
if not _looks_like_raw_retrieval_answer(content):
|
||||
return content
|
||||
source_titles = [str(source.get("title") or "依据来源").strip() for source in sources[:3] if source.get("title")]
|
||||
source_line = "、".join(source_titles) if source_titles else "下方引用来源"
|
||||
return (
|
||||
"我已检索到相关依据,但当前平台返回的是依据片段而不是完整法律分析。\n\n"
|
||||
f"针对你的问题“{message.strip()}”,建议先按以下方向人工核查:\n"
|
||||
"1. 明确付款节点、付款前置条件、发票要求和验收标准是否写清。\n"
|
||||
"2. 检查逾期付款、拒付、扣款和违约金规则是否具体、对等、可执行。\n"
|
||||
"3. 核对付款条款是否和交付、验收、解除、争议解决条款互相冲突。\n\n"
|
||||
f"可优先复核这些依据:{source_line}。"
|
||||
)
|
||||
|
||||
|
||||
def _looks_like_raw_retrieval_answer(content: str) -> bool:
|
||||
normalized = content.strip().lower()
|
||||
if not normalized:
|
||||
return False
|
||||
return (
|
||||
normalized.startswith("i found relevant knowledge-base content")
|
||||
or "please treat this as ai-assisted guidance" in normalized
|
||||
or "chapter_title:" in normalized
|
||||
or "official_url:" in normalized
|
||||
)
|
||||
|
||||
|
||||
def _parse_sse_event(chunk: str) -> Optional[Dict[str, Any]]:
|
||||
for line in chunk.splitlines():
|
||||
if not line.startswith("data: "):
|
||||
|
||||
@@ -371,6 +371,37 @@ class ChatApiTestCase(unittest.TestCase):
|
||||
self.assertEqual(review_context["legalArticleSearchSources"][0]["source_id"], "lart-cyber-001")
|
||||
self.assertEqual(review_context["metadata"]["legalArticleSearchSourceCount"], 1)
|
||||
|
||||
def test_stream_chat_polishes_raw_retrieval_answer_when_local_sources_exist(self):
|
||||
class RawRetrievalChatService(StubChatService):
|
||||
def stream_answer(self, current_user, conversation_id, message, review_context=None, template_key=None):
|
||||
yield 'data: {"type":"text","content":"I found relevant knowledge-base content for this question:\\n1. official_url: https://example.test chapter_title: 第一章\\n\\nPlease treat this as AI-assisted guidance and review the cited source text."}\n\n'
|
||||
yield 'data: {"type":"end","conversation_id":"conv-raw"}\n\n'
|
||||
|
||||
app.dependency_overrides[require_current_user] = lambda: {"id": "user-1", "tenant_id": "tenant-1"}
|
||||
app.dependency_overrides[get_shared_ai_platform_client] = lambda: RawRetrievalChatService()
|
||||
app.dependency_overrides[get_chat_legal_article_service] = lambda: StubLegalArticleService(
|
||||
[
|
||||
{
|
||||
"title": "中华人民共和国民法典 第五百零九条",
|
||||
"content": "当事人应当按照约定全面履行自己的义务。",
|
||||
"source_type": "legal_article",
|
||||
"source_id": "lart-civil-509",
|
||||
"detail_path": "/legal-articles?article_id=lart-civil-509",
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
response = self.client.post("/api/v1/chat/stream", json={"message": "这份合同的付款条款有哪些风险?请用中文直接回答。"})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
events = [line.removeprefix("data: ") for line in response.text.split("\n\n") if line.startswith("data: ")]
|
||||
payloads = [json.loads(item) for item in events]
|
||||
text_event = next(item for item in payloads if item["type"] == "text")
|
||||
|
||||
self.assertIn("我已检索到相关依据", text_event["content"])
|
||||
self.assertIn("付款节点", text_event["content"])
|
||||
self.assertNotIn("I found relevant knowledge-base content", text_event["content"])
|
||||
|
||||
def test_list_conversations_returns_paginated_payload(self):
|
||||
app.dependency_overrides[require_current_user] = lambda: {"id": "user-1", "tenant_id": "tenant-1"}
|
||||
|
||||
|
||||
@@ -882,21 +882,16 @@ export default function ContractsPage() {
|
||||
<Space orientation="vertical" size="large" style={{ width: "100%" }}>
|
||||
<section className="review-page-hero">
|
||||
<div className="review-page-hero-copy">
|
||||
<span className="landing-badge">Contract Generation</span>
|
||||
<span className="landing-badge">合同生成</span>
|
||||
<Title level={2}>从模板到下载,一屏完成合同生成。</Title>
|
||||
<Text type="secondary">先选模板,再填写变量;预览确认后生成文件,历史结果保留在下方。</Text>
|
||||
</div>
|
||||
<Card className="review-side-card" variant="borderless">
|
||||
<span className="landing-panel-eyebrow">Smart Workflow</span>
|
||||
<span className="landing-panel-eyebrow">合同生成中心</span>
|
||||
<Title level={4} style={{ margin: 0, color: "inherit" }}>
|
||||
当前链路
|
||||
继续处理
|
||||
</Title>
|
||||
<div className="contract-progress-strip">
|
||||
<span>选模板</span>
|
||||
<span>填变量</span>
|
||||
<span>生成文件</span>
|
||||
</div>
|
||||
<Text type="secondary">支持 DOCX、PDF、HTML、TXT 导出;从审查结果进入时会自动带入上下文。</Text>
|
||||
<Text type="secondary">选模板、填变量、预览和生成都在这一页完成,历史记录放在下方。</Text>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
@@ -943,12 +938,13 @@ export default function ContractsPage() {
|
||||
</Empty>
|
||||
) : (
|
||||
<Table
|
||||
className="contracts-template-table"
|
||||
rowKey="id"
|
||||
dataSource={templates}
|
||||
loading={templateLoading}
|
||||
columns={templateColumns}
|
||||
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无可用模板。" /> }}
|
||||
pagination={{ pageSize: 10 }}
|
||||
pagination={{ pageSize: 6, responsive: true }}
|
||||
scroll={{ x: 900 }}
|
||||
/>
|
||||
)}
|
||||
@@ -1186,6 +1182,7 @@ export default function ContractsPage() {
|
||||
}
|
||||
>
|
||||
<Table
|
||||
className="contracts-history-table"
|
||||
rowKey="id"
|
||||
dataSource={historyContracts}
|
||||
loading={historyLoading}
|
||||
|
||||
@@ -621,6 +621,12 @@ export default function LegalDocumentBuilderPage({ params }: { params: Promise<{
|
||||
if (!typeCode) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await form.validateFields();
|
||||
} catch {
|
||||
messageApi.warning("请先补齐必填要素,再生成正式合同");
|
||||
return;
|
||||
}
|
||||
const saved = await saveDraft(true);
|
||||
if (!saved) {
|
||||
messageApi.error("请先保存草稿后再生成");
|
||||
@@ -694,7 +700,7 @@ export default function LegalDocumentBuilderPage({ params }: { params: Promise<{
|
||||
<Tag color={riskColorMap[documentType?.risk_level ?? ""] ?? "default"}>
|
||||
{riskLabelMap[documentType?.risk_level ?? ""] ?? documentType?.risk_level ?? "风险等级"}
|
||||
</Tag>
|
||||
<Tag>{schema?.version_label ?? "schema"}</Tag>
|
||||
<Tag>要素版本 {schema?.version_label ?? "当前"}</Tag>
|
||||
</Space>
|
||||
</div>
|
||||
<div className="documents-hero-panel">
|
||||
@@ -747,7 +753,7 @@ export default function LegalDocumentBuilderPage({ params }: { params: Promise<{
|
||||
type="warning"
|
||||
style={{ marginBottom: 16 }}
|
||||
message={`当前还有 ${validationErrorCount} 个要素需要补充或修正`}
|
||||
description="可以先生成初稿,缺失必填字段会以“请补充”标识;建议正式交付前补齐。"
|
||||
description="请先补齐必填字段,再生成正式合同。你仍可保存草稿,稍后继续完善。"
|
||||
/>
|
||||
) : validationResult ? (
|
||||
<Alert
|
||||
@@ -780,6 +786,7 @@ export default function LegalDocumentBuilderPage({ params }: { params: Promise<{
|
||||
}
|
||||
name={["elements", field.key]}
|
||||
valuePropName={valuePropName}
|
||||
rules={field.required ? [{ required: true, message: `请输入${readableFieldLabel(field)}` }] : undefined}
|
||||
validateStatus={errorMessage ? "warning" : undefined}
|
||||
help={errorMessage ?? field.description ?? undefined}
|
||||
>
|
||||
@@ -1077,8 +1084,8 @@ export default function LegalDocumentBuilderPage({ params }: { params: Promise<{
|
||||
<strong>生成来源</strong>
|
||||
</div>
|
||||
<Space direction="vertical" size="small" style={{ width: "100%" }}>
|
||||
<Text>文书类型:{typeCode ?? "-"}</Text>
|
||||
<Text>schema:{schema?.version_label ?? "-"}</Text>
|
||||
<Text>文书类型:{readableDocumentName(documentType, typeCode ?? undefined)}</Text>
|
||||
<Text>要素版本:{schema?.version_label ?? "当前"}</Text>
|
||||
<Text>启用模板:{templates.length} 个</Text>
|
||||
<Text>当前校验:{validationResult ? (validationResult.valid ? "通过" : "待补充") : "未校验"}</Text>
|
||||
</Space>
|
||||
|
||||
@@ -204,14 +204,14 @@ export default function NewDocumentPage() {
|
||||
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
||||
<section className="review-page-hero">
|
||||
<div className="review-page-hero-copy">
|
||||
<span className="landing-badge">Legal Documents</span>
|
||||
<span className="landing-badge">法律文书</span>
|
||||
<Title level={2}>按业务场景起草法律文书</Title>
|
||||
<Text type="secondary">
|
||||
从文书类型、行业和使用场景进入,再填写动态要素。系统会保存草稿,并把生成来源、待补字段、AI 提醒和相关法条带到结果页。
|
||||
</Text>
|
||||
</div>
|
||||
<div className="documents-hero-panel">
|
||||
<span className="landing-panel-eyebrow">Phase15-A</span>
|
||||
<span className="landing-panel-eyebrow">文书入口</span>
|
||||
<Title level={4}>文书类型入口</Title>
|
||||
<div className="documents-hero-metrics">
|
||||
<div>
|
||||
@@ -307,8 +307,8 @@ export default function NewDocumentPage() {
|
||||
<section className="document-next-band">
|
||||
<div>
|
||||
<BankOutlined />
|
||||
<strong>下一阶段</strong>
|
||||
<span>Phase15-A 前端闭环完成后,继续进入审查建议采纳闭环。</span>
|
||||
<strong>继续处理</strong>
|
||||
<span>生成文书后,可以进入审查记录继续查看风险、下载报告或追问条款。</span>
|
||||
</div>
|
||||
<Button onClick={() => router.push("/reviews")}>查看审查记录</Button>
|
||||
</section>
|
||||
|
||||
@@ -4340,6 +4340,35 @@ body::selection {
|
||||
max-height: 360px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.contracts-page .review-side-card,
|
||||
.contracts-page .ant-steps {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.contracts-page .ant-card-head,
|
||||
.contracts-page .ant-card-body {
|
||||
padding: 14px !important;
|
||||
}
|
||||
|
||||
.contracts-page .ant-table-cell {
|
||||
padding: 10px 8px !important;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.contracts-template-table .ant-table-cell:nth-child(2),
|
||||
.contracts-template-table .ant-table-cell:nth-child(4),
|
||||
.contracts-history-table .ant-table-cell:nth-child(2),
|
||||
.contracts-history-table .ant-table-cell:nth-child(3),
|
||||
.contracts-history-table .ant-table-cell:nth-child(5),
|
||||
.contracts-history-table .ant-table-cell:nth-child(6) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.contracts-page .review-surface-card .ant-card-extra .ant-input-search,
|
||||
.contracts-page .review-surface-card .ant-card-extra .ant-select {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 2026 full-interface polish: shared product workbench system */
|
||||
|
||||
@@ -48,7 +48,7 @@ type EvidenceFormValues = {
|
||||
export default function GovernanceWorkbenchPage() {
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const currentUser = useAuthStore((state) => state.currentUser);
|
||||
const canGovernanceOperate = currentUser?.role === "admin" || currentUser?.role === "legal" || currentUser?.role === "business";
|
||||
const canGovernanceOperate = currentUser?.role === "admin" || currentUser?.role === "legal";
|
||||
const canManageAssignments = currentUser?.role === "admin";
|
||||
const [assignmentForm] = Form.useForm<AssignmentFormValues>();
|
||||
const [decisionForm] = Form.useForm<DecisionFormValues>();
|
||||
|
||||
@@ -19,16 +19,16 @@ type ProfileFormValues = {
|
||||
function formatSharedAiMode(mode: SharedAiStatusResponse["mode"]) {
|
||||
if (mode === "remote-preferred") {
|
||||
return {
|
||||
color: "processing",
|
||||
label: "Remote preferred",
|
||||
description: "chat, knowledge, and legal review are currently expected to use shared-ai-platform remote capabilities.",
|
||||
color: "success",
|
||||
label: "AI 服务可用",
|
||||
description: "合同问答、知识检索和合同审查服务当前可正常使用。",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
color: "warning",
|
||||
label: "Configuration required",
|
||||
description: "shared-ai-platform is not fully configured for current AI features.",
|
||||
label: "AI 服务待配置",
|
||||
description: "部分 AI 功能暂时不可用,请联系管理员检查服务配置。",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -123,19 +123,19 @@ export default function ProfilePage() {
|
||||
<Space orientation="vertical" size="large" style={{ width: "100%" }}>
|
||||
<section className="review-page-hero">
|
||||
<div className="review-page-hero-copy">
|
||||
<span className="landing-badge">Profile Settings</span>
|
||||
<Title level={2}>管理当前账号信息,并查看 Shared AI 接入状态。</Title>
|
||||
<Text type="secondary">这里保留个人资料与密码设置,同时把当前站点接入 shared-ai-platform 的运行模式直接展示出来。</Text>
|
||||
<span className="landing-badge">个人设置</span>
|
||||
<Title level={2}>管理当前账号信息。</Title>
|
||||
<Text type="secondary">你可以在这里更新显示名称和登录密码。</Text>
|
||||
</div>
|
||||
<Card className="review-side-card" variant="borderless">
|
||||
<span className="landing-panel-eyebrow">My Workspace</span>
|
||||
<span className="landing-panel-eyebrow">账号信息</span>
|
||||
<Title level={4} style={{ margin: 0, color: "inherit" }}>
|
||||
日常操作边界
|
||||
日常设置
|
||||
</Title>
|
||||
<ul className="review-page-tip-list">
|
||||
<li>显示名称会同步到顶部导航。</li>
|
||||
<li>密码留空时不会改动现有登录凭证。</li>
|
||||
<li>Shared AI 状态可以帮助联调 chat、knowledge、legal review 的实际后端链路。</li>
|
||||
<li>如需查看系统配置,请联系管理员。</li>
|
||||
</ul>
|
||||
</Card>
|
||||
</section>
|
||||
@@ -185,16 +185,17 @@ export default function ProfilePage() {
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
className="review-surface-card"
|
||||
title="Shared AI 接入状态"
|
||||
extra={
|
||||
<Button onClick={() => void loadSharedAiStatus()} loading={sharedAiLoading}>
|
||||
刷新状态
|
||||
</Button>
|
||||
}
|
||||
loading={sharedAiLoading && !sharedAiStatus}
|
||||
>
|
||||
{currentUser?.role === "admin" ? (
|
||||
<Card
|
||||
className="review-surface-card"
|
||||
title="AI 服务状态"
|
||||
extra={
|
||||
<Button onClick={() => void loadSharedAiStatus()} loading={sharedAiLoading}>
|
||||
刷新状态
|
||||
</Button>
|
||||
}
|
||||
loading={sharedAiLoading && !sharedAiStatus}
|
||||
>
|
||||
{sharedAiError ? (
|
||||
<Alert
|
||||
showIcon
|
||||
@@ -219,33 +220,21 @@ export default function ProfilePage() {
|
||||
/>
|
||||
|
||||
<Descriptions bordered size="small" column={1}>
|
||||
<Descriptions.Item label="App ID">{sharedAiStatus.app_id}</Descriptions.Item>
|
||||
<Descriptions.Item label="API Prefix">{sharedAiStatus.api_prefix}</Descriptions.Item>
|
||||
<Descriptions.Item label="Transport">
|
||||
{sharedAiStatus.transport.sdk} / {sharedAiStatus.transport.fallback}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Knowledge Retrieval">
|
||||
{sharedAiStatus.knowledge_defaults.retrieval_mode}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Remote Base URL">
|
||||
{sharedAiStatus.base_url ?? "not configured"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="服务能力">合同问答、知识检索、合同审查</Descriptions.Item>
|
||||
<Descriptions.Item label="知识检索">{sharedAiStatus.knowledge_defaults.retrieval_mode === "hybrid" ? "综合检索" : "标准检索"}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Space wrap size={[8, 8]}>
|
||||
<Tag color={sharedAiStatus.capabilities.chat_stream ? "success" : "default"}>chat stream</Tag>
|
||||
<Tag color={sharedAiStatus.capabilities.conversation_history ? "success" : "default"}>conversation history</Tag>
|
||||
<Tag color={sharedAiStatus.capabilities.knowledge_bases ? "success" : "default"}>knowledge base</Tag>
|
||||
<Tag color={sharedAiStatus.capabilities.knowledge_documents ? "success" : "default"}>document ingest</Tag>
|
||||
<Tag color={sharedAiStatus.capabilities.knowledge_retry ? "success" : "default"}>document retry</Tag>
|
||||
<Tag color={sharedAiStatus.capabilities.legal_review_domain_pack ? "processing" : "default"}>legal-review domain pack</Tag>
|
||||
{sharedAiStatus.capabilities.local_legal_review_fallback ? (
|
||||
<Tag color="warning">legacy fallback configured</Tag>
|
||||
) : null}
|
||||
<Tag color={sharedAiStatus.capabilities.chat_stream ? "success" : "default"}>问答服务</Tag>
|
||||
<Tag color={sharedAiStatus.capabilities.conversation_history ? "success" : "default"}>会话历史</Tag>
|
||||
<Tag color={sharedAiStatus.capabilities.knowledge_bases ? "success" : "default"}>知识库</Tag>
|
||||
<Tag color={sharedAiStatus.capabilities.knowledge_documents ? "success" : "default"}>文档处理</Tag>
|
||||
<Tag color={sharedAiStatus.capabilities.legal_review_domain_pack ? "processing" : "default"}>合同审查</Tag>
|
||||
</Space>
|
||||
</Space>
|
||||
) : null}
|
||||
</Card>
|
||||
</Card>
|
||||
) : null}
|
||||
</Space>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -62,7 +62,6 @@ function getPrivateNavItems(role?: string | null) {
|
||||
...sharedItems,
|
||||
{ href: "/documents/new", label: "文书起草", icon: <FileTextOutlined /> },
|
||||
{ href: "/contracts", label: "合同生成", icon: <FileDoneOutlined /> },
|
||||
...(normalizedRole === "business" ? [{ href: "/governance/workbench", label: "复核工作台", icon: <DeploymentUnitOutlined /> }] : []),
|
||||
{ href: "/reviews", label: "审查记录", icon: <HistoryOutlined /> },
|
||||
{ href: "/chat", label: "法律问答", icon: <CommentOutlined /> },
|
||||
{ href: "/profile", label: "个人设置", icon: <UserOutlined /> },
|
||||
@@ -147,10 +146,10 @@ export function AppTopbar() {
|
||||
<header className="app-topbar">
|
||||
<div className="app-topbar-inner">
|
||||
<Link className="app-brand" href="/">
|
||||
<span className="app-brand-mark">AI</span>
|
||||
<span className="app-brand-mark" aria-hidden="true">AI</span>
|
||||
<div className="app-brand-copy">
|
||||
<strong>AI 合同平台</strong>
|
||||
<span>Contract Review Workbench</span>
|
||||
<strong>合同平台</strong>
|
||||
<span>合同审查工作台</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user