streamlit-web-crawler / src /document_generator_models.py
Muhammad Risqi Firdaus
fix letter auth
2b326e3
from pydantic import BaseModel
from typing import Optional, List, Dict, Any
from enum import StrEnum
# ── Authorized Representatives lookup map ──────────────────────────────────────
# Keys: full name (displayed in dropdown)
# Values: dict of data_key β†’ value for the other rep fields
REPRESENTATIVES: Dict[str, Dict[str, str]] = {
"Ahmad Royhan": {
"rep_id": "3174050905990008",
"rep_address": "Jl. Kramat 1 No 15, Kwitang",
"rep_city": "Jakarta Selatan",
},
# Add more representatives here:
# "Full Name": {
# "rep_id": "...",
# "rep_address": "...",
# "rep_city": "...",
# },
}
class DocumentType(StrEnum):
COVER_LETTER_SCHENGEN = "cover_letter_schengen"
COVER_LETTER_UK = "cover_letter_uk"
LETTER_OF_AUTHORIZATION = "letter_of_authorization"
LETTER_OF_AUTHORIZATION_MINOR = "letter_of_authorization_minor"
# INVITATION_LETTER = "invitation_letter"
# BANK_REFERENCE = "bank_reference"
class FieldSchema(BaseModel):
"""Describes one editable field for a document type."""
widget_key: str # Streamlit session-state key
data_key: str # Key inside the data dict
label: str
placeholder: str = ""
description: Optional[str] = None # Sent to /prefill-document; falls back to placeholder
field_type: str = "text_input" # "text_input" | "text_area" | "select"
options: Optional[List[str]] = None # For field_type="select"
section: str = "general" # Maps to a column/section in the form
nested_under: Optional[str] = None # e.g. "personal_details"
class DocumentSchema(BaseModel):
"""Full metadata for a document type.
Drives form rendering, session-state key management, and API calls.
The backend uses `document_name` to ask an LLM to generate the right SQL.
"""
document_type: DocumentType
title: str
icon: str = "πŸ“"
# section_key -> display header, order is preserved
sections: Dict[str, str]
fields: List[FieldSchema]
has_trip_type: bool = False
has_group_members: bool = False
def widget_keys(self) -> List[str]:
"""All Streamlit widget keys that must be cleared on undo/redo."""
keys: List[str] = []
if self.has_trip_type:
keys.append(f"{self.document_type}_trip_type")
keys.extend(f.widget_key for f in self.fields)
if self.has_group_members:
keys.append(f"{self.document_type}_group_members_editor")
return keys
def fields_in_section(self, section: str) -> List[FieldSchema]:
return [f for f in self.fields if f.section == section]
def nested_fields(self, nested_under: str) -> List[FieldSchema]:
return [f for f in self.fields if f.nested_under == nested_under]
def top_level_fields(self, section: str) -> List[FieldSchema]:
return [f for f in self.fields if f.section == section and f.nested_under is None]
def generate_person_fields(person_type: str, section: str, role_label: str) -> List[FieldSchema]:
"""Helper to keep the schema DRY by generating common personal fields for Minor LOA."""
prefix = f"loa_minor_{person_type}"
return [
FieldSchema(widget_key=f"{prefix}_name", data_key=f"{person_type}_name", label=f"{role_label} Name", placeholder="e.g. John Doe", section=section),
FieldSchema(widget_key=f"{prefix}_id", data_key=f"{person_type}_id", label=f"{role_label} National ID", placeholder="e.g. 3174000000000000", section=section),
FieldSchema(widget_key=f"{prefix}_address", data_key=f"{person_type}_address", label=f"{role_label} Address", placeholder="e.g. Jl. Bangka XI...", section=section),
FieldSchema(widget_key=f"{prefix}_city", data_key=f"{person_type}_city", label=f"{role_label} City/Province", placeholder="e.g. Jakarta Selatan", section=section),
]
DOCUMENT_REGISTRY: Dict[str, DocumentSchema] = {
DocumentType.COVER_LETTER_UK: DocumentSchema(
document_type=DocumentType.COVER_LETTER_UK,
title="UK Visa Cover Letter",
icon="πŸ‡¬πŸ‡§",
sections={
"trip": "🌍 Trip & Embassy Details",
"applicant": "πŸ‘€ Main Applicant",
"employment": "πŸ’Ό Employment & Ties",
"financials": "πŸ’° Financial Clarifications",
"contact": "πŸ“‹ Contact & Documents",
},
fields=[
# ── Trip & Embassy Section ────────────────────────────────
FieldSchema(widget_key="uk_cl_city", data_key="city", label="City of Application", placeholder="e.g. Jakarta", section="trip"),
FieldSchema(widget_key="uk_cl_emb_city", data_key="embassy_city", label="Embassy City", placeholder="e.g. Jakarta", section="trip"),
FieldSchema(widget_key="uk_cl_emb_country", data_key="embassy_country", label="Embassy Country", placeholder="e.g. Indonesia", section="trip"),
FieldSchema(widget_key="uk_cl_visa_type", data_key="visa_type", label="Visa Type", placeholder="e.g. Standard Visitor", section="trip"),
FieldSchema(widget_key="uk_cl_purpose", data_key="visit_purpose", label="Visit Purpose", placeholder="e.g. tourism and cultural exploration", section="trip"),
FieldSchema(widget_key="uk_cl_start", data_key="start_date", label="Travel Start Date", placeholder="e.g. 17 May 2026", section="trip"),
FieldSchema(widget_key="uk_cl_end", data_key="end_date", label="Travel End Date", placeholder="e.g. 23 May 2026", section="trip"),
FieldSchema(widget_key="uk_cl_duration", data_key="duration", label="Duration", placeholder="e.g. seven days and six nights", section="trip"),
FieldSchema(widget_key="uk_cl_locations", data_key="locations_to_visit", label="Locations to Visit", placeholder="e.g. London and Edinburgh", section="trip"),
FieldSchema(widget_key="uk_cl_highlight", data_key="trip_highlight", label="Trip Highlight", placeholder="e.g. attend an Orchestra Concert", field_type="text_area", section="trip"),
# ── Applicant Section (nested under personal_details) ─────
FieldSchema(widget_key="uk_cl_pd_name", data_key="name", label="Full Name", placeholder="e.g. John Doe", section="applicant", nested_under="personal_details"),
FieldSchema(widget_key="uk_cl_pd_dob", data_key="dob", label="Date of Birth", placeholder="e.g. 3 October 2000", section="applicant", nested_under="personal_details"),
FieldSchema(widget_key="uk_cl_pd_nationality", data_key="nationality", label="Nationality", placeholder="e.g. Indonesian", section="applicant", nested_under="personal_details"),
FieldSchema(widget_key="uk_cl_pd_passport", data_key="passport_number", label="Passport Number", placeholder="e.g. X3236019", section="applicant", nested_under="personal_details"),
FieldSchema(widget_key="uk_cl_home_country", data_key="home_country", label="Home Country", placeholder="e.g. Indonesia", section="applicant"),
# ── Employment Section ────────────────────────────────────
FieldSchema(widget_key="uk_cl_job_title", data_key="job_title", label="Job Title", placeholder="e.g. Front End Developer", section="employment"),
FieldSchema(widget_key="uk_cl_company", data_key="company_name", label="Company Name", placeholder="e.g. PT Radya Anugrah", section="employment"),
FieldSchema(widget_key="uk_cl_job_resp", data_key="job_responsibilities", label="Job Responsibilities", placeholder="e.g. designing and building web interfaces...", field_type="text_area", section="employment"),
FieldSchema(widget_key="uk_cl_projects", data_key="project_details", label="Project Details (Ties)", placeholder="e.g. pharmaceutical tracking systems...", field_type="text_area", section="employment"),
FieldSchema(widget_key="uk_cl_add_income", data_key="additional_income_source", label="Additional Income Source", placeholder="e.g. project-based freelance income", section="employment"),
FieldSchema(widget_key="uk_cl_family", data_key="family_members", label="Family Members (Ties)", placeholder="e.g. parents", section="employment"),
# ── Financials Section ────────────────────────────────────
FieldSchema(widget_key="uk_cl_bank_names", data_key="bank_names", label="Bank Names", placeholder="e.g. BNI & Jago", section="financials"),
FieldSchema(widget_key="uk_cl_acc_usage", data_key="account_usage_explanation", label="Account Usage Explanation", placeholder="e.g. BNI for payroll, Jago for daily operations", field_type="text_area", section="financials"),
FieldSchema(widget_key="uk_cl_other_funds_src", data_key="other_funds_source", label="Other Funds Source", placeholder="e.g. Freelance Inflows", section="financials"),
FieldSchema(widget_key="uk_cl_other_funds_exp", data_key="other_funds_explanation", label="Other Funds Explanation", placeholder="Explain freelance invoices...", field_type="text_area", section="financials"),
FieldSchema(widget_key="uk_cl_turnover_rsn", data_key="turnover_reason", label="High Turnover Reason", placeholder="e.g. Investments", section="financials"),
FieldSchema(widget_key="uk_cl_turnover_exp", data_key="turnover_explanation", label="High Turnover Explanation", placeholder="Explain high turnover...", field_type="text_area", section="financials"),
FieldSchema(widget_key="uk_cl_outflow_rsn", data_key="outflow_reason", label="Large Outflow Reason", placeholder="e.g. One-off Asset", section="financials"),
FieldSchema(widget_key="uk_cl_outflow_exp", data_key="outflow_explanation", label="Large Outflow Explanation", placeholder="Explain large outflow...", field_type="text_area", section="financials"),
FieldSchema(widget_key="uk_cl_monthly_exp", data_key="monthly_expenditure_amount", label="Monthly Expenditure", placeholder="e.g. IDR 9,000,000", section="financials"),
FieldSchema(widget_key="uk_cl_liquid", data_key="liquid_funds_amount", label="Liquid Funds Available", placeholder="e.g. IDR 75,000,000", section="financials"),
# ── Contact & Documents Section ───────────────────────────
FieldSchema(widget_key="uk_cl_email", data_key="email", label="Email", placeholder="e.g. m.irfan@gmail.com", section="contact"),
FieldSchema(widget_key="uk_cl_phone", data_key="phone_number", label="Phone Number", placeholder="e.g. +6283829851734", section="contact"),
FieldSchema(widget_key="uk_cl_add_docs", data_key="list_of_documents", label="List of Documents", placeholder="List any docs like 'β€’ Payroll slip'...", field_type="text_area", section="contact"),
],
has_trip_type=True,
has_group_members=True,
),
DocumentType.COVER_LETTER_SCHENGEN: DocumentSchema(
document_type=DocumentType.COVER_LETTER_SCHENGEN,
title="Schengen Visa Cover Letter",
icon="πŸ“",
sections={
"trip": "🌍 Trip Details",
"applicant": "πŸ‘€ Main Applicant Details",
"contact": "πŸ“‹ Contact & Financials",
},
fields=[
# ── Trip section ───────────────────────────────
# data_key matches the prefill response keys returned by /prefill-document
FieldSchema(widget_key="cl_country", data_key="country", label="Country of Embassy", placeholder="e.g. Germany", section="trip"),
FieldSchema(widget_key="cl_city", data_key="city", label="City of Application", placeholder="e.g. Jakarta", section="trip"),
FieldSchema(widget_key="cl_purpose", data_key="purpose", label="Purpose of Trip", placeholder="e.g. tourism", section="trip"),
FieldSchema(widget_key="cl_main_dest", data_key="main_dest", label="Main Destination", placeholder="e.g. Germany", section="trip"),
FieldSchema(widget_key="cl_event", data_key="event", label="Event", placeholder="e.g. personal vacation", section="trip"),
FieldSchema(widget_key="cl_other_dest", data_key="other_dest", label="Other Destinations", placeholder="e.g. France and Italy", section="trip"),
FieldSchema(widget_key="cl_start", data_key="start", label="Travel Start Date", placeholder="e.g. 2026-05-01", section="trip"),
FieldSchema(widget_key="cl_end", data_key="end", label="Travel End Date", placeholder="e.g. 2026-05-14", section="trip"),
FieldSchema(widget_key="cl_duration", data_key="duration", label="Duration", placeholder="e.g. 14 days", section="trip"),
# ── Applicant section (nested under personal_details) ──
FieldSchema(widget_key="cl_pd_name", data_key="name", label="Full Name", placeholder="e.g. John Doe", section="applicant", nested_under="personal_details"),
FieldSchema(widget_key="cl_pd_dob", data_key="dob", label="Date of Birth", placeholder="e.g. 1st Jan 1990", section="applicant", nested_under="personal_details"),
FieldSchema(widget_key="cl_pd_nationality", data_key="nationality", label="Nationality", placeholder="e.g. Indonesian", section="applicant", nested_under="personal_details"),
FieldSchema(widget_key="cl_pd_occupation", data_key="occupation", label="Occupation", placeholder="e.g. Engineer", section="applicant", nested_under="personal_details"),
FieldSchema(widget_key="cl_pd_passport_number", data_key="passport_number", label="Passport Number", placeholder="e.g. A1234567", section="applicant", nested_under="personal_details"),
# ── Contact section ────────────────────────────
FieldSchema(widget_key="cl_trip_highlight", data_key="trip_highlight", label="Trip Highlight", placeholder="Key highlights of your trip...", field_type="text_area", section="contact"),
FieldSchema(widget_key="cl_contact_email", data_key="contact_email", label="Contact Email", placeholder="e.g. john@email.com", section="contact"),
FieldSchema(widget_key="cl_contact_phone", data_key="contact_phone", label="Contact Phone", placeholder="e.g. +62 812 345 6789", section="contact"),
FieldSchema(widget_key="cl_job_commitment", data_key="job_commitment", label="Job Commitment", placeholder="e.g. returning to work on...", section="contact"),
FieldSchema(widget_key="cl_financial_status", data_key="financial_status", label="Financial Status", placeholder="e.g. sufficient funds", section="contact"),
FieldSchema(widget_key="cl_list_of_documents", data_key="list_of_documents", label="List of Documents", placeholder="e.g. passport, bank statement, hotel booking...", field_type="text_area", section="contact"),
],
has_trip_type=True,
has_group_members=True,
),
DocumentType.LETTER_OF_AUTHORIZATION: DocumentSchema(
document_type=DocumentType.LETTER_OF_AUTHORIZATION,
title="Letter of Authorization for Visa",
icon="✍️",
sections={
"grantor": "πŸ‘€ First Party (Grantor)",
"representative": "🀝 Authorized Representative",
"authorization": "πŸ“ Authorization Details",
},
fields=[
# ── Grantor section ───────────────────────────────
FieldSchema(widget_key="loa_grantor_name", data_key="grantor_name", label="Full Name based on Passport", placeholder="e.g. Neni Diankrisna Putri", description="Full name of the applicant as it appears on their passport (maps to applicant_name or full_name from the profile). This person is granting authorization.", section="grantor"),
FieldSchema(widget_key="loa_grantor_address", data_key="grantor_address", label="Address", placeholder="e.g. Jl. Mustika Jaya I...", description="Home address of the applicant (grantor).", section="grantor"),
FieldSchema(widget_key="loa_grantor_id", data_key="grantor_id", label="National ID Number (Ktp)", placeholder="e.g. 3175025510900006", description="Indonesian national ID number (NIK/KTP) of the applicant (grantor).", section="grantor"),
FieldSchema(widget_key="loa_grantor_city", data_key="grantor_city", label="City/Province", placeholder="e.g. Jakarta Timur", description="City and/or province where the applicant (grantor) resides.", section="grantor"),
# ── Representative section ────────────────────────
FieldSchema(widget_key="loa_rep_name", data_key="rep_name", label="Full Name", placeholder="Select a representative", section="representative", field_type="select", options=list(REPRESENTATIVES.keys())),
FieldSchema(widget_key="loa_rep_id", data_key="rep_id", label="National ID Number", placeholder="e.g. 3174050905990008", section="representative"),
FieldSchema(widget_key="loa_rep_address", data_key="rep_address", label="Address", placeholder="e.g. Jl. Kramat 1 No 15...", section="representative"),
FieldSchema(widget_key="loa_rep_city", data_key="rep_city", label="City/Province", placeholder="e.g. Jakarta Selatan", section="representative"),
# ── Authorization section ─────────────────────────
FieldSchema(widget_key="loa_passport_num", data_key="passport_number", label="Passport Number", placeholder="e.g. X1373768", section="authorization"),
FieldSchema(widget_key="loa_passport_name", data_key="passport_name", label="Passport Name", placeholder="e.g. Neni Diankrisna Putri", section="authorization"),
FieldSchema(widget_key="loa_place", data_key="city", label="Signing City", placeholder="e.g. Jakarta", section="authorization"),
FieldSchema(widget_key="loa_date", data_key="date", label="Signing Date", placeholder="e.g. 13 February 2026", section="authorization"),
],
has_trip_type=False,
has_group_members=False,
),
DocumentType.LETTER_OF_AUTHORIZATION_MINOR: DocumentSchema(
document_type=DocumentType.LETTER_OF_AUTHORIZATION_MINOR,
title="Letter of Authorization for Minor's Visa",
icon="πŸ§’",
sections={
"parents": "πŸ‘ͺ Parents (First Party)",
"representative": "🀝 Authorized Representative",
"authorization": "πŸ“ Authorization Details",
},
fields=(
# ── Parents (Grantors) section ────────────────────────
generate_person_fields("father", "parents", "Father's") +
generate_person_fields("mother", "parents", "Mother's") +
# ── Representative section (name is a dropdown, rest auto-filled) ──
[FieldSchema(widget_key="loa_minor_rep_name", data_key="rep_name", label="Representative's Name",
section="representative", field_type="select", options=list(REPRESENTATIVES.keys()))] +
generate_person_fields("rep", "representative", "Representative's")[1:] +
# ── Authorization & Minor section ─────────────────────
[
FieldSchema(widget_key="loa_minor_passport", data_key="passport_number", label="Child's Passport Number", placeholder="e.g. X2588429", section="authorization"),
FieldSchema(widget_key="loa_minor_pass_name", data_key="passport_name", label="Child's Passport Name", placeholder="e.g. Mohamad Noah...", section="authorization"),
FieldSchema(widget_key="loa_minor_place", data_key="city", label="Signing City", placeholder="e.g. Jakarta", section="authorization"),
FieldSchema(widget_key="loa_minor_date", data_key="date", label="Signing Date", placeholder="e.g. 24 November 2025", section="authorization"),
]
),
has_trip_type=False,
has_group_members=False,
),
}
# ── API request models ─────────────────────────────────────
class PrefillDocumentRequest(BaseModel):
"""
Sent to /prefill-document (structure mode).
`structure` describes the fields the LLM should map from DB data.
Returns a flat dict of { field_key: value_or_null, ..., "_missing_required": [...] }.
"""
application_id: int
structure: List[Dict[str, Any]]
class GenerateDocumentRequest(BaseModel):
"""
Sent to /generate-document.
`doc_data` carries all form fields as-is; the backend merges them on top
of the DB data and generates the document.
Adding a new document type requires no change to this model.
"""
application_id: int
document_name: str # value from DocumentType
doc_data: Optional[Dict[str, Any]] = None
class DocChatRequest(BaseModel):
"""Sent to /generate-document/chat (new stateless draft-revision flow)."""
query: str
history: List[Dict[str, str]]
session_uuid: Optional[str] = None
current_document_content: str = ""
structure: Optional[List[Dict[str, Any]]] = None
class GenerateDraftRequest(BaseModel):
"""
Sent to /generate-document/draft.
Generates a Markdown draft without creating a Google Doc.
`doc_type` and `structure` are stored on the frontend for later handover to the core API.
"""
doc_type: str
data: Dict[str, Any]
structure: Optional[List[Dict[str, Any]]] = None # field definitions for core API handover
session_uuid: Optional[str] = None
class ExportDocumentRequest(BaseModel):
"""Sent to /export-document once the user is satisfied with the draft."""
document_content: str
title: Optional[str] = None
export_format: str = "gdocs"
# ── Shared sub-models (used for type hints / validation internally) ──
class PersonalDetails(BaseModel):
name: Optional[str] = None
dob: Optional[str] = None
nationality: Optional[str] = None
occupation: Optional[str] = None
passport_number: Optional[str] = None
class GroupMember(BaseModel):
relationship: Optional[str] = None
name: Optional[str] = None
dob: Optional[str] = None
occupation: Optional[str] = None
nationality: Optional[str] = None
passport_number: Optional[str] = None