Implements docs/api-contract-v2.md. Migration 002 adds follow-up fields. EchoTransport replaced by SMTP->Clipboard selection behind unchanged approval gate. Scheduler (APScheduler) daily 07:00 fetch+score, env-gated, default off. Test image installs workspace packages; build context moved to repo root. Recovered and committed by integration lead after W2 worker hit iteration limit.
322 lines
No EOL
6.3 KiB
Python
322 lines
No EOL
6.3 KiB
Python
"""Pydantic schemas for all API contract payloads."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date
|
|
from typing import Any
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
# --- Profile ---
|
|
|
|
class ProfileBase(BaseModel):
|
|
full_name: str = ""
|
|
email: str = ""
|
|
phone: str | None = None
|
|
location: str | None = None
|
|
headline: str | None = None
|
|
summary: str | None = None
|
|
languages: list[dict[str, Any]] = Field(default_factory=list)
|
|
hard_rules: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
|
|
class ProfileUpdate(BaseModel):
|
|
full_name: str | None = None
|
|
email: str | None = None
|
|
phone: str | None = None
|
|
location: str | None = None
|
|
headline: str | None = None
|
|
summary: str | None = None
|
|
languages: list[dict[str, Any]] | None = None
|
|
hard_rules: dict[str, Any] | None = None
|
|
|
|
|
|
class ProfileOut(BaseModel):
|
|
id: str
|
|
full_name: str
|
|
email: str
|
|
phone: str | None = None
|
|
location: str | None = None
|
|
headline: str | None = None
|
|
summary: str | None = None
|
|
languages: list[dict[str, Any]] = []
|
|
hard_rules: dict[str, Any] = {}
|
|
|
|
|
|
# --- CV Section ---
|
|
|
|
class CvSectionCreate(BaseModel):
|
|
kind: str
|
|
title: str
|
|
org: str | None = None
|
|
location: str | None = None
|
|
start_date: date | None = None
|
|
end_date: date | None = None
|
|
bullets: list[str] = Field(default_factory=list)
|
|
tags: list[str] = Field(default_factory=list)
|
|
sort_order: int = 0
|
|
|
|
|
|
class CvSectionUpdate(BaseModel):
|
|
kind: str | None = None
|
|
title: str | None = None
|
|
org: str | None = None
|
|
location: str | None = None
|
|
start_date: date | None = None
|
|
end_date: date | None = None
|
|
bullets: list[str] | None = None
|
|
tags: list[str] | None = None
|
|
sort_order: int | None = None
|
|
|
|
|
|
class CvSectionOut(BaseModel):
|
|
id: str
|
|
profile_id: str
|
|
kind: str
|
|
title: str
|
|
org: str | None = None
|
|
location: str | None = None
|
|
start_date: date | None = None
|
|
end_date: date | None = None
|
|
bullets: list[str] = []
|
|
tags: list[str] = []
|
|
sort_order: int = 0
|
|
|
|
|
|
class AiAssistRequest(BaseModel):
|
|
instruction: str
|
|
|
|
|
|
class AiAssistResponse(BaseModel):
|
|
suggestions: list[str]
|
|
|
|
|
|
# --- Job Posting ---
|
|
|
|
class JobPostingCreate(BaseModel):
|
|
url: str
|
|
|
|
|
|
class JobPostingOut(BaseModel):
|
|
id: str
|
|
source: str
|
|
external_id: str | None = None
|
|
url: str
|
|
company: str
|
|
title: str
|
|
location: str | None = None
|
|
description: str = ""
|
|
fetched_at: str | None = None
|
|
|
|
|
|
class ScoreResponse(BaseModel):
|
|
score: float
|
|
rationale: dict[str, Any]
|
|
|
|
|
|
# --- Application ---
|
|
|
|
class ApplicationOut(BaseModel):
|
|
id: str
|
|
job_posting_id: str
|
|
state: str
|
|
score: float | None = None
|
|
score_rationale: dict[str, Any] | None = None
|
|
notes: str | None = None
|
|
state_changed_at: str | None = None
|
|
created_at: str | None = None
|
|
# joined posting info
|
|
company: str | None = None
|
|
title: str | None = None
|
|
location: str | None = None
|
|
# v1 follow-up fields
|
|
follow_up_after_days: int = 7
|
|
last_activity_at: str | None = None
|
|
follow_up_snoozed_until: str | None = None
|
|
interview_prep_artifact_id: str | None = None
|
|
|
|
|
|
class TransitionRequest(BaseModel):
|
|
to: str
|
|
|
|
|
|
# --- Artifact ---
|
|
|
|
class ArtifactCreate(BaseModel):
|
|
kind: str
|
|
content: str
|
|
|
|
|
|
class ArtifactOut(BaseModel):
|
|
id: str
|
|
application_id: str
|
|
kind: str
|
|
filename: str
|
|
content_hash: str
|
|
storage_path: str
|
|
version: int
|
|
origin: str
|
|
|
|
|
|
class CoverLetterRequest(BaseModel):
|
|
letter_text: str
|
|
|
|
|
|
class CoverLetterResponse(BaseModel):
|
|
artifact: ArtifactOut
|
|
critique: list[dict[str, Any]]
|
|
|
|
|
|
# --- Approval ---
|
|
|
|
class ApprovalCreate(BaseModel):
|
|
action: str
|
|
artifact_id: str
|
|
|
|
|
|
class ApprovalOut(BaseModel):
|
|
id: str
|
|
application_id: str
|
|
artifact_id: str
|
|
artifact_hash: str
|
|
action: str
|
|
confirmed_by_user: bool
|
|
confirmed_at: str | None = None
|
|
expires_at: str
|
|
created_at: str | None = None
|
|
|
|
|
|
# --- Outbox ---
|
|
|
|
class OutboxSendRequest(BaseModel):
|
|
approval_id: str
|
|
payload: dict[str, Any]
|
|
|
|
|
|
class OutboxOut(BaseModel):
|
|
id: str
|
|
approval_id: str
|
|
kind: str
|
|
payload: dict[str, Any]
|
|
status: str
|
|
sent_at: str | None = None
|
|
error: str | None = None
|
|
|
|
|
|
# --- Telemetry ---
|
|
|
|
class TaskRunOut(BaseModel):
|
|
id: str
|
|
task: str
|
|
model: str
|
|
provider: str
|
|
input_tokens: int
|
|
output_tokens: int
|
|
cost_usd: float | None = None
|
|
duration_ms: int
|
|
application_id: str | None = None
|
|
created_at: str | None = None
|
|
|
|
|
|
# --- Errors ---
|
|
|
|
class ErrorOut(BaseModel):
|
|
error: dict[str, str]
|
|
|
|
|
|
# --- v1: CV Import ---
|
|
|
|
class CvImportRequest(BaseModel):
|
|
filename: str
|
|
content_base64: str
|
|
|
|
|
|
class CvDraft(BaseModel):
|
|
kind: str = "experience"
|
|
title: str = ""
|
|
org: str | None = None
|
|
location: str | None = None
|
|
start_date: str | None = None
|
|
end_date: str | None = None
|
|
bullets: list[str] = Field(default_factory=list)
|
|
tags: list[str] = Field(default_factory=list)
|
|
|
|
|
|
class CvImportResponse(BaseModel):
|
|
drafts: list[dict[str, Any]]
|
|
|
|
|
|
class CvImportConfirmRequest(BaseModel):
|
|
drafts: list[CvDraft]
|
|
|
|
|
|
class CvImportConfirmResponse(BaseModel):
|
|
created: int
|
|
sections: list[CvSectionOut]
|
|
|
|
|
|
# --- v1: Postings Fetch ---
|
|
|
|
class PostingsFetchRequest(BaseModel):
|
|
query: str
|
|
region: str | None = None
|
|
|
|
|
|
class PostingsFetchResponse(BaseModel):
|
|
new: int
|
|
dupes: int
|
|
|
|
|
|
# --- v1: Batch Scoring ---
|
|
|
|
class BatchScoreRequest(BaseModel):
|
|
application_ids: list[str]
|
|
|
|
|
|
class BatchScoreResult(BaseModel):
|
|
application_id: str
|
|
score: float
|
|
rationale: dict[str, Any]
|
|
red_flags: list[str] = Field(default_factory=list)
|
|
|
|
|
|
class BatchScoreResponse(BaseModel):
|
|
results: list[BatchScoreResult]
|
|
|
|
|
|
# --- v1: Today ---
|
|
|
|
class DigestItem(BaseModel):
|
|
application_id: str
|
|
title: str
|
|
company: str
|
|
score: float | None = None
|
|
|
|
|
|
class NudgeItem(BaseModel):
|
|
application_id: str
|
|
days_since_sent: int
|
|
suggestion: str
|
|
|
|
|
|
class TodayResponse(BaseModel):
|
|
digest: list[DigestItem]
|
|
nudges: list[NudgeItem]
|
|
pending_approvals: int
|
|
|
|
|
|
# --- v1: Interview Prep ---
|
|
|
|
class InterviewPrepResponse(BaseModel):
|
|
artifact_id: str
|
|
content: str
|
|
|
|
|
|
# --- v1: Concierge Seed ---
|
|
|
|
class SeedDemoResponse(BaseModel):
|
|
profile: str
|
|
postings: int
|
|
applications: int
|
|
sections: int |