Project: Multi-Currency Invoice
What this AI agent actually does?
This self-improving AI agent takes multi-currency invoices, extracts all data, and automatically normalizes all monetary values to a target currency (header currency) using historical exchange rates based on the invoice issue date. The crazy part? It gets smarter the more you use it.
Full code open source at: https://github.com/Handit-AI/handit-examples/tree/main/examples/multi-currency-invoice
Let's dive in!
Table of Contents
- What this AI agent actually does?
- 1. It gets smarter the more you use it (Best Part)
- 2. Architecture Overview
- 3. Stack
- 4. Conclusions
1. It gets smarter the more you use it (Best Part)
I'm going to start with the best part — the cherry on top
Here’s a really cool thing — this AI agent actually gets better over time. Here is the secret weapon Handit.ai
Every action, every response is fully observed and analyzed. The system can see:
- Which fields contain monetary values
- What currencies are being used
- When conversion is needed
- How to apply real-time exchange rates
- Whether normalization was successful
- LLM output accuracy
- LLM output format
- And more...
And yes sir! When this powerful tool detects any mistakes it fixes automatically.
This means the AI agent can actually gets smarter the more you use it. If the LLM extracts the wrong field or generates incorrect outputs, Handit.ai tracks that failure and automatically adjusts the AI agent to prevent the same mistake from happening again. It's like having an AI engineer who is constantly monitoring, evaluating and improving your AI agent.
Here are the results before and after processing:
Before Handit
Prompt:
`You are a financial data processor. Your task is to normalize all monetary values in the following JSON data to ${targetCurrency}.
IMPORTANT: Use the convert_currency tool whenever you encounter a monetary value that needs to be converted to ${targetCurrency}.
The data structure follows this pattern:
- Each field has a "value" property containing the actual data
- Monetary values may be in various currencies (USD, EUR, GBP, etc.)
- You need to identify monetary values and convert them to ${targetCurrency}
- Use the convert_currency tool for each conversion needed
JSON Data to process:
${JSON.stringify(data, null, 2)}
Instructions:
1. Analyze the JSON data to identify all monetary values
2. For each monetary value that is NOT in ${targetCurrency}, use the convert_currency tool
3. The tool will return conversion results that you should use to add a "normalizedValue" field
4. Preserve the original structure completely - only add the normalizedValue field
5. If a value is already in ${targetCurrency}, you can skip conversion but still add a normalizedValue field with the same values
Remember: Always use the convert_currency tool for conversions. Do not attempt to convert currencies manually.`
Input: Multi-currency Invoice with mixed currencies
Ouput:
{
"extractedData": {
"documentType": {
"value": "invoice",
"confidence": 0.98,
"reason": "Large header text 'INVOICE' at top of document (clear OCR region)."
},
"headerCurrency": {
"value": "SGD",
"confidence": 0.62,
"reason": "The printed final/Total line shows '5000 SGD' which indicates the invoice's presented total currency. Document contains multiple currencies, but the final Total is in SGD."
},
"invoiceDate": {
"value": "16-04-2024",
"confidence": 0.99,
"reason": "Date field shows '04/16/2024' near the top; converted to DD-MM-YYYY as requested."
...
"lineItems": [
{
"description": "Consulting",
"quantity": null,
"rate": null,
"amount": "5,000.00",
"currency": "SEK",
"confidence": 0.82,
"reason": "Row shows 'Consulting' with amount '5,000.00 SK'. Interpreted as 5,000.00 SEK (subtotal later also shows SEK), symbol truncated in row but subtotal confirms SEK."
},
{
"description": "Software License",
"quantity": null,
"rate": null,
"amount": "150,000",
"currency": "JPY",
"confidence": 0.95,
"reason": "Row shows '¥150,000' next to 'Software License' — yen symbol indicates Japanese Yen."
},
{
"description": "Maintenance",
"quantity": null,
"rate": null,
"amount": "1,000.0",
"currency": "CAD",
"confidence": 0.94,
"reason": "Row displays '1,000.0 CAD' beside 'Maintenance' (explicit CAD currency code)."
},
...
"_conversions": [
{
"originalArgs": {
"amount": 150000,
"fromCurrency": "JPY",
"toCurrency": "SGD"
},
"result": {
"convertedAmount": 1309.5,
"originalAmount": 150000,
"fromCurrency": "JPY",
"toCurrency": "SGD",
"exchangeRate": 0.00873,
"conversionDate": "2025-09-01",
"method": "api_conversion",
"apiResponse": {
"originalAmount": 150000,
"originalCurrency": "JPY",
"convertedAmount": 1309.5,
"targetCurrency": "SGD",
"exchangeRate": 0.00873,
"date": "2025-09-01",
"success": true,
"apiVersion": "v4",
"historical": false
}
},
"timestamp": "2025-09-01T20:35:55.758Z",
"status": "unmatched"
},
{
"originalArgs": {
"amount": 1000,
"fromCurrency": "CAD",
"toCurrency": "SGD"
},
"result": {
"convertedAmount": 934,
"originalAmount": 1000,
"fromCurrency": "CAD",
"toCurrency": "SGD",
"exchangeRate": 0.934,
"conversionDate": "2025-09-01",
"method": "api_conversion",
"apiResponse": {
"originalAmount": 1000,
"originalCurrency": "CAD",
"convertedAmount": 934,
"targetCurrency": "SGD",
"exchangeRate": 0.934,
"date": "2025-09-01",
"success": true,
"apiVersion": "v4",
"historical": false
}
},
"timestamp": "2025-09-01T20:35:55.856Z",
"status": "unmatched"
}
]
}
Whoa! It looks like the output isn’t what we expected
First issue ❌: The extracted output does not adhere to the system prompt's requirement to add a 'normalizedValue' field for each monetary value
Second issue ❌: There is an array at the end called _conversions
, but it doesn’t contain all the conversions
Results:
Accuracy: 44%
Correctness Evaluation: 48%
Lack of clarity on output format for monetary values.
After Handit
First, let’s see what results Handit gives us
Here it is — it highlights several insights, including the fact that the extracted output does not adhere to the system prompt's requirement to add a 'normalizedValue' field.
HERE IS THE BEST PART!
It automatically sent us a PR with the fixes from our AI agent, ready to deploy, reaching an accuracy of 90%!
After merging the PR, here are the results:
Input: Same Multi-currency Invoice
Output: All amounts normalized to SGD with normalizedValue
fields
{
"extractedData": {
"lineItems": [
{
"description": "Consulting",
"quantity": null,
"rate": null,
"amount": "5,000.00",
"currency": "SEK",
"fromCurrency": "SEK",
"toCurrency": "SGD",
"confidence": 0.82,
"reason": "Row shows 'Consulting' with amount '5,000.00 SEK'. Interpreted as 5,000.00 SEK (subtotal later also shows SEK).",
"normalizedValue": 683.41
},
{
"description": "Software License",
"quantity": null,
"rate": null,
"amount": "150,000",
"currency": "JPY",
"fromCurrency": "JPY",
"toCurrency": "SGD",
"confidence": 0.95,
"reason": "Row shows '¥150,000' next to 'Software License' — yen symbol indicates Japanese Yen.",
"normalizedValue": 1309.5
},
{
"description": "Maintenance",
"quantity": null,
"rate": null,
"amount": "1,000.0",
"currency": "CAD",
"fromCurrency": "CAD",
"toCurrency": "SGD",
"confidence": 0.94,
"reason": "Row displays '1,000.0 CAD' beside 'Maintenance' (explicit CAD currency code).",
"normalizedValue": 934
},
...
],
...
}
}
}
The mistakes have been fixed automatically!
All monetary values now have normalizedValue
fields with:
- ✅ Converted amounts in SGD
- ✅ Original amounts preserved
Results:
Accuracy: 90%
Correctness Evaluation: 90%
Overall Performance Boost: ↗️ +46%
2. Architecture Overview
So I built this thing called "Multi-Currency Invoice", and honestly, it's doing some pretty wild stuff. Let me break down what's actually happening under the hood.
Let's understand the architecture of our AI agent at a very high level:
This architecture separates concerns into distinct nodes:
-
Ingestion
- Purpose: Manages file uploads and session organization for invoice processing
- Input: Images, PDFs, documents (max 10 files, 50MB each)
- Output: Organized file storage with session directories
- Core capability: File ingestion pipeline with automatic session management
-
Extraction
- Purpose: AI analyzes uploaded documents and extracts all data from invoices and other financial documents
- Input: Images, PDFs, Office documents with base64 encoding support
- Output: Structured JSON containing all extracted fields and confidence scores
- AI capability: Multimodal analysis (vision + text), parallel processing, and VLLM support
-
CurrencyNormalization
- Purpose: AI normalizes monetary values across different currencies to a target currency using intelligent analysis
- Input: Structured JSON containing all fields extracted in the previous step
- Output: Normalized JSON containing all fields, converted values, and applied exchange rates
- API capability: Historical exchange rates based on the invoice issue date
- AI capability: Detects monetary value that needs to be converted
And... How does this AI agent gets better over time?
Here is the secret weapon: Handit.ai
-
Observability
- Every interaction with this AI agent is monitored by handit
-
Failure Detection
- Handit automatically identifies errors in any of our LLMs — like when a monetary value was not normalized correctly (Really important for this AI agent)
-
Automated Fix Generation
- If a failure is detected, Handit automatically fixes our prompts for us
Want to deep dive into the tools, prompts, and nodes? Check the repo here: https://github.com/Handit-AI/handit-examples/tree/main/examples/unstructured-to-structured
3. Stack
Tech Stack
- Framework: Node.js 18+, Express 4 (MVC)
- Middleware: helmet, cors, morgan, express.json/urlencoded, multer (uploads)
- Config/Runtime typing: dotenv, zod
-
AI/LLM: LangChain (
langchain
,@langchain/openai
); OpenAI models (OPENAI_MODEL
) — defaultgpt-5-mini-2025-08-07
(extraction) andgpt-4o-mini
(normalization) -
Observability, Evaluation and Optimization:
@handit.ai/node
External APIs
-
OpenAI API
- Purpose: LLM for document data extraction (multimodal) and currency normalization with tools
- Access: LangChain/OpenAI SDKs
- Auth:
OPENAI_API_KEY
-
ExchangeRate-API
- Purpose: Latest and historical FX rates
- Endpoints:
- v4 (free, latest):
https://api.exchangerate-api.com/v4/latest/{base}
- v6 (historical, with key):
https://v6.exchangerate-api.com/v6/{API_KEY}/history/{base}/{year}/{month}/{day}[/{amount}]
- Auth:
EXCHANGE_RATE_API_KEY
(required for v6)
-
Handit.ai
- Purpose: Execution tracing/observability, Evaluation, Optimization
- Access:
@handit.ai/node
SDK - Auth:
HANDIT_API_KEY
(required to start server)
4. Conclusions
Thanks for reading!
I hope this deep dive into building a self-improving AI agent that speaks all currencies has been valuable for you
The project is fully open source - feel free to:
🔧 Modify it for your specific needs
🏭 Adapt it to any industry you want
🚀 Use it as a foundation for your own AI agents
🤝 Contribute improvements back to the community
Full code open source at: https://github.com/Handit-AI/handit-examples/tree/main/examples/multi-currency-invoice
This project demonstrates how to:
- Integrate LangChain with real APIs for intelligent document processing
- Use LLM tools for automated currency conversion
- Handle multi-currency documents intelligently
- Preserve document structure while adding normalized values
- Fetch real-time exchange rates for accurate conversions
- Build a self-improvement AI agent gets smarter the more you use it
This project comes with Handit.ai configured. If you want to configure Handit.ai for your own projects, I suggest following the documentation: https://docs.handit.ai/quickstart
Top comments (0)