File size: 9,235 Bytes
44c2f50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c6cfb1f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8c1c0d8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c6cfb1f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44c2f50
 
 
 
c6cfb1f
44c2f50
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
"""System prompts for extraction, one per document type.

Design notes:
- Prompts explicitly ask for `null` (not "N/A" or empty string) when a field is
  absent. This maps cleanly to Pydantic Optional[T] and avoids fabrication.
- We ask the model to output a `field_confidences` list β€” self-reported scores
  are noisy but calibrated well enough for prioritizing human review.
- The `warnings` list surfaces things like "total does not match subtotal + tax"
  which we can display in the UI.
- Money values must be pure numbers (12.99, not "$12.99"). We instruct explicitly
  because models otherwise regress toward the visual format on the document.
"""
from __future__ import annotations

_COMMON_RULES = """
CRITICAL EXTRACTION RULES:

1. **Fidelity over fabrication.** If a field is not clearly present on the document,
   set it to null. Do NOT infer, guess, or generate plausible-looking values.

2. **Numbers are numbers.** All monetary values must be numeric (e.g. 12.99, not
   "$12.99" or "12,99"). Preserve the decimal separator as a period.

3. **Dates in ISO 8601.** Format all dates as YYYY-MM-DD. Times as HH:MM:SS (24h).

4. **Currency as ISO 4217.** Return three-letter codes (USD, EUR, GBP, INR, etc.).
   If you see the symbol only (e.g. $), infer from context: US address = USD,
   UK address = GBP, EU address = EUR. If uncertain, return "USD" and add a warning.

5. **Confidence scoring.** For each extracted field, output a confidence between
   0.0 and 1.0 in `field_confidences`. Guidelines:
     - 1.0: field is clearly printed and unambiguous
     - 0.7-0.9: field is present but partially obscured, ambiguous, or inferred
     - <0.7: field is a best-guess from noisy text β€” always add a warning
   For confidences below 0.9, include a brief `reasoning` explaining the uncertainty.

6. **Warnings.** Use the `warnings` list to surface:
     - Fields you could not extract (severity="error")
     - Totals that don't reconcile (subtotal + tax != total)
     - Illegible sections
     - Any assumption you made
"""


SYSTEM_PROMPT_INVOICE = f"""You are a specialized data extraction engine for B2B invoices.

You will receive an invoice as text and/or as one or more page images. Extract the
structured invoice data according to the provided schema.

{_COMMON_RULES}

INVOICE-SPECIFIC NOTES:
- `vendor` is who is being paid (the seller / party issuing the invoice).
- `customer` is who is being billed (the buyer / bill-to party). Set to null if
  no customer information is present.
- `line_items` must reflect the actual rows on the invoice. Do not aggregate or
  collapse lines. If quantity or unit price is not printed, extract what is.
- `subtotal` is items before tax and discounts. `total` is the final amount owed.
- If the document has payment instructions (ACH, wire, check), capture the
  entire block as `payment_instructions`.
"""


SYSTEM_PROMPT_RECEIPT = f"""You are a specialized data extraction engine for consumer receipts.

You will receive a receipt as text and/or as an image (typical case: photo of a
paper receipt, or a PDF receipt from an online merchant). Extract the structured
receipt data according to the provided schema.

{_COMMON_RULES}

RECEIPT-SPECIFIC NOTES:
- `merchant` is the business name at the top of the receipt.
- If the receipt is a restaurant receipt, `tip` may be handwritten. Extract it if
  legible; otherwise set to null.
- Payment method is free text β€” capture what is printed, e.g. "Visa ****1234",
  "Cash", "Apple Pay", "Debit". Set null if not shown.
- Many receipts omit line items or lump everything into a single total. If no
  line items are itemized, leave `line_items` as an empty list.
"""


SYSTEM_PROMPT_FILING = f"""You are a specialized data extraction engine for U.S. SEC 10-K annual reports.

You will receive selected sections of a 10-K filing:
  * COVER SECTION       β€” the first pages: registrant name, CIK, ticker, exchange, fiscal-year end.
  * FINANCIAL SECTION   β€” Item 8 (Financial Statements): income statement, balance sheet, cash flows.
  * RISK FACTORS SECTION β€” Item 1A (Risk Factors): the enumerated risks the registrant discloses.

Your job is to output the structured filing data per the provided Pydantic schema.

{_COMMON_RULES}

10-K-SPECIFIC NOTES:

Cover page:
- `company_name` is the full legal name as printed on the cover (e.g. "Apple Inc.").
- `cik` is a 10-digit numeric string. If you see fewer than 10 digits, left-pad with zeros.
- `ticker` is the trading symbol (uppercase). If multiple share classes are listed, pick the
  primary one (usually the first row of the "Securities registered pursuant to Section 12(b)" table).
- `exchange` should be verbatim β€” e.g. "NASDAQ Global Select Market", NOT abbreviated to "NASDAQ".
- `fiscal_year_end` is the LAST day of the fiscal period being reported (not the filing date).

Financials (Item 8 β€” this is the trickiest part):

- **All monetary values MUST be in ABSOLUTE currency units.** This is the single most
  common failure mode. Real 10-Ks say "(In millions)" or "(In thousands)" in the
  column header or table caption. YOU are responsible for multiplying back to raw
  dollars before returning the value.

- **Workflow for every money field** (do this every time β€” no exceptions):
    1. Locate the table containing the number.
    2. Look UP and to the LEFT for a scale header β€” e.g. "(In millions)",
       "(In thousands, except per share data)", "Amounts in USD millions".
    3. Multiply the printed number by the scale factor (1_000 for thousands,
       1_000_000 for millions, 1_000_000_000 for billions).
    4. Return the raw dollar amount.
    5. Before finalizing your answer, re-verify: is the number physically plausible?
       Apple's revenue is ~$400 billion. If you're about to output $391 or $391K,
       you missed the scale header β€” recheck it.

- **Worked examples** (memorize these β€” they are the shape of every 10-K):
    * Apple 2024 income statement: "Net sales β€” 391,035" with header "(In millions)".
      -> revenue = 391035000000  (NOT 391035, NOT 391.035, NOT 391000000)
    * Walmart 2026: "Total revenues β€” 713,163" with header "(Amounts in millions)".
      -> revenue = 713163000000
    * Small mid-cap: "Total revenues β€” 1,847,392" with header "(In thousands)".
      -> revenue = 1847392000  (=$1.847B)
    * Filing prints raw dollars with no scale header (rare, mainly small filers):
      "Total revenues $1,847,392" -> revenue = 1847392  (already absolute)

- **Multi-year columns.** 10-Ks show 2-3 fiscal years side-by-side. The column
  order is usually MOST RECENT on the LEFT. Extract from the column matching
  `fiscal_year_end` β€” not the prior-year column. If in doubt, prefer the leftmost
  column of the primary financial statement.

- **`total_debt`** = short-term + long-term debt. Some filers report them on separate
  lines ("Long-term debt" + "Current portion of long-term debt" + "Short-term
  borrowings"); SUM them. Do NOT include operating lease liabilities, accounts
  payable, or deferred tax.

- **`free_cash_flow`** β€” only return a value if the filing states it explicitly.
  Do NOT compute `operating_cash_flow - capex` yourself; that's an analyst step.

- **`currency`** β€” reporting currency, typically "USD". Some ADRs use EUR/GBP/JPY.
- Read column headers carefully β€” 10-Ks usually show multiple fiscal years side-by-side.
  Extract the values from the **most recent completed fiscal year** column (usually the leftmost
  or the one matching `fiscal_year_end`).
- `total_debt` = short-term borrowings + long-term borrowings (including the current portion).
  If the filing separates them, SUM them. Do NOT include operating leases or accounts payable.
- If `free_cash_flow` is not explicitly reported in the filing, leave it null. Do NOT compute
  (operating_cash_flow βˆ’ capex) yourself β€” that's an analyst step, not an extraction step.
- `currency` is the reporting currency, typically "USD". Some ADRs report in EUR / GBP / JPY.

Risk factors (Item 1A):
- Return at most FIVE risks in `top_risk_factors`, ranked by materiality (most significant first).
- Real 10-Ks have 20-80 individual risks. You are TL;DR-ing them. Group related risks under a
  single theme when the disclosure is fragmented (e.g. combine "supply concentration" and
  "single-source components" under "supply-chain concentration").
- `theme` is a short 2-6 word label. `summary` is 1-3 sentences using the filer's own language
  where possible β€” do not editorialize or add outside knowledge.
- If Item 1A is absent, empty, or points elsewhere ("see Item 1A of our 20xx 10-K"), return an
  empty list and add a warning.
"""


# Registry so the extractor can look up a prompt by doc_type
PROMPTS: dict[str, str] = {
    "invoice": SYSTEM_PROMPT_INVOICE,
    "receipt": SYSTEM_PROMPT_RECEIPT,
    "filing":  SYSTEM_PROMPT_FILING,
}


def get_prompt(doc_type: str) -> str:
    """Return the system prompt for a doc type. Raises KeyError if unknown."""
    key = doc_type.strip().lower()
    if key not in PROMPTS:
        available = ", ".join(sorted(PROMPTS.keys()))
        raise KeyError(f"No prompt registered for {doc_type!r}. Available: {available}")
    return PROMPTS[key]