Spaces:
Sleeping
Sleeping
File Upload Fix
#15
by palexis3 - opened
- .DS_Store +0 -0
- app/.DS_Store +0 -0
- app/api/routers/file_upload.py +78 -12
- app/categorization/categorizer.py +73 -41
- app/categorization/categorizer_list.py +87 -45
- app/categorization/file_processing.py +143 -63
- app/categorization/template.py +2 -1
- app/settings.py +1 -0
- app/transactions_rag/transactions_2024.csv +0 -29
- migration/versions/e8e988ebb7c7_.py +30 -0
- tests/test_file_upload.py +20 -0
- transactions_2024.csv +29 -0
.DS_Store
ADDED
|
Binary file (8.2 kB). View file
|
|
|
app/.DS_Store
ADDED
|
Binary file (8.2 kB). View file
|
|
|
app/api/routers/file_upload.py
CHANGED
|
@@ -1,14 +1,18 @@
|
|
| 1 |
from typing import Annotated
|
| 2 |
-
from fastapi import APIRouter, UploadFile, Depends
|
| 3 |
from app.categorization.file_processing import process_file, save_results
|
| 4 |
from app.schema.index import FileUploadCreate
|
| 5 |
import asyncio
|
| 6 |
import os
|
| 7 |
import csv
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
from app.engine.postgresdb import get_db_session
|
| 10 |
from sqlalchemy.ext.asyncio import AsyncSession
|
| 11 |
|
|
|
|
| 12 |
file_upload_router = r = APIRouter(prefix="/api/v1/file_upload", tags=["file_upload"])
|
| 13 |
|
| 14 |
@r.post(
|
|
@@ -21,23 +25,85 @@ file_upload_router = r = APIRouter(prefix="/api/v1/file_upload", tags=["file_upl
|
|
| 21 |
)
|
| 22 |
async def create_file(input_file: UploadFile, db: AsyncSession = Depends(get_db_session)):
|
| 23 |
try:
|
|
|
|
|
|
|
| 24 |
# Create directory to store all uploaded .csv files
|
| 25 |
file_upload_directory_path = "data/tx_data/input"
|
|
|
|
| 26 |
if not os.path.exists(file_upload_directory_path):
|
| 27 |
os.makedirs(file_upload_directory_path)
|
| 28 |
|
| 29 |
-
#
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
| 38 |
|
| 39 |
-
except Exception:
|
| 40 |
-
|
| 41 |
-
|
| 42 |
|
| 43 |
return {"message": f"Successfully uploaded {input_file.filename}"}
|
|
|
|
| 1 |
from typing import Annotated
|
| 2 |
+
from fastapi import APIRouter, UploadFile, Depends, HTTPException
|
| 3 |
from app.categorization.file_processing import process_file, save_results
|
| 4 |
from app.schema.index import FileUploadCreate
|
| 5 |
import asyncio
|
| 6 |
import os
|
| 7 |
import csv
|
| 8 |
+
import codecs
|
| 9 |
+
from io import StringIO
|
| 10 |
+
import pandas as pd
|
| 11 |
|
| 12 |
from app.engine.postgresdb import get_db_session
|
| 13 |
from sqlalchemy.ext.asyncio import AsyncSession
|
| 14 |
|
| 15 |
+
|
| 16 |
file_upload_router = r = APIRouter(prefix="/api/v1/file_upload", tags=["file_upload"])
|
| 17 |
|
| 18 |
@r.post(
|
|
|
|
| 25 |
)
|
| 26 |
async def create_file(input_file: UploadFile, db: AsyncSession = Depends(get_db_session)):
|
| 27 |
try:
|
| 28 |
+
result = {"file_name": input_file.filename, "output": "", "result": "", "processed_file": "", "error": ""}
|
| 29 |
+
|
| 30 |
# Create directory to store all uploaded .csv files
|
| 31 |
file_upload_directory_path = "data/tx_data/input"
|
| 32 |
+
output_csv_file_path = os.path.join(file_upload_directory_path, input_file.filename)
|
| 33 |
if not os.path.exists(file_upload_directory_path):
|
| 34 |
os.makedirs(file_upload_directory_path)
|
| 35 |
|
| 36 |
+
# input_csv_file = open(output_csv_file_path, "a")
|
| 37 |
+
|
| 38 |
+
try:
|
| 39 |
+
if input_file.filename.endswith(".csv"):
|
| 40 |
+
input_contents = await input_file.read()
|
| 41 |
+
buffer = StringIO(input_contents.decode('utf-8'))
|
| 42 |
+
read_csv = csv.reader(buffer)
|
| 43 |
+
|
| 44 |
+
# Write items of .csv filte to directory
|
| 45 |
+
with codecs.open(output_csv_file_path, 'w+', encoding='utf-8') as csv_file:
|
| 46 |
+
[csv_file.write(" ".join(row)+'\n') for row in read_csv]
|
| 47 |
+
csv_file.close()
|
| 48 |
+
|
| 49 |
+
new_output_csv_file_path = os.path.join(file_upload_directory_path, f"new_{input_file.filename}")
|
| 50 |
+
|
| 51 |
+
with codecs.open(output_csv_file_path, 'r', encoding='utf-8') as csv_file:
|
| 52 |
+
with codecs.open(new_output_csv_file_path, 'w+', encoding='utf-8') as new_csv_file:
|
| 53 |
+
csvreader = csv.reader(csv_file)
|
| 54 |
+
header = next(csvreader)[0]
|
| 55 |
+
new_header = header.replace(" ", ",")
|
| 56 |
+
# print(f"header: \n{header} headerItem: {header[0]} new_header: {new_header}\n")
|
| 57 |
+
new_csv_file.write(" ".join(new_header)+'\n')
|
| 58 |
+
for row in csvreader:
|
| 59 |
+
new_row_items = row[0].split(' ')
|
| 60 |
+
new_row = ""
|
| 61 |
+
description = ""
|
| 62 |
+
for idx, element in enumerate(new_row_items):
|
| 63 |
+
# Since our string has been delimited by spaces, we want to get all the words of the description which comes after the 'date' index (0) and
|
| 64 |
+
# before the second to last 'type' index
|
| 65 |
+
if idx > 0 and idx < len(new_row_items) - 2:
|
| 66 |
+
description = description + f" {element}"
|
| 67 |
+
else:
|
| 68 |
+
delim = ", " if idx > 0 else ""
|
| 69 |
+
item = element if len(description) == 0 else f"{description}, {element}"
|
| 70 |
+
new_row = new_row + delim + item
|
| 71 |
+
description = ""
|
| 72 |
+
|
| 73 |
+
# print(f"row: {row} rowType: \n{type(row)} new_row: {new_row}\n")
|
| 74 |
+
new_csv_file.write(" ".join(new_row)+'\n')
|
| 75 |
+
new_csv_file.close()
|
| 76 |
+
csv_file.close()
|
| 77 |
+
|
| 78 |
+
df = pd.read_csv(new_output_csv_file_path, names=['transaction_date', 'name_description', 'type', 'amount'], header=0, dtype=str, encoding='utf-8', engine='python')
|
| 79 |
+
# df = df.reindex(columns=['transaction_date', 'name_description', 'type', 'amount'])
|
| 80 |
+
# transaction_date_index = df.columns.get_loc('transaction_date')
|
| 81 |
+
result["output"] = df
|
| 82 |
+
# print(f"column: \n{df['transaction_date']} \n")
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
# With the newly created file and it's path, process and save it for embedding
|
| 86 |
+
# processed_file = await process_file(new_output_csv_file_path)
|
| 87 |
+
processed_file = await process_file(new_output_csv_file_path, df)
|
| 88 |
+
result['processed_file'] = processed_file
|
| 89 |
+
# print(f"\ncreate_file \n processed_file_type: {type(processed_file)} \n processed_file: \n {result['processed_file']}\n")
|
| 90 |
+
|
| 91 |
+
# result = await asyncio.gather(processed_file)
|
| 92 |
+
# result['result'] = result
|
| 93 |
+
|
| 94 |
+
# Maybe should be passing in the dataframe from the processed_file dict insead of processed_file
|
| 95 |
+
result = await save_results(db, processed_file)
|
| 96 |
+
print(f"\create_file result: {result}\n")
|
| 97 |
|
| 98 |
+
buffer.close()
|
| 99 |
+
input_file.close()
|
| 100 |
+
|
| 101 |
+
except Exception as e:
|
| 102 |
+
result["error"] = str(e)
|
| 103 |
+
raise HTTPException(status_code = 500, detail=f"create_file inner exception: {result} \n")
|
| 104 |
|
| 105 |
+
except Exception as e:
|
| 106 |
+
# detail=f"create_file outer exception: {result} \n"
|
| 107 |
+
raise HTTPException(status_code = 500)
|
| 108 |
|
| 109 |
return {"message": f"Successfully uploaded {input_file.filename}"}
|
app/categorization/categorizer.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
# Standard library imports
|
| 2 |
import re
|
| 3 |
import ast
|
| 4 |
import json
|
|
@@ -6,7 +5,6 @@ import logging
|
|
| 6 |
import os
|
| 7 |
from typing import Any, List, Tuple, Optional, Dict, Union
|
| 8 |
|
| 9 |
-
# Third-party library imports
|
| 10 |
import numpy as np
|
| 11 |
import pandas as pd
|
| 12 |
import asyncio
|
|
@@ -14,12 +12,13 @@ from rapidfuzz import process
|
|
| 14 |
from tenacity import retry, wait_random_exponential, stop_after_attempt
|
| 15 |
from pydantic import ValidationError
|
| 16 |
|
| 17 |
-
|
|
|
|
| 18 |
from langchain_openai import ChatOpenAI
|
| 19 |
from langchain.chains import LLMChain
|
| 20 |
from langchain.output_parsers import PydanticOutputParser, OutputFixingParser
|
| 21 |
from langchain.prompts import PromptTemplate
|
| 22 |
-
|
| 23 |
from app.categorization.config import CATEGORY_REFERENCE_OUTPUT_FILE, TX_PER_LLM_RUN
|
| 24 |
|
| 25 |
|
|
@@ -27,7 +26,7 @@ def fuzzy_match_list_categorizer(
|
|
| 27 |
description: str,
|
| 28 |
descriptions: np.ndarray,
|
| 29 |
description_category_pairs: pd.DataFrame,
|
| 30 |
-
threshold: int =
|
| 31 |
) -> Optional[str]:
|
| 32 |
"""Find the most similar transaction description and return its category.
|
| 33 |
|
|
@@ -44,55 +43,81 @@ def fuzzy_match_list_categorizer(
|
|
| 44 |
Returns:
|
| 45 |
str or None: Category of the matched description, or None if no match found.
|
| 46 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
| 51 |
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
|
| 59 |
-
async def llm_list_categorizer(
|
| 60 |
"""Categorize a list of transactions using a language model.
|
| 61 |
|
| 62 |
This function uses a Language Model (LLM) to categorize a list of transaction descriptions.
|
| 63 |
It splits the input DataFrame into chunks and processes each chunk asynchronously to improve performance.
|
| 64 |
|
| 65 |
Args:
|
| 66 |
-
|
| 67 |
|
| 68 |
Returns:
|
| 69 |
pd.DataFrame: DataFrame mapping transaction descriptions to their inferred categories.
|
| 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 |
-
# Return a DataFrame with the valid outputs
|
| 95 |
-
return pd.DataFrame(valid_outputs, columns=['name/description', 'category'])
|
| 96 |
|
| 97 |
|
| 98 |
@retry(wait=wait_random_exponential(min=1, max=20), stop=stop_after_attempt(6))
|
|
@@ -115,7 +140,8 @@ async def llm_sublist_categorizer(
|
|
| 115 |
dict: Dictionary containing a 'valid' flag and a list of categorized descriptions.
|
| 116 |
"""
|
| 117 |
|
| 118 |
-
raw_result = await chain.
|
|
|
|
| 119 |
|
| 120 |
logger = logging.getLogger(__name__)
|
| 121 |
result = {'valid': True, 'output': []}
|
|
@@ -124,24 +150,30 @@ async def llm_sublist_categorizer(
|
|
| 124 |
pattern = r"\['([^']+)', '([^']+)'\]"
|
| 125 |
|
| 126 |
# Use it to extract all the correctly formatted pairs from the raw result
|
| 127 |
-
matches = re.findall(pattern, raw_result.replace("\\'", "'"))
|
|
|
|
|
|
|
| 128 |
|
| 129 |
# Loop over the matches, and try to parse them to ensure the content is valid
|
| 130 |
valid_outputs = []
|
| 131 |
for match in matches:
|
| 132 |
try:
|
| 133 |
parsed_pair = ast.literal_eval(str(list(match)))
|
|
|
|
| 134 |
valid_outputs.append(parsed_pair)
|
| 135 |
except Exception as e:
|
| 136 |
logger.log(logging.ERROR,
|
| 137 |
f"Parsing Error: {e}\nMatch: {match}\n")
|
| 138 |
result['valid'] = False
|
| 139 |
|
|
|
|
|
|
|
| 140 |
result['output'] = valid_outputs
|
| 141 |
|
| 142 |
except Exception as e:
|
| 143 |
-
logging.log(
|
| 144 |
-
|
| 145 |
result['valid'] = False
|
|
|
|
| 146 |
|
| 147 |
return result
|
|
|
|
|
|
|
| 1 |
import re
|
| 2 |
import ast
|
| 3 |
import json
|
|
|
|
| 5 |
import os
|
| 6 |
from typing import Any, List, Tuple, Optional, Dict, Union
|
| 7 |
|
|
|
|
| 8 |
import numpy as np
|
| 9 |
import pandas as pd
|
| 10 |
import asyncio
|
|
|
|
| 12 |
from tenacity import retry, wait_random_exponential, stop_after_attempt
|
| 13 |
from pydantic import ValidationError
|
| 14 |
|
| 15 |
+
from fastapi import HTTPException
|
| 16 |
+
|
| 17 |
from langchain_openai import ChatOpenAI
|
| 18 |
from langchain.chains import LLMChain
|
| 19 |
from langchain.output_parsers import PydanticOutputParser, OutputFixingParser
|
| 20 |
from langchain.prompts import PromptTemplate
|
| 21 |
+
from app.categorization.template import CATEGORY_TEMPLATE
|
| 22 |
from app.categorization.config import CATEGORY_REFERENCE_OUTPUT_FILE, TX_PER_LLM_RUN
|
| 23 |
|
| 24 |
|
|
|
|
| 26 |
description: str,
|
| 27 |
descriptions: np.ndarray,
|
| 28 |
description_category_pairs: pd.DataFrame,
|
| 29 |
+
threshold: int = 90,
|
| 30 |
) -> Optional[str]:
|
| 31 |
"""Find the most similar transaction description and return its category.
|
| 32 |
|
|
|
|
| 43 |
Returns:
|
| 44 |
str or None: Category of the matched description, or None if no match found.
|
| 45 |
"""
|
| 46 |
+
result = {"description": description, "descriptions": descriptions, "description_category_pairs": description_category_pairs, "match_results": "", "error": ""}
|
| 47 |
+
try:
|
| 48 |
+
if description_category_pairs.empty:
|
| 49 |
+
return None
|
| 50 |
+
|
| 51 |
+
# Fuzzy-match this description against the reference descriptions
|
| 52 |
+
description_stripped = description.strip()
|
| 53 |
+
descriptions_stripped = [s.strip() for s in descriptions]
|
| 54 |
|
| 55 |
+
match_results = process.extractOne(
|
| 56 |
+
description_stripped, descriptions_stripped, score_cutoff=threshold)
|
| 57 |
+
|
| 58 |
+
result["match_results"] = match_results
|
| 59 |
+
# print(f"\pairs_index: {description_category_pairs.columns}\n nmatch_results0: {match_results[0]}")
|
| 60 |
|
| 61 |
+
# If a match is found, return the category of the matched description
|
| 62 |
+
if match_results and (description_category_pairs.index == match_results[0]).any:
|
| 63 |
+
return description_category_pairs.at[match_results[0], 'category']
|
| 64 |
|
| 65 |
+
return None
|
| 66 |
+
|
| 67 |
+
except Exception as e:
|
| 68 |
+
result["error"] = str(e)
|
| 69 |
+
raise HTTPException(status_code = 500, detail=f"fuzzy_match_list_categorizer result: {result}")
|
| 70 |
|
| 71 |
|
| 72 |
+
async def llm_list_categorizer(df: pd.DataFrame) -> pd.DataFrame:
|
| 73 |
"""Categorize a list of transactions using a language model.
|
| 74 |
|
| 75 |
This function uses a Language Model (LLM) to categorize a list of transaction descriptions.
|
| 76 |
It splits the input DataFrame into chunks and processes each chunk asynchronously to improve performance.
|
| 77 |
|
| 78 |
Args:
|
| 79 |
+
df (pd.DataFrame): DataFrame containing the transaction descriptions to categorize.
|
| 80 |
|
| 81 |
Returns:
|
| 82 |
pd.DataFrame: DataFrame mapping transaction descriptions to their inferred categories.
|
| 83 |
"""
|
| 84 |
+
|
| 85 |
+
result = {"df": "", "results": "", "valid results": "", "valid outputs": "", "error": ""}
|
| 86 |
|
| 87 |
+
try:
|
| 88 |
+
# Initialize language model and prompt
|
| 89 |
+
openai_api_key = os.environ['OPENAI_API_KEY']
|
| 90 |
+
llm = ChatOpenAI(temperature=0, model="gpt-3.5-turbo-0125",
|
| 91 |
+
api_key=openai_api_key)
|
| 92 |
+
prompt = PromptTemplate.from_template(template=CATEGORY_TEMPLATE)
|
| 93 |
+
chain = prompt | llm
|
| 94 |
+
# chain = LLMChain(llm=llm, prompt=prompt)
|
| 95 |
+
|
| 96 |
+
# Iterate over the DataFrame in batches of TX_PER_LLM_RUN transactions
|
| 97 |
+
tasks = [llm_sublist_categorizer(df.attrs['file_name'], chain=chain, tx_descriptions="\n".join(chunk['name_description']).strip())
|
| 98 |
+
for chunk in np.array_split(df, df.shape[0] // TX_PER_LLM_RUN + 1)]
|
| 99 |
+
|
| 100 |
+
# Gather results and extract (valid) outputs
|
| 101 |
+
# The results variable is a list of 'results', each 'result' being the output of a single LLM run
|
| 102 |
+
results = await asyncio.gather(*tasks)
|
| 103 |
+
result["results"] = results
|
| 104 |
+
|
| 105 |
+
# Extract valid results (each valid result is a list of description-category pairs)
|
| 106 |
+
valid_results = [result['output'] for result in results if result['valid']]
|
| 107 |
+
result["valid results"] = valid_results
|
| 108 |
+
|
| 109 |
+
# Flatten the list of valid results to obtain a single list of description-category pairs
|
| 110 |
+
valid_outputs = [
|
| 111 |
+
output for valid_result in valid_results for output in valid_result]
|
| 112 |
+
result["valid outputs"] = valid_outputs
|
| 113 |
+
|
| 114 |
+
# Return a DataFrame with the valid outputs
|
| 115 |
+
return pd.DataFrame(valid_outputs, columns=['name_description', 'category'])
|
| 116 |
+
|
| 117 |
+
except Exception as e:
|
| 118 |
+
result["error"] = str(e)
|
| 119 |
+
raise HTTPException(status_code = 500, detail=f"llm_list_categorizer result: {result}")
|
| 120 |
|
|
|
|
|
|
|
| 121 |
|
| 122 |
|
| 123 |
@retry(wait=wait_random_exponential(min=1, max=20), stop=stop_after_attempt(6))
|
|
|
|
| 140 |
dict: Dictionary containing a 'valid' flag and a list of categorized descriptions.
|
| 141 |
"""
|
| 142 |
|
| 143 |
+
raw_result = await chain.ainvoke(tx_descriptions)
|
| 144 |
+
# raw_result = await chain.arun(input_data=tx_descriptions)
|
| 145 |
|
| 146 |
logger = logging.getLogger(__name__)
|
| 147 |
result = {'valid': True, 'output': []}
|
|
|
|
| 150 |
pattern = r"\['([^']+)', '([^']+)'\]"
|
| 151 |
|
| 152 |
# Use it to extract all the correctly formatted pairs from the raw result
|
| 153 |
+
matches = re.findall(pattern, raw_result.content.replace("\\'", "'"))
|
| 154 |
+
|
| 155 |
+
# print(f"llm_sublist_categorizer type_matches: {type(matches)} match: {matches}\n")
|
| 156 |
|
| 157 |
# Loop over the matches, and try to parse them to ensure the content is valid
|
| 158 |
valid_outputs = []
|
| 159 |
for match in matches:
|
| 160 |
try:
|
| 161 |
parsed_pair = ast.literal_eval(str(list(match)))
|
| 162 |
+
# print(f"llm_sublist_categorizer \n parsed_pair: {parsed_pair} \n match: {match}\n list(match): {list(match)}\n str(list(match): {str(list(match))} \n\n")
|
| 163 |
valid_outputs.append(parsed_pair)
|
| 164 |
except Exception as e:
|
| 165 |
logger.log(logging.ERROR,
|
| 166 |
f"Parsing Error: {e}\nMatch: {match}\n")
|
| 167 |
result['valid'] = False
|
| 168 |
|
| 169 |
+
# print(f"llm_sublist_categorizer valid_outputs \n{valid_outputs}\n")
|
| 170 |
+
|
| 171 |
result['output'] = valid_outputs
|
| 172 |
|
| 173 |
except Exception as e:
|
| 174 |
+
# logging.log(
|
| 175 |
+
# logging.ERROR, f"| File: {file_name} | Unexpected Error: {e}\nRaw Result: {raw_result}")
|
| 176 |
result['valid'] = False
|
| 177 |
+
raise HTTPException(status_code = 500, detail=f"llm_sublist_categorizer result: {result}")
|
| 178 |
|
| 179 |
return result
|
app/categorization/categorizer_list.py
CHANGED
|
@@ -1,16 +1,16 @@
|
|
| 1 |
-
# Standard library imports
|
| 2 |
import os
|
| 3 |
from datetime import datetime
|
| 4 |
|
| 5 |
-
|
|
|
|
| 6 |
import pandas as pd
|
|
|
|
| 7 |
|
| 8 |
-
# Local application/library specific imports
|
| 9 |
from app.categorization.config import CATEGORY_REFERENCE_OUTPUT_FILE
|
| 10 |
from app.categorization.categorizer import llm_list_categorizer, fuzzy_match_list_categorizer
|
| 11 |
|
| 12 |
|
| 13 |
-
async def categorize_list(
|
| 14 |
"""Asynchronously categorize a list of transactions.
|
| 15 |
|
| 16 |
This function categorizes a list of transactions using a combination of fuzzy matching
|
|
@@ -20,51 +20,93 @@ async def categorize_list(tx_list: pd.DataFrame) -> pd.DataFrame:
|
|
| 20 |
pairs are added to the reference file.
|
| 21 |
|
| 22 |
Args:
|
| 23 |
-
|
| 24 |
|
| 25 |
Returns:
|
| 26 |
pd.DataFrame: The original DataFrame with an additional column for the category.
|
| 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 |
-
# Fill remaining NaN values in 'category' with 'Other'
|
| 68 |
-
tx_list['category'] = tx_list['category'].fillna('Other')
|
| 69 |
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
from datetime import datetime
|
| 3 |
|
| 4 |
+
from fastapi import HTTPException
|
| 5 |
+
|
| 6 |
import pandas as pd
|
| 7 |
+
import codecs
|
| 8 |
|
|
|
|
| 9 |
from app.categorization.config import CATEGORY_REFERENCE_OUTPUT_FILE
|
| 10 |
from app.categorization.categorizer import llm_list_categorizer, fuzzy_match_list_categorizer
|
| 11 |
|
| 12 |
|
| 13 |
+
async def categorize_list(df: pd.DataFrame) -> pd.DataFrame:
|
| 14 |
"""Asynchronously categorize a list of transactions.
|
| 15 |
|
| 16 |
This function categorizes a list of transactions using a combination of fuzzy matching
|
|
|
|
| 20 |
pairs are added to the reference file.
|
| 21 |
|
| 22 |
Args:
|
| 23 |
+
df (pd.DataFrame): The list of transactions to categorize.
|
| 24 |
|
| 25 |
Returns:
|
| 26 |
pd.DataFrame: The original DataFrame with an additional column for the category.
|
| 27 |
"""
|
| 28 |
+
result = {"df": pd.DataFrame(), "description_category": "", "description_category_pairs": "", "uncategorized_descriptions": "", "categorized_descriptions": "", "error": ""}
|
| 29 |
|
| 30 |
+
try:
|
| 31 |
+
if not os.path.exists(CATEGORY_REFERENCE_OUTPUT_FILE):
|
| 32 |
+
os.makedirs(CATEGORY_REFERENCE_OUTPUT_FILE)
|
| 33 |
+
|
| 34 |
+
if os.path.exists(CATEGORY_REFERENCE_OUTPUT_FILE):
|
| 35 |
+
# Read description-category pairs from the reference file
|
| 36 |
+
|
| 37 |
+
# TODO FIRST ISSUE: Must save categorize_list categorized_descriptions items to output file
|
| 38 |
+
description_category_pairs = pd.read_csv(
|
| 39 |
+
CATEGORY_REFERENCE_OUTPUT_FILE, names=['name_description', 'category'], header=0
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
print(f"\ncategorize_list description_category_pairs \n{description_category_pairs}\n")
|
| 43 |
+
df['name_description'] = df['name_description'].astype(str)
|
| 44 |
+
df['category'] = df['category'].astype(str)
|
| 45 |
+
df['type'] = df['type'].astype(str)
|
| 46 |
+
# df['amount'] = df['amount'].str.strip().replace("\s", "").astype(float)
|
| 47 |
+
# df['transaction_date'] = pd.to_datetime(df['transaction_date'].str.strip().replace("\s", ""), format='%d/%m/%Y')
|
| 48 |
+
|
| 49 |
+
# Extract only descriptions for faster matching
|
| 50 |
+
# description_category_pairs.columns = description_category_pairs.columns.str.strip()
|
| 51 |
+
# descriptions = description_category_pairs['name_description'].values
|
| 52 |
+
descriptions = df['name_description'].values
|
| 53 |
+
print(f"\ncategorize_list descriptions \n{descriptions}\n")
|
| 54 |
+
|
| 55 |
+
result['description_category'] = descriptions
|
| 56 |
+
result['df'] = df
|
| 57 |
+
temp_df = df
|
| 58 |
+
|
| 59 |
+
print(f"\ncategorize_list df \n{df}\n")
|
| 60 |
+
|
| 61 |
+
# Use fuzzy matching to find similar descriptions and assign the category
|
| 62 |
+
# TODO SECOND ISSUE: Why isn't category being updated
|
| 63 |
+
df['category'] = df['name_description'].apply(
|
| 64 |
+
fuzzy_match_list_categorizer,
|
| 65 |
+
args=(descriptions, description_category_pairs)
|
| 66 |
)
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
+
print(f"\ncategorize_list setting fuzzy match list: \n{df}\n")
|
| 69 |
+
|
| 70 |
+
# Filter out uncategorized transactions, deduplicate, and sort by description
|
| 71 |
+
uncategorized_descriptions = (
|
| 72 |
+
df[df['category'].isnull()]
|
| 73 |
+
.drop_duplicates(subset=['name_description'])
|
| 74 |
+
.sort_values(by=['name_description'])
|
| 75 |
+
)
|
| 76 |
+
result['uncategorized_descriptions'] = uncategorized_descriptions
|
| 77 |
+
print(f"\ncategorize_list uncategorized_descriptions: \n{uncategorized_descriptions}\n")
|
| 78 |
+
|
| 79 |
+
# Ask the language model to categorize the remaining descriptions
|
| 80 |
+
if not uncategorized_descriptions.empty:
|
| 81 |
+
categorized_descriptions = await llm_list_categorizer(
|
| 82 |
+
uncategorized_descriptions[['name_description', 'category']]
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
categorized_descriptions.dropna(inplace=True)
|
| 86 |
+
result['categorized_descriptions'] = categorized_descriptions
|
| 87 |
+
print(f"\ncategorize_list categorized_descriptions: \n{categorized_descriptions}\n")
|
| 88 |
+
|
| 89 |
+
# Update the category for uncategorized transactions based on the language model results
|
| 90 |
+
if not categorized_descriptions.empty:
|
| 91 |
+
# temp_df['category'] = temp_df['name_description'].map(categorized_descriptions.set_index('name_description')['category'])
|
| 92 |
+
print(f"\ncategorize_list current dataframe:\n {temp_df}\n")
|
| 93 |
+
|
| 94 |
+
# df['category'] = df['category'].fillna(
|
| 95 |
+
# df['name_description'].map(
|
| 96 |
+
# categorized_descriptions.set_index('name_description')['category']
|
| 97 |
+
# )
|
| 98 |
+
# )
|
| 99 |
+
# print(f"\ncategorize_list categorized_description df[category]: \n{df['category']}\n")
|
| 100 |
+
|
| 101 |
+
# Fill remaining NaN values in 'category' with 'Other'
|
| 102 |
+
with pd.option_context("future.no_silent_downcasting", True):
|
| 103 |
+
df['category'] = df['category'].fillna('Other').infer_objects(copy=False)
|
| 104 |
+
# print(f"\ncategorize_list Fill remaining NaN df[category]: \n{df['category']}\n")
|
| 105 |
+
|
| 106 |
+
return df
|
| 107 |
+
|
| 108 |
+
except Exception as e:
|
| 109 |
+
# Return an error indicator and exception info
|
| 110 |
+
print(f"ERROR categorizer_list: {str(e)}")
|
| 111 |
+
result["error"] = str(e)
|
| 112 |
+
raise HTTPException(status_code = 500, detail=f"categorize_list result: {result}")
|
app/categorization/file_processing.py
CHANGED
|
@@ -8,6 +8,8 @@ from datetime import datetime
|
|
| 8 |
|
| 9 |
import pandas as pd
|
| 10 |
from dateparser import parse
|
|
|
|
|
|
|
| 11 |
|
| 12 |
from app.categorization.categorizer_list import categorize_list
|
| 13 |
from app.categorization.config import RESULT_OUTPUT_FILE, CATEGORY_REFERENCE_OUTPUT_FILE
|
|
@@ -18,37 +20,39 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|
| 18 |
|
| 19 |
|
| 20 |
# Read file and process it (e.g. categorize transactions)
|
| 21 |
-
async def process_file(file_path: str) -> Dict[str, Union[str, pd.DataFrame]]:
|
| 22 |
"""
|
| 23 |
Process the input file by reading, cleaning, standardizing, and categorizing the transactions.
|
| 24 |
|
| 25 |
Args:
|
| 26 |
file_path (str): Path to the input file.
|
|
|
|
| 27 |
|
| 28 |
Returns:
|
| 29 |
Dict[str, Union[str, pd.DataFrame]]: Dictionary containing the file name, processed output, and error information if any
|
| 30 |
"""
|
| 31 |
|
| 32 |
file_name = os.path.basename(file_path)
|
| 33 |
-
result = {
|
| 34 |
try:
|
| 35 |
-
# Read file into standardized tx format:
|
| 36 |
-
tx_list = standardize_csv_file(file_path)
|
|
|
|
| 37 |
|
| 38 |
# Categorize transactions
|
| 39 |
result["output"] = await categorize_list(tx_list)
|
| 40 |
-
print(f"File processed sucessfully: {file_name}")
|
| 41 |
|
| 42 |
except Exception as e:
|
| 43 |
# Return an error indicator and exception info
|
| 44 |
-
logging.
|
| 45 |
-
print(f"ERROR processing file {file_name}: {e}")
|
| 46 |
result["error"] = str(e)
|
|
|
|
| 47 |
|
| 48 |
return result
|
| 49 |
|
| 50 |
|
| 51 |
-
def standardize_csv_file(file_path: str) -> pd.DataFrame:
|
| 52 |
"""
|
| 53 |
Read and prepare the data from the input file.
|
| 54 |
|
|
@@ -58,22 +62,62 @@ def standardize_csv_file(file_path: str) -> pd.DataFrame:
|
|
| 58 |
Returns:
|
| 59 |
pd.DataFrame: Prepared transaction data.
|
| 60 |
"""
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
|
| 76 |
-
async def save_results(db: AsyncSession, results: List) ->
|
| 77 |
"""
|
| 78 |
Merge all interim results in the input folder and write the merged results to the output file.
|
| 79 |
|
|
@@ -84,45 +128,81 @@ async def save_results(db: AsyncSession, results: List) -> None:
|
|
| 84 |
Returns:
|
| 85 |
None
|
| 86 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
for result in results:
|
| 97 |
-
if not result["error"]:
|
| 98 |
-
ok_files.append(result["file_name"])
|
| 99 |
-
result_df = result["output"]
|
| 100 |
-
result_df.columns = col_list
|
| 101 |
-
tx_list = pd.concat([tx_list, result_df], ignore_index=True)
|
| 102 |
-
else:
|
| 103 |
-
ko_files.append(result["file_name"])
|
| 104 |
-
error_messages.append(f"{result['file_name']}: {result['error']}")
|
| 105 |
-
|
| 106 |
-
# Save to database
|
| 107 |
-
# FIXME: get user_id from session
|
| 108 |
-
txn_list_to_save = [TransactionCreate(**row.to_dict(), user_id=1) for _, row in tx_list.iterrows()]
|
| 109 |
-
await Transaction.bulk_create(db, txn_list_to_save)
|
| 110 |
-
|
| 111 |
-
new_ref_data = tx_list[["name/description", "category"]]
|
| 112 |
-
if os.path.exists(CATEGORY_REFERENCE_OUTPUT_FILE):
|
| 113 |
-
# If it exists, add master file to interim results
|
| 114 |
-
old_ref_data = pd.read_csv(CATEGORY_REFERENCE_OUTPUT_FILE, names=["name/description", "category"], header=0)
|
| 115 |
-
new_ref_data = pd.concat([old_ref_data, new_ref_data], ignore_index=True)
|
| 116 |
-
|
| 117 |
-
# Drop duplicates, sort, and write to create new Master File
|
| 118 |
-
new_ref_data.drop_duplicates(subset=["name/description"]).sort_values(by=["name/description"]).to_csv(
|
| 119 |
-
CATEGORY_REFERENCE_OUTPUT_FILE, mode="w", index=False, header=True
|
| 120 |
-
)
|
| 121 |
-
|
| 122 |
-
# Summarize results
|
| 123 |
-
print(f"\nProcessed {len(results)} files: {len(ok_files)} successful, {len(ko_files)} with errors\n")
|
| 124 |
-
if len(ko_files):
|
| 125 |
-
print(f"Errors in the following files:")
|
| 126 |
-
for message in error_messages:
|
| 127 |
-
print(f" {message}")
|
| 128 |
-
print("\n")
|
|
|
|
| 8 |
|
| 9 |
import pandas as pd
|
| 10 |
from dateparser import parse
|
| 11 |
+
from fastapi import HTTPException
|
| 12 |
+
import csv
|
| 13 |
|
| 14 |
from app.categorization.categorizer_list import categorize_list
|
| 15 |
from app.categorization.config import RESULT_OUTPUT_FILE, CATEGORY_REFERENCE_OUTPUT_FILE
|
|
|
|
| 20 |
|
| 21 |
|
| 22 |
# Read file and process it (e.g. categorize transactions)
|
| 23 |
+
async def process_file(file_path: str, df: pd.DataFrame) -> Dict[str, Union[str, pd.DataFrame]]:
|
| 24 |
"""
|
| 25 |
Process the input file by reading, cleaning, standardizing, and categorizing the transactions.
|
| 26 |
|
| 27 |
Args:
|
| 28 |
file_path (str): Path to the input file.
|
| 29 |
+
df (dataframe): Dataframe representing input file
|
| 30 |
|
| 31 |
Returns:
|
| 32 |
Dict[str, Union[str, pd.DataFrame]]: Dictionary containing the file name, processed output, and error information if any
|
| 33 |
"""
|
| 34 |
|
| 35 |
file_name = os.path.basename(file_path)
|
| 36 |
+
result = {'file_path': file_name, 'output': pd.DataFrame(), 'error': ''}
|
| 37 |
try:
|
| 38 |
+
# Read file into standardized tx format: transaction_date, name_description, type, amount, category, source
|
| 39 |
+
tx_list = standardize_csv_file(file_path, df)
|
| 40 |
+
result["columns"] = tx_list.columns.tolist()
|
| 41 |
|
| 42 |
# Categorize transactions
|
| 43 |
result["output"] = await categorize_list(tx_list)
|
| 44 |
+
# print(f"File processed sucessfully: {file_name}")
|
| 45 |
|
| 46 |
except Exception as e:
|
| 47 |
# Return an error indicator and exception info
|
| 48 |
+
logging.debug(logging.ERROR, f"| File: {file_name} | Unexpected Error: {e}")
|
|
|
|
| 49 |
result["error"] = str(e)
|
| 50 |
+
raise HTTPException(status_code = 500, detail=f"\n\n process_file result: \n{result}")
|
| 51 |
|
| 52 |
return result
|
| 53 |
|
| 54 |
|
| 55 |
+
def standardize_csv_file(file_path: str, d: pd.DataFrame) -> pd.DataFrame:
|
| 56 |
"""
|
| 57 |
Read and prepare the data from the input file.
|
| 58 |
|
|
|
|
| 62 |
Returns:
|
| 63 |
pd.DataFrame: Prepared transaction data.
|
| 64 |
"""
|
| 65 |
+
result = {"csv_file": "", "file_path": file_path, "new_csv_file": "", "transaction_date": "", "tx_list_new_columns": "", "error": ""}
|
| 66 |
+
|
| 67 |
+
try:
|
| 68 |
+
#1
|
| 69 |
+
# result["csv_file"] = csv_file.read()
|
| 70 |
+
# reader = csv.reader(csv_file)
|
| 71 |
+
# data = list(reader)
|
| 72 |
+
# df = pd.DataFrame(data, columns=data[0])
|
| 73 |
+
|
| 74 |
+
#2
|
| 75 |
+
# df = pd.read_csv(file_path)
|
| 76 |
+
|
| 77 |
+
#3
|
| 78 |
+
# with open(file_path, 'r', encoding='utf-8') as file:
|
| 79 |
+
# csv_reader = csv.reader(file)
|
| 80 |
+
# data = list(csv_reader)
|
| 81 |
+
|
| 82 |
+
# df = pd.DataFrame(data)
|
| 83 |
+
|
| 84 |
+
headers = d.columns
|
| 85 |
+
df = pd.DataFrame(d.values[1:], columns=headers)
|
| 86 |
+
|
| 87 |
+
result["csv_file"] = df
|
| 88 |
+
df.attrs['file_name'] = file_path
|
| 89 |
+
# print(f"\n\n standardize_csv_file columns: \n{df} \n date:\n{df['transaction_date']} \n")
|
| 90 |
+
# tx_list.columns = ['transaction_date', 'name_description', 'type', 'amount', 'category']
|
| 91 |
+
|
| 92 |
+
# # Standardize dates to YYYY/MM/DD format
|
| 93 |
+
warnings.filterwarnings('ignore', 'Parsing dates', category=UserWarning)
|
| 94 |
+
# df['transaction_date'] = pd.to_datetime(df['transaction_date']).dt.strftime('%d/%m/%Y')
|
| 95 |
+
|
| 96 |
+
# Add source and reindex to desired tx format; category column is new and therefore empty
|
| 97 |
+
# tx_list.insert(0, "source", [os.path.basename(file_path) for i in range(len(tx_list.columns))])
|
| 98 |
+
# tx_list.insert(0, "category", [])
|
| 99 |
+
|
| 100 |
+
# df = df.reindex(columns=['transaction_date', 'name_description', 'type', 'amount', 'category'])
|
| 101 |
+
# tx_list['transaction_date'] = pd.to_datetime(tx_list['transaction_date']).dt.strftime('%Y/%m/%d')
|
| 102 |
+
# tx_list.set_index(['transaction_date', 'name_description', 'type', 'amount', 'category'])
|
| 103 |
+
df['source'] = pd.Series([os.path.basename(file_path)] * len(df.index))
|
| 104 |
+
df['category'] = pd.Series([])
|
| 105 |
+
result["new_csv_file"] = df
|
| 106 |
+
# print(f"\n\n standardize_csv_file result: \n{df} \n")
|
| 107 |
+
|
| 108 |
+
return df
|
| 109 |
+
|
| 110 |
+
except Exception as e:
|
| 111 |
+
# Return an error indicator and exception info
|
| 112 |
+
logging.debug("standardize_csv_file Error: {e}")
|
| 113 |
+
if hasattr(e, 'message'):
|
| 114 |
+
result["error"] = str(e.message)
|
| 115 |
+
else:
|
| 116 |
+
result["error"] = str(e)
|
| 117 |
+
raise HTTPException(status_code = 401, detail=f"standardize_csv_file exception: {result}")
|
| 118 |
|
| 119 |
|
| 120 |
+
async def save_results(db: AsyncSession, results: List) -> Dict[str, Union[List[str], List[TransactionCreate], pd.DataFrame]]:
|
| 121 |
"""
|
| 122 |
Merge all interim results in the input folder and write the merged results to the output file.
|
| 123 |
|
|
|
|
| 128 |
Returns:
|
| 129 |
None
|
| 130 |
"""
|
| 131 |
+
result = {'ok_files': '', 'ko_files': '', 'error_messages': '', 'txn_list_to_save': '', 'old_ref_data': '', 'new_ref_data': ''}
|
| 132 |
+
try:
|
| 133 |
+
# Concatenate all (valid) results into a single DataFrame
|
| 134 |
+
# Print errors to console
|
| 135 |
+
ok_files = []
|
| 136 |
+
ko_files = []
|
| 137 |
+
error_messages = []
|
| 138 |
+
# print(f"save_results results: {results}")
|
| 139 |
+
|
| 140 |
+
col_list = ["transaction_date", "name_description", "type", "amount", "category"]
|
| 141 |
+
tx_list = pd.DataFrame(columns=col_list)
|
| 142 |
+
# print(f"save_results tx_list: {tx_list}")
|
| 143 |
+
for result in results:
|
| 144 |
+
if not result["error"]:
|
| 145 |
+
ok_files.append(result["file_name"])
|
| 146 |
+
print(f"save_results ok_files: {ok_files}\n")
|
| 147 |
+
result_df = result["output"]
|
| 148 |
+
print(f"save_results result_df: {result_df}\n")
|
| 149 |
+
result_df.columns = col_list
|
| 150 |
+
print(f"save_results result_df.columns: {result_df.columns}\n")
|
| 151 |
+
tx_list = pd.concat([tx_list, result_df], ignore_index=True)
|
| 152 |
+
print(f"save_results tx_list: {tx_list}\n")
|
| 153 |
+
else:
|
| 154 |
+
ko_files.append(result["file_name"])
|
| 155 |
+
print(f"save_results ko_files: {ko_files}\n")
|
| 156 |
+
error_messages.append(f"{result['file_name']}: {result['error']}")
|
| 157 |
+
print(f"save_results error_messages: {error_messages}\n")
|
| 158 |
+
|
| 159 |
+
result['ok_files'] = ok_files
|
| 160 |
+
result['ko_files'] = ko_files
|
| 161 |
+
result['error_messages'] = error_messages
|
| 162 |
+
|
| 163 |
+
# Save to database
|
| 164 |
+
# FIXME: get user_id from session
|
| 165 |
+
txn_list_to_save = [TransactionCreate(**row.to_dict(), user_id=1) for _, row in tx_list.iterrows()]
|
| 166 |
+
result['txn_list_to_save'] = txn_list_to_save
|
| 167 |
+
print(f"save_results txn_list_to_save: {txn_list_to_save}\n")
|
| 168 |
+
await Transaction.bulk_create(db, txn_list_to_save)
|
| 169 |
+
|
| 170 |
+
new_ref_data = tx_list[["name_description", "category"]]
|
| 171 |
+
|
| 172 |
+
if not os.path.exists(CATEGORY_REFERENCE_OUTPUT_FILE):
|
| 173 |
+
os.makedirs(CATEGORY_REFERENCE_OUTPUT_FILE)
|
| 174 |
+
|
| 175 |
+
if os.path.exists(CATEGORY_REFERENCE_OUTPUT_FILE):
|
| 176 |
+
# If it exists, add master file to interim results
|
| 177 |
+
old_ref_data = pd.read_csv(CATEGORY_REFERENCE_OUTPUT_FILE, names=["name_description", "category"], header=0)
|
| 178 |
+
new_ref_data = pd.concat([old_ref_data, new_ref_data], ignore_index=True)
|
| 179 |
+
print(f"save_results new_ref_data: {new_ref_data}\n")
|
| 180 |
+
print(f"save_results old_ref_data: {old_ref_data}\n")
|
| 181 |
+
|
| 182 |
+
# Drop duplicates, sort, and write to create new Master File
|
| 183 |
+
new_ref_data.drop_duplicates(subset=["name_description"]).sort_values(by=["name_description"]).to_csv(
|
| 184 |
+
CATEGORY_REFERENCE_OUTPUT_FILE, mode="w", index=False, header=True
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
result['old_ref_data'] = old_ref_data
|
| 188 |
+
result['new_ref_data'] = new_ref_data
|
| 189 |
+
|
| 190 |
+
print(f"\nsave_results result:\n {result}\n")
|
| 191 |
+
|
| 192 |
+
# Summarize results
|
| 193 |
+
# print(f"\nProcessed {len(results)} files: {len(ok_files)} successful, {len(ko_files)} with errors\n")
|
| 194 |
+
logging.debug(f"\nProcessed {len(results)} files: {len(ok_files)} successful, {len(ko_files)} with errors\n")
|
| 195 |
+
if len(ko_files):
|
| 196 |
+
print(f"Errors in the following files:")
|
| 197 |
+
for message in error_messages:
|
| 198 |
+
print(f" {message}")
|
| 199 |
+
print("\n")
|
| 200 |
|
| 201 |
+
except Exception as e:
|
| 202 |
+
# Return an error indicator and exception info
|
| 203 |
+
# print(f"standardize_csv_file exception: {e}")
|
| 204 |
+
logging.debug("save_results Error: {e}")
|
| 205 |
+
result["error"] = str(e)
|
| 206 |
+
raise HTTPException(status_code = 401, detail=f"save_results exception: {error_messages}")
|
| 207 |
+
|
| 208 |
+
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/categorization/template.py
CHANGED
|
@@ -53,4 +53,5 @@ CATEGORY_TEMPLATE = """
|
|
| 53 |
|
| 54 |
<financial_transactions>
|
| 55 |
{input_data}
|
| 56 |
-
</financial_transactions>
|
|
|
|
|
|
| 53 |
|
| 54 |
<financial_transactions>
|
| 55 |
{input_data}
|
| 56 |
+
</financial_transactions>
|
| 57 |
+
"""
|
app/settings.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import os
|
| 2 |
from typing import Dict
|
|
|
|
| 3 |
from llama_index.core.settings import Settings
|
| 4 |
|
| 5 |
|
|
|
|
| 1 |
import os
|
| 2 |
from typing import Dict
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
from llama_index.core.settings import Settings
|
| 5 |
|
| 6 |
|
app/transactions_rag/transactions_2024.csv
DELETED
|
@@ -1,29 +0,0 @@
|
|
| 1 |
-
Date,Name / Description,Expense/Income,Amount
|
| 2 |
-
2023-12-30,Comcast Internet,Expense,9.96
|
| 3 |
-
2023-12-30,Lemonade Home Insurance,Expense,17.53
|
| 4 |
-
2023-12-30,Monthly Appartment Rent,Expense,2000.0
|
| 5 |
-
2023-12-30,Staples Office Supplies,Expense,12.46
|
| 6 |
-
2023-12-29,Selling Paintings,Income,13.63
|
| 7 |
-
2023-12-29,Spotify,Expense,12.19
|
| 8 |
-
2023-12-23,Target,Expense,27.08
|
| 9 |
-
2023-12-22,IT Consulting,Income,541.57
|
| 10 |
-
2023-12-22,Phone,Expense,10.7
|
| 11 |
-
2023-12-20,ML Consulting,Income,2641.93
|
| 12 |
-
2023-12-19,Chipotle,Expense,18.9
|
| 13 |
-
2023-12-18,Cold Brew Coffee,Expense,17.67
|
| 14 |
-
2023-12-18,Gas,Expense,8.80
|
| 15 |
-
2023-12-18,CA Property Tax,Expense,1670.34
|
| 16 |
-
2022-11-26,Salary,Income,4000.36
|
| 17 |
-
2022-11-26,Cellphone,Expense,19.27
|
| 18 |
-
2022-11-26,Gym Membership,Expense,24.71
|
| 19 |
-
2022-11-25,Wholefoods,Expense,17.35
|
| 20 |
-
2022-11-24,Freelancing,Income,2409.55
|
| 21 |
-
2022-11-19,Spotify,Expense,20.76
|
| 22 |
-
2022-10-25,Blogging,Income,4044.27
|
| 23 |
-
2022-10-24,Uber Taxi,Expense,18.9
|
| 24 |
-
2022-10-23,Uber Taxi,Expense,27.54
|
| 25 |
-
2022-10-22,Apple Services,Expense,41.25
|
| 26 |
-
2022-10-21,Netflix,Expense,22.8
|
| 27 |
-
2022-01-16,Amazon Lux,Expense,24.11
|
| 28 |
-
2022-01-15,Burger King,Expense,30.08
|
| 29 |
-
2022-01-14,Amazon,Expense,11.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
migration/versions/e8e988ebb7c7_.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
|
| 3 |
+
Revision ID: e8e988ebb7c7
|
| 4 |
+
Revises: 4e76691ab103
|
| 5 |
+
Create Date: 2024-06-08 18:13:46.160432
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
from typing import Sequence, Union
|
| 9 |
+
|
| 10 |
+
from alembic import op
|
| 11 |
+
import sqlalchemy as sa
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# revision identifiers, used by Alembic.
|
| 15 |
+
revision: str = 'e8e988ebb7c7'
|
| 16 |
+
down_revision: Union[str, None] = '4e76691ab103'
|
| 17 |
+
branch_labels: Union[str, Sequence[str], None] = None
|
| 18 |
+
depends_on: Union[str, Sequence[str], None] = None
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def upgrade() -> None:
|
| 22 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 23 |
+
pass
|
| 24 |
+
# ### end Alembic commands ###
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def downgrade() -> None:
|
| 28 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 29 |
+
pass
|
| 30 |
+
# ### end Alembic commands ###
|
tests/test_file_upload.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime
|
| 2 |
+
from typing import List
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from fastapi import Depends
|
| 5 |
+
from fastapi.testclient import TestClient
|
| 6 |
+
import pytest
|
| 7 |
+
|
| 8 |
+
from app.model.transaction import Transaction
|
| 9 |
+
from app.schema.index import TransactionType, TransactionCreate
|
| 10 |
+
|
| 11 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
| 12 |
+
from app.engine.postgresdb import get_db_session
|
| 13 |
+
|
| 14 |
+
@pytest.mark.asyncio
|
| 15 |
+
async def test_file_upload(client: TestClient, get_db_session_fixture: AsyncSession) -> None:
|
| 16 |
+
_test_upload_file = Path('/Users/patrickalexis/Documents/codepath-ai-course/codepath-group-project/backend/app/transactions_rag/transactions_2024.csv', 'new-index.json')
|
| 17 |
+
_files = {'input_file': _test_upload_file.open('rb')}
|
| 18 |
+
|
| 19 |
+
response = client.post(("/api/v1/file_upload/"),files=_files)
|
| 20 |
+
assert response.status_code == 201
|
transactions_2024.csv
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
transaction_date,name_description,type,amount
|
| 2 |
+
20/11/2023,Comcast Internet,Expense,9.96
|
| 3 |
+
21/04/2023,Lemonade Home Insurance,Expense,17.53
|
| 4 |
+
22/05/2023,Monthly Appartment Rent,Expense,2000.0
|
| 5 |
+
14/06/2023,Staples Office Supplies,Expense,12.46
|
| 6 |
+
17/07/2023,Selling Paintings,Income,13.63
|
| 7 |
+
19/12/2023,Spotify,Expense,12.19
|
| 8 |
+
05/10/2023,Target,Expense,27.08
|
| 9 |
+
03/08/2023,IT Consulting,Income,541.57
|
| 10 |
+
06/04/2023,Phone,Expense,10.7
|
| 11 |
+
02/01/2023,ML Consulting,Income,2641.93
|
| 12 |
+
05/04/2023,Chipotle,Expense,18.9
|
| 13 |
+
18/05/2023,Cold Brew Coffee,Expense,17.67
|
| 14 |
+
13/03/2023,Gas,Expense,8.80
|
| 15 |
+
19/07/2023,CA Property Tax,Expense,1670.34
|
| 16 |
+
20/08/2023,Salary,Income,4000.36
|
| 17 |
+
20/09/2023,Cellphone,Expense,19.27
|
| 18 |
+
06/10/2023,Gym Membership,Expense,24.71
|
| 19 |
+
02/01/2023,Wholefoods,Expense,17.35
|
| 20 |
+
11/06/2023,Freelancing,Income,2409.55
|
| 21 |
+
12/11/2023,Spotify,Expense,20.76
|
| 22 |
+
20/03/2023,Blogging,Income,4044.27
|
| 23 |
+
12/02/2023,Uber Taxi,Expense,18.9
|
| 24 |
+
23/07/2023,Uber Taxi,Expense,27.54
|
| 25 |
+
01/09/2023,Apple Services,Expense,41.25
|
| 26 |
+
07/05/2023,Netflix,Expense,22.8
|
| 27 |
+
09/08/2023,Amazon Lux,Expense,24.11
|
| 28 |
+
17/01/2023,Burger King,Expense,30.08
|
| 29 |
+
14/01/2023,Amazon,Expense,11.0
|