generated from kgod/ai-review-template
自内部仓库剥离深度优化与 RAG 知识库后的交付版本: - Builder 对话式简历生成(FSM + 意图路由 LLM 兜底增强) - 条目级轻度优化:事实覆盖门禁 + STAR/bullet 修复链,功能/简介/成果与技术栈同级保护 - 简历导入:DOCX/PDF 解析、结构归一、手机号脱敏 - PostgreSQL 运行时 + Alembic 迁移链 Co-Authored-By: Claude <noreply@anthropic.com>
240 lines
8.8 KiB
Python
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()
|