10 Commits

Author SHA1 Message Date
70913c4b3e v1.16.0
Some checks failed
Docker (tags) / security (push) Successful in 28s
Docker (tags) / test (push) Failing after 7m38s
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-01-20 17:14:26 +00:00
2ed419f6e4 feat(invoices): add line_items extraction and normalization for invoice parsing 2026-01-20 17:14:26 +00:00
45cb87e9e7 v1.15.3
Some checks failed
Docker (tags) / security (push) Successful in 22s
Docker (tags) / test (push) Failing after 7m38s
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-01-20 04:15:45 +00:00
74a5b37e92 fix(tests(nanonets)): allow / when normalizing invoice strings in tests 2026-01-20 04:15:45 +00:00
2bdcc74df0 v1.15.2
Some checks failed
Docker (tags) / security (push) Successful in 30s
Docker (tags) / test (push) Failing after 7m38s
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-01-20 04:12:57 +00:00
981c031c6e fix(dev-deps): bump devDependencies @push.rocks/smartagent to ^1.6.2 and @push.rocks/smartai to ^0.13.3 2026-01-20 04:12:57 +00:00
26d2de824f v1.15.1
Some checks failed
Docker (tags) / security (push) Successful in 22s
Docker (tags) / test (push) Failing after 7m38s
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-01-20 03:19:58 +00:00
969d21c51a fix(tests): enable progress events in invoice tests and bump @push.rocks/smartagent devDependency to ^1.5.4 2026-01-20 03:19:58 +00:00
da2b827ba3 chore: update smartagent to v1.5.2 (streaming support for native tool calling) 2026-01-20 02:55:28 +00:00
9bc1f74978 feat(test): enable native tool calling for GPT-OSS invoice extraction
- Update smartai to v0.13.2 (native tool calling support)
- Update smartagent to v1.5.1 (useNativeToolCalling option)
- Enable think: true for GPT-OSS reasoning mode in Ollama config
- Enable useNativeToolCalling: true in DualAgentOrchestrator
- Simplify driver system message (native tools don't need XML instructions)

Native tool calling uses Ollama's built-in Harmony format parser
instead of requiring XML generation, which is more efficient for GPT-OSS models.
2026-01-20 02:51:52 +00:00
4 changed files with 102 additions and 34 deletions

View File

@@ -1,5 +1,34 @@
# Changelog # Changelog
## 2026-01-20 - 1.16.0 - feat(invoices)
add line_items extraction and normalization for invoice parsing
- Introduce ILineItem interface and add line_items array to IInvoice.
- Add extractLineItems helper to normalize item fields (position, product, description, quantity, unit_price, total_price).
- Include line_items in parsed invoice output and sample JSON in test, defaulting to [] when absent.
- Update logging to include extracted line item count.
- Clarify test instructions to extract items from invoice tables and skip subtotal/total rows.
## 2026-01-20 - 1.15.3 - fix(tests(nanonets))
allow '/' when normalizing invoice strings in tests
- Adjust regex in test/test.invoices.nanonets.ts to preserve forward slashes when cleaning invoice values
- Changed pattern from [^A-Z0-9-] to [^A-Z0-9\/-] to prevent accidental removal of '/' characters in invoice identifiers
## 2026-01-20 - 1.15.2 - fix(dev-deps)
bump devDependencies @push.rocks/smartagent to ^1.6.2 and @push.rocks/smartai to ^0.13.3
- Bumped @push.rocks/smartagent from ^1.5.4 to ^1.6.2 in devDependencies
- Bumped @push.rocks/smartai from ^0.13.2 to ^0.13.3 in devDependencies
- Updated test/test.invoices.nanonets.ts JSON extraction prompt: instruct not to omit special characters in invoice_number and to use the json validate tool
- No breaking changes; only dev dependency updates and test prompt adjustments
## 2026-01-20 - 1.15.1 - fix(tests)
enable progress events in invoice tests and bump @push.rocks/smartagent devDependency to ^1.5.4
- Added an onProgress handler in test/test.invoices.nanonets.ts to log progress events (console.log(event.logMessage)) so tool calls and progress are visible during tests.
- Bumped devDependency @push.rocks/smartagent from ^1.5.2 to ^1.5.4 in package.json.
## 2026-01-20 - 1.15.0 - feat(tests) ## 2026-01-20 - 1.15.0 - feat(tests)
integrate SmartAi/DualAgentOrchestrator into extraction tests and add JSON self-validation integrate SmartAi/DualAgentOrchestrator into extraction tests and add JSON self-validation

View File

@@ -1,6 +1,6 @@
{ {
"name": "@host.today/ht-docker-ai", "name": "@host.today/ht-docker-ai",
"version": "1.15.0", "version": "1.16.0",
"type": "module", "type": "module",
"private": false, "private": false,
"description": "Docker images for AI vision-language models including MiniCPM-V 4.5", "description": "Docker images for AI vision-language models including MiniCPM-V 4.5",
@@ -15,8 +15,8 @@
"devDependencies": { "devDependencies": {
"@git.zone/tsrun": "^2.0.1", "@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^3.1.5", "@git.zone/tstest": "^3.1.5",
"@push.rocks/smartagent": "^1.3.0", "@push.rocks/smartagent": "^1.6.2",
"@push.rocks/smartai": "^0.12.0" "@push.rocks/smartai": "^0.13.3"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

22
pnpm-lock.yaml generated
View File

@@ -19,11 +19,11 @@ importers:
specifier: ^3.1.5 specifier: ^3.1.5
version: 3.1.6(socks@2.8.7)(typescript@5.9.3) version: 3.1.6(socks@2.8.7)(typescript@5.9.3)
'@push.rocks/smartagent': '@push.rocks/smartagent':
specifier: ^1.3.0 specifier: ^1.6.2
version: 1.3.0(typescript@5.9.3)(ws@8.19.0)(zod@3.25.76) version: 1.6.2(typescript@5.9.3)(ws@8.19.0)(zod@3.25.76)
'@push.rocks/smartai': '@push.rocks/smartai':
specifier: ^0.12.0 specifier: ^0.13.3
version: 0.12.0(typescript@5.9.3)(ws@8.19.0)(zod@3.25.76) version: 0.13.3(typescript@5.9.3)(ws@8.19.0)(zod@3.25.76)
packages: packages:
@@ -868,11 +868,11 @@ packages:
'@push.rocks/qenv@6.1.3': '@push.rocks/qenv@6.1.3':
resolution: {integrity: sha512-+z2hsAU/7CIgpYLFqvda8cn9rUBMHqLdQLjsFfRn5jPoD7dJ5rFlpkbhfM4Ws8mHMniwWaxGKo+q/YBhtzRBLg==} resolution: {integrity: sha512-+z2hsAU/7CIgpYLFqvda8cn9rUBMHqLdQLjsFfRn5jPoD7dJ5rFlpkbhfM4Ws8mHMniwWaxGKo+q/YBhtzRBLg==}
'@push.rocks/smartagent@1.3.0': '@push.rocks/smartagent@1.6.2':
resolution: {integrity: sha512-MuiJVJcl9Pdr03k1zVwgxTqprbIHKwqPqXdOmYFYn0xYnixOX1tBUYkGsu6xIntXq8t4WazBJiF9hCiMpDTiRA==} resolution: {integrity: sha512-JaYZ7tRbmS0fVrF73Z+RF9plJ/Va0H+81zvEACT8YZRf+WhhIT+P7kKh7IcTRrgudtA7aw6eVXUmOGCMeszm3Q==}
'@push.rocks/smartai@0.12.0': '@push.rocks/smartai@0.13.3':
resolution: {integrity: sha512-T4HRaSSxO6TQGGXlQeswX2eYkB+gMu0FbKF9qCUri6FdRlYzmPDn19jgPrPJxyg5m3oj6TzflvfYwcBCFlWo/A==} resolution: {integrity: sha512-VDZzHs101hpGMmUaectuLfcME4kHpuOS7o5ffuGk5lYl383foyAN71+5v441jpk/gLDNf2KhDACR/d2O4n90Ag==}
'@push.rocks/smartarchive@5.2.1': '@push.rocks/smartarchive@5.2.1':
resolution: {integrity: sha512-TNv5q6QuBRX7jrzffiyb6A8AALNAr0kyAcJswa0l3ahBP1Q6zszNo9xOVXmW2gKX2KShtO/Y+Cn0i46n8lbnaQ==} resolution: {integrity: sha512-TNv5q6QuBRX7jrzffiyb6A8AALNAr0kyAcJswa0l3ahBP1Q6zszNo9xOVXmW2gKX2KShtO/Y+Cn0i46n8lbnaQ==}
@@ -5206,9 +5206,9 @@ snapshots:
'@push.rocks/smartlog': 3.1.10 '@push.rocks/smartlog': 3.1.10
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartagent@1.3.0(typescript@5.9.3)(ws@8.19.0)(zod@3.25.76)': '@push.rocks/smartagent@1.6.2(typescript@5.9.3)(ws@8.19.0)(zod@3.25.76)':
dependencies: dependencies:
'@push.rocks/smartai': 0.12.0(typescript@5.9.3)(ws@8.19.0)(zod@3.25.76) '@push.rocks/smartai': 0.13.3(typescript@5.9.3)(ws@8.19.0)(zod@3.25.76)
'@push.rocks/smartbrowser': 2.0.8(typescript@5.9.3) '@push.rocks/smartbrowser': 2.0.8(typescript@5.9.3)
'@push.rocks/smartdeno': 1.2.0 '@push.rocks/smartdeno': 1.2.0
'@push.rocks/smartfs': 1.3.1 '@push.rocks/smartfs': 1.3.1
@@ -5230,7 +5230,7 @@ snapshots:
- ws - ws
- zod - zod
'@push.rocks/smartai@0.12.0(typescript@5.9.3)(ws@8.19.0)(zod@3.25.76)': '@push.rocks/smartai@0.13.3(typescript@5.9.3)(ws@8.19.0)(zod@3.25.76)':
dependencies: dependencies:
'@anthropic-ai/sdk': 0.71.2(zod@3.25.76) '@anthropic-ai/sdk': 0.71.2(zod@3.25.76)
'@mistralai/mistralai': 1.12.0 '@mistralai/mistralai': 1.12.0

View File

@@ -30,8 +30,10 @@ const smartAi = new SmartAi({
baseUrl: OLLAMA_URL, baseUrl: OLLAMA_URL,
model: EXTRACTION_MODEL, model: EXTRACTION_MODEL,
defaultOptions: { defaultOptions: {
num_ctx: 32768, // Larger context for long invoices + thinking num_ctx: 65536, // 64K context for long invoices + reasoning chains
temperature: 0, // Deterministic for JSON extraction temperature: 0, // Deterministic for JSON extraction
repeat_penalty: 1.3, // Penalty to prevent repetition loops
think: true, // Enable thinking mode for GPT-OSS reasoning
}, },
defaultTimeout: 600000, // 10 minute timeout for large documents defaultTimeout: 600000, // 10 minute timeout for large documents
}, },
@@ -40,6 +42,15 @@ const smartAi = new SmartAi({
// DualAgentOrchestrator for structured task execution // DualAgentOrchestrator for structured task execution
let orchestrator: DualAgentOrchestrator; let orchestrator: DualAgentOrchestrator;
interface ILineItem {
position: number;
product: string;
description: string;
quantity: number;
unit_price: number;
total_price: number;
}
interface IInvoice { interface IInvoice {
invoice_number: string; invoice_number: string;
invoice_date: string; invoice_date: string;
@@ -48,6 +59,7 @@ interface IInvoice {
net_amount: number; net_amount: number;
vat_amount: number; vat_amount: number;
total_amount: number; total_amount: number;
line_items: ILineItem[];
} }
interface IImageData { interface IImageData {
@@ -76,8 +88,9 @@ Page numbers should be wrapped in brackets. Ex: <page_number>14</page_number>.`;
const JSON_EXTRACTION_PROMPT = `Extract key fields from the invoice. Return ONLY valid JSON. const JSON_EXTRACTION_PROMPT = `Extract key fields from the invoice. Return ONLY valid JSON.
WHERE TO FIND DATA: WHERE TO FIND DATA:
- invoice_number, invoice_date, vendor_name: Look in the HEADER section at the TOP of PAGE 1 (near "Invoice no.", "Invoice date:", "Rechnungsnummer"). Use common sense. Btw. an invoice number might start on INV* . - invoice_number, invoice_date, vendor_name: Look in the HEADER section at the TOP of PAGE 1 (near "Invoice no.", "Invoice date:", "Rechnungsnummer"). Use common sense. Btw. an invoice number might start on INV* . Also be sure to not omit special chars like / - and sp on. They are part of the invoice number.
- net_amount, vat_amount, total_amount: Look in the SUMMARY section at the BOTTOM (look for "Total", "Amount due", "Gesamtbetrag") - net_amount, vat_amount, total_amount: Look in the SUMMARY section at the BOTTOM (look for "Total", "Amount due", "Gesamtbetrag")
- line_items: Look in the TABLE(s) with columns like Pos, Product, Description, Quantity, Unit Price, Price
RULES: RULES:
1. Use common sense. 1. Use common sense.
@@ -87,11 +100,23 @@ RULES:
5. net_amount: Total before tax 5. net_amount: Total before tax
6. vat_amount: Tax amount 6. vat_amount: Tax amount
7. total_amount: Final total with tax 7. total_amount: Final total with tax
8. line_items: Array of items from the invoice table. Skip subtotal/total rows.
JSON only: JSON format:
{"invoice_number":"X","invoice_date":"YYYY-MM-DD","vendor_name":"X","currency":"EUR","net_amount":0,"vat_amount":0,"total_amount":0} {
"invoice_number": "X",
"invoice_date": "YYYY-MM-DD",
"vendor_name": "X",
"currency": "EUR",
"net_amount": 0,
"vat_amount": 0,
"total_amount": 0,
"line_items": [
{"position": 1, "product": "X", "description": "X", "quantity": 1, "unit_price": 0, "total_price": 0}
]
}
Double check for valid JSON syntax. Double check for valid JSON syntax. use the json validate tool.
`; `;
@@ -308,7 +333,7 @@ function extractInvoiceNumber(s: string | undefined): string {
const match = clean.match(pattern); const match = clean.match(pattern);
if (match) return match[1]; if (match) return match[1];
} }
return clean.replace(/[^A-Z0-9-]/gi, '').trim() || clean; return clean.replace(/[^A-Z0-9\/-]/gi, '').trim() || clean;
} }
/** /**
@@ -338,6 +363,21 @@ function extractCurrency(s: string | undefined): string {
return 'EUR'; return 'EUR';
} }
/**
* Extract and normalize line items array
*/
function extractLineItems(items: unknown): ILineItem[] {
if (!Array.isArray(items)) return [];
return items.map((item: Record<string, unknown>, index: number) => ({
position: typeof item.position === 'number' ? item.position : index + 1,
product: String(item.product || '').trim(),
description: String(item.description || '').trim(),
quantity: parseAmount(item.quantity as string | number) || 1,
unit_price: parseAmount(item.unit_price as string | number),
total_price: parseAmount(item.total_price as string | number),
}));
}
/** /**
* Try to extract valid JSON from a response string * Try to extract valid JSON from a response string
*/ */
@@ -446,6 +486,7 @@ ${JSON_EXTRACTION_PROMPT}`;
net_amount: parseAmount(jsonData.net_amount as string | number), net_amount: parseAmount(jsonData.net_amount as string | number),
vat_amount: parseAmount(jsonData.vat_amount as string | number), vat_amount: parseAmount(jsonData.vat_amount as string | number),
total_amount: parseAmount(jsonData.total_amount as string | number), total_amount: parseAmount(jsonData.total_amount as string | number),
line_items: extractLineItems(jsonData.line_items),
}; };
} catch (error) { } catch (error) {
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
@@ -469,6 +510,7 @@ async function extractInvoice(markdown: string, docName: string): Promise<IInvoi
net_amount: 0, net_amount: 0,
vat_amount: 0, vat_amount: 0,
total_amount: 0, total_amount: 0,
line_items: [],
}; };
} }
console.log(` [${docName}] Extracted: ${invoice.invoice_number}`); console.log(` [${docName}] Extracted: ${invoice.invoice_number}`);
@@ -636,7 +678,7 @@ tap.test('Stage 2: Setup Ollama + GPT-OSS 20B', async () => {
console.log(' [SmartAgent] Starting SmartAi...'); console.log(' [SmartAgent] Starting SmartAi...');
await smartAi.start(); await smartAi.start();
console.log(' [SmartAgent] Creating DualAgentOrchestrator...'); console.log(' [SmartAgent] Creating DualAgentOrchestrator with native tool calling...');
orchestrator = new DualAgentOrchestrator({ orchestrator = new DualAgentOrchestrator({
smartAiInstance: smartAi, smartAiInstance: smartAi,
defaultProvider: 'ollama', defaultProvider: 'ollama',
@@ -652,24 +694,21 @@ tap.test('Stage 2: Setup Ollama + GPT-OSS 20B', async () => {
CRITICAL RULES: CRITICAL RULES:
1. Output valid JSON with the exact format requested 1. Output valid JSON with the exact format requested
2. If you cannot find a value, use empty string "" or 0 for numbers 2. If you cannot find a value, use empty string "" or 0 for numbers
3. IMPORTANT: Before completing, validate your JSON using the json.validate tool: 3. Before completing, validate your JSON using the json_validate tool
4. Only complete after validation passes`,
<tool_call>
<tool>json</tool>
<action>validate</action>
<params>{"jsonString": "YOUR_JSON", "requiredFields": ["invoice_number", "invoice_date", "vendor_name", "currency", "net_amount", "vat_amount", "total_amount"]}</params>
</tool_call>
4. Only complete after validation passes
When done, wrap your JSON in <task_complete></task_complete> tags.`,
maxIterations: 5, maxIterations: 5,
// Enable native tool calling for GPT-OSS (uses Harmony format instead of XML)
useNativeToolCalling: true,
// Enable streaming for real-time progress visibility // Enable streaming for real-time progress visibility
onToken: (token, source) => { onToken: (token, source) => {
if (source === 'driver') { if (source === 'driver') {
process.stdout.write(token); process.stdout.write(token);
} }
}, },
// Enable progress events to see tool calls
onProgress: (event: { logMessage: string }) => {
console.log(event.logMessage);
},
}); });
// Register JsonValidatorTool for self-validation // Register JsonValidatorTool for self-validation
@@ -704,7 +743,7 @@ for (const tc of testCases) {
const elapsedMs = Date.now() - startTime; const elapsedMs = Date.now() - startTime;
processingTimes.push(elapsedMs); processingTimes.push(elapsedMs);
console.log(` Extracted: ${extracted.invoice_number} | ${extracted.invoice_date} | ${extracted.total_amount} ${extracted.currency}`); console.log(` Extracted: ${extracted.invoice_number} | ${extracted.invoice_date} | ${extracted.total_amount} ${extracted.currency} | ${extracted.line_items.length} items`);
const result = compareInvoice(extracted, expected); const result = compareInvoice(extracted, expected);