File size: 6,087 Bytes
06671aa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
183
/**
 * One-shot real LLM smoke test for tabular batch extraction parsing.
 * Usage: npx tsx scripts/tabularExtractionSmoke.ts
 */
import "dotenv/config";
import { streamChatWithTools } from "../src/lib/llm";
import { DEFAULT_TABULAR_MODEL } from "../src/lib/llm/models";
import { buildExtractionBatchUser } from "../src/prompts/tabular/extraction-batch-user";
import { TabularJsonStreamParser } from "../src/lib/tabularJsonStreamParser";

function debugLog(
    message: string,
    data: Record<string, unknown>,
) {
    console.log(JSON.stringify({ smoke: { message, data } }));
}

const SAMPLE_DOC = `
COMMERCIAL PROPOSAL — SUPPLY & ERECTION

1. Document Type: This is a commercial quotation / proposal for pre-engineered building supply and erection.

2. Zamil Role: Zamil Steel acts as the Seller / Supplier for the materials listed herein.

3. Contract Type: This proposal is issued on a lump-sum fixed price basis.

4. Price Basis: All prices are firm and fixed for 90 days from proposal date.

5. Price Escalation: No price escalation applies unless expressly agreed in writing.
`.trim();

const COLUMNS = [
    {
        index: 0,
        name: "Document Type",
        prompt: "Identify the document type.",
        format: "text",
    },
    {
        index: 1,
        name: "Zamil Role Stated",
        prompt: "State Zamil's role as described in the document.",
        format: "text",
    },
    {
        index: 2,
        name: "Contract Type",
        prompt: "Extract the contract/pricing type (e.g. lump sum, unit rate).",
        format: "text",
    },
    {
        index: 3,
        name: "Price Basis",
        prompt: "Extract whether pricing is fixed, firm, or subject to adjustment.",
        format: "text",
    },
    {
        index: 4,
        name: "Price Escalation",
        prompt: "Extract any price escalation or variation clause.",
        format: "text",
    },
];

async function main() {
    const model = process.env.TABULAR_SMOKE_MODEL || DEFAULT_TABULAR_MODEL;
    const batch = buildExtractionBatchUser({
        filename: "PEB-sample-proposal.docx",
        documentText: SAMPLE_DOC,
        columns: COLUMNS,
    });

    debugLog("starting real LLM tabular smoke test", {
        model,
        columnCount: COLUMNS.length,
        columnIndices: COLUMNS.map((c) => c.index),
    });

    const parser = new TabularJsonStreamParser();
    const received: Record<
        number,
        { summary: string; flag: string; reasoning: string }
    > = {};
    let streamError: string | null = null;
    let fullText = "";

    try {
        const result = await streamChatWithTools({
            model,
            systemPrompt: batch.system,
            messages: [{ role: "user", content: batch.user }],
            tools: [],
            apiKeys: {
                openai: process.env.OPENAI_API_KEY ?? null,
                gemini: process.env.GEMINI_API_KEY ?? null,
            },
            callbacks: {
                onContentDelta: (delta) => {
                    fullText += delta;
                    for (const obj of parser.append(delta)) {
                        const rawIndex = obj.column_index;
                        const idx =
                            typeof rawIndex === "number"
                                ? rawIndex
                                : typeof rawIndex === "string"
                                  ? Number(rawIndex)
                                  : NaN;
                        if (!Number.isFinite(idx)) continue;
                        received[idx] = {
                            summary: String(obj.summary ?? "").trim(),
                            flag: String(obj.flag ?? "grey"),
                            reasoning: String(obj.reasoning ?? "").slice(
                                0,
                                120,
                            ),
                        };
                    }
                },
            },
        });
        fullText = result.fullText || fullText;
    } catch (err) {
        streamError = err instanceof Error ? err.message : String(err);
        debugLog("provider stream failed", { model, error: streamError });
    }

    for (const obj of parser.flush()) {
        const rawIndex = obj.column_index;
        const idx =
            typeof rawIndex === "number"
                ? rawIndex
                : typeof rawIndex === "string"
                  ? Number(rawIndex)
                  : NaN;
        if (!Number.isFinite(idx)) continue;
        received[idx] = {
            summary: String(obj.summary ?? "").trim(),
            flag: String(obj.flag ?? "grey"),
            reasoning: String(obj.reasoning ?? "").slice(0, 120),
        };
    }

    const expected = COLUMNS.map((c) => c.index);
    const got = Object.keys(received).map(Number).sort((a, b) => a - b);
    const missing = expected.filter((i) => !(i in received));

    const report = {
        model,
        streamError,
        expectedColumns: expected,
        receivedColumns: got,
        missingColumns: missing,
        parseFailures: parser.parseFailureCount,
        remainderLength: parser.remainder.length,
        remainderPreview: parser.remainder.slice(0, 300),
        responseLength: fullText.length,
        responsePreview: fullText.slice(0, 400),
        cells: COLUMNS.map((col) => ({
            index: col.index,
            name: col.name,
            status: col.index in received ? "parsed" : "missing",
            summary: received[col.index]?.summary?.slice(0, 120) ?? null,
            flag: received[col.index]?.flag ?? null,
        })),
    };

    debugLog("real LLM smoke test finished", report);
    console.log("\n=== TABULAR LLM SMOKE TEST REPORT ===");
    console.log(JSON.stringify(report, null, 2));

    if (streamError || missing.length > 0 || parser.parseFailureCount > 0) {
        process.exitCode = 1;
    }
}

main().catch((err) => {
    debugLog("smoke script crashed", {
        error: err instanceof Error ? err.message : String(err),
    });
    console.error(err);
    process.exit(1);
});