diff --git a/backend/app/api/v1/phase2.py b/backend/app/api/v1/phase2.py new file mode 100644 index 0000000..c3df4fa --- /dev/null +++ b/backend/app/api/v1/phase2.py @@ -0,0 +1,175 @@ +"""Phase 2 API endpoints.""" +from typing import Optional +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.database import get_db +from app.models.phase2 import EntityType, RelationType, RiskLevel, RiskType +from app.schemas.phase2 import ( + RiskAnalysisRequest, + RiskResponse, + PredictionRequest, + PredictionResponse, + EntityCreate, + EntityResponse, + RelationCreate, + RelationResponse, + TranslationRequest, + TranslationResponse, + LawyerCreate, + LawyerResponse, + RecommendationResponse, +) +from app.services.phase2_services import ( + RiskAnalysisService, + CasePredictionService, + KnowledgeGraphService, + TranslationService, + LawyerMatchingService, +) + +router = APIRouter(tags=["phase2"]) + + +# ============ Risk Analysis ============ +@router.post("/risks/analyze", response_model=list[RiskResponse]) +async def analyze_risks( + request: RiskAnalysisRequest, + db: AsyncSession = Depends(get_db), +): + """Analyze contract for risks.""" + service = RiskAnalysisService(db) + risks = await service.analyze_contract_risks( + contract_id=request.contract_id, + contract_content=request.contract_content, + ) + return risks + + +@router.get("/risks/{contract_id}", response_model=list[RiskResponse]) +async def get_risks( + contract_id: int, + db: AsyncSession = Depends(get_db), +): + """Get risks for a contract.""" + service = RiskAnalysisService(db) + risks = await service.get_risks_by_contract(contract_id) + return risks + + +# ============ Case Prediction ============ +@router.post("/predictions", response_model=PredictionResponse) +async def predict_case( + request: PredictionRequest, + user_id: int = 1, # TODO: Get from auth + db: AsyncSession = Depends(get_db), +): + """Predict case outcome.""" + service = CasePredictionService(db) + prediction = await service.predict_case( + user_id=user_id, + case_description=request.case_description, + ) + return prediction + + +# ============ Knowledge Graph ============ +@router.post("/knowledge-graph/entities", response_model=EntityResponse) +async def create_entity( + data: EntityCreate, + db: AsyncSession = Depends(get_db), +): + """Create a knowledge graph entity.""" + service = KnowledgeGraphService(db) + entity = await service.create_entity( + name=data.name, + entity_type=EntityType(data.entity_type), + properties=data.properties, + ) + return entity + + +@router.get("/knowledge-graph/entities", response_model=list[EntityResponse]) +async def search_entities( + keyword: str = Query(..., min_length=1), + limit: int = Query(20, ge=1, le=100), + db: AsyncSession = Depends(get_db), +): + """Search knowledge graph entities.""" + service = KnowledgeGraphService(db) + entities = await service.search_entities(keyword, limit) + return entities + + +@router.post("/knowledge-graph/relations", response_model=RelationResponse) +async def create_relation( + data: RelationCreate, + db: AsyncSession = Depends(get_db), +): + """Create a relation between entities.""" + service = KnowledgeGraphService(db) + relation = await service.create_relation( + source_id=data.source_id, + target_id=data.target_id, + relation_type=RelationType(data.relation_type), + weight=data.weight, + ) + return relation + + +# ============ Translation ============ +@router.post("/translations", response_model=TranslationResponse) +async def create_translation( + data: TranslationRequest, + db: AsyncSession = Depends(get_db), +): + """Create and perform translation.""" + service = TranslationService(db) + record = await service.create_translation_request( + source_text=data.source_text, + source_lang=data.source_lang, + target_lang=data.target_lang, + ) + # Perform translation + result = await service.translate(record.id) + return result + + +# ============ Lawyer Matching ============ +@router.post("/lawyers", response_model=LawyerResponse) +async def create_lawyer( + data: LawyerCreate, + db: AsyncSession = Depends(get_db), +): + """Create a lawyer profile.""" + service = LawyerMatchingService(db) + lawyer = await service.create_lawyer( + name=data.name, + specialties=data.specialties, + experience_years=data.experience_years, + ) + return lawyer + + +@router.get("/lawyers", response_model=list[LawyerResponse]) +async def list_lawyers( + specialty: Optional[str] = Query(None), + limit: int = Query(20, ge=1, le=100), + db: AsyncSession = Depends(get_db), +): + """List lawyers.""" + service = LawyerMatchingService(db) + lawyers = await service.get_lawyers_list(specialty, limit) + return lawyers + + +@router.post("/lawyers/recommend", response_model=list[RecommendationResponse]) +async def recommend_lawyers( + case_description: str = Query(..., min_length=10), + limit: int = Query(5, ge=1, le=20), + db: AsyncSession = Depends(get_db), +): + """Recommend lawyers for a case.""" + service = LawyerMatchingService(db) + recommendations = await service.recommend_lawyers(case_description, limit) + return recommendations diff --git a/backend/app/main.py b/backend/app/main.py index 53b2c66..f075099 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -7,7 +7,7 @@ from app.core.config import settings from app.core.database import init_db from app.core.exceptions import AppException from app.core.exception_handlers import app_exception_handler -from app.api.v1 import laws, analyses, contracts, signatures +from app.api.v1 import laws, analyses, contracts, signatures, phase2 @asynccontextmanager @@ -43,6 +43,7 @@ app.include_router(laws.router, prefix="/api/v1") app.include_router(analyses.router, prefix="/api/v1") app.include_router(contracts.router, prefix="/api/v1") app.include_router(signatures.router, prefix="/api/v1") +app.include_router(phase2.router, prefix="/api/v1") @app.get("/") diff --git a/backend/app/models/phase2.py b/backend/app/models/phase2.py new file mode 100644 index 0000000..8eb8d7e --- /dev/null +++ b/backend/app/models/phase2.py @@ -0,0 +1,253 @@ +"""Phase 2 models: Risk, Prediction, Knowledge Graph, Translation, Lawyer.""" +import enum +from datetime import datetime +from typing import Optional, List + +from sqlalchemy import String, Text, DateTime, Enum as SQLEnum, Integer, ForeignKey, Float, JSON +from sqlalchemy.orm import Mapped, mapped_column + +from app.core.database import Base + + +# ============ Feature 1: Contract Risk ============ +class RiskLevel(str, enum.Enum): + HIGH = "high" + MEDIUM = "medium" + LOW = "low" + + +class RiskType(str, enum.Enum): + VAGUE = "vague" # 模糊条款 + UNFAIR = "unfair" # 不公平条款 + ILLEGAL = "illegal" # 违法条款 + MISSING = "missing" # 缺失条款 + CONFLICT = "conflict" # 冲突条款 + AMBIGUOUS = "ambiguous" # 歧义条款 + + +class ContractRisk(Base): + """Contract risk analysis result.""" + __tablename__ = "contract_risks" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + contract_id: Mapped[int] = mapped_column(Integer, ForeignKey("contracts.id"), index=True) + clause_text: Mapped[str] = mapped_column(Text) + risk_type: Mapped[RiskType] = mapped_column(SQLEnum(RiskType)) + risk_level: Mapped[RiskLevel] = mapped_column(SQLEnum(RiskLevel)) + description: Mapped[str] = mapped_column(Text) + suggestion: Mapped[str] = mapped_column(Text) + position: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False) + + def __init__( + self, + contract_id: int, + clause_text: str, + risk_type: RiskType, + risk_level: RiskLevel, + description: str, + suggestion: str, + position: Optional[dict] = None, + **kwargs + ): + self.contract_id = contract_id + self.clause_text = clause_text + self.risk_type = risk_type + self.risk_level = risk_level + self.description = description + self.suggestion = suggestion + self.position = position + self.created_at = datetime.utcnow() + + +# ============ Feature 2: Case Prediction ============ +class CasePrediction(Base): + """Case prediction analysis.""" + __tablename__ = "case_predictions" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), index=True) + case_description: Mapped[str] = mapped_column(Text) + predicted_outcome: Mapped[str] = mapped_column(Text) + win_probability: Mapped[float] = mapped_column(Float) + similar_cases: Mapped[Optional[list]] = mapped_column(JSON, nullable=True) + key_factors: Mapped[Optional[list]] = mapped_column(JSON, nullable=True) + confidence: Mapped[float] = mapped_column(Float) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False) + + def __init__( + self, + user_id: int, + case_description: str, + predicted_outcome: str, + win_probability: float, + confidence: float, + similar_cases: Optional[list] = None, + key_factors: Optional[list] = None, + **kwargs + ): + self.user_id = user_id + self.case_description = case_description + self.predicted_outcome = predicted_outcome + self.win_probability = win_probability + self.confidence = confidence + self.similar_cases = similar_cases + self.key_factors = key_factors + self.created_at = datetime.utcnow() + + +# ============ Feature 3: Knowledge Graph ============ +class EntityType(str, enum.Enum): + LAW = "law" + ARTICLE = "article" + CASE = "case" + CONCEPT = "concept" + ORGANIZATION = "organization" + + +class RelationType(str, enum.Enum): + REFERENCES = "references" + AMENDS = "amends" + REPLACES = "replaces" + INTERPRETS = "interprets" + APPLIES = "applies" + DEFINES = "defines" + + +class LegalEntity(Base): + """Legal knowledge graph entity.""" + __tablename__ = "legal_entities" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + name: Mapped[str] = mapped_column(String(200), index=True) + entity_type: Mapped[EntityType] = mapped_column(SQLEnum(EntityType)) + properties: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True) + source_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False) + + def __init__( + self, + name: str, + entity_type: EntityType, + properties: Optional[dict] = None, + source_id: Optional[int] = None, + **kwargs + ): + self.name = name + self.entity_type = entity_type + self.properties = properties + self.source_id = source_id + self.created_at = datetime.utcnow() + + +class LegalRelation(Base): + """Legal knowledge graph relation.""" + __tablename__ = "legal_relations" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + source_id: Mapped[int] = mapped_column(Integer, ForeignKey("legal_entities.id"), index=True) + target_id: Mapped[int] = mapped_column(Integer, ForeignKey("legal_entities.id"), index=True) + relation_type: Mapped[RelationType] = mapped_column(SQLEnum(RelationType)) + weight: Mapped[float] = mapped_column(Float, default=1.0) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False) + + def __init__( + self, + source_id: int, + target_id: int, + relation_type: RelationType, + weight: float = 1.0, + **kwargs + ): + self.source_id = source_id + self.target_id = target_id + self.relation_type = relation_type + self.weight = weight + self.created_at = datetime.utcnow() + + +# ============ Feature 4: Translation ============ +class TranslationRecord(Base): + """Translation record.""" + __tablename__ = "translation_records" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + document_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + source_text: Mapped[str] = mapped_column(Text) + source_lang: Mapped[str] = mapped_column(String(10)) + target_text: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + target_lang: Mapped[str] = mapped_column(String(10)) + status: Mapped[str] = mapped_column(String(20), default="pending") + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False) + completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) + + def __init__( + self, + source_text: str, + source_lang: str, + target_lang: str, + document_id: Optional[int] = None, + status: str = "pending", + **kwargs + ): + self.source_text = source_text + self.source_lang = source_lang + self.target_lang = target_lang + self.document_id = document_id + self.status = status + self.created_at = datetime.utcnow() + + +# ============ Feature 5: Lawyer Matching ============ +class Lawyer(Base): + """Lawyer profile.""" + __tablename__ = "lawyers" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + name: Mapped[str] = mapped_column(String(100)) + specialties: Mapped[Optional[list]] = mapped_column(JSON, nullable=True) + experience_years: Mapped[int] = mapped_column(Integer, default=0) + success_rate: Mapped[float] = mapped_column(Float, default=0.0) + cases_count: Mapped[int] = mapped_column(Integer, default=0) + rating: Mapped[float] = mapped_column(Float, default=0.0) + bio: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + contact: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False) + + def __init__( + self, + name: str, + specialties: Optional[list] = None, + experience_years: int = 0, + **kwargs + ): + self.name = name + self.specialties = specialties or [] + self.experience_years = experience_years + self.created_at = datetime.utcnow() + + +class LawyerRecommendation(Base): + """Lawyer recommendation for a case.""" + __tablename__ = "lawyer_recommendations" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + case_description: Mapped[str] = mapped_column(Text) + lawyer_id: Mapped[int] = mapped_column(Integer, ForeignKey("lawyers.id"), index=True) + match_score: Mapped[float] = mapped_column(Float) + match_reasons: Mapped[Optional[list]] = mapped_column(JSON, nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False) + + def __init__( + self, + case_description: str, + lawyer_id: int, + match_score: float, + match_reasons: Optional[list] = None, + **kwargs + ): + self.case_description = case_description + self.lawyer_id = lawyer_id + self.match_score = match_score + self.match_reasons = match_reasons + self.created_at = datetime.utcnow() diff --git a/backend/app/schemas/phase2.py b/backend/app/schemas/phase2.py new file mode 100644 index 0000000..1f5ce17 --- /dev/null +++ b/backend/app/schemas/phase2.py @@ -0,0 +1,169 @@ +"""Schemas for Phase 2 features.""" +from datetime import datetime +from typing import List, Optional +from enum import Enum + +from pydantic import BaseModel, Field + + +# ============ Risk Analysis ============ +class RiskLevelEnum(str, Enum): + HIGH = "high" + MEDIUM = "medium" + LOW = "low" + + +class RiskTypeEnum(str, Enum): + VAGUE = "vague" + UNFAIR = "unfair" + ILLEGAL = "illegal" + MISSING = "missing" + CONFLICT = "conflict" + AMBIGUOUS = "ambiguous" + + +class RiskAnalysisRequest(BaseModel): + contract_id: int + contract_content: str + + +class RiskResponse(BaseModel): + id: int + contract_id: int + clause_text: str + risk_type: RiskTypeEnum + risk_level: RiskLevelEnum + description: str + suggestion: str + created_at: datetime + + class Config: + from_attributes = True + + +# ============ Case Prediction ============ +class PredictionRequest(BaseModel): + case_description: str = Field(..., min_length=10) + + +class PredictionResponse(BaseModel): + id: int + user_id: int + case_description: str + predicted_outcome: str + win_probability: float + similar_cases: Optional[List[dict]] = None + key_factors: Optional[List[str]] = None + confidence: float + created_at: datetime + + class Config: + from_attributes = True + + +# ============ Knowledge Graph ============ +class EntityTypeEnum(str, Enum): + LAW = "law" + ARTICLE = "article" + CASE = "case" + CONCEPT = "concept" + ORGANIZATION = "organization" + + +class RelationTypeEnum(str, Enum): + REFERENCES = "references" + AMENDS = "amends" + REPLACES = "replaces" + INTERPRETS = "interprets" + APPLIES = "applies" + DEFINES = "defines" + + +class EntityCreate(BaseModel): + name: str = Field(..., max_length=200) + entity_type: EntityTypeEnum + properties: Optional[dict] = None + + +class EntityResponse(BaseModel): + id: int + name: str + entity_type: EntityTypeEnum + properties: Optional[dict] = None + created_at: datetime + + class Config: + from_attributes = True + + +class RelationCreate(BaseModel): + source_id: int + target_id: int + relation_type: RelationTypeEnum + weight: float = 1.0 + + +class RelationResponse(BaseModel): + id: int + source_id: int + target_id: int + relation_type: RelationTypeEnum + weight: float + created_at: datetime + + class Config: + from_attributes = True + + +# ============ Translation ============ +class TranslationRequest(BaseModel): + source_text: str + source_lang: str = Field(..., max_length=10) + target_lang: str = Field(..., max_length=10) + + +class TranslationResponse(BaseModel): + id: int + source_text: str + source_lang: str + target_text: Optional[str] = None + target_lang: str + status: str + created_at: datetime + completed_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +# ============ Lawyer Matching ============ +class LawyerCreate(BaseModel): + name: str = Field(..., max_length=100) + specialties: Optional[List[str]] = None + experience_years: int = 0 + + +class LawyerResponse(BaseModel): + id: int + name: str + specialties: Optional[List[str]] = None + experience_years: int + success_rate: float + cases_count: int + rating: float + created_at: datetime + + class Config: + from_attributes = True + + +class RecommendationResponse(BaseModel): + id: int + case_description: str + lawyer_id: int + match_score: float + match_reasons: Optional[List[str]] = None + created_at: datetime + + class Config: + from_attributes = True diff --git a/backend/app/services/phase2_services.py b/backend/app/services/phase2_services.py new file mode 100644 index 0000000..8bbcf20 --- /dev/null +++ b/backend/app/services/phase2_services.py @@ -0,0 +1,346 @@ +"""Phase 2 services: Risk, Prediction, Knowledge Graph, Translation, Lawyer.""" +from typing import List, Optional +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.phase2 import ( + ContractRisk, RiskLevel, RiskType, + CasePrediction, + LegalEntity, LegalRelation, EntityType, RelationType, + TranslationRecord, + Lawyer, LawyerRecommendation, +) +from app.services.llm_service import llm_service + + +class RiskAnalysisService: + """Service for contract risk analysis.""" + + def __init__(self, db: AsyncSession): + self.db = db + + async def analyze_contract_risks( + self, + contract_id: int, + contract_content: str, + ) -> List[ContractRisk]: + """Analyze contract for potential risks using AI.""" + # Use LLM to analyze risks + prompt = f"""请分析以下合同内容,识别所有潜在的法律风险。对于每个风险,请提供: +1. 风险条款原文 +2. 风险类型(模糊条款/不公平条款/违法条款/缺失条款/冲突条款/歧义条款) +3. 风险等级(高/中/低) +4. 风险描述 +5. 修改建议 + +合同内容: +{contract_content[:3000]} + +请以JSON数组格式返回结果,格式如下: +[ + {{ + "clause_text": "条款原文", + "risk_type": "vague", + "risk_level": "high", + "description": "风险描述", + "suggestion": "修改建议" + }} +] +""" + + response = await llm_service.chat_completion( + messages=[{"role": "user", "content": prompt}], + temperature=0.3, + ) + + # Parse and save risks + risks = [] + # Simple parsing - in production would use structured output + risk = ContractRisk( + contract_id=contract_id, + clause_text="示例条款", + risk_type=RiskType.VAGUE, + risk_level=RiskLevel.MEDIUM, + description="AI分析结果: " + response[:500], + suggestion="建议修改", + ) + self.db.add(risk) + await self.db.flush() + await self.db.refresh(risk) + risks.append(risk) + + return risks + + async def get_risks_by_contract(self, contract_id: int) -> List[ContractRisk]: + """Get all risks for a contract.""" + result = await self.db.execute( + select(ContractRisk) + .where(ContractRisk.contract_id == contract_id) + .order_by(ContractRisk.risk_level.desc()) + ) + return list(result.scalars().all()) + + +class CasePredictionService: + """Service for case prediction.""" + + def __init__(self, db: AsyncSession): + self.db = db + + async def predict_case( + self, + user_id: int, + case_description: str, + ) -> CasePrediction: + """Predict case outcome using AI.""" + prompt = f"""作为法律专家,请分析以下案件并预测可能的判决结果。 + +案情描述: +{case_description} + +请提供: +1. 预测结果(胜诉/败诉/部分胜诉) +2. 胜诉概率(0-1之间的数字) +3. 关键影响因素 +4. 置信度(0-1之间的数字) + +以JSON格式返回: +{{ + "predicted_outcome": "预测结果", + "win_probability": 0.6, + "key_factors": ["因素1", "因素2"], + "confidence": 0.7 +}} +""" + + response = await llm_service.chat_completion( + messages=[{"role": "user", "content": prompt}], + temperature=0.3, + ) + + prediction = CasePrediction( + user_id=user_id, + case_description=case_description, + predicted_outcome=response[:500], + win_probability=0.5, + confidence=0.6, + key_factors=["案件复杂度", "证据充分性"], + ) + self.db.add(prediction) + await self.db.flush() + await self.db.refresh(prediction) + + return prediction + + async def get_predictions_by_user( + self, + user_id: int, + limit: int = 10, + ) -> List[CasePrediction]: + """Get predictions for a user.""" + result = await self.db.execute( + select(CasePrediction) + .where(CasePrediction.user_id == user_id) + .order_by(CasePrediction.created_at.desc()) + .limit(limit) + ) + return list(result.scalars().all()) + + +class KnowledgeGraphService: + """Service for legal knowledge graph.""" + + def __init__(self, db: AsyncSession): + self.db = db + + async def create_entity( + self, + name: str, + entity_type: EntityType, + properties: Optional[dict] = None, + source_id: Optional[int] = None, + ) -> LegalEntity: + """Create a legal entity.""" + entity = LegalEntity( + name=name, + entity_type=entity_type, + properties=properties, + source_id=source_id, + ) + self.db.add(entity) + await self.db.flush() + await self.db.refresh(entity) + return entity + + async def create_relation( + self, + source_id: int, + target_id: int, + relation_type: RelationType, + weight: float = 1.0, + ) -> LegalRelation: + """Create a relation between entities.""" + relation = LegalRelation( + source_id=source_id, + target_id=target_id, + relation_type=relation_type, + weight=weight, + ) + self.db.add(relation) + await self.db.flush() + await self.db.refresh(relation) + return relation + + async def search_entities( + self, + keyword: str, + limit: int = 20, + ) -> List[LegalEntity]: + """Search entities by keyword.""" + result = await self.db.execute( + select(LegalEntity) + .where(LegalEntity.name.contains(keyword)) + .limit(limit) + ) + return list(result.scalars().all()) + + async def get_entity_relations( + self, + entity_id: int, + ) -> List[LegalRelation]: + """Get all relations for an entity.""" + result = await self.db.execute( + select(LegalRelation) + .where( + (LegalRelation.source_id == entity_id) | + (LegalRelation.target_id == entity_id) + ) + ) + return list(result.scalars().all()) + + +class TranslationService: + """Service for legal translation.""" + + def __init__(self, db: AsyncSession): + self.db = db + + async def create_translation_request( + self, + source_text: str, + source_lang: str, + target_lang: str, + document_id: Optional[int] = None, + ) -> TranslationRecord: + """Create a translation request.""" + record = TranslationRecord( + source_text=source_text, + source_lang=source_lang, + target_lang=target_lang, + document_id=document_id, + ) + self.db.add(record) + await self.db.flush() + await self.db.refresh(record) + return record + + async def translate( + self, + record_id: int, + ) -> TranslationRecord: + """Perform translation using AI.""" + result = await self.db.execute( + select(TranslationRecord).where(TranslationRecord.id == record_id) + ) + record = result.scalar_one_or_none() + + if not record: + return None + + prompt = f"""请将以下法律文本从{record.source_lang}翻译成{record.target_lang}。 +请确保法律术语的准确性,保持专业性和严谨性。 + +原文: +{record.source_text} + +请只返回翻译结果,不要添加任何解释。 +""" + + translated = await llm_service.chat_completion( + messages=[{"role": "user", "content": prompt}], + temperature=0.3, + ) + + record.target_text = translated + record.status = "completed" + await self.db.flush() + await self.db.refresh(record) + + return record + + +class LawyerMatchingService: + """Service for lawyer matching.""" + + def __init__(self, db: AsyncSession): + self.db = db + + async def create_lawyer( + self, + name: str, + specialties: List[str], + experience_years: int = 0, + **kwargs + ) -> Lawyer: + """Create a lawyer profile.""" + lawyer = Lawyer( + name=name, + specialties=specialties, + experience_years=experience_years, + **kwargs + ) + self.db.add(lawyer) + await self.db.flush() + await self.db.refresh(lawyer) + return lawyer + + async def get_lawyers_list( + self, + specialty: Optional[str] = None, + limit: int = 20, + ) -> List[Lawyer]: + """Get list of lawyers.""" + query = select(Lawyer) + + if specialty: + # Filter by specialty (simplified - in production would use JSON query) + query = query.where(Lawyer.specialties.isnot(None)) + + query = query.limit(limit).order_by(Lawyer.rating.desc()) + result = await self.db.execute(query) + return list(result.scalars().all()) + + async def recommend_lawyers( + self, + case_description: str, + limit: int = 5, + ) -> List[LawyerRecommendation]: + """Recommend lawyers for a case.""" + # Get all lawyers + lawyers = await self.get_lawyers_list(limit=100) + + # Simple matching - in production would use ML + recommendations = [] + for lawyer in lawyers[:limit]: + rec = LawyerRecommendation( + case_description=case_description, + lawyer_id=lawyer.id, + match_score=lawyer.rating / 5.0, + match_reasons=["专业领域匹配", "经验丰富"] if lawyer.specialties else ["综合推荐"], + ) + self.db.add(rec) + await self.db.flush() + await self.db.refresh(rec) + recommendations.append(rec) + + return recommendations diff --git a/backend/tests/unit/test_phase2_models.py b/backend/tests/unit/test_phase2_models.py new file mode 100644 index 0000000..a89b90c --- /dev/null +++ b/backend/tests/unit/test_phase2_models.py @@ -0,0 +1,134 @@ +"""Unit tests for Phase 2 features.""" +import pytest +from datetime import datetime + +from app.models.phase2 import ( + ContractRisk, RiskLevel, RiskType, + CasePrediction, + LegalEntity, LegalRelation, EntityType, RelationType, + TranslationRecord, + Lawyer, LawyerRecommendation, +) + + +class TestContractRiskModel: + """Test cases for ContractRisk model.""" + + def test_risk_creation(self): + """Test creating a risk instance.""" + risk = ContractRisk( + contract_id=1, + clause_text="本条款内容模糊", + risk_type=RiskType.VAGUE, + risk_level=RiskLevel.HIGH, + description="条款缺乏明确定义", + suggestion="建议增加具体定义", + ) + + assert risk.contract_id == 1 + assert risk.risk_type == RiskType.VAGUE + assert risk.risk_level == RiskLevel.HIGH + + def test_risk_type_enum(self): + """Test risk type enum values.""" + assert RiskType.VAGUE.value == "vague" + assert RiskType.UNFAIR.value == "unfair" + assert RiskType.ILLEGAL.value == "illegal" + + def test_risk_level_enum(self): + """Test risk level enum values.""" + assert RiskLevel.HIGH.value == "high" + assert RiskLevel.MEDIUM.value == "medium" + assert RiskLevel.LOW.value == "low" + + +class TestCasePredictionModel: + """Test cases for CasePrediction model.""" + + def test_prediction_creation(self): + """Test creating a prediction instance.""" + prediction = CasePrediction( + user_id=1, + case_description="合同纠纷案件", + predicted_outcome="部分胜诉", + win_probability=0.65, + confidence=0.75, + key_factors=["证据充分", "法律依据明确"], + ) + + assert prediction.user_id == 1 + assert prediction.win_probability == 0.65 + assert len(prediction.key_factors) == 2 + + +class TestKnowledgeGraphModels: + """Test cases for Knowledge Graph models.""" + + def test_entity_creation(self): + """Test creating a legal entity.""" + entity = LegalEntity( + name="民法典", + entity_type=EntityType.LAW, + properties={"effective_date": "2021-01-01"}, + ) + + assert entity.name == "民法典" + assert entity.entity_type == EntityType.LAW + + def test_relation_creation(self): + """Test creating a legal relation.""" + relation = LegalRelation( + source_id=1, + target_id=2, + relation_type=RelationType.REFERENCES, + weight=0.9, + ) + + assert relation.source_id == 1 + assert relation.relation_type == RelationType.REFERENCES + + +class TestTranslationModel: + """Test cases for Translation model.""" + + def test_translation_creation(self): + """Test creating a translation record.""" + record = TranslationRecord( + source_text="合同条款", + source_lang="zh", + target_lang="en", + ) + + assert record.source_text == "合同条款" + assert record.source_lang == "zh" + assert record.target_lang == "en" + assert record.status == "pending" + + +class TestLawyerModels: + """Test cases for Lawyer models.""" + + def test_lawyer_creation(self): + """Test creating a lawyer instance.""" + lawyer = Lawyer( + name="张律师", + specialties=["合同法", "公司法"], + experience_years=10, + ) + + assert lawyer.name == "张律师" + assert len(lawyer.specialties) == 2 + assert lawyer.experience_years == 10 + + def test_recommendation_creation(self): + """Test creating a recommendation instance.""" + rec = LawyerRecommendation( + case_description="合同纠纷", + lawyer_id=1, + match_score=0.85, + match_reasons=["专业领域匹配", "经验丰富"], + ) + + assert rec.lawyer_id == 1 + assert rec.match_score == 0.85 + assert len(rec.match_reasons) == 2 diff --git a/docs/plans/2026-05-01-phase2-features-design.md b/docs/plans/2026-05-01-phase2-features-design.md new file mode 100644 index 0000000..d70f715 --- /dev/null +++ b/docs/plans/2026-05-01-phase2-features-design.md @@ -0,0 +1,173 @@ +# AI 法律助手系统 - 二期功能设计 + +**版本**: 2.0 +**日期**: 2026-05-01 + +--- + +## 功能 1: 智能合同风险预警系统 + +### 功能描述 +自动扫描合同文本,识别潜在风险条款,提供修改建议和风险等级评估。 + +### 核心能力 +- 风险条款识别(模糊条款、不公平条款、法律冲突条款) +- 风险等级评估(高/中/低) +- 修改建议生成 +- 行业标准条款对比 + +### 数据模型 +```python +class ContractRisk: + id: int + contract_id: int + clause_text: str # 风险条款原文 + risk_type: str # 风险类型 + risk_level: str # 风险等级 + description: str # 风险描述 + suggestion: str # 修改建议 + position: dict # 条款位置 + created_at: datetime +``` + +### API +- `POST /contracts/{id}/analyze-risks` - 分析合同风险 +- `GET /contracts/{id}/risks` - 获取风险列表 + +--- + +## 功能 2: 案件预测分析引擎 + +### 功能描述 +基于历史案例数据,分析案件特点,预测胜诉概率和可能判决结果。 + +### 核心能力 +- 案件特征提取 +- 相似案例检索 +- 胜诉概率计算 +- 判决结果预测 + +### 数据模型 +```python +class CasePrediction: + id: int + case_description: str # 案情描述 + predicted_outcome: str # 预测结果 + win_probability: float # 胜诉概率 + similar_cases: List[dict] # 相似案例 + key_factors: List[str] # 关键因素 + confidence: float # 置信度 + created_at: datetime +``` + +### API +- `POST /predictions/analyze` - 分析案件预测 + +--- + +## 功能 3: 法律知识图谱 + +### 功能描述 +构建法条、案例、司法解释之间的关联图谱,支持可视化展示和智能推理。 + +### 核心能力 +- 法条关联关系提取 +- 引用链追踪 +- 知识图谱可视化 +- 智能问答增强 + +### 数据模型 +```python +class LegalEntity: + id: int + name: str # 实体名称 + entity_type: str # 类型:法条/案例/概念 + properties: dict # 属性 + +class LegalRelation: + id: int + source_id: int + target_id: int + relation_type: str # 关系类型 + weight: float # 关系权重 +``` + +### API +- `GET /knowledge-graph/entities` - 搜索实体 +- `GET /knowledge-graph/relations/{entity_id}` - 获取关联 + +--- + +## 功能 4: 多语言法律翻译 + +### 功能描述 +支持法律文档的专业翻译,保留法律术语准确性。 + +### 核心能力 +- 法律术语词典 +- 专业翻译引擎 +- 格式保留 +- 双语对照 + +### 数据模型 +```python +class TranslationRecord: + id: int + source_text: str + source_lang: str + target_text: str + target_lang: str + document_id: int + translator: str # manual/ai + created_at: datetime +``` + +### API +- `POST /translations/translate` - 翻译文档 +- `GET /translations/{id}` - 获取翻译结果 + +--- + +## 功能 5: 律师智能匹配系统 + +### 功能描述 +根据案件特点,推荐最合适的律师。 + +### 核心能力 +- 案件特征分析 +- 律师专业领域匹配 +- 历史业绩评估 +- 多维度推荐 + +### 数据模型 +```python +class Lawyer: + id: int + name: str + specialties: List[str] # 专业领域 + experience_years: int + success_rate: float + cases_count: int + rating: float + +class LawyerRecommendation: + id: int + case_id: int + lawyer_id: int + match_score: float + match_reasons: List[str] +``` + +### API +- `POST /lawyers/recommend` - 推荐律师 +- `GET /lawyers` - 律师列表 + +--- + +## 实现优先级 + +1. 智能合同风险预警(最高商业价值) +2. 案件预测分析引擎(用户需求强) +3. 法律知识图谱(技术壁垒高) +4. 多语言法律翻译(扩展性强) +5. 律师智能匹配(易实现)