fix somethings

This commit is contained in:
2026-06-08 11:16:28 +08:00
parent 9fea9c6a53
commit e7963b267e
34 changed files with 5195 additions and 246 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,328 @@
# Regulatory Signals Intelligence Enhancement — Design Spec
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Replace the 20-item hardcoded MockEventStore with real regulatory data from Chinese and international sources, add LLM-driven structured extraction, impact assessment, and semantic change diff — all accessible through a manual-trigger crawl in the frontend.
**Architecture:** Crawler Service (httpx + BeautifulSoup) → PostgreSQL EventStore → LLM Pipeline (extract → assess → diff) → existing PerceptionService interface. New code follows `api → application → domain ports → infrastructure` layering; no new files in `services/*` or `workflows/*`; `shared/bootstrap.py` is the composition root.
**Tech Stack:** httpx, BeautifulSoup4, sentence-transformers (for diff), existing LLM factory (deepseek/qwen), existing KnowledgeRetrievalService (RAG), PostgreSQL (already available), existing SSE infrastructure.
---
## 1. Data Sources
| Source | URL | Method | Coverage |
|--------|-----|--------|----------|
| CATARC 汽车标准 | `https://www.catarc.org.cn/bzzxd/qcbz/index.html` | httpx + BeautifulSoup (static pages) | 国家/行业汽车标准列表 |
| 国标委强制性标准 | `https://openstd.samr.gov.cn/bzgk/std/std_list_type?p.p1=1&p.p2=车&p.p90=circulation_date&p.p91=desc` | httpx + JSON API parse | 强制性国家标准,按"车"过滤 |
| 国标委推荐性标准 | `https://openstd.samr.gov.cn/bzgk/std/std_list_type?p.p1=2&p.p2=车&p.p90=circulation_date&p.p91=desc` | httpx + JSON API parse | 推荐性国家标准,按"车"过滤 |
| EUR-Lex | RSS + CELLAR REST API | pyeurlex / httpx | EU AI Act, automotive directives |
| UN R155/R156 | CELLAR REST API (CELEX lookup) | httpx | UN-ECE cybersecurity/OTA regulations |
Crawl is **manual-trigger only** — no cron/Celery Beat. Admin clicks "刷新数据源" in the frontend UI.
---
## 2. Database Schema
### New table: `regulation_events`
```sql
CREATE TABLE IF NOT EXISTS regulation_events (
id TEXT PRIMARY KEY, -- sha256(source + standard_code)[:12]
source TEXT NOT NULL, -- 'CATARC' | '国标委' | 'EUR-Lex' | 'UN-ECE'
source_label TEXT, -- Human-readable source label
standard_code TEXT NOT NULL, -- e.g. "GB 18384-2025", "EU/2024/1689"
title TEXT NOT NULL,
summary TEXT, -- Crawled abstract or first paragraph
full_text_url TEXT, -- Original page URL
status TEXT, -- 'enacted' | 'draft' | 'consultation'
impact_level TEXT, -- 'high' | 'medium' | 'low' (LLM-assigned)
published_at DATE,
effective_at DATE,
category TEXT,
tags TEXT[],
-- LLM structured extraction
obligations JSONB, -- [{text, deontic, subject, object, condition}]
deadlines JSONB, -- [{date, description}]
scope TEXT, -- Applicability scope summary
penalties TEXT, -- Penalty / consequence summary
-- Change tracking
content_hash TEXT, -- SHA256 of crawled full text
previous_hash TEXT, -- Hash from prior crawl (NULL on first crawl)
change_summary TEXT, -- LLM-generated description of changes
changed_sections JSONB, -- [{old_text, new_text, change_type}] where cosine<0.85
-- Impact assessment
affected_docs JSONB, -- [{doc_id, doc_name, score, key_clauses, recommendation}]
-- Metadata
crawled_at TIMESTAMPTZ DEFAULT now(),
processed_at TIMESTAMPTZ,
raw_storage_key TEXT -- MinIO path for raw HTML/PDF (optional)
);
CREATE INDEX IF NOT EXISTS regulation_events_source_date
ON regulation_events (source, published_at DESC);
CREATE INDEX IF NOT EXISTS regulation_events_impact_date
ON regulation_events (impact_level, published_at DESC);
CREATE INDEX IF NOT EXISTS regulation_events_tags
ON regulation_events USING gin(tags);
```
---
## 3. Backend Architecture
### 3.1 File Map
**New files (infrastructure layer):**
- `backend/app/infrastructure/perception/crawlers/catarc_crawler.py` — CATARC scraper
- `backend/app/infrastructure/perception/crawlers/guobiao_crawler.py` — 国标委 JSON API crawler
- `backend/app/infrastructure/perception/crawlers/eurlex_crawler.py` — EUR-Lex RSS + CELLAR
- `backend/app/infrastructure/perception/crawlers/base.py` — Abstract base class
- `backend/app/infrastructure/perception/postgres_event_store.py` — PostgresEventStore (replaces MockEventStore)
- `backend/app/infrastructure/perception/llm_pipeline.py` — Extract / assess / diff pipeline
**New files (application layer):**
- `backend/app/application/perception/crawl_service.py` — Orchestrates crawlers + LLM pipeline, exposes `run_crawl(sources)` + progress generator
**Modified files:**
- `backend/app/api/routes/perception.py` — Add `POST /crawl`, `GET /crawl/status` (SSE), `POST /events/{id}/process`, `GET /events/{id}/diff`
- `backend/app/shared/bootstrap.py` — Wire `PostgresEventStore` + `CrawlService` + `LlmPipeline` when `DOCUMENT_REPOSITORY_BACKEND=postgres`; fallback to `MockEventStore` when `json`
- `backend/app/config/settings.py` — Add `perception_crawl_timeout_seconds`, `perception_max_events_per_source`
**Unchanged files:**
- `backend/app/application/perception/services.py``PerceptionService` interface unchanged; only `_store` swap
- `backend/app/infrastructure/perception/mock_event_store.py` — Kept for `json` backend mode
### 3.2 Domain Port (Abstract Interface)
```python
# backend/app/infrastructure/perception/base_event_store.py
from abc import ABC, abstractmethod
class BaseEventStore(ABC):
@abstractmethod
def all(self) -> list[dict]: ...
@abstractmethod
def get(self, event_id: str) -> dict | None: ...
@abstractmethod
def filter(self, source=None, impact_level=None, limit=50) -> list[dict]: ...
@abstractmethod
def stats(self) -> dict: ...
@abstractmethod
def upsert(self, event: dict) -> None: ... # new — needed for crawl writes
@abstractmethod
def get_by_standard_code(self, code: str) -> dict | None: ... # for change detection
```
`MockEventStore` and `PostgresEventStore` both implement this interface.
### 3.3 Crawler Base Contract
```python
# backend/app/infrastructure/perception/crawlers/base.py
from abc import ABC, abstractmethod
from dataclasses import dataclass
@dataclass
class RawEvent:
source: str
source_label: str
standard_code: str
title: str
summary: str
full_text_url: str
status: str # 'enacted' | 'draft' | 'consultation'
published_at: str # YYYY-MM-DD string
effective_at: str | None
category: str
tags: list[str]
raw_text: str # full crawled text for hashing + LLM
class BaseCrawler(ABC):
@abstractmethod
def fetch(self, limit: int = 50) -> list[RawEvent]: ...
```
### 3.4 LLM Pipeline
```python
# backend/app/infrastructure/perception/llm_pipeline.py
class LlmPipeline:
"""Runs three sequential LLM steps on a regulation event."""
def extract_structure(self, event: dict) -> dict:
"""Step 1: Extract obligations, deadlines, scope, penalties, impact_level.
Returns dict with keys: obligations, deadlines, scope, penalties, impact_level.
Uses JSON-mode or structured prompt; model retries once on parse failure.
"""
def assess_impact(self, event: dict, retrieval_service) -> list[dict]:
"""Step 2: RAG-based impact on existing knowledge base documents.
Query = standard_code + title + first obligation texts.
Returns list of {doc_id, doc_name, score, key_clauses, recommendation}.
"""
def compute_diff(self, old_text: str, new_text: str) -> dict:
"""Step 3: Semantic diff between old and new regulation text.
Splits both texts by paragraph. Calls existing EmbeddingService (text-embedding-v3
via EMBEDDING_BASE_URL) to embed each paragraph, then computes cosine similarity.
Changed paragraphs (cosine < 0.85) sent to LLM for change_type classification:
'tightened' | 'relaxed' | 'added' | 'removed'
Returns {changed_sections: [...], change_summary: str}.
Only called when content_hash differs from previous_hash.
"""
```
### 3.5 CrawlService
```python
# backend/app/application/perception/crawl_service.py
class CrawlService:
def __init__(self, crawlers, event_store, llm_pipeline, retrieval_service): ...
def run_crawl(self, sources: list[str] | None = None) -> Generator[dict, None, None]:
"""Manual-trigger crawl. Yields progress SSE dicts:
{event: 'progress', data: {source, fetched, new, updated, stage}}
{event: 'done', data: {total_new, total_updated, duration_ms}}
{event: 'error', data: {source, message}}
For each crawler:
1. fetch() RawEvents
2. hash check vs stored event → skip if unchanged
3. upsert raw event to DB
4. run LLM pipeline (extract → assess → diff)
5. upsert enriched event to DB
6. yield progress
"""
```
---
## 4. API Endpoints
### Existing (unchanged interface, new store backend)
- `GET /api/v1/perception/stats`
- `GET /api/v1/perception/events`
- `GET /api/v1/perception/events/{id}`
- `POST /api/v1/perception/events/{id}/analyze` (streaming)
### New endpoints
```
POST /api/v1/perception/crawl
Body: { sources?: ["CATARC", "国标委", "EUR-Lex", "UN-ECE"] }
Response: text/event-stream (SSE)
Auth: requires current_user (admin/legal role)
Streams progress events until done or error.
POST /api/v1/perception/events/{id}/process
Trigger LLM pipeline for a single already-crawled event.
Response: { status: "ok", processed_at: "..." }
Auth: requires current_user
GET /api/v1/perception/events/{id}/diff
Returns: { changed_sections: [...], change_summary: str, previous_hash: str }
Returns 404 if no diff available (first crawl or no change detected).
```
---
## 5. Frontend Changes
### 5.1 New: Crawl Control Bar (top of PerceptionPage)
Above the stats-bar, add a `<CrawlBar>` component:
- "刷新数据源" button — triggers `POST /crawl` (all sources)
- Inline progress display: shows SSE progress events as a mini status line
- e.g. "CATARC: 抓取中… | 国标委: 12 条新增 | EUR-Lex: 等待中"
- On completion: shows "更新完成 — 新增 N 条,更新 M 条"
- Disabled while crawl is in progress (prevents double-trigger)
### 5.2 Signal Card Enhancement
Existing cards get two new indicators:
- **NEW badge** — shown when `crawled_at` is within last 24h (green dot)
- **CHANGED badge** — shown when `previous_hash != content_hash` and `change_summary` exists
### 5.3 Right Panel — Structured Tab
Right detail panel adds a tab bar: **概览 | 义务条款 | 影响评估 | 变更对比**
**义务条款 tab:**
- Table: 义务描述 | 主体 | 对象 | 截止日期
- Tags for deontic type: 强制 / 禁止 / 允许
- Shows `obligations[]` + `deadlines[]` from DB
**影响评估 tab:**
- Replaces hardcoded MOCK_DOCS with real `affected_docs[]` from DB
- Each row: document name, similarity score (%), key clause excerpt, LLM recommendation
- "Run fresh assessment" button → triggers `POST /events/{id}/process`
**变更对比 tab:**
- Only visible when `change_summary` is non-null
- Top: `change_summary` text (LLM prose)
- Below: diff table with old/new paragraph pairs, change_type badge per row
- Hidden (tab disabled) on first-crawl events with no prior version
### 5.4 Existing behavior preserved
- `analyze` streaming (AI analysis) unchanged
- Search/filter (source, impact) unchanged — now hits real DB data
- Stats bar — now reflects real counts from PostgreSQL
---
## 6. Settings Additions
```python
# backend/app/config/settings.py additions
perception_crawl_timeout_seconds: int = Field(default=120, ...)
perception_max_events_per_source: int = Field(default=100, ...)
perception_diff_similarity_threshold: float = Field(default=0.85, ...)
```
```env
# .env additions
PERCEPTION_CRAWL_TIMEOUT_SECONDS=120
PERCEPTION_MAX_EVENTS_PER_SOURCE=100
PERCEPTION_DIFF_SIMILARITY_THRESHOLD=0.85
```
---
## 7. Dependencies
```
# requirements.txt additions
httpx>=0.27.0 # already likely present; confirm
beautifulsoup4>=4.12.0 # HTML parsing for CATARC
lxml>=5.0.0 # BeautifulSoup parser backend
# sentence-transformers NOT added — diff uses existing text-embedding-v3 API (EMBEDDING_BASE_URL)
```
No new infrastructure required (PostgreSQL + MinIO + Milvus already available).
---
## 8. Backward Compatibility
- `DOCUMENT_REPOSITORY_BACKEND=json``bootstrap.py` uses `MockEventStore` (unchanged behavior)
- `DOCUMENT_REPOSITORY_BACKEND=postgres` → uses `PostgresEventStore`
- Migration: run `CREATE TABLE` SQL on first startup (idempotent `CREATE TABLE IF NOT EXISTS`)
- Existing 20 mock events are not seeded to PostgreSQL; PostgreSQL starts empty until first crawl
---
## 9. Out of Scope (this phase)
- Automatic/scheduled crawling (Celery Beat) — manual trigger only
- Playwright-based JS-rendered pages — all target sites work with httpx
- Knowledge Graph (Neo4j / LightRAG) — future phase
- Email/Slack webhook notifications — future phase
- User-facing diff history (versioning beyond one prior snapshot) — future phase