File size: 14,201 Bytes
d813e31
 
 
 
 
 
630a5df
 
 
 
 
 
 
 
beab094
1519cac
630a5df
d813e31
 
 
 
2e932f1
 
d813e31
 
 
 
53b9009
d2d2e8d
53b9009
 
4e56772
630a5df
 
 
d2d2e8d
630a5df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d813e31
4e56772
d813e31
 
 
 
4e56772
 
 
d813e31
4e56772
5dd7fbf
 
4e56772
5dd7fbf
4e56772
5dd7fbf
 
4e56772
 
 
 
 
 
 
 
 
 
 
d2d2e8d
d813e31
5dd7fbf
d813e31
5dd7fbf
d813e31
 
 
5dd7fbf
 
d813e31
 
4e56772
 
 
d813e31
4e56772
d813e31
 
 
 
5dd7fbf
 
d813e31
4e56772
 
 
 
d813e31
 
4e56772
 
 
d813e31
f281e50
d813e31
 
 
 
 
 
 
 
 
 
 
 
4e56772
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d813e31
 
 
 
 
 
 
 
 
 
4589222
d813e31
 
 
 
4589222
d813e31
 
 
 
4e56772
d813e31
 
 
 
 
 
 
 
 
 
 
4589222
d813e31
4e56772
d813e31
4589222
d813e31
 
4e56772
d813e31
 
 
 
 
 
 
 
 
 
4589222
d813e31
 
 
 
4e56772
 
d813e31
 
 
 
decf374
 
 
630a5df
decf374
 
5dd7fbf
decf374
 
630a5df
8f20afd
d7a4e43
630a5df
 
 
decf374
 
36cedf9
d813e31
4e56772
36cedf9
d813e31
 
 
 
 
36cedf9
 
 
4e56772
36cedf9
e6ff214
 
36cedf9
4e56772
 
36cedf9
 
 
 
 
 
 
 
 
 
 
 
4e56772
d813e31
 
 
 
 
 
 
 
 
 
4acd89f
d813e31
 
 
 
 
 
 
4acd89f
d813e31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f281e50
 
 
 
 
 
 
 
 
 
 
 
630a5df
 
d813e31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
68
69
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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
import re
import os
from datetime import datetime
import openai
from google.cloud import firestore
from dotenv import load_dotenv
from pandasai import SmartDatalake
from pandasai import Agent
from pandasai.responses.response_parser import  ResponseParser
import pandas as pd
from pandasai.llm import OpenAI
#from langchain.llms.sambanova import Sambaverse
#from langchain_community.llms.sambanova import Sambaverse
from langchain_community.chat_models.sambanova import ChatSambaNovaCloud
import ast
import json


# Make API connection
load_dotenv()
# gemini_api_key = os.environ['Gemini']
# o_api_key = os.getenv("openai_api_key")
# openai.api_key = o_api_key

# Authenticate to Firesotre with the JSON account key
db = firestore.Client.from_service_account_json("firestore-key.json")

client = openai.OpenAI(
    api_key=os.environ.get("SAMBANOVA_API_KEY"),
    base_url="https://api.sambanova.ai/v1",
)




sambaverse_api_key = os.environ.get("SAMBANOVA_API_KEY")


llm = ChatSambaNovaCloud(
    model="Meta-Llama-3.1-70B-Instruct",
    max_tokens=1024,
    temperature=0.7,
    top_k=1,
    top_p=0.01,
)


print(llm.invoke("Why should I use open source models?"))



class FlaskResponse(ResponseParser):
  def __init__(self, context) -> None:
      super().__init__(context)

  def format_dataframe(self, result):
      return result['value'].to_html()

  def format_plot(self, result):
        # Save the plot using savefig
      try:
          
          img_path = result['value']
      
        
      except ValueError:
          img_path = str(result['value'])
          print("value error!", img_path)
        
      print("response_class_path:", img_path)
      return img_path

  def format_other(self, result):
      return str(result['value'])
      
# Generate AI response from user input
def generateResponse(prompt,model='Meta-Llama-3.1-70B-Instruct'):
  #----- Call API to classify and extract relevant transaction information
    # These templates help provide a unified response format for use as context clues when
    # parsing the AI generated response into a structured data format
    relevant_info_template = """
          Intent: The CRUD operation, one of create, read, update, or delete
          Transaction Type: The type of transaction such as purchases, sales
          Details: as a sublist of the key details like name of item, quantity, cost price, currency, unit for the quantity, description, among other details you are able to extract. 
      """
    sample_single_transaction_template = """
      The information provided indicates that you want to *create/record* a new transaction.
      *Extracted Information*:

      *Intent*: Create

      *Transaction Type*: Purchase
      *Details*: 
              - Item: Car
              - Purpose: Business
              - Quantity: 1
              - Unit: None
              - Cost: 10000
              - Tax: 200
              - Currency: USD
              - Note: A new car for business
              """
    
    sample_multi_transaction_template = """
      The information provided indicates that you want to *create/record* a new transaction.

      *Extracted Information*:

      *Intent*: Create

      Transaction 1: 

      *Transaction Type*: Purchase
      *Details*: 
              - Item: Car
              - Purpose: Business
              - Quantity: 1
              - Unit: None
              - Cost: 10000
              - Tax: 200
              - Currency: USD
              - Note: A new car for business

      Transaction 2:

      *Transaction Type*: Expense  
      *Details*:  
            - Item: Office Chair  
            - Quantity: 2
            - Unit: None
            - Cost: 300 
            - Currency: USD  
            - Category: Furniture  
      """
    response = client.chat.completions.create(
        model = model,
        # model="gpt-4o",
        messages=[
              {"role": "system", "content": f"You are a helpful assistant that classifies transactions written in natural language into CRUD operations (Create, Read, Update, and Delete) and extracts relevant information. For update and delete queries, the transaction type should either be sales or inventory. You should be able to recognize the currency being used and any quantity units into separate fields. Format the relevant information extracted from the transaction text in this format: {relevant_info_template}. A sample response for a single transaction could look like this: {sample_single_transaction_template}, while multiple transactions could look like this: {sample_multi_transaction_template}. There should be only one intent even in the case of multiple transactions."},
              {"role": "user", "content": prompt}
        ]
    )
    #----- Process response
    try:
      response = response.choices[0].message.content
    except Exception as e:
        print(f'An error occurred: {str(e)}')
        response = None

    return response


def parse_value(value):
    """
    Parses a value string into the appropriate data type and detects currency.
    Handles various currencies, percentages, numbers, and text.
    """
    value = value.strip()
    try:
        # Match currency codes or symbols dynamically
        currency_match = re.search(r"([A-Z]{3}|\$|€|£)", value)
        currency = currency_match.group(1) if currency_match else None

        # Remove currency symbols or codes for numeric conversion
        cleaned_value = re.sub(r"[A-Z]{3}|\$|€|£", "", value).replace(",", "").strip()

        # Handle percentages
        if "%" in cleaned_value:
            return float(cleaned_value.replace("%", "")), currency
        # Handle plain numbers (integers or floats)
        elif cleaned_value.replace(".", "", 1).isdigit():
            return float(cleaned_value) if "." in cleaned_value else int(cleaned_value), currency
        # Return as text if no numeric parsing is possible
        return value, currency
    except ValueError:
        # Fallback to original value if parsing fails
        return value, None

def extract_transaction_details(text):
    """
    Extracts transaction details from a given text input.
    Handles both bold (**field**) and non-bold (field) formats.
    """
    details = {}
    transaction_currency = None  # Default currency field

    # Regex to match key-value pairs
    detail_matches = re.findall(
        r"-\s*\*{0,2}([\w\s]+)\*{0,2}:\s*([\w\s,.$%-]+?)(?:\s*[\n]|$)",  # Stop matching before newline or end of string
        text,
        re.DOTALL
    )
    # print("Detail matches:", detail_matches)  # Debugging

    for field, value in detail_matches:
        # Standardize the field name (convert to snake_case)
        field = field.strip().lower().replace(" ", "_")
        
        # Parse the value and dynamically detect currency
        parsed_value, detected_currency = parse_value(value)
        if detected_currency and not transaction_currency:
            transaction_currency = detected_currency  # Set the transaction-level currency if not already set
        
        details[field] = parsed_value

    # Add currency as a separate field
    if transaction_currency:
        details["currency"] = transaction_currency

    return details


# Parsing single transactions
def parse_ai_response(response_text):
    # Initialize the structured data dictionary
    data = {
        "intent": None,
        "transaction_type": None,
        "details": {},
        "created_at": datetime.now().isoformat() # Add current date and time
    }
    
    # Extract the intent
    intent_match = re.search(r"\*Intent\*:\s*(\w+)", response_text)
    if intent_match:
        data["intent"] = intent_match.group(1)
    
    # Extract the transaction type
    transaction_type_match = re.search(r"\*Transaction Type\*:\s*(\w+)", response_text)
    if transaction_type_match:
        data["transaction_type"] = transaction_type_match.group(1)
    
    # Store details in the structured data
    data["details"] = extract_transaction_details(response_text)
    
    return data

def parse_multiple_transactions(response_text):
    transactions = []
    # Split the response into transaction sections based on keyword 'Transaction #'
    transaction_sections = re.split(r"Transaction \d+:", response_text, flags=re.IGNORECASE)
    # Adjust regex to handle variations in "Transaction X:"
    # transaction_sections = re.split(r"(?i)(?<=\n)transaction\s+\d+:", response_text)
    transaction_sections = [section.strip() for section in transaction_sections if section.strip()]
    # Remove the first section if it's not a valid transaction
    if not re.search(r"\*Transaction Type\*", transaction_sections[0], re.IGNORECASE):
        transaction_sections.pop(0)
    
    # Extract intent: with support for a single intent per user prompt
    intent_match = re.search(r"\*Intent\*:\s*(\w+)", response_text)
    if intent_match:
        intent = intent_match.group(1)
    
    for section in transaction_sections:
        # Initialize transaction data
        transaction_data = {
            "intent": intent, # global intent
            "transaction_type": None,
            "details": {},
            "created_at": datetime.now().isoformat()
        }

        # Extract transaction type
        transaction_type_match = re.search(r"\*Transaction Type\*:\s*(\w+)", section)
        if transaction_type_match:
            transaction_data["transaction_type"] = transaction_type_match.group(1)

        # Extract details
        transaction_data["details"] = extract_transaction_details(section)

        transactions.append(transaction_data)

    return transactions

def read_datalake(user_phone, user_question): 
    inventory_ref = db.collection("users").document(user_phone).collection("inventory")
    sales_ref = db.collection("users").document(user_phone).collection('sales')

    inventory_list = [doc.to_dict() for doc in inventory_ref.stream()]
    sales_list = [doc.to_dict() for doc in sales_ref.stream()]
    
    inventory_df = pd.DataFrame(inventory_list)
    sales_df = pd.DataFrame(sales_list)

    lake = SmartDatalake([inventory_df, sales_df], config={"llm": llm, "custom_whitelisted_dependencies":["ast"], "response_parser": FlaskResponse, "enable_cache": False, "save_logs": False})
    response = lake.chat(user_question)

    return response



def create_inventory(user_phone, transaction_data):
    for transaction in transaction_data:
        item_name = transaction['details']['item'] # assumes unique item name
        doc_ref = db.collection("users").document(user_phone).collection("inventory").document(item_name)
        # transaction_data['transaction_id'] # let's default to random generated document ids
        doc_ref.set(transaction)
        # print("Transaction created successfully!")
    return True

def create_sale(user_phone, transaction_data):
    
    for transaction in transaction_data:
        item_name = transaction['details']['item'] # assumes item names are unique per transactions
        # fetch the inventory
        inventory = fetch_transaction(user_phone, item_name)
        
        # Do the sales calculations
        new_stock_level = inventory['details']['quantity'] - transaction['details']['quantity']
        inventory['details']['quantity'] = new_stock_level

        # update the inventory
        doc_ref = db.collection("users").document(user_phone).collection("inventory").document(item_name)
        doc_ref.set(inventory)
        
        # Create the sale
        doc_ref = db.collection("users").document(user_phone).collection("sales").document(item_name)
        doc_ref.set(transaction)

        # print("Transaction created successfully!")
    return True


# Update logic:
# - 
def update_transaction(user_phone, transaction_id, update_data):
    doc_ref = db.collection("users").document(user_phone).collection("transactions").document(transaction_id)
    doc_ref.update(update_data)
    # print("Transaction updated successfully!")
    return True

def fetch_transaction(user_phone, transaction_id=None):
    if transaction_id:
        doc_ref = db.collection("users").document(user_phone).collection("inventory").document(transaction_id)
        transaction = doc_ref.get()
        if transaction.exists:
            return transaction.to_dict()
        else:
            # print("Transaction not found.")
            return None
    else:
        collection_ref = db.collection("users").document(user_phone).collection("inventory")
        transactions = [doc.to_dict() for doc in collection_ref.stream()]
        return transactions

# Delete fields or an entire transaction document.
# Delete specific fields
def delete_transaction_fields(user_phone, transaction_id, fields_to_delete):
    doc_ref = db.collection("users").document(user_phone).collection("transactions").document(transaction_id)
    updates = {field: firestore.DELETE_FIELD for field in fields_to_delete}
    doc_ref.update(updates)
    print("Fields deleted successfully!")

# Delete an entire transaction
def delete_transaction(user_phone, transaction_id):
    doc_ref = db.collection("users").document(user_phone).collection("transactions").document(transaction_id)
    doc_ref.delete()
    print("Transaction deleted successfully!")


# Delete an entire transaction
def delete_transaction(user_phone, transaction_data):
    # the transaction id currently defaults to the item name
    transaction_id = transaction_data[0]['details']['item']
    transaction_type = transaction_data[0]['transaction_type'].lower()
    doc_ref = db.collection("users").document(user_phone).collection(transaction_type).document(transaction_id)
    item_doc = doc_ref.get()
    if item_doc.exists:
        doc_ref.delete()
        return True
    else:
        return False


# Example usage
# response_text = """
# The information provided indicates that you want to **create/record** a new transaction.

# **Extracted Information**:

# **Intent**: Create  
# **Transaction Type**: Purchase  
# **Details**:  
#   - Item: Car  
#   - Purpose: Business  
#   - Amount: 100000 USD  
#   - Tax: 1000 USD  
# """

# parsed_data = parse_ai_response(response_text)
# print(parsed_data)