File Upload Fix

#15
by palexis3 - opened
.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
- # Write items of .csv filte to directory
30
- with open(os.path.join(file_upload_directory_path, input_file.filename)) as output_file:
31
- [output_file.write(" ".join(row)+'\n') for row in csv.reader(input_file)]
32
- output_file.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
- # With the newly created file and it's path, process and save it for embedding
35
- processed_file = process_file(os.path.realpath(input_file.filename))
36
- result = await asyncio.gather(processed_file)
37
- await save_results(db, result)
 
 
38
 
39
- except Exception:
40
- return {"message": "There was an error uploading this file. Ensure you have a .csv file with the following columns:"
41
- "\n transaction_date, type, category, name_description, amount"}
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
- # Local application/library specific imports
 
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
- import app.categorization.template as CATEGORY_TEMPLATE
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 = 75,
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
- # Fuzzy-match this description against the reference descriptions
49
- match_results = process.extractOne(
50
- description, descriptions, score_cutoff=threshold)
 
 
51
 
52
- # If a match is found, return the category of the matched description
53
- if match_results:
54
- return description_category_pairs.at[match_results[2], 'category']
55
 
56
- return None
 
 
 
 
57
 
58
 
59
- async def llm_list_categorizer(tx_list: pd.DataFrame) -> pd.DataFrame:
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
- tx_list (pd.DataFrame): DataFrame containing the transaction descriptions to categorize.
67
 
68
  Returns:
69
  pd.DataFrame: DataFrame mapping transaction descriptions to their inferred categories.
70
  """
 
 
71
 
72
- # Initialize language model and prompt
73
- openai_api_key = os.environ['OPENAI_API_KEY']
74
- llm = ChatOpenAI(temperature=0, model="gpt-3.5-turbo-0125",
75
- api_key=openai_api_key)
76
- prompt = PromptTemplate.from_template(template=CATEGORY_TEMPLATE)
77
- chain = LLMChain(llm=llm, prompt=prompt)
78
-
79
- # Iterate over the DataFrame in batches of TX_PER_LLM_RUN transactions
80
- tasks = [llm_sublist_categorizer(tx_list.attrs['file_name'], chain=chain, tx_descriptions="\n".join(chunk['name/description']).strip())
81
- for chunk in np.array_split(tx_list, tx_list.shape[0] // TX_PER_LLM_RUN + 1)]
82
-
83
- # Gather results and extract (valid) outputs
84
- # The results variable is a list of 'results', each 'result' being the output of a single LLM run
85
- results = await asyncio.gather(*tasks)
86
-
87
- # Extract valid results (each valid result is a list of description-category pairs)
88
- valid_results = [result['output'] for result in results if result['valid']]
89
-
90
- # Flatten the list of valid results to obtain a single list of description-category pairs
91
- valid_outputs = [
92
- output for valid_result in valid_results for output in valid_result]
 
 
 
 
 
 
 
 
 
 
 
 
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.arun(input_data=tx_descriptions)
 
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
- logging.ERROR, f"| File: {file_name} | Unexpected Error: {e}\nRaw Result: {raw_result}")
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
- # Third-party library imports
 
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(tx_list: 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,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
- tx_list (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
 
29
- if os.path.exists(CATEGORY_REFERENCE_OUTPUT_FILE):
30
- # Read description-category pairs from the reference file
31
- description_category_pairs = pd.read_csv(
32
- CATEGORY_REFERENCE_OUTPUT_FILE, header=None, names=['name/description', 'category']
33
- )
34
-
35
- # Extract only descriptions for faster matching
36
- descriptions = description_category_pairs['name/description'].values
37
-
38
- # Use fuzzy matching to find similar descriptions and assign the category
39
- tx_list['category'] = tx_list['name/description'].apply(
40
- fuzzy_match_list_categorizer,
41
- args=(descriptions, description_category_pairs),
42
- )
43
-
44
- # Filter out uncategorized transactions, deduplicate, and sort by description
45
- uncategorized_descriptions = (
46
- tx_list[tx_list['category'].isnull()]
47
- .drop_duplicates(subset=['name/description'])
48
- .sort_values(by=['name/description'])
49
- )
50
-
51
- # Ask the language model to categorize the remaining descriptions
52
- if not uncategorized_descriptions.empty:
53
- categorized_descriptions = await llm_list_categorizer(
54
- uncategorized_descriptions[['name/description', 'category']]
55
- )
56
-
57
- categorized_descriptions.dropna(inplace=True)
58
-
59
- # Update the category for uncategorized transactions based on the language model results
60
- if not categorized_descriptions.empty:
61
- tx_list['category'] = tx_list['category'].fillna(
62
- tx_list['name/description'].map(
63
- categorized_descriptions.set_index('name/description')['category']
64
- )
65
  )
66
-
67
- # Fill remaining NaN values in 'category' with 'Other'
68
- tx_list['category'] = tx_list['category'].fillna('Other')
69
 
70
- return tx_list
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 = {"file_name": file_name, "output": pd.DataFrame(), "error": ""}
34
  try:
35
- # Read file into standardized tx format: source, date, type, category, description, amount
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.log(logging.ERROR, f"| File: {file_name} | Unexpected Error: {e}")
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
- tx_list = pd.read_csv(file_path, index_col=False)
63
- tx_list.attrs["file_name"] = file_path
64
- tx_list.columns = tx_list.columns.str.lower().str.strip()
65
-
66
- # Standardize dates to YYYY/MM/DD format
67
- tx_list["date"] = pd.to_datetime(tx_list["date"]).dt.strftime("%Y/%m/%d")
68
-
69
- # Add source and reindex to desired tx format; category column is new and therefore empty
70
- tx_list.loc[:, "source"] = os.path.basename(file_path)
71
- tx_list = tx_list.reindex(columns=["date", "expense/income", "category", "name/description", "amount"])
72
-
73
- return tx_list
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
 
76
- async def save_results(db: AsyncSession, results: List) -> None:
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
- # Concatenate all (valid) results into a single DataFrame
89
- # Print errors to console
90
- ok_files = []
91
- ko_files = []
92
- error_messages = []
93
-
94
- col_list = ["transaction_date", "type", "category", "name_description", "amount"]
95
- tx_list = pd.DataFrame(columns=col_list)
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