Commit
·
f11a85d
1
Parent(s):
92a2adc
dockerfile
Browse files- Dockerfile +25 -0
- app.py +5 -0
- poetry.lock +0 -0
- pyproject.toml +29 -0
- src/RAG.py +173 -0
- src/model.py +55 -0
- src/pipelines.py +149 -0
- src/prompts.py +72 -0
Dockerfile
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.10
|
2 |
+
|
3 |
+
# Create a non-root user
|
4 |
+
RUN useradd -m -u 1000 user
|
5 |
+
USER user
|
6 |
+
ENV PATH="/home/user/.local/bin:$PATH"
|
7 |
+
|
8 |
+
# Set the working directory
|
9 |
+
WORKDIR /app
|
10 |
+
|
11 |
+
# Copy Poetry config and install Poetry
|
12 |
+
COPY --chown=user ./pyproject.toml ./poetry.lock* ./
|
13 |
+
RUN pip install --no-cache-dir --upgrade pip \
|
14 |
+
&& pip install --no-cache-dir poetry \
|
15 |
+
&& poetry config virtualenvs.create false \
|
16 |
+
&& poetry install --no-dev --no-interaction --no-ansi
|
17 |
+
|
18 |
+
# Copy the rest of the app
|
19 |
+
COPY --chown=user . /app
|
20 |
+
|
21 |
+
# Expose the Streamlit default port
|
22 |
+
EXPOSE 8501
|
23 |
+
|
24 |
+
# Run the Streamlit app
|
25 |
+
CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
|
app.py
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# streamlit example
|
2 |
+
|
3 |
+
import streamlit as st
|
4 |
+
|
5 |
+
st.title('Hello World!')
|
poetry.lock
ADDED
The diff for this file is too large to render.
See raw diff
|
|
pyproject.toml
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[tool.poetry]
|
2 |
+
name = "demo-pixtral-qwen"
|
3 |
+
version = "0.1.0"
|
4 |
+
description = ""
|
5 |
+
authors = ["Your Name <[email protected]>"]
|
6 |
+
|
7 |
+
[tool.poetry.dependencies]
|
8 |
+
python = "^3.10"
|
9 |
+
clip = {git = "https://github.com/openai/CLIP.git", rev = "main"}
|
10 |
+
torchvision = "0.19"
|
11 |
+
vllm = "^0.6.3"
|
12 |
+
pillow = "10.3.0"
|
13 |
+
PyMuPDF = "^1.24.13"
|
14 |
+
pandas = "^2.2.3"
|
15 |
+
faiss-gpu = "^1.7.2"
|
16 |
+
huggingface = "^0.0.1"
|
17 |
+
huggingface-hub = "^0.26.2"
|
18 |
+
md2pdf = "^1.0.1"
|
19 |
+
pypdf = "^5.1.0"
|
20 |
+
wheel = "^0.45.1"
|
21 |
+
rerankers = {extras = ["all"], version = "^0.6.0"}
|
22 |
+
streamlit = "^1.40.1"
|
23 |
+
fuzzywuzzy = "^0.18.0"
|
24 |
+
|
25 |
+
[tool.poetry.dev-dependencies]
|
26 |
+
|
27 |
+
[build-system]
|
28 |
+
requires = ["poetry-core>=1.0.0"]
|
29 |
+
build-backend = "poetry.core.masonry.api"
|
src/RAG.py
ADDED
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import clip
|
2 |
+
import faiss
|
3 |
+
from PIL import Image
|
4 |
+
from pypdf import PdfReader
|
5 |
+
import pandas as pd
|
6 |
+
import re
|
7 |
+
import os
|
8 |
+
import fitz
|
9 |
+
import torch
|
10 |
+
import numpy as np
|
11 |
+
from tqdm import tqdm
|
12 |
+
import base64
|
13 |
+
|
14 |
+
|
15 |
+
class RAG:
|
16 |
+
def __init__(
|
17 |
+
self,
|
18 |
+
fais_index_path,
|
19 |
+
clip_model="ViT-B/32",
|
20 |
+
reranker=None,
|
21 |
+
device="cpu",
|
22 |
+
image_invoice_index_path=None,
|
23 |
+
path_to_invoices=None,
|
24 |
+
path_to_images=None
|
25 |
+
):
|
26 |
+
self.index = faiss.read_index(fais_index_path)
|
27 |
+
self.model, self.preprocess = clip.load(clip_model, device=device)
|
28 |
+
self.device = device
|
29 |
+
if image_invoice_index_path:
|
30 |
+
self.image_invoice_index = pd.read_csv(image_invoice_index_path)
|
31 |
+
self.path_to_invoices = path_to_invoices
|
32 |
+
self.path_to_images = path_to_images
|
33 |
+
self.reranker = reranker
|
34 |
+
|
35 |
+
@staticmethod
|
36 |
+
def image_to_base64(image_path):
|
37 |
+
with open(image_path, "rb") as image_file:
|
38 |
+
return base64.b64encode(image_file.read())
|
39 |
+
|
40 |
+
def search_text(self, text, k=1):
|
41 |
+
text_features = self.model.encode_text(clip.tokenize([text]).to(self.device))
|
42 |
+
text_features /= text_features.norm(dim=-1, keepdim=True)
|
43 |
+
text_features = text_features.detach().numpy()
|
44 |
+
distances, indices = self.index.search(text_features, k)
|
45 |
+
return distances, indices
|
46 |
+
|
47 |
+
def search_image(self, image=None, image_path=None, k=1):
|
48 |
+
if image is None and image_path is None:
|
49 |
+
raise ValueError("Either image or image_path must be provided.")
|
50 |
+
if image is None:
|
51 |
+
image = Image.open(image_path)
|
52 |
+
image_input = self.preprocess(image).unsqueeze(0).to(self.device)
|
53 |
+
image_features = self.model.encode_image(image_input)
|
54 |
+
image_features /= image_features.norm(dim=-1, keepdim=True)
|
55 |
+
image_features = image_features.detach().numpy()
|
56 |
+
distances, indices = self.index.search(image_features, k)
|
57 |
+
return distances, indices
|
58 |
+
|
59 |
+
def find_invoice(self, image=None, image_path=None, return_only_path=True, k=1, damage_description=None):
|
60 |
+
if self.image_invoice_index is None:
|
61 |
+
raise ValueError("No index for invoices found.")
|
62 |
+
_, indices = self.search_image(image=image, image_path=image_path, k=k)
|
63 |
+
img_ids = self.image_invoice_index.iloc[indices[0]]['img_id'].values
|
64 |
+
invoices = self.image_invoice_index[self.image_invoice_index['img_id'].isin(img_ids)]['invoice'].values.tolist()
|
65 |
+
images_paths = self.image_invoice_index[self.image_invoice_index['img_id'].isin(img_ids)]['image'].values.tolist()
|
66 |
+
|
67 |
+
if self.reranker:
|
68 |
+
if damage_description is None:
|
69 |
+
raise ValueError("Damage description must be provided.")
|
70 |
+
# images = [self.image_to_base64(f"{self.path_to_images}/{img_path}") for img_path in images_paths]
|
71 |
+
|
72 |
+
images = [f"{self.path_to_images}/{img_path}" for img_path in images_paths]
|
73 |
+
results = self.reranker.rank(damage_description, images, doc_ids=invoices)
|
74 |
+
invoices = [doc.doc_id for doc in results]
|
75 |
+
print(invoices)
|
76 |
+
|
77 |
+
if return_only_path:
|
78 |
+
return invoices, images_paths
|
79 |
+
|
80 |
+
if not self.path_to_invoices:
|
81 |
+
raise ValueError("Path to data must be provided.")
|
82 |
+
|
83 |
+
invoices_tables = []
|
84 |
+
|
85 |
+
for invoice in invoices:
|
86 |
+
pdf_path = f"{self.path_to_invoices}/{invoice}"
|
87 |
+
reader = PdfReader(pdf_path)
|
88 |
+
page = reader.pages[0]
|
89 |
+
text = page.extract_text()
|
90 |
+
|
91 |
+
table_text = re.search(r"Beschädigtes Teil.*?Gesamtsumme:.*?EUR", text, re.DOTALL).group()
|
92 |
+
|
93 |
+
lines = table_text.splitlines()
|
94 |
+
header = lines[0]
|
95 |
+
other_text = "\n".join(lines[1:])
|
96 |
+
cleaned_text = re.sub(r"(?<!\d)\n", " ", other_text)
|
97 |
+
|
98 |
+
table = header + "\n" + cleaned_text
|
99 |
+
|
100 |
+
inv = table.split("\n")
|
101 |
+
reformatted_inv = "Beschädigtes Teil | Teilkosten (EUR) | Arbeitsstunden | Arbeitskosten (EUR/Stunde) | Gesamtkosten (EUR)\n" + "\n".join(
|
102 |
+
" ".join(inv[i].split(" ")[:-4]) + " | " + ' | '.join(inv[i].split(" ")[-4:]) for i in
|
103 |
+
range(1, len(inv) - 1)) + "\n" + inv[-1]
|
104 |
+
|
105 |
+
invoices_tables.append(reformatted_inv)
|
106 |
+
|
107 |
+
return invoices_tables, invoices
|
108 |
+
|
109 |
+
|
110 |
+
def build_rag(directory):
|
111 |
+
invoices = os.listdir(f"{directory}/invoices_validated")
|
112 |
+
invoices = [i for i in invoices if i.endswith(".pdf")]
|
113 |
+
|
114 |
+
image_invoice = []
|
115 |
+
os.makedirs(f"{directory}/images", exist_ok=True)
|
116 |
+
os.makedirs(f"{directory}/invoices", exist_ok=True)
|
117 |
+
|
118 |
+
for invoice in invoices:
|
119 |
+
doc = fitz.open(f"{directory}/invoices_validated/{invoice}")
|
120 |
+
|
121 |
+
page = doc[1]
|
122 |
+
image_list = page.get_images(full=True)
|
123 |
+
text = page.get_text()
|
124 |
+
|
125 |
+
xref = image_list[0][0]
|
126 |
+
base_image = doc.extract_image(xref)
|
127 |
+
image_bytes = base_image["image"]
|
128 |
+
image_name = invoice.replace(".pdf", ".png")
|
129 |
+
with open(f"{directory}/images/{image_name}", "wb") as img_file:
|
130 |
+
img_file.write(image_bytes)
|
131 |
+
|
132 |
+
doc.delete_pages(range(1, doc.page_count))
|
133 |
+
doc.save(f"{directory}/invoices/{invoice}")
|
134 |
+
doc.close()
|
135 |
+
|
136 |
+
image_invoice.append({
|
137 |
+
"invoice": invoice,
|
138 |
+
"image": image_name,
|
139 |
+
"description": text
|
140 |
+
})
|
141 |
+
|
142 |
+
image_invoice = pd.DataFrame(image_invoice)
|
143 |
+
|
144 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
145 |
+
model, preprocess = clip.load("ViT-B/32", device=device)
|
146 |
+
images = image_invoice["image"].tolist()
|
147 |
+
|
148 |
+
embeddings = []
|
149 |
+
image_indices = []
|
150 |
+
img_ids = []
|
151 |
+
|
152 |
+
for idx, img_path in enumerate(tqdm(images)):
|
153 |
+
image = Image.open(f"{directory}/images/{img_path}")
|
154 |
+
img_ids.append(idx)
|
155 |
+
inputs = preprocess(image).unsqueeze(0).to(device)
|
156 |
+
|
157 |
+
with torch.no_grad():
|
158 |
+
image_embedding = model.encode_image(inputs)
|
159 |
+
|
160 |
+
image_embedding = image_embedding / image_embedding.norm(dim=-1, keepdim=True)
|
161 |
+
embeddings.append(image_embedding.cpu().numpy().astype("float32"))
|
162 |
+
image_indices.append(img_path)
|
163 |
+
|
164 |
+
image_invoice["img_id"] = img_ids
|
165 |
+
image_invoice.to_csv(f"{directory}/image_invoice.csv", index=False)
|
166 |
+
|
167 |
+
embeddings_np = np.vstack(embeddings)
|
168 |
+
|
169 |
+
dimension = embeddings_np.shape[1]
|
170 |
+
index = faiss.IndexFlatIP(dimension)
|
171 |
+
index.add(embeddings_np)
|
172 |
+
|
173 |
+
faiss.write_index(index, f"{directory}/invoice_index.faiss")
|
src/model.py
ADDED
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from vllm import LLM
|
2 |
+
from vllm.sampling_params import SamplingParams
|
3 |
+
import base64
|
4 |
+
|
5 |
+
|
6 |
+
def encode_image(image_path: str):
|
7 |
+
with open(image_path, "rb") as image_file:
|
8 |
+
return base64.b64encode(image_file.read()).decode("utf-8")
|
9 |
+
|
10 |
+
|
11 |
+
class Pixtral:
|
12 |
+
def __init__(self, max_model_len=4096, max_tokens=4096, gpu_memory_utilization=0.95, temperature=0.35):
|
13 |
+
self.model_name = "mistralai/Pixtral-12B-2409"
|
14 |
+
|
15 |
+
self.sampling_params = SamplingParams(max_tokens=max_tokens, temperature=temperature)
|
16 |
+
|
17 |
+
self.llm = LLM(
|
18 |
+
model=self.model_name,
|
19 |
+
tokenizer_mode="mistral",
|
20 |
+
gpu_memory_utilization=gpu_memory_utilization,
|
21 |
+
load_format="mistral",
|
22 |
+
config_format="mistral",
|
23 |
+
max_model_len=max_model_len
|
24 |
+
)
|
25 |
+
|
26 |
+
def generate_message_from_image(self, prompt, image_path):
|
27 |
+
base64_image = encode_image(image_path)
|
28 |
+
|
29 |
+
messages = [
|
30 |
+
{
|
31 |
+
"role": "user",
|
32 |
+
"content": [
|
33 |
+
{"type": "text", "text": prompt},
|
34 |
+
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}}
|
35 |
+
]
|
36 |
+
},
|
37 |
+
]
|
38 |
+
|
39 |
+
outputs = self.llm.chat(messages, sampling_params=self.sampling_params)
|
40 |
+
|
41 |
+
return outputs[0].outputs[0].text
|
42 |
+
|
43 |
+
def generate_message(self, prompt):
|
44 |
+
messages = [
|
45 |
+
{
|
46 |
+
"role": "user",
|
47 |
+
"content": [
|
48 |
+
{"type": "text", "text": prompt},
|
49 |
+
]
|
50 |
+
},
|
51 |
+
]
|
52 |
+
|
53 |
+
outputs = self.llm.chat(messages, sampling_params=self.sampling_params)
|
54 |
+
|
55 |
+
return outputs[0].outputs[0].text
|
src/pipelines.py
ADDED
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from src.RAG import RAG
|
2 |
+
from src.model import Pixtral
|
3 |
+
from src.prompts import GENERATE_INVOICE_PROMPT, GENERATE_BRIEF_DAMAGE_DESCRIPTION_PROMPT, \
|
4 |
+
GENERATE_DETAILED_DAMAGE_DESCRIPTION_PROMPT
|
5 |
+
from md2pdf.core import md2pdf
|
6 |
+
from rerankers import Reranker
|
7 |
+
import re
|
8 |
+
from fuzzywuzzy import fuzz
|
9 |
+
|
10 |
+
|
11 |
+
class InvoiceGenerator:
|
12 |
+
def __init__(
|
13 |
+
self,
|
14 |
+
fais_index_path,
|
15 |
+
image_invoice_index_path,
|
16 |
+
path_to_invoices,
|
17 |
+
path_to_images,
|
18 |
+
reranker_model=None,
|
19 |
+
device="cuda",
|
20 |
+
max_model_len=4096, max_tokens=2048, gpu_memory_utilization=0.95
|
21 |
+
):
|
22 |
+
self.model = Pixtral(max_model_len=max_model_len, max_tokens=max_tokens,
|
23 |
+
gpu_memory_utilization=gpu_memory_utilization)
|
24 |
+
if reranker_model:
|
25 |
+
self.reranker = Reranker(model_name=reranker_model, device=device)
|
26 |
+
|
27 |
+
self.device = device
|
28 |
+
self.rag = RAG(
|
29 |
+
fais_index_path=fais_index_path,
|
30 |
+
image_invoice_index_path=image_invoice_index_path,
|
31 |
+
path_to_invoices=path_to_invoices,
|
32 |
+
path_to_images=path_to_images,
|
33 |
+
reranker=self.reranker
|
34 |
+
)
|
35 |
+
self.path_to_invoices = path_to_invoices
|
36 |
+
self.path_to_images = path_to_images
|
37 |
+
|
38 |
+
def format_invoice(self, generated_invoice, output_path, template_path="data/template.md"):
|
39 |
+
with open(template_path, "r") as f:
|
40 |
+
md_text = f.read()
|
41 |
+
md_text = md_text.replace(r"<<table>>", generated_invoice)
|
42 |
+
md2pdf(output_path, md_content=md_text)
|
43 |
+
|
44 |
+
@staticmethod
|
45 |
+
def check_within_range(generated_invoice, car_parts):
|
46 |
+
def get_part_info(part_name, car_parts):
|
47 |
+
part_name = part_name.lower()
|
48 |
+
max_match = [None, 0]
|
49 |
+
for part in car_parts:
|
50 |
+
ratio = fuzz.WRatio(part_name, part.lower())
|
51 |
+
if ratio >= 90 and ratio > max_match[1]:
|
52 |
+
max_match[0] = part
|
53 |
+
max_match[1] = ratio
|
54 |
+
return max_match[0]
|
55 |
+
|
56 |
+
all_lines = generated_invoice.split("\n")
|
57 |
+
first_cost_line = 3 if all_lines[0] == '' else 2
|
58 |
+
last_cost_line = -2 if all_lines[-1] == '' else -1
|
59 |
+
lines = generated_invoice.split("\n")[first_cost_line:last_cost_line]
|
60 |
+
cost_lines = [[line.strip() for line in cost_line.split("|")] for cost_line in lines]
|
61 |
+
|
62 |
+
comparing_results = {}
|
63 |
+
|
64 |
+
for line in cost_lines:
|
65 |
+
part = line[0]
|
66 |
+
cost = line[1]
|
67 |
+
hours = line[2]
|
68 |
+
found_part = get_part_info(part, car_parts)
|
69 |
+
if found_part:
|
70 |
+
comparing_results[part] = {
|
71 |
+
"cost_within_range": car_parts[found_part]["cost_range"][0] <= float(cost) <=
|
72 |
+
car_parts[found_part]["cost_range"][1],
|
73 |
+
"hours_within_range": car_parts[found_part]["hours_range"][0] <= float(hours) <=
|
74 |
+
car_parts[found_part]["hours_range"][1],
|
75 |
+
"cost_diff": float(cost) - car_parts[found_part]["average_cost"],
|
76 |
+
"hours_diff": float(hours) - car_parts[found_part]["average_hours"],
|
77 |
+
"part_info": found_part
|
78 |
+
}
|
79 |
+
else:
|
80 |
+
comparing_results[part] = {}
|
81 |
+
|
82 |
+
return comparing_results
|
83 |
+
|
84 |
+
@staticmethod
|
85 |
+
def check_calculations(generated_invoice):
|
86 |
+
all_lines = generated_invoice.split("\n")
|
87 |
+
first_cost_line = 3 if all_lines[0] == '' else 2
|
88 |
+
last_cost_line = -2 if all_lines[-1] == '' else -1
|
89 |
+
total_cost_line = all_lines[last_cost_line]
|
90 |
+
lines = generated_invoice.split("\n")[first_cost_line:last_cost_line]
|
91 |
+
cost_lines = [[line.strip() for line in cost_line.split("|")] for cost_line in lines]
|
92 |
+
costs = [int(line[1]) + int(line[2]) * int(line[3]) for line in cost_lines]
|
93 |
+
cost_lines = list(map(lambda x, y: [x[0], x[1], x[2], x[3], str(y)], cost_lines, costs))
|
94 |
+
total_cost = sum(costs)
|
95 |
+
total_cost_line = re.sub(r"\d+", f"{total_cost}", total_cost_line)
|
96 |
+
|
97 |
+
all_lines[last_cost_line] = total_cost_line
|
98 |
+
all_lines[first_cost_line:last_cost_line] = list(map(lambda x: " | ".join(x), cost_lines))
|
99 |
+
return "\n".join(all_lines)
|
100 |
+
|
101 |
+
def generate_invoice(self, image_path, output_path=None, template_path="data/template.md", car_parts=None):
|
102 |
+
|
103 |
+
result = {}
|
104 |
+
|
105 |
+
damage_description = self.model.generate_message_from_image(
|
106 |
+
GENERATE_BRIEF_DAMAGE_DESCRIPTION_PROMPT, image_path
|
107 |
+
)
|
108 |
+
if damage_description == "Irrelevant." or len(damage_description.split()) < 5:
|
109 |
+
return None
|
110 |
+
|
111 |
+
result["damage_description"] = damage_description
|
112 |
+
|
113 |
+
print("Damage Description:", damage_description)
|
114 |
+
|
115 |
+
invoice_info, invoice_path = self.rag.find_invoice(
|
116 |
+
image_path=image_path, return_only_path=False, damage_description=damage_description, k=5
|
117 |
+
)
|
118 |
+
invoice_info = invoice_info[0]
|
119 |
+
invoice_path = invoice_path[0]
|
120 |
+
result["invoice_info"] = invoice_info
|
121 |
+
result["invoice_path"] = invoice_path
|
122 |
+
result["similar_image"] = invoice_path.replace(".pdf", ".png")
|
123 |
+
|
124 |
+
print("Invoice Path:", invoice_path)
|
125 |
+
|
126 |
+
detailed_damage_description = self.model.generate_message_from_image(
|
127 |
+
GENERATE_DETAILED_DAMAGE_DESCRIPTION_PROMPT, image_path
|
128 |
+
)
|
129 |
+
result["detailed_damage_description"] = detailed_damage_description
|
130 |
+
|
131 |
+
print("Detailed Damage Description:", detailed_damage_description)
|
132 |
+
|
133 |
+
generated_invoice = self.model.generate_message_from_image(
|
134 |
+
GENERATE_INVOICE_PROMPT(invoice_info, detailed_damage_description), image_path
|
135 |
+
).replace("```markdown", "").replace("```", "")
|
136 |
+
generated_invoice = self.check_calculations(generated_invoice)
|
137 |
+
|
138 |
+
result["generated_invoice"] = generated_invoice
|
139 |
+
|
140 |
+
if car_parts:
|
141 |
+
comparing_results = self.check_within_range(generated_invoice, car_parts)
|
142 |
+
result["comparing_results"] = comparing_results
|
143 |
+
print(comparing_results)
|
144 |
+
|
145 |
+
if output_path:
|
146 |
+
self.format_invoice(generated_invoice=generated_invoice, output_path=output_path,
|
147 |
+
template_path=template_path)
|
148 |
+
|
149 |
+
return result
|
src/prompts.py
ADDED
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
GENERATE_INVOICE_PROMPT = """
|
2 |
+
Given an image of a car accident, damages description, and an example of invoice for a similar car accident case, generate a repair invoice specifically for the provided image.
|
3 |
+
The invoice should include details on the parts needing replacement, labor hours, and costs. Structure it as a standard repair invoice typical for Bavaria Direct, in German.
|
4 |
+
Include only repair and cost information, with no contact details or extra data.
|
5 |
+
|
6 |
+
Important: Use the example invoice as a reference only. Do not copy it directly, but you can use labor costs and part names as a reference, if they are relevant to the image.
|
7 |
+
|
8 |
+
The invoice should include a list of items, where each item has the following fields:
|
9 |
+
|
10 |
+
Beschädigtes Teil (Damaged Part)
|
11 |
+
Teilkosten (Part Cost, EUR)
|
12 |
+
Arbeitsstunden (Labor Hours)
|
13 |
+
Arbeitskosten (Labor Cost per Hour, EUR/Stunde)
|
14 |
+
Gesamtkosten (Total Cost, EUR)
|
15 |
+
|
16 |
+
**Invoice for the similar car accident (for reference only):**
|
17 |
+
|
18 |
+
{0}
|
19 |
+
|
20 |
+
**Damages description**
|
21 |
+
|
22 |
+
|
23 |
+
{1}
|
24 |
+
|
25 |
+
|
26 |
+
---
|
27 |
+
|
28 |
+
**Guidelines for Generation:**
|
29 |
+
1. **Unique Response**: Generate reasonable variations in part types, costs, and labor hours based on the damage visible in the provided image.
|
30 |
+
2. **Check Calculations**: Ensure that **Gesamtkosten** for each item is calculated as:
|
31 |
+
\[
|
32 |
+
Gesamtkosten = Teilkosten + (Arbeitsstunden x Arbeitskosten)
|
33 |
+
\]
|
34 |
+
The **Gesamtsumme** should reflect the sum of all **Gesamtkosten** entries.
|
35 |
+
|
36 |
+
Generate a detailed and realistic invoice tailored specifically to the image, without replicating the example.
|
37 |
+
|
38 |
+
OUTPUT REQUIREMENTS:
|
39 |
+
Your output should be a table in markdown format WITHOUT ANY ADDITIONAL COMMENTS. The format of a table is provided in reference invoice examle.
|
40 |
+
""".format
|
41 |
+
|
42 |
+
|
43 |
+
GENERATE_BRIEF_DAMAGE_DESCRIPTION_PROMPT = """
|
44 |
+
You will be provided with an image of a car accident.
|
45 |
+
If the provided photo is not a photo of a car accident or a damaged car (the picture should look like "First Notice of Loss" photo), you must write "Irrelevant." and nothing else.
|
46 |
+
If a car is completely destroyed and it is not possible to repair it, you must write "Irrelevant." and nothing else and stop following the instructions.
|
47 |
+
Otherwise, provide a brief description of the damage.
|
48 |
+
The description should include the type of damage and the affected area of the vehicle. Pay special attention to the parts of the vehicle that are damaged. Be concise and specific, focusing on the visible damage in the image.
|
49 |
+
Write no more than 1-2 sentences describing the damage in detail.
|
50 |
+
Do not add any additional information beyond the visible damage in the image and any comments.
|
51 |
+
"""
|
52 |
+
|
53 |
+
|
54 |
+
GENERATE_DETAILED_DAMAGE_DESCRIPTION_PROMPT = """
|
55 |
+
Given an image of a car accident, provide a detailed and cosine description of all visible damage.
|
56 |
+
The description should include the type of damage, the affected area of the vehicle, and any other relevant details.
|
57 |
+
Be thorough and specific, covering all visible damage in the image.
|
58 |
+
Write no more than 3-4 sentences describing the damage in detail.
|
59 |
+
Do not add any additional information beyond the visible damage in the image and any comments.
|
60 |
+
"""
|
61 |
+
|
62 |
+
|
63 |
+
ESTIMATE_COST_OF_CAR_PARTS_REPLACEMENT_PROMPT = """
|
64 |
+
I will provide you with a list of car parts that need to be replaced due to damage in a car accident (in German).
|
65 |
+
Your task is to estimate the cost of each part in Euros (EUR) based on the damage description provided.
|
66 |
+
Also, you need to estimate approximate labor hours required to replace each part.
|
67 |
+
There may be duplicate parts in the list (for example, for right and left sides of the car).
|
68 |
+
You may also expand the list with new parts which are not in the list but may be necessary for the repair.
|
69 |
+
Delete any duplicate entries and provide the cost and labor hours for one side only.
|
70 |
+
Sort the parts in the list in ascending order based on name of each part (in alphabetical order).
|
71 |
+
Your output should be a python dictionary where the keys are the names of the parts and the values are lists containing the estimated cost and labor hours for each part.
|
72 |
+
"""
|