diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..353dd3f Binary files /dev/null and b/.DS_Store differ diff --git a/lending-poc/alembic/versions/0006_create_intermediate_field_mapping_table.py b/lending-poc/alembic/versions/0006_create_intermediate_field_mapping_table.py new file mode 100644 index 0000000..0608076 --- /dev/null +++ b/lending-poc/alembic/versions/0006_create_intermediate_field_mapping_table.py @@ -0,0 +1,34 @@ +"""create intermediate_field_mapping table + +Revision ID: 0006_create_intermediate_field_mapping_table +Revises: +Create Date: 2026-08-11 15:05:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '0006_create_intermediate_field_mapping_table' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + 'intermediate_field_mapping', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('case_id', sa.Integer(), nullable=False), + sa.Column('field_mapper_output', postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column('processing_date', sa.DateTime(), nullable=True), + sa.Column('validated', sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + + +def downgrade() -> None: + op.drop_table('intermediate_field_mapping') diff --git a/lending-poc/modules/field_mapping_poc/README.md b/lending-poc/modules/field_mapping_poc/README.md new file mode 100644 index 0000000..a6ccd28 --- /dev/null +++ b/lending-poc/modules/field_mapping_poc/README.md @@ -0,0 +1,163 @@ +# LegalAI Field Mapping POC + +Generalized, schema-agnostic field mapping using a local Ollama model +(default: `gemma3:4b`). Takes raw OCR/translated text + a target +schema (arbitrary JSON, not a fixed Pydantic model) and returns a +schema-shaped JSON with values filled in, `null` where nothing was +found, and any extra fields the model discovers flagged separately. + +## Project layout + +``` +field_mapping_poc/ +├── config.py # model name, ollama host, retries, flagging keys +├── main.py # CLI demo entry point +├── core/ +│ ├── ollama_client.py # thin wrapper around the ollama lib + retries +│ ├── prompt_builder.py # builds the system/user prompt from schema + text +│ ├── response_parser.py # JSON repair + schema reconciliation +│ └── mapper.py # FieldMapper — orchestrates the three above +├── schemas/examples/ +│ ├── salary_slip.json # flat schema example +│ └── identity_card.json # nested schema example (address object) +├── samples/ +│ └── sample_ocr_text.txt # messy OCR-style salary slip for the demo +``` + +## Setup + +```bash +pip install -r requirements.txt --break-system-packages # or use a venv + +# pull the model once +ollama pull gemma3:4b +ollama serve # if not already running +``` + +## Run the demo + +```bash +python main.py +# or point it at a different schema/text pair +python main.py --schema schemas/examples/identity_card.json --text samples/sample_ocr_text.txt +``` + + +## HOW TO USE THIS IN THIS PROJECT + +To integrate this POC into the wider project pipeline, you only need to interact with the `FieldMapper` object. + +### 1. Object to create + +Create an instance of `FieldMapper` (located in `core/mapper.py`). By default, it will initialize its own `OllamaClient`, but you can also pass in a custom configured client if needed. + +```python +from core.mapper import FieldMapper + +mapper = FieldMapper() +``` + +### 2. Method to call + +Call the `map_fields()` method on your `FieldMapper` instance. + +### 3. Parameters to give + +The `map_fields` method takes two arguments: +- `schema` (`Dict[str, Any]`): An arbitrary JSON dictionary describing the expected fields (keys are field names, values are type/description hints). +- `document_text` (`str`): The raw OCR (or translated) text that you want to extract information from. + +### Example Usage + +```python +from core.mapper import FieldMapper + +# 1. Define your schema +target_schema = { + "employeeName": "string", + "grossSalary": "number", + "dateOfJoining": "date (DD-MM-YYYY)" +} + +# 2. Provide the raw text +ocr_text = "..." + +# 3. Create the mapper and extract fields +mapper = FieldMapper() +extracted_data = mapper.map_fields(schema=target_schema, document_text=ocr_text) + +print(extracted_data) +``` + +## Design notes + +**Why the schema isn't a Pydantic model.** This needs to work across +arbitrary document types (salary slips, identity cards, court orders, +affidavits, ...) without writing a new Python class per type. So the +"schema" is just a JSON object where each key is a field name and each +value is a short type/description hint (`"string"`, `"number"`, +`"date (DD-MM-YYYY)"`). It can nest (see `identity_card.json`'s +`address` object). `FieldMapper.map_fields(schema, text)` works the +same way regardless of what schema you pass in. + +**Extra-field convention.** The model is explicitly allowed to surface +fields it's confident about that aren't in the schema (e.g. a UAN +number on a salary slip). These stay at the same nesting level as +everything else ("flat"), but get wrapped: + +```json +"uanNumber": { "value": "101234567890", "source": "llm_added" } +``` + +while schema-defined fields stay as plain values: + +```json +"employeeName": "Ishaan Deshmukh" +``` + +This means code that only cares about the schema fields doesn't need +to change at all — it can keep reading plain values. Code that wants +to review/promote extra fields (e.g. before Neo4j insertion) can just +filter on `isinstance(v, dict) and v.get("source") == "llm_added"`. + +**Reliability layers.** +1. `format="json"` is passed to Ollama, which constrains the model to + emit syntactically valid JSON (supported by Gemma 3 and other + JSON-mode-capable models). +2. `response_parser.py` still does a repair pass (strips stray + markdown fences, extracts the `{...}` substring) in case the model + wraps the JSON in commentary anyway. +3. `reconcile_with_schema()` guarantees every schema key exists in the + final output — even if the model silently dropped one — by + filling it with `null`. +4. `ollama_client.py` retries transient failures with linear backoff + (configurable via `OLLAMA_MAX_RETRIES`). + +**What's deliberately out of scope for this POC** (per the ask): +- Neo4j insertion — this only produces the clean per-document JSON + that would later be written to the graph. +- Confidence scoring per field. +- Batching / async processing across many documents at once. +- The LoRA/QLoRA fine-tuning step — this POC targets a base/instruct + Gemma model via prompting only, to establish a baseline before any + fine-tuning work. + +## Suggested next steps + +- Run this against a handful of real document types (salary slip, + Aadhaar/PAN, property extract, etc.) and eyeball how often + `llm_added` fields are genuinely useful vs. noise — that ratio + should inform whether the "add new fields" permission stays as + loose as it is now, or gets tightened (e.g. require a `confidence` + field, or a controlled vocabulary of allowed extra fields per + document type). +- Once field mapping is trusted, this is the natural point to bolt on + the Neo4j writer: schema fields become properties on a `:Document` + node (or field-specific nodes, depending on how the graph model is + set up for cross-document consistency checks), and `llm_added` + fields can be written with an extra `source: "llm_added"` property + so later Cypher queries can distinguish provenance. +- If `gemma3:4b` under-performs on domain-specific field names (legal + terminology, regional document formats), that's the point to swap + in the QLoRA-fine-tuned checkpoint — `ollama_client.py` only needs + `OLLAMA_MODEL` changed, nothing else in the pipeline changes. diff --git a/lending-poc/modules/field_mapping_poc/config.py b/lending-poc/modules/field_mapping_poc/config.py new file mode 100644 index 0000000..55d6ca3 --- /dev/null +++ b/lending-poc/modules/field_mapping_poc/config.py @@ -0,0 +1,37 @@ +""" +Central configuration for the Field Mapping POC. + +Keep all environment-tunable values here so nothing is hardcoded deep +inside business logic. Override any of these via environment variables +without touching code. +""" +import os +from dataclasses import dataclass + + +@dataclass(frozen=True) +class OllamaConfig: + host: str = os.getenv("OLLAMA_HOST", "http://localhost:11434") + model: str = os.getenv("OLLAMA_MODEL", "gemma3:4b") + temperature: float = float(os.getenv("OLLAMA_TEMPERATURE", "0.0")) + num_ctx: int = int(os.getenv("OLLAMA_NUM_CTX", "8192")) + request_timeout: int = int(os.getenv("OLLAMA_TIMEOUT_SECONDS", "120")) + max_retries: int = int(os.getenv("OLLAMA_MAX_RETRIES", "2")) + + +OLLAMA = OllamaConfig() + +# --- Extra-field flagging convention ----------------------------------- +# Fields that ARE part of the caller's target schema are passed through +# as plain values (no wrapper) so existing consumers of the schema shape +# don't need to change. +# +# Fields the model discovers that are NOT part of the schema are kept +# flat, at the same nesting level, but wrapped like: +# "uanNumber": {"value": "101234567890", "source": "llm_added"} +# so downstream code (e.g. the future Neo4j writer) can easily filter +# them out, route them through review, or promote them into the schema. +EXTRA_FIELD_VALUE_KEY = "value" +EXTRA_FIELD_SOURCE_KEY = "source" +EXTRA_FIELD_SOURCE_TAG = "llm_added" +SCHEMA_FIELD_SOURCE_TAG = "schema" diff --git a/lending-poc/modules/field_mapping_poc/core/__init__.py b/lending-poc/modules/field_mapping_poc/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lending-poc/modules/field_mapping_poc/core/mapper.py b/lending-poc/modules/field_mapping_poc/core/mapper.py new file mode 100644 index 0000000..09d9d79 --- /dev/null +++ b/lending-poc/modules/field_mapping_poc/core/mapper.py @@ -0,0 +1,54 @@ +""" +FieldMapper: the main entry point of the POC. + +Given (target_schema, document_text) it returns a schema-shaped JSON +with values filled in from the text, nulls where nothing was found, +and any extra model-discovered fields flagged with a source marker. +""" +from __future__ import annotations + +import logging +from typing import Any, Dict, Optional + +from core.ollama_client import OllamaClient +from core.prompt_builder import build_system_prompt, build_user_prompt +from core.response_parser import ResponseParseError, normalize_response + +logger = logging.getLogger(__name__) + + +class FieldMapper: + def __init__(self, client: Optional[OllamaClient] = None): + self.client = client or OllamaClient() + + def map_fields( + self, + schema: Dict[str, Any], + document_text: str, + ) -> Dict[str, Any]: + """ + :param schema: arbitrary JSON describing expected fields + (values are type/description hints, not data). + :param document_text: raw OCR (post-translation) text. + :return: schema-shaped dict, values filled or null, with any + extra fields flagged as {"value": ..., "source": "llm_added"}. + :raises ValueError: on empty inputs. + :raises OllamaClientError: if the model backend fails. + :raises ResponseParseError: if the model's output can't be + salvaged into valid JSON. + """ + if not document_text or not document_text.strip(): + raise ValueError("document_text is empty") + if not schema: + raise ValueError("schema must not be empty") + + system_prompt = build_system_prompt() + user_prompt = build_user_prompt(schema, document_text) + + raw_response = self.client.generate_json(system_prompt, user_prompt) + + try: + return normalize_response(schema, raw_response) + except ResponseParseError: + logger.error("Failed to parse model response:\n%s", raw_response) + raise diff --git a/lending-poc/modules/field_mapping_poc/core/ollama_client.py b/lending-poc/modules/field_mapping_poc/core/ollama_client.py new file mode 100644 index 0000000..e0aa131 --- /dev/null +++ b/lending-poc/modules/field_mapping_poc/core/ollama_client.py @@ -0,0 +1,78 @@ +""" +Thin wrapper around the `ollama` python library. + +Keeping this isolated means: +- if Ollama gets swapped for vLLM / a hosted endpoint later, this is + the only file that needs to change. +- retry / timeout / error-handling logic lives in exactly one place. +""" +from __future__ import annotations + +import logging +import time +from typing import Optional + +import ollama + +from config import OLLAMA + +logger = logging.getLogger(__name__) + + +class OllamaClientError(Exception): + """Raised when the Ollama backend fails after all retries.""" + + +class OllamaClient: + def __init__( + self, + model: str = OLLAMA.model, + host: str = OLLAMA.host, + temperature: float = OLLAMA.temperature, + num_ctx: int = OLLAMA.num_ctx, + max_retries: int = OLLAMA.max_retries, + ): + self.model = model + self.temperature = temperature + self.num_ctx = num_ctx + self.max_retries = max_retries + self._client = ollama.Client(host=host, timeout=OLLAMA.request_timeout) + + def generate_json(self, system_prompt: str, user_prompt: str) -> str: + """ + Calls the model in JSON mode and returns the raw string response. + Retries on transient failures with linear backoff (1s, 2s, ...). + """ + last_error: Optional[Exception] = None + + for attempt in range(1, self.max_retries + 2): + try: + response = self._client.chat( + model=self.model, + format="json", # forces the model to emit valid JSON only + options={ + "temperature": self.temperature, + "num_ctx": self.num_ctx, + }, + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt}, + ], + ) + content = response["message"]["content"] + if not content or not content.strip(): + raise OllamaClientError("Model returned empty content") + return content + + except Exception as exc: # noqa: BLE001 - retry on anything transient + last_error = exc + logger.warning( + "Ollama call failed (attempt %d/%d): %s", + attempt, self.max_retries + 1, exc, + ) + if attempt <= self.max_retries: + time.sleep(attempt) + + raise OllamaClientError( + f"Ollama call failed after {self.max_retries + 1} attempts" + ) from last_error diff --git a/lending-poc/modules/field_mapping_poc/core/prompt_builder.py b/lending-poc/modules/field_mapping_poc/core/prompt_builder.py new file mode 100644 index 0000000..9c86892 --- /dev/null +++ b/lending-poc/modules/field_mapping_poc/core/prompt_builder.py @@ -0,0 +1,58 @@ +""" +Builds the system + user prompt sent to the model. + +The schema is intentionally NOT a strict Pydantic model — callers pass +an arbitrary JSON object describing the fields they expect (a "target +shape"), and the model returns a same-shaped JSON with values filled +in, `null` where nothing was found, and permission to append genuinely +new fields it discovers. +""" +from __future__ import annotations + +import json +from typing import Any, Dict + +SYSTEM_PROMPT = """You are a precise information-extraction engine used in a \ +document processing pipeline. You will be given: +1. A JSON "target schema" describing the fields expected in this document type. \ +Each key is a field name; each value is a short type/description hint \ +(e.g. "string", "number", "date (DD-MM-YYYY)"), not the actual value. +2. Raw OCR-extracted text of a document (may contain OCR noise, spacing \ +errors, or be a translation of a non-English original). + +Your job: +- Fill in the target schema with values found in the text. +- If a field's value cannot be confidently found in the text, set it to null. \ +Never guess or fabricate a value. +- Preserve the exact key names and nesting structure of the target schema. +- Extract numbers as they appear in the text; do not silently reformat \ +currency separators unless it is clearly OCR noise. +- If you notice other clearly-named, clearly-valued fields in the text \ +that are NOT part of the target schema but would be genuinely useful \ +for this document type (e.g. a UAN number on a salary slip), ADD them \ +to your JSON output at the same nesting level where they logically belong. \ +Only add fields you are confident about — do not pad the output with guesses. +- Return ONLY a single valid JSON object. No commentary, no markdown fences, \ +no explanation text before or after the JSON. +""" + +USER_PROMPT_TEMPLATE = """TARGET SCHEMA: +{schema} + +DOCUMENT TEXT: +\"\"\" +{document_text} +\"\"\" + +Return the filled JSON now.""" + + +def build_system_prompt() -> str: + return SYSTEM_PROMPT + + +def build_user_prompt(schema: Dict[str, Any], document_text: str) -> str: + return USER_PROMPT_TEMPLATE.format( + schema=json.dumps(schema, indent=2, ensure_ascii=False), + document_text=document_text.strip(), + ) diff --git a/lending-poc/modules/field_mapping_poc/core/response_parser.py b/lending-poc/modules/field_mapping_poc/core/response_parser.py new file mode 100644 index 0000000..2845205 --- /dev/null +++ b/lending-poc/modules/field_mapping_poc/core/response_parser.py @@ -0,0 +1,123 @@ +""" +Parses and normalizes the raw model response. + +Responsibilities: +- Safely parse JSON, with a light repair pass for common LLM slip-ups + (markdown fences, stray prose around the JSON object). +- Guarantee every key from the original schema is present in the + output (filled with null if the model dropped it). +- Tag every field NOT present in the original schema as an "extra + field" using the {"value": ..., "source": "llm_added"} wrapper + convention, so downstream code (e.g. the future Neo4j writer) can + decide whether to trust, review, or discard it. +""" +from __future__ import annotations + +import json +import logging +import re +from typing import Any, Dict + +from config import ( + EXTRA_FIELD_SOURCE_KEY, + EXTRA_FIELD_SOURCE_TAG, + EXTRA_FIELD_VALUE_KEY, +) + +logger = logging.getLogger(__name__) + + +class ResponseParseError(Exception): + """Raised when the model's response cannot be salvaged into JSON.""" + + +_FENCE_RE = re.compile(r"^```(?:json)?\s*|\s*```$", re.MULTILINE) + + +def _strip_code_fences(text: str) -> str: + return _FENCE_RE.sub("", text).strip() + + +def _extract_json_substring(text: str) -> str: + """ + Fallback for when the model wraps JSON in stray prose: grabs the + substring between the first '{' and the last '}'. + """ + start = text.find("{") + end = text.rfind("}") + if start == -1 or end == -1 or end <= start: + raise ResponseParseError("No JSON object found in model output") + return text[start : end + 1] + + +def parse_raw_json(raw_text: str) -> Dict[str, Any]: + """Best-effort parse of the model's raw string into a dict.""" + candidate = _strip_code_fences(raw_text) + try: + return json.loads(candidate) + except json.JSONDecodeError: + logger.debug("Direct JSON parse failed, attempting substring extraction") + + candidate = _extract_json_substring(candidate) + try: + return json.loads(candidate) + except json.JSONDecodeError as exc: + raise ResponseParseError(f"Could not parse model output as JSON: {exc}") from exc + + +def _wrap_extra(value: Any) -> Dict[str, Any]: + return { + EXTRA_FIELD_VALUE_KEY: value, + EXTRA_FIELD_SOURCE_KEY: EXTRA_FIELD_SOURCE_TAG, + } + + +def reconcile_with_schema( + schema: Dict[str, Any], + model_output: Dict[str, Any], +) -> Dict[str, Any]: + """ + Recursively walks `schema` and `model_output` together. + + - Keys present in schema: pass the model's value through as-is + (or null if the model omitted / couldn't find it). No wrapper — + these stay flat so existing consumers of the schema shape don't + need to change. + - Keys present in model_output but NOT in schema: kept flat, at + the same nesting level, but wrapped as + {"value": ..., "source": "llm_added"} so they're clearly + distinguishable from schema-backed fields. + - Nested dicts are handled recursively, so multi-level schemas + (e.g. an "address" object) work the same way. + """ + result: Dict[str, Any] = {} + + if not isinstance(model_output, dict): + logger.warning("Model output at this level was not a dict; discarding") + model_output = {} + + # 1. Walk schema fields first, guaranteeing they all exist in the output. + for key, expected in schema.items(): + model_value = model_output.get(key) + if isinstance(expected, dict) and isinstance(model_value, dict): + result[key] = reconcile_with_schema(expected, model_value) + elif isinstance(expected, dict): + # Model dropped a whole nested object -> recurse against {} so + # every nested schema key still shows up, null-filled. + result[key] = reconcile_with_schema(expected, {}) + else: + result[key] = model_value + + # 2. Anything the model added that wasn't in the schema. + for key, value in model_output.items(): + if key in schema: + continue + result[key] = _wrap_extra(value) + + return result + + +def normalize_response(schema: Dict[str, Any], raw_text: str) -> Dict[str, Any]: + """Full pipeline: raw model string -> parsed JSON -> schema-reconciled dict.""" + parsed = parse_raw_json(raw_text) + return reconcile_with_schema(schema, parsed) diff --git a/lending-poc/modules/field_mapping_poc/main.py b/lending-poc/modules/field_mapping_poc/main.py new file mode 100644 index 0000000..f568e1a --- /dev/null +++ b/lending-poc/modules/field_mapping_poc/main.py @@ -0,0 +1,58 @@ +""" +Demo runner for the Field Mapping POC. + +Usage: + python main.py + python main.py --schema schemas/examples/identity_card.json --text samples/sample_ocr_text.txt + +Requires a running local Ollama instance with the target model pulled: + ollama pull gemma3:4b +""" +from __future__ import annotations + +import argparse +import json +import logging +import sys +from pathlib import Path + +from core.mapper import FieldMapper +from core.ollama_client import OllamaClientError +from core.response_parser import ResponseParseError + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", +) + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="LegalAI Field Mapping POC") + parser.add_argument("--schema", type=Path, default=Path("schemas/examples/salary_slip.json"), + help="Path to a JSON file describing the target schema") + parser.add_argument("--text", type=Path, default=Path("samples/sample_ocr_text.txt"), + help="Path to a text file containing raw OCR output") + + args = parser.parse_args() + return args + + +def main() -> int: + args = parse_args() + + schema = json.loads(args.schema.read_text(encoding="utf-8")) + document_text = args.text.read_text(encoding="utf-8") + + mapper = FieldMapper() + + try: + result = mapper.map_fields(schema, document_text) + except (OllamaClientError, ResponseParseError, ValueError) as exc: + logging.error("Field mapping failed: %s", exc) + return 1 + + print(json.dumps(result, indent=2, ensure_ascii=False)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/lending-poc/modules/field_mapping_poc/requirements.txt b/lending-poc/modules/field_mapping_poc/requirements.txt new file mode 100644 index 0000000..1111ae2 --- /dev/null +++ b/lending-poc/modules/field_mapping_poc/requirements.txt @@ -0,0 +1 @@ +ollama>=0.3.0 diff --git a/lending-poc/modules/field_mapping_poc/run_samples.py b/lending-poc/modules/field_mapping_poc/run_samples.py new file mode 100644 index 0000000..1d00d8e --- /dev/null +++ b/lending-poc/modules/field_mapping_poc/run_samples.py @@ -0,0 +1,46 @@ +import json +import logging +from pathlib import Path + +from core.mapper import FieldMapper +from core.ollama_client import OllamaClientError +from core.response_parser import ResponseParseError + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", +) + +def run_sample(schema_path: str, text_path: str): + print(f"\n{'='*50}") + print(f"Running Mapping Pipeline") + print(f"Schema: {schema_path}") + print(f"Input Text: {text_path}") + print(f"{'='*50}") + + try: + # Load schema and text + schema = json.loads(Path(schema_path).read_text(encoding="utf-8")) + document_text = Path(text_path).read_text(encoding="utf-8") + + # Initialize mapper and run + mapper = FieldMapper() + result = mapper.map_fields(schema, document_text) + + # Print results + print("\nMapping Result:") + print(json.dumps(result, indent=2, ensure_ascii=False)) + + except (OllamaClientError, ResponseParseError, ValueError, FileNotFoundError) as exc: + logging.error("Field mapping failed: %s", exc) + +if __name__ == "__main__": + schema_file = "schemas/examples/salary_slip.json" + + # Run the clean sample + clean_text = "samples/sample_salary_slip.txt" + run_sample(schema_file, clean_text) + + # Run the noisy OCR sample + noisy_text = "samples/sample_salary_slip_noisy.txt" + run_sample(schema_file, noisy_text) diff --git a/lending-poc/modules/field_mapping_poc/samples/sample_ocr_text.txt b/lending-poc/modules/field_mapping_poc/samples/sample_ocr_text.txt new file mode 100644 index 0000000..efc0e43 --- /dev/null +++ b/lending-poc/modules/field_mapping_poc/samples/sample_ocr_text.txt @@ -0,0 +1,25 @@ +XYZ TECHNOLOGIES PVT LTD +Payslip for the month of March 2026 + +Employee Name : Ishaan Deshmukh +Employee ID : JSW-1042 +Designation : Associate Software Engineer +PAN : ABCDE1234F +UAN Number : 101234567890 + +Earnings Amount (Rs.) +Basic Pay 45,000 +HRA 18,000 +Special Allowance 12,000 +Gross Salary 75,000 + +Deductions +Provident Fund 5,400 +Professional Tax 200 +Income Tax (TDS) 4,400 + +Net Salary (in words: Rupees Sixty Five Thousand Only) +Net Salary 65,000 + +Bank A/c No. : XXXXXXXX7890 +Bank Name : HDFC Bank diff --git a/lending-poc/modules/field_mapping_poc/samples/sample_salary_slip.txt b/lending-poc/modules/field_mapping_poc/samples/sample_salary_slip.txt new file mode 100644 index 0000000..33b535c --- /dev/null +++ b/lending-poc/modules/field_mapping_poc/samples/sample_salary_slip.txt @@ -0,0 +1,29 @@ +TechCorp Innovations Pvt. Ltd. +123 Tech Park, Bengaluru, 560001 +=================================== +PAYSLIP FOR THE MONTH OF: August 2026 + +Employee Information: +Name: Jane Doe +Employee ID: TC-8492 +PAN: ABCDE1234F +Designation: Senior Software Engineer +Bank A/C Number: 09876543211234 + +Earnings: +Basic Pay: $ 4,500.00 +HRA: $ 1,500.00 +Special Allowance: $ 1,000.00 +----------------------------------- +Gross Salary: $ 7,000.00 + +Deductions: +PF: $ 200.00 +Professional Tax: $ 50.00 +Income Tax: $ 750.00 +----------------------------------- +Total Deductions: $ 1,000.00 + +Net Salary: $ 6,000.00 +=================================== +This is a computer generated document and does not require a signature. diff --git a/lending-poc/modules/field_mapping_poc/samples/sample_salary_slip_noisy.txt b/lending-poc/modules/field_mapping_poc/samples/sample_salary_slip_noisy.txt new file mode 100644 index 0000000..495ce7c --- /dev/null +++ b/lending-poc/modules/field_mapping_poc/samples/sample_salary_slip_noisy.txt @@ -0,0 +1,23 @@ +T3chC0rp lnnovations Pvt Ltd. +123 T#ch Pa rk , 8engaluru 560OO1 +- - - - - - - -- - - - - - - - - - - +PA Y SLlP FQR THE M0NTH OF: Aug 2026 + +EmpIo yee |nfo: +N ame: J ane Doe +Emp |D : TC -84 92 +P AN: A B C D E 1 2 3 4 F +Designati0n: Senior Softwar e Engineer +8ank A/C Number: 098 765 4321 1234 + +Ea r n i n g s D e d u c t i o n s +Basic P ay 4,5OO .00 PF 200. 00 +HR A 1,500.00 Prof Tax 50 .00 +Spc Allwnce 1,000 .00 |ncme T ax 7 5 0 .00 + +Gr oss SaIary 7,00 0.00 Total Ded 1,000 .00 + +N et SaIary : 6 ,00 0 . 0 0 +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +This i s a com pu ter gener ated doc +& d0es n0t require a sign atur e . diff --git a/lending-poc/modules/field_mapping_poc/schemas/examples/identity_card.json b/lending-poc/modules/field_mapping_poc/schemas/examples/identity_card.json new file mode 100644 index 0000000..3997228 --- /dev/null +++ b/lending-poc/modules/field_mapping_poc/schemas/examples/identity_card.json @@ -0,0 +1,12 @@ +{ + "fullName": "string", + "documentNumber": "string", + "dateOfBirth": "date (DD-MM-YYYY)", + "gender": "string", + "address": { + "line1": "string", + "city": "string", + "state": "string", + "pincode": "string" + } +} diff --git a/lending-poc/modules/field_mapping_poc/schemas/examples/salary_slip.json b/lending-poc/modules/field_mapping_poc/schemas/examples/salary_slip.json new file mode 100644 index 0000000..cd7d6e0 --- /dev/null +++ b/lending-poc/modules/field_mapping_poc/schemas/examples/salary_slip.json @@ -0,0 +1,11 @@ +{ + "employeeName": "string", + "employeeId": "string", + "panNumber": "string", + "designation": "string", + "payPeriod": "string, e.g. 'March 2026'", + "basicPay": "number", + "grossSalary": "number", + "netSalary": "number", + "bankAccountNumber": "string" +}