Files
resume-agent/backend/app/main.py
T
2026-07-20 14:48:41 +08:00

154 lines
4.9 KiB
Python

from __future__ import annotations
import os
from pathlib import Path
from typing import Any
from uuid import uuid4
from fastapi import FastAPI, Response, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from .agent import ResumeAgent
from .database import Database
from .fsm import FSMError
from .llm_services import build_services
from .models import (
ActionResponse,
ComponentEventRequest,
CreateResumeRequest,
CreateResumeResponse,
CreateSessionRequest,
ErrorDetail,
MessageRequest,
TimelineResponse,
)
from .services import (
ExperienceExtractor,
ResumeRewriter,
)
from .settings import Settings, load_settings
API_PREFIX = "/ai-api/resume-agent"
def create_app(
*,
database_path: str | Path | None = None,
extractor: ExperienceExtractor | None = None,
rewriter: ResumeRewriter | None = None,
cors_origins: list[str] | None = None,
settings: Settings | None = None,
openai_client: Any | None = None,
) -> FastAPI:
default_database = Path(__file__).resolve().parent.parent / "data" / "resume_agent.db"
database = Database(database_path or os.getenv("RESUME_AGENT_DATABASE", default_database))
database.initialize()
if extractor is None or rewriter is None:
default_extractor, default_rewriter = build_services(
settings or load_settings(), openai_client
)
extractor = extractor or default_extractor
rewriter = rewriter or default_rewriter
agent = ResumeAgent(database, extractor, rewriter)
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", "DELETE", "OPTIONS"],
allow_headers=["*"],
)
application.state.database = database
application.state.resume_agent = agent
@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}}/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())
@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()