mirror of
https://github.com/velocitatem/cvfs.git
synced 2026-05-31 16:53:38 +00:00
feat: NLP patch insights + standalone demo mode
- dlib/ai/insights.py: pure-Python NLP analysis that correlates accepted AI suggestion operations/keywords/sections with submission outcomes (pending_review / published = positive, archived = negative) - Backend: GET /api/v1/insights route + service + Pydantic schema - Frontend: InsightsPanel component with bar charts for operation impact, section impact, and keyword signal lift scores - Insights tab added to the version panel; compact preview on doc overview - NEXT_PUBLIC_DEMO=true makes the webapp fully standalone: loads DEMO_DOCUMENTS / DEMO_SUBMISSIONS / DEMO_INSIGHTS from demo-data.ts, disables all mutating actions, shows a DEMO badge in the top bar - apps/webapp/public/demo-cv.docx: static dummy CV (Alex Rivera) for demo - scripts/gen_demo_cv.py: script to regenerate the demo DOCX - .env.example: document NEXT_PUBLIC_DEMO flag https://claude.ai/code/session_01LWxu2qrwY6BRjUFXXn7NiM
This commit is contained in:
@@ -2,10 +2,11 @@ from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.routes import documents, versions, submissions, public
|
||||
from app.api.routes import documents, insights, versions, submissions, public
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(documents.router)
|
||||
api_router.include_router(versions.router)
|
||||
api_router.include_router(submissions.router)
|
||||
api_router.include_router(public.router)
|
||||
api_router.include_router(insights.router)
|
||||
|
||||
41
apps/backend/fastapi/app/api/routes/insights.py
Normal file
41
apps/backend/fastapi/app/api/routes/insights.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api.deps import get_current_user, get_db
|
||||
from app.schemas.insights import InsightsResponse
|
||||
from app.services.insights import get_insights
|
||||
from dlib.auth import AuthenticatedUser
|
||||
|
||||
router = APIRouter(prefix="/insights", tags=["insights"])
|
||||
|
||||
|
||||
@router.get("", response_model=InsightsResponse)
|
||||
async def insights_endpoint(
|
||||
session: AsyncSession = Depends(get_db),
|
||||
user: AuthenticatedUser = Depends(get_current_user),
|
||||
):
|
||||
result = await get_insights(session, owner_id=user.sub)
|
||||
return InsightsResponse(
|
||||
total_submissions=result.total_submissions,
|
||||
positive_count=result.positive_count,
|
||||
positive_rate=result.positive_rate,
|
||||
operation_impact=[
|
||||
{"operation": o.operation, "total": o.total, "positive": o.positive, "rate": o.rate}
|
||||
for o in result.operation_impact
|
||||
],
|
||||
top_positive_keywords=[
|
||||
{"keyword": k.keyword, "positive_count": k.positive_count, "negative_count": k.negative_count, "lift": k.lift}
|
||||
for k in result.top_positive_keywords
|
||||
],
|
||||
top_negative_keywords=[
|
||||
{"keyword": k.keyword, "positive_count": k.positive_count, "negative_count": k.negative_count, "lift": k.lift}
|
||||
for k in result.top_negative_keywords
|
||||
],
|
||||
section_impact=[
|
||||
{"section": s.section, "positive_rate": s.positive_rate, "count": s.count}
|
||||
for s in result.section_impact
|
||||
],
|
||||
has_data=result.has_data,
|
||||
)
|
||||
34
apps/backend/fastapi/app/schemas/insights.py
Normal file
34
apps/backend/fastapi/app/schemas/insights.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class OperationImpactSchema(BaseModel):
|
||||
operation: str
|
||||
total: int
|
||||
positive: int
|
||||
rate: float
|
||||
|
||||
|
||||
class KeywordSignalSchema(BaseModel):
|
||||
keyword: str
|
||||
positive_count: int
|
||||
negative_count: int
|
||||
lift: float
|
||||
|
||||
|
||||
class SectionImpactSchema(BaseModel):
|
||||
section: str
|
||||
positive_rate: float
|
||||
count: int
|
||||
|
||||
|
||||
class InsightsResponse(BaseModel):
|
||||
total_submissions: int
|
||||
positive_count: int
|
||||
positive_rate: float
|
||||
operation_impact: list[OperationImpactSchema]
|
||||
top_positive_keywords: list[KeywordSignalSchema]
|
||||
top_negative_keywords: list[KeywordSignalSchema]
|
||||
section_impact: list[SectionImpactSchema]
|
||||
has_data: bool
|
||||
37
apps/backend/fastapi/app/services/insights.py
Normal file
37
apps/backend/fastapi/app/services/insights.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from dlib.ai.insights import InsightsResult, SubmissionRecord, SuggestionRecord, analyze
|
||||
from app.models import AiSuggestion, CvDocument, CvVersion, Submission
|
||||
|
||||
|
||||
async def get_insights(session: AsyncSession, *, owner_id: str) -> InsightsResult:
|
||||
stmt = (
|
||||
select(Submission)
|
||||
.join(Submission.version)
|
||||
.join(CvVersion.document)
|
||||
.where(CvDocument.owner_id == owner_id)
|
||||
.options(selectinload(Submission.suggestions))
|
||||
)
|
||||
rows = list((await session.execute(stmt)).scalars().all())
|
||||
|
||||
records = [
|
||||
SubmissionRecord(
|
||||
status=s.status.value,
|
||||
suggestions=[
|
||||
SuggestionRecord(
|
||||
operation=sug.operation,
|
||||
target_path=sug.target_path,
|
||||
proposed_text=sug.proposed_text,
|
||||
rationale=sug.rationale,
|
||||
accepted=sug.accepted,
|
||||
)
|
||||
for sug in s.suggestions
|
||||
],
|
||||
)
|
||||
for s in rows
|
||||
]
|
||||
return analyze(records)
|
||||
Reference in New Issue
Block a user