Files
resume-agent/backend/app/main.py
T
hypandClaude ae2d9b128d feat: builder 简历生成 + 轻度优化 + 简历导入交付副本
自内部仓库剥离深度优化与 RAG 知识库后的交付版本:
- Builder 对话式简历生成(FSM + 意图路由 LLM 兜底增强)
- 条目级轻度优化:事实覆盖门禁 + STAR/bullet 修复链,功能/简介/成果与技术栈同级保护
- 简历导入:DOCX/PDF 解析、结构归一、手机号脱敏
- PostgreSQL 运行时 + Alembic 迁移链

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 11:08:30 +08:00

240 lines
8.8 KiB
Python

from __future__ import annotations
import logging
import os
from pathlib import Path
from typing import Any
from uuid import uuid4
from fastapi import FastAPI, Header, HTTPException, Response, UploadFile, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from .agent import ResumeAgent
from .experience_optimizer import ExperienceOptimizer, build_experience_optimizer
from .target_position_suggester import TargetPositionSuggester, build_target_position_suggester
from .resume_expansion import build_expander
from .profile_summary import ProfileSummaryGenerator, build_profile_summary_generator
from .database import Database
from .postgres_database import PostgresDatabase
from .fsm import FSMError
from .builder_sse import stream_builder_message
from .llm_services import OpenAICompatibleStructuredClient, build_services
from .models import (
ActionResponse,
ComponentEventRequest,
CreateResumeRequest,
CreateResumeResponse,
CreateSessionRequest,
ErrorDetail,
MessageRequest,
TimelineResponse,
)
from .resume_routes import register_resume_routes
from .resume_import_routes import register_resume_import_routes
from .resume_import_service import ResumeImportService, RuleBasedResumeImportParser
from .import_parser import OpenAIResumeImportParser
from .rate_limit import SlidingWindowRateLimiter
from .services import (
EntryExpander,
ExperienceExtractor,
ResumeRewriter,
RuleBasedEntryExpander,
)
from .settings import Settings, load_settings
from .skill_suggester import SkillSuggester, build_skill_suggester
API_PREFIX = "/ai-api/resume-agent"
def create_app(
*,
database_path: str | Path | None = None,
extractor: ExperienceExtractor | None = None,
rewriter: ResumeRewriter | None = None,
expander: EntryExpander | None = None,
skill_suggester: SkillSuggester | None = None,
experience_optimizer: ExperienceOptimizer | None = None,
target_position_suggester: TargetPositionSuggester | None = None,
cors_origins: list[str] | None = None,
settings: Settings | None = None,
openai_client: Any | None = None,
resume_import_service: ResumeImportService | None = None,
profile_summary_generator: ProfileSummaryGenerator | None = None,
) -> FastAPI:
resolved_settings = settings or load_settings()
if database_path is not None:
database = Database(database_path)
else:
if not resolved_settings.database_url:
raise ValueError("DATABASE_URL is required; pass database_path explicitly for SQLite tests")
database = PostgresDatabase(
resolved_settings.database_url,
schema=os.getenv("RESUME_AGENT_DATABASE_SCHEMA", "resume_agent"),
)
database.initialize()
if extractor is None or rewriter is None:
default_extractor, default_rewriter = build_services(
resolved_settings, openai_client
)
extractor = extractor or default_extractor
rewriter = rewriter or default_rewriter
configured_default_tier = os.environ.get("RESUME_AGENT_DEFAULT_TIER", "free").strip().lower()
if configured_default_tier != "free":
logging.getLogger(__name__).warning(
"TIER BACKDOOR ACTIVE: all sessions default to %s",
configured_default_tier,
)
if expander is None:
expander = build_expander(resolved_settings, openai_client)
skill_suggester = skill_suggester or build_skill_suggester(resolved_settings, openai_client)
experience_optimizer = experience_optimizer or build_experience_optimizer(
resolved_settings, openai_client
)
target_position_suggester = target_position_suggester or build_target_position_suggester(
resolved_settings, openai_client
)
profile_summary_generator = profile_summary_generator or build_profile_summary_generator(
resolved_settings, openai_client
)
agent = ResumeAgent(
database=database,
extractor=extractor,
rewriter=rewriter,
expander=expander,
skill_suggester=skill_suggester,
experience_optimizer=experience_optimizer,
target_position_suggester=target_position_suggester,
profile_summary_generator=profile_summary_generator,
)
application = FastAPI(
title="Resume Agent MVP",
version="0.1.0",
description="SQLite-backed resume workflow implemented as an explicit finite-state machine.",
)
origins = cors_origins or _cors_origins_from_environment()
application.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials="*" not in origins,
allow_methods=["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
allow_headers=["*"],
)
application.state.database = database
application.state.resume_agent = agent
if resume_import_service is None:
import_fallback = RuleBasedResumeImportParser()
import_parser = import_fallback
if resolved_settings.use_openai:
import_parser = OpenAIResumeImportParser(
completion=OpenAICompatibleStructuredClient(resolved_settings, openai_client),
fallback=import_fallback,
)
resume_import_service = ResumeImportService(
storage_root=Path(__file__).resolve().parent.parent / "data" / "resume_imports",
parser=import_parser,
)
application.state.resume_import_service = resume_import_service
application.state.light_opt_limiter = SlidingWindowRateLimiter(
limit=resolved_settings.light_opt_rate_limit,
window_seconds=resolved_settings.light_opt_rate_window_seconds,
)
@application.exception_handler(FSMError)
async def handle_fsm_error(_request: Any, exc: FSMError) -> JSONResponse:
trace_id = f"trace_{uuid4().hex}"
detail = ErrorDetail(
code=exc.code,
message=exc.message,
missing_fields=exc.missing_fields,
trace_id=trace_id,
)
return JSONResponse(
status_code=exc.status_code,
content={"error": detail.model_dump(mode="json"), "trace_id": trace_id},
)
@application.get("/health", tags=["system"])
def health() -> dict[str, str]:
return {"status": "ok"}
@application.post(
f"{API_PREFIX}/sessions",
response_model=TimelineResponse,
status_code=status.HTTP_201_CREATED,
tags=["resume-agent"],
)
def create_session(request: CreateSessionRequest | None = None) -> TimelineResponse:
return agent.create_session(request or CreateSessionRequest())
@application.get(
f"{API_PREFIX}/sessions/{{session_id}}/timeline",
response_model=TimelineResponse,
tags=["resume-agent"],
)
def get_timeline(session_id: str) -> TimelineResponse:
return agent.timeline(session_id)
@application.post(
f"{API_PREFIX}/sessions/{{session_id}}/component-events",
response_model=ActionResponse,
tags=["resume-agent"],
)
def post_component_event(
session_id: str, request: ComponentEventRequest
) -> ActionResponse:
return agent.component_event(session_id, request)
@application.post(
f"{API_PREFIX}/sessions/{{session_id}}/messages",
response_model=ActionResponse,
tags=["resume-agent"],
)
def post_message(session_id: str, request: MessageRequest) -> ActionResponse:
return agent.add_message(session_id, request)
@application.post(
f"{API_PREFIX}/sessions/{{session_id}}/messages/stream",
tags=["resume-agent"],
)
def post_message_stream(session_id: str, request: MessageRequest):
return stream_builder_message(lambda: agent.add_message(session_id, request))
@application.post(
f"{API_PREFIX}/sessions/{{session_id}}/create",
response_model=CreateResumeResponse,
tags=["resume-agent"],
)
def create_resume(
session_id: str, request: CreateResumeRequest | None = None
) -> CreateResumeResponse:
return agent.create_resume(session_id, request or CreateResumeRequest())
register_resume_routes(application, agent, API_PREFIX)
register_resume_import_routes(
application, agent, application.state.resume_import_service, API_PREFIX
)
@application.delete(
f"{API_PREFIX}/sessions/{{session_id}}",
status_code=status.HTTP_204_NO_CONTENT,
tags=["resume-agent"],
)
def delete_session(session_id: str) -> Response:
agent.delete_session(session_id)
return Response(status_code=status.HTTP_204_NO_CONTENT)
return application
def _cors_origins_from_environment() -> list[str]:
configured = os.getenv("RESUME_AGENT_CORS_ORIGINS")
if configured:
return [origin.strip() for origin in configured.split(",") if origin.strip()]
return ["http://localhost:5173", "http://127.0.0.1:5173"]
app = create_app()