diff --git a/.dockerignore b/.dockerignore index 04d5e732..a0e74c98 100644 --- a/.dockerignore +++ b/.dockerignore @@ -25,3 +25,8 @@ qdrant_db/ qdrant_storage/ sample-data/ user_data/ +*.log +cache/ +*.db +*.parquet +exports/ diff --git a/.gitignore b/.gitignore index ef2f37e7..8a18392f 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,8 @@ user_data/ pgdata/ *.bak models_config.yaml +*.log +cache/ +*.db +*.parquet +exports/ diff --git a/.tfyignore b/.tfyignore index 9190102e..71b98dd6 100644 --- a/.tfyignore +++ b/.tfyignore @@ -33,3 +33,8 @@ node_modules/ sample-data/ .github/ docs/ +*.log +cache/ +*.parquet +*.db +exports/ diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..0bd27851 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +build: + docker compose --env-file compose.env.bak up --build -d + +down: + docker compose --env-file compose.env.bak down + +up: + docker compose --env-file compose.env.bak up -d + +deploy: + python -m deployment.deploy --workspace_fqn ${ws_fqn} --application_set_name ${app_set_name} --ml_repo ${ml_repo} --base_domain_url ${domain} --dockerhub-images-registry ${registry} --secrets-base ${secrets_base} diff --git a/backend/constants.py b/backend/constants.py index b7094072..95dc0261 100644 --- a/backend/constants.py +++ b/backend/constants.py @@ -39,3 +39,4 @@ class DataSourceType(StrEnum): TRUEFOUNDRY = "truefoundry" LOCAL = "localdir" WEB = "web" + STRUCTURED = "structured" diff --git a/backend/modules/dataloaders/__init__.py b/backend/modules/dataloaders/__init__.py index 5cd66958..43377de1 100644 --- a/backend/modules/dataloaders/__init__.py +++ b/backend/modules/dataloaders/__init__.py @@ -1,11 +1,14 @@ from backend.constants import DataSourceType from backend.modules.dataloaders.loader import register_dataloader from backend.modules.dataloaders.local_dir_loader import LocalDirLoader +from backend.modules.dataloaders.structured_loader import StructuredLoader from backend.modules.dataloaders.web_loader import WebLoader from backend.settings import settings register_dataloader(DataSourceType.LOCAL, LocalDirLoader) register_dataloader(DataSourceType.WEB, WebLoader) +register_dataloader(DataSourceType.STRUCTURED, StructuredLoader) + if settings.TFY_API_KEY: from backend.modules.dataloaders.truefoundry_loader import TrueFoundryLoader diff --git a/backend/modules/dataloaders/structured_loader.py b/backend/modules/dataloaders/structured_loader.py new file mode 100644 index 00000000..307d1724 --- /dev/null +++ b/backend/modules/dataloaders/structured_loader.py @@ -0,0 +1,239 @@ +import os +import time +from typing import AsyncGenerator, Dict, List, Tuple + +import pandas as pd +from pandasai import Agent +from truefoundry.ml import get_client as get_tfy_client + +from backend.logger import logger +from backend.modules.dataloaders.loader import BaseDataLoader +from backend.types import DataIngestionMode, DataSource, LoadedDataPoint +from backend.utils import unzip_file + + +class CacheItem: + """Class to hold cached item with timestamp""" + + def __init__(self, items: List[any]): + self.items = items + self.timestamp = time.time() + + +class StructuredLoader(BaseDataLoader): + """ + Load structured data from various sources (CSV, Excel, and Databases) + """ + + _instance = None + CACHE_TTL = 300 # 5 minutes in seconds + + def __new__(cls): + if cls._instance is None: + cls._instance = super(StructuredLoader, cls).__new__(cls) + cls._instance.dataframes = {} # Cache for lists of dataframes + cls._instance.agents = {} # Cache for PandasAI agents + cls._instance._last_cleanup = time.time() + return cls._instance + + def _load_file(self, filepath: str) -> pd.DataFrame: + """Load data from CSV or Excel file""" + file_extension = os.path.splitext(filepath)[1].lower() + + if file_extension == ".csv": + return pd.read_csv(filepath) + elif file_extension in [".xlsx", ".xls"]: + return pd.read_excel(filepath) + else: + raise ValueError(f"Unsupported file type: {file_extension}") + + def _load_files_from_directory( + self, directory: str + ) -> List[Tuple[str, pd.DataFrame]]: + """Load all structured data files from a directory""" + dataframes = [] + files = [ + f for f in os.listdir(directory) if f.endswith((".csv", ".xlsx", ".xls")) + ] + + for file in files: + file_path = os.path.join(directory, file) + try: + df = self._load_file(file_path) + dataframes.append((file, df)) + except Exception as e: + logger.warning(f"Failed to load file {file}: {e}") + continue + + return dataframes + + def _cleanup_cache(self): + """Remove expired items from cache""" + current_time = time.time() + + # Only run cleanup every minute to avoid too frequent checks + if current_time - self._last_cleanup < 60: + return + + expired_dfs = [ + fqn + for fqn, item in self.dataframes.items() + if current_time - item.timestamp > self.CACHE_TTL + ] + expired_agents = [ + fqn + for fqn, item in self.agents.items() + if current_time - item.timestamp > self.CACHE_TTL + ] + + # Remove expired items + for fqn in expired_dfs: + logger.debug(f"Removing expired dataframe from cache: {fqn}") + del self.dataframes[fqn] + + for fqn in expired_agents: + logger.debug(f"Removing expired agent from cache: {fqn}") + del self.agents[fqn] + + self._last_cleanup = current_time + + def _cache_items(self, data_source_fqn: str, df: pd.DataFrame, agent: Agent): + """Cache dataframe and agent with timestamps""" + self.dataframes[data_source_fqn] = CacheItem(df) + self.agents[data_source_fqn] = CacheItem(agent) + + async def load_filtered_data( + self, + data_source: DataSource, + dest_dir: str, + previous_snapshot: Dict[str, str], + batch_size: int, + data_ingestion_mode: DataIngestionMode, + ) -> AsyncGenerator[List[LoadedDataPoint], None]: + """Load structured data from source""" + self._cleanup_cache() + source_type = self._detect_source_type(data_source.uri) + + try: + if source_type in ["csv", "excel"]: + loaded_files = [] # List to store (filename, dataframe) tuples + working_dir = None + if data_source.uri.startswith("data-dir:"): + # Handle remote (TrueFoundry) files + tfy_files_dir = None + try: + tfy_client = get_tfy_client() + dataset = tfy_client.get_data_directory_by_fqn(data_source.uri) + working_dir = dataset.download(path=dest_dir) + logger.debug(f"Data directory download info: {working_dir}") + + if os.path.exists(os.path.join(working_dir, "files")): + working_dir = os.path.join(working_dir, "files") + + # Handle zip files + for file_name in os.listdir(working_dir): + if file_name.endswith(".zip"): + unzip_file( + file_path=os.path.join(working_dir, file_name), + dest_dir=working_dir, + ) + + loaded_files = self._load_files_from_directory(working_dir) + + except Exception as e: + logger.exception(f"Error downloading data directory: {str(e)}") + raise ValueError(f"Failed to download data directory: {str(e)}") + + else: + # Handle local files + working_dir = ( + data_source.uri + if os.path.isdir(data_source.uri) + else os.path.dirname(data_source.uri) + ) + loaded_files = self._load_files_from_directory(working_dir) + + if not loaded_files: + raise Exception(f"No valid structured data files found") + + # Cache the dataframes + self.dataframes[data_source.fqn] = CacheItem( + [df for _, df in loaded_files] + ) + + # Create LoadedDataPoints for each file + data_points = [] + for filename, _ in loaded_files: + file_path = os.path.join(working_dir, filename) + data_point_hash = ( + f"{os.path.getsize(file_path)}:{dataset.updated_at}" + if data_source.uri.startswith("data-dir:") + else str(os.lstat(file_path)) + ) + + data_points.append( + LoadedDataPoint( + data_point_hash=data_point_hash, + data_point_uri=filename, + data_source_fqn=data_source.fqn, + local_filepath=file_path, + file_extension=os.path.splitext(filename)[1], + metadata={"structured_type": source_type}, + ) + ) + + yield data_points + + elif source_type in ["sql", "gsheet"]: + # Handle SQL and Google Sheets as before + data_point = LoadedDataPoint( + data_point_hash=str(hash(data_source.uri)), + data_point_uri=data_source.uri, + data_source_fqn=data_source.fqn, + local_filepath=data_source.uri, + metadata={"structured_type": source_type}, + ) + yield [data_point] + + except Exception as e: + logger.exception(f"Error loading structured data: {e}") + raise + + def _detect_source_type(self, uri: str) -> str: + """Detect the type of structured data source""" + # For TrueFoundry data directories + if uri.startswith("data-dir:"): + return "csv" # Default to CSV for data-dir + + # For local directories + if os.path.isdir(uri): + files = [ + f for f in os.listdir(uri) if f.endswith((".csv", ".xlsx", ".xls")) + ] + if not files: + raise ValueError(f"No structured data files found in directory: {uri}") + return "csv" if files[0].endswith(".csv") else "excel" + + # For direct file or connection paths + if uri.endswith(".csv"): + return "csv" + elif uri.endswith((".xlsx", ".xls")): + return "excel" + elif uri.startswith(("postgresql://", "mysql://", "sqlite://")): + return "sql" + elif "docs.google.com/spreadsheets" in uri: + return "gsheet" + else: + raise ValueError(f"Unsupported structured data source: {uri}") + + def get_dataframes(self, data_source_fqn: str) -> List[pd.DataFrame]: + """Get list of cached dataframes for a data source""" + self._cleanup_cache() + cached_item = self.dataframes.get(data_source_fqn) + return cached_item.items if cached_item else None + + def get_agent(self, data_source_fqn: str) -> Agent: + """Get cached agent for a data source""" + self._cleanup_cache() + cached_item = self.agents.get(data_source_fqn) + return cached_item.items[0] if cached_item else None diff --git a/backend/modules/model_gateway/model_gateway.py b/backend/modules/model_gateway/model_gateway.py index 4301e636..d932f9da 100644 --- a/backend/modules/model_gateway/model_gateway.py +++ b/backend/modules/model_gateway/model_gateway.py @@ -7,6 +7,7 @@ from langchain_core.language_models.chat_models import BaseChatModel from langchain_openai import OpenAIEmbeddings from langchain_openai.chat_models import ChatOpenAI +from pandasai.llm.local_llm import LocalLLM from backend.logger import logger from backend.modules.model_gateway.audio_processing_svc import AudioProcessingSvc @@ -41,6 +42,7 @@ def __init__(self): """ self._embedder_cache = create_cache() self._llm_cache = create_cache() + self._pandas_ai_cache = create_cache() self._reranker_cache = create_cache() self._audio_cache = create_cache() @@ -270,6 +272,27 @@ def get_llm_from_model_config( return self._llm_cache[cache_key] + def get_pandas_ai_model_from_model_config(self, model_config: ModelConfig): + """ + Get a PandasAI model instance for the specified model configuration. + """ + cache_key = model_config.name + if cache_key not in self._pandas_ai_cache: + if model_config.name not in self.model_name_to_provider_config: + raise ValueError( + f"Model {model_config.name} not registered in the model gateway." + ) + + provider_config = self.model_name_to_provider_config[model_config.name] + api_key = self._get_api_key(provider_config) + model_id = "/".join(model_config.name.split("/")[1:]) + + self._pandas_ai_cache[cache_key] = LocalLLM( + api_base=provider_config.base_url, model=model_id, api_key=api_key + ) + + return self._pandas_ai_cache[cache_key] + def get_reranker_from_model_config(self, model_name: str, top_k: int = 3): """ Get a reranker model instance for the specified model configuration. Uses caching to avoid diff --git a/backend/modules/query_controllers/__init__.py b/backend/modules/query_controllers/__init__.py index 145cfacc..5cdb1d7e 100644 --- a/backend/modules/query_controllers/__init__.py +++ b/backend/modules/query_controllers/__init__.py @@ -3,6 +3,10 @@ MultiModalRAGQueryController, ) from backend.modules.query_controllers.query_controller import register_query_controller +from backend.modules.query_controllers.structured.controller import ( + StructuredQueryController, +) register_query_controller("basic-rag", BasicRAGQueryController) register_query_controller("multimodal", MultiModalRAGQueryController) +register_query_controller("structured", StructuredQueryController) diff --git a/backend/modules/query_controllers/structured/__init__.py b/backend/modules/query_controllers/structured/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/modules/query_controllers/structured/controller.py b/backend/modules/query_controllers/structured/controller.py new file mode 100644 index 00000000..20b7cb29 --- /dev/null +++ b/backend/modules/query_controllers/structured/controller.py @@ -0,0 +1,258 @@ +import base64 +import json +import os +import tempfile +from typing import Any +from urllib.parse import urlparse + +from fastapi import Body, HTTPException +from langchain_core.messages import HumanMessage +from pandasai import Agent +from pandasai.connectors import MySQLConnector, PostgreSQLConnector, SqliteConnector + +from backend.constants import DataSourceType +from backend.logger import logger +from backend.modules.dataloaders.loader import get_loader_for_data_source +from backend.modules.metadata_store.client import get_client +from backend.modules.model_gateway.model_gateway import model_gateway +from backend.modules.query_controllers.base import BaseQueryController +from backend.modules.query_controllers.structured.payload import ( + CSV_STRUCTURED_PAYLOAD, + CSV_STRUCTURED_PLOTTING_PAYLOAD, + DB_STRUCTURED_PAYLOAD, + DB_STRUCTURED_WHERE_PAYLOAD, + GSHEET_STRUCTURED_PAYLOAD, + RESPONSE_REFORMAT_QUERY, +) +from backend.modules.query_controllers.structured.types import StructuredQueryInput +from backend.modules.query_controllers.types import Answer +from backend.server.decorators import post, query_controller +from backend.types import DataIngestionMode + +EXAMPLES = { + "csv": CSV_STRUCTURED_PAYLOAD, + "csv-plotting": CSV_STRUCTURED_PLOTTING_PAYLOAD, + "gsheet": GSHEET_STRUCTURED_PAYLOAD, + "db": DB_STRUCTURED_PAYLOAD, + "db-where": DB_STRUCTURED_WHERE_PAYLOAD, +} + + +@query_controller("/structured") +class StructuredQueryController(BaseQueryController): + """Controller for handling structured data queries using PandasAI""" + + def _detect_source_type(self, uri: str) -> str: + """Detect the type of structured data source""" + # For TrueFoundry data directories + if uri.startswith("data-dir:"): + return "csv" # Default to CSV for data-dir + + # For local directories + if os.path.isdir(uri): + files = [ + f for f in os.listdir(uri) if f.endswith((".csv", ".xlsx", ".xls")) + ] + if not files: + raise ValueError(f"No structured data files found in directory: {uri}") + return "csv" if files[0].endswith(".csv") else "excel" + + # For direct file or connection paths + if uri.endswith(".csv"): + return "csv" + elif uri.endswith((".xlsx", ".xls")): + return "excel" + elif uri.startswith(("postgresql://", "mysql://", "sqlite://")): + return "sql" + elif "docs.google.com/spreadsheets" in uri: + return "gsheet" + else: + raise ValueError(f"Unsupported structured data source: {uri}") + + def _create_sql_connector(self, uri: str, request: StructuredQueryInput): + """Create appropriate SQL connector based on URI and request parameters""" + parsed = urlparse(uri) + db_type = parsed.scheme + + # Base config with common fields + config = { + "host": parsed.hostname, + "port": parsed.port, + "database": parsed.path.lstrip("/"), + "username": parsed.username, + "password": parsed.password, + } + + # Add table if provided in request + if request.table: + config["table"] = request.table + + # Add where clause if provided in request + if request.where: + config["where"] = [[f.column, f.operator, f.value] for f in request.where] + + logger.info(f"DB Config: {config}") + + if db_type == "mysql": + return MySQLConnector(config=config) + elif db_type == "postgresql": + return PostgreSQLConnector(config=config) + elif db_type == "sqlite": + return SqliteConnector( + config={ + "database": parsed.path, + "table": request.table, + "where": config.get("where"), + } + ) + else: + raise ValueError(f"Unsupported database type: {db_type}") + + def _get_response_reformat_llm(self, query: str, response: Any): + if response is None or query is None: + raise ValueError("Query and response are required") + + if isinstance(response, str): + response = response.replace("\n", " ") + elif isinstance(response, list): + response = ", ".join(response) + elif isinstance(response, dict): + response = json.dumps(response) + else: + # Serialize response to string + response = str(response) + + payload = RESPONSE_REFORMAT_QUERY.format(query=query, response=response) + return [HumanMessage(content=payload)] + + @post("/answer") + async def answer( + self, request: StructuredQueryInput = Body(openapi_examples=EXAMPLES) + ): + """Handle queries for structured data using PandasAI""" + try: + # Get data source + client = await get_client() + logger.info(f"Getting data source from FQN: {request.data_source_fqn}") + data_source = await client.aget_data_source_from_fqn( + request.data_source_fqn + ) + if not data_source: + raise HTTPException( + status_code=404, + detail=f"Data source {request.data_source_fqn} not found", + ) + + # Get source type + source_type = self._detect_source_type(data_source.uri) + + # Get LLM + pandas_ai_llm = model_gateway.get_pandas_ai_model_from_model_config( + request.model_configuration + ) + + # Create a temp dir for charts + chart_dir = tempfile.mkdtemp() + + # PandasAI config + pandas_ai_config = { + "llm": pandas_ai_llm, + "save_charts": True, + "save_charts_path": chart_dir, + } + + # Create PandasAI agent based on source type + if source_type in ["csv", "excel"]: + # Use loader for CSV/Excel files + loader = get_loader_for_data_source(DataSourceType.STRUCTURED) + dfs = loader.get_dataframes(request.data_source_fqn) + if dfs is None: + # Load the data if not cached + async for _ in loader.load_filtered_data( + data_source=data_source, + dest_dir=tempfile.mkdtemp(), + previous_snapshot={}, + batch_size=1, + data_ingestion_mode=DataIngestionMode.NONE, + ): + pass + dfs = loader.get_dataframes(request.data_source_fqn) + + if not dfs: + raise Exception( + f"Failed to load data for {request.data_source_fqn}" + ) + + # Create agent with multiple dataframes + agent = Agent( + dfs, # Pass list of dataframes + config=pandas_ai_config, + description=request.description, + ) + + elif source_type == "sql": + # Create appropriate SQL connector with request parameters + connector = self._create_sql_connector(data_source.uri, request) + agent = Agent( + connector, + config=pandas_ai_config, + description=request.description, + ) + + # FIX: Not that efficient. + # elif source_type == "gsheet": + # logger.info(f"Using Google Sheets connector for {data_source.uri}") + # # Let PandasAI handle Google Sheets directly + # agent = Agent( + # data_source.uri, + # config=pandas_ai_config, + # description=request.description, + # ) + + else: + raise ValueError(f"Unsupported data source type: {source_type}") + + # Get answer + response = agent.chat(request.query) + logger.info(f"Raw response: {response}") + + # Reformat response to a natural language using llm + reformat_llm = model_gateway.get_llm_from_model_config( + request.model_configuration + ) + reformat_prompt = self._get_response_reformat_llm(request.query, response) + response = (await reformat_llm.ainvoke(reformat_prompt)).content + logger.info(f"Formatted response: {response}") + + # Check if there is an image in the chart dir, if so, load the image as bytes + chart_files = [ + f + for f in os.listdir(chart_dir) + if os.path.isfile(os.path.join(chart_dir, f)) + ] + if chart_files: + image_base64 = None + try: + with open(os.path.join(chart_dir, chart_files[0]), "rb") as f: + image_data = f.read() + # Encode image data as base64 + image_base64 = base64.b64encode(image_data).decode("utf-8") + except Exception as e: + logger.exception(f"Error encoding image to base64: {e}") + finally: + # Clean up the individual file + os.remove(os.path.join(chart_dir, chart_files[0])) + # Clean up the temp directory + os.rmdir(chart_dir) + return Answer(content=response, image_base64=image_base64) + else: + # Clean up the temp directory + os.rmdir(chart_dir) + # Return response without image + return Answer(content=response) + + except Exception as e: + logger.exception(f"Error in structured query: {e}") + raise HTTPException( + status_code=500, detail=f"Failed to process structured query: {str(e)}" + ) diff --git a/backend/modules/query_controllers/structured/payload.py b/backend/modules/query_controllers/structured/payload.py new file mode 100644 index 00000000..613c152e --- /dev/null +++ b/backend/modules/query_controllers/structured/payload.py @@ -0,0 +1,123 @@ +RESPONSE_REFORMAT_QUERY = """Given the user query, reformat the response to a natural language response. +Do not include any code, markdown or file paths in the response. +Also do not approximate any numberical values or facts, state them as is. +If the user asks for a chart, Just reply with: "Here is the requested chart for ." Do not send the image path. +Query: {query} +Response: {response} +""" + +# OpenAPI examples + +## CSV +CSV_STRUCTURED_QUERY = { + "data_source_fqn": "structured::/app/user_data/loan-payments", + "model_configuration": { + "name": "truefoundry/openai-main/gpt-4o", + }, + "query": "What is the average age of people?", +} + +CSV_STRUCTURED_PAYLOAD = { + "summary": "Request payload for csv files", + "description": "This payload is used to query csv files using PandasAI", + "value": CSV_STRUCTURED_QUERY, +} + +## GSheet +DESCRIPTION_OF_COLUMNS = """Description of columns: +1. Timestamp: Timestamp +2. Your Gender: Gender +3. Your Age in Years: Age +4. Which of these fields are you currently working in?: Department +5. What is the target market for the work you produce?: Target Market +6. Please select which of these best describes your current role: Role +7. If it applies, please list the rank of your position: Rank +8. How many years experience in your current position do you have?: Years in current role +9. How many years experience working in the visual effects industry do you have?: Total years of experience in VFX industry +10. Where are you working?: Location +11. What currency do you want to use for filling out this form?: Currency +12. What type of employment contract do you have?: Employment contract +13. How long, in months, have you been employed in your current job?: Months employed in current role +14. What payment cycle should we use to calculate your rate?: Payment cycle +15. Please specify how much you charge per payment cycle, not including overtime: Rate per payment cycle +16. How much overtime do you work, on average, per week?: Average weekly overtime hours +17. Do you get paid overtime?: Overtime pay +18. Paid OT: How many hours does your employer consider a standard working week, before overtime is calculated?: Standard working hours before overtime applies +19. Paid OT: What is your overtime rate, per hour, as a multiple of your normal pay?: Overtime rate as a multiple of normal pay +20. What percentage of your income would you attribute to Over Time pay?: The share of total income derived from overtime, expressed as a percentage. +21. Approximately how many weeks of the year do you actually charge for?: The number of weeks in a year the respondent actively works and earns income. +22. What percentage of tax would you pay on your earnings over the course of a single financial year?: Tax rate on annual earnings +23. Which of the following benefits, if any, do you receive from your employer?: Benefits provided by the employer +24. What is the total value of benefits you receive in the average financial year?: The monetary value of the benefits the respondent receives annually +25. Do you support movements to unionise the VFX workforce?: The respondent’s opinion on unionizing the visual effects workforce +26. Are you happy with the hours you are required to work in VFX?: The respondent’s satisfaction with their working hours in VFX +27. Do you like your career?: The respondent’s overall satisfaction with their career +28. Do you like the city or hub you're working in?: The respondent’s satisfaction with their current location or work hub +29. How do you feel about overtime?: The respondent’s opinion on working overtime +30. What advice would you give to people looking to get into a role like yours in the visual effects industry?: Recommendations for newcomers entering the VFX industry +31. Please estimate what you think your total income is for a financial year, before tax: Estimated annual income, including overtime but excluding benefits +32. How can we improve the survey?: Suggestions from respondents for improving the survey design or questions""" + + +GSHEET_STRUCTURED_QUERY = { + "data_source_fqn": "structured::https://docs.google.com/spreadsheets/d/1hupDq5dsOVCObvEbLAJvhwoG_4l8Yec-qHSMfLx27H4/edit?usp=sharing", + "model_configuration": { + "name": "truefoundry/openai-main/gpt-4o", + }, + "query": "How many departments are there?", + "description": DESCRIPTION_OF_COLUMNS, +} + +GSHEET_STRUCTURED_PAYLOAD = { + "summary": "Request payload for google sheets", + "description": "This payload is used to query google sheets using PandasAI", + "value": GSHEET_STRUCTURED_QUERY, +} + +## DB +DB_STRUCTURED_QUERY = { + "data_source_fqn": "structured::postgresql://postgres:test@cognita-db:5432/cognita-config", + "model_configuration": { + "name": "truefoundry/openai-main/gpt-4o", + }, + "query": "Give me details of all the collections", + "table": "collections", +} + +DB_STRUCTURED_PAYLOAD = { + "summary": "Request payload for database files", + "description": "This payload is used to query database files using PandasAI", + "value": DB_STRUCTURED_QUERY, +} + +## DB with where clause +DB_STRUCTURED_WHERE_QUERY = { + "data_source_fqn": "structured::postgresql://postgres:test@cognita-db:5432/cognita-config", + "model_configuration": { + "name": "truefoundry/openai-main/gpt-4o", + }, + "query": "Give me count of indexing runs for collection finance-m", + "table": "ingestion_runs", + "where": [{"column": "collection_name", "operator": "=", "value": "finance-m"}], +} + +DB_STRUCTURED_WHERE_PAYLOAD = { + "summary": "Request payload for database files with where clause", + "description": "This payload is used to query database files with where clause using PandasAI", + "value": DB_STRUCTURED_WHERE_QUERY, +} + +## CSV with plotting +CSV_STRUCTURED_PLOTTING_QUERY = { + "data_source_fqn": "structured::/app/user_data/employee", + "model_configuration": { + "name": "truefoundry/openai-main/gpt-4o", + }, + "query": "Plot a pie chart of the percentage of employees in each department", +} + +CSV_STRUCTURED_PLOTTING_PAYLOAD = { + "summary": "Request payload for csv files with plotting", + "description": "This payload is used to query csv files with plotting using PandasAI", + "value": CSV_STRUCTURED_PLOTTING_QUERY, +} diff --git a/backend/modules/query_controllers/structured/types.py b/backend/modules/query_controllers/structured/types.py new file mode 100644 index 00000000..1a82e43e --- /dev/null +++ b/backend/modules/query_controllers/structured/types.py @@ -0,0 +1,19 @@ +from typing import List, Optional, Union + +from backend.modules.query_controllers.types import ModelConfig +from backend.types import ConfiguredBaseModel + + +class SQLFilter(ConfiguredBaseModel): + column: str + operator: str + value: Union[str, int, float, bool] + + +class StructuredQueryInput(ConfiguredBaseModel): + data_source_fqn: str + model_configuration: ModelConfig + query: str + description: Optional[str] = None + table: Optional[str] = None + where: Optional[List[SQLFilter]] = None diff --git a/backend/modules/query_controllers/types.py b/backend/modules/query_controllers/types.py index d4c9a428..4fb1bce0 100644 --- a/backend/modules/query_controllers/types.py +++ b/backend/modules/query_controllers/types.py @@ -16,6 +16,7 @@ class Document(ConfiguredBaseModel): class Answer(ConfiguredBaseModel): type: str = "answer" content: str + image_base64: Optional[str] = None class Docs(ConfiguredBaseModel): diff --git a/backend/requirements.txt b/backend/requirements.txt index d492cfbf..41b9ce02 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -63,3 +63,8 @@ pre-commit==3.7.0 ### Web Crawling crawl4ai==0.3.73 + +### PandasAI +pandasai==2.4.0 +seaborn==0.13.2 +psycopg2==2.9.10 diff --git a/backend/server/routers/data_source.py b/backend/server/routers/data_source.py index 233bb5d7..8e6b2bc4 100644 --- a/backend/server/routers/data_source.py +++ b/backend/server/routers/data_source.py @@ -71,7 +71,7 @@ async def delete_data_source( ) # Upon successful deletion of the data source, delete the data from `/users_data` directory if data source is of type `localdir` - if deleted_data_source.type == DataSourceType.LOCAL: + if deleted_data_source.type in [DataSourceType.LOCAL, DataSourceType.STRUCTURED]: data_source_uri = deleted_data_source.uri # data_source_uri is of the form: `/app/users_data/folder_name` folder_name = data_source_uri.split("/")[-1] @@ -87,7 +87,10 @@ async def delete_data_source( shutil.rmtree(folder_path) logger.info(f"Deleted folder: {folder_path}") - if deleted_data_source.type == DataSourceType.TRUEFOUNDRY: + if deleted_data_source.type in [ + DataSourceType.TRUEFOUNDRY, + DataSourceType.STRUCTURED, + ]: # Delete the data directory from truefoundry data directory try: # Log into TrueFoundry diff --git a/backend/server/routers/internal.py b/backend/server/routers/internal.py index ce76ceb3..2ccaa351 100644 --- a/backend/server/routers/internal.py +++ b/backend/server/routers/internal.py @@ -10,6 +10,7 @@ from truefoundry.ml import get_client as get_tfy_client from truefoundry.ml.autogen.client.models.signed_url_dto import SignedURLDto +from backend.constants import DataSourceType from backend.logger import logger from backend.modules.model_gateway.model_gateway import model_gateway from backend.server.routers.data_source import add_data_source @@ -26,14 +27,26 @@ async def upload_to_docker_directory( default_factory=lambda: str(uuid.uuid4()), regex=r"^[a-z][a-z0-9-]*$" ), files: List[UploadFile] = File(...), + is_structured: bool = Form(default=False), ): - """This function uploads files within `settings.LOCAL_DATA_DIRECTORY` given by the name req.upload_name""" + """This function uploads files within `settings.LOCAL_DATA_DIRECTORY`""" if not settings.LOCAL: return JSONResponse( content={"error": "API only supported for local docker environment"}, status_code=500, ) logger.info(f"Uploading files to directory: {upload_name}") + + # Validate files if structured data + if is_structured: + if not files[0].filename.endswith((".csv", ".xlsx", ".xls")): + return JSONResponse( + content={ + "error": "Structured data upload only supports CSV and Excel files" + }, + status_code=400, + ) + # create a folder within `/volumes/user_data/` that maps to `/app/user_data/` in the docker volume # this folder will be used to store the uploaded files folder_path = os.path.realpath( @@ -68,10 +81,14 @@ async def upload_to_docker_directory( with open(file_path, "wb") as f: f.write(file.file.read()) - # Add the data source to the metadata store. + # Add the data source to the metadata store with appropriate type + data_source_type = ( + DataSourceType.STRUCTURED if is_structured else DataSourceType.LOCAL + ) + return await add_data_source( CreateDataSource( - type="localdir", + type=data_source_type, uri=folder_path, ) ) diff --git a/deployment/deploy.py b/deployment/deploy.py index 4247ad4f..23659882 100644 --- a/deployment/deploy.py +++ b/deployment/deploy.py @@ -47,12 +47,11 @@ def run_deploy( api_key_env_var: TFY_API_KEY llm_model_ids: - "openai-main/gpt-4o-mini" + - "openai-main/gpt-4o" - "openai-main/gpt-4-turbo" - - "openai-main/gpt-3-5-turbo" - - "azure-openai/gpt-4" - - "together-ai/llama-3-70b-chat-hf" + - "qwen-coder/qwen2-7b-coder" embedding_model_ids: - - "openai-main/text-embedding-ada-002" + - "openai-main/text-embedding-3-small" reranking_model_ids: [] default_headers: "X-TFY-METADATA": '{{"tfy_log_request": "true", "Custom-Metadata": "Cognita-LLM-Request"}}' diff --git a/docker-compose.yaml b/docker-compose.yaml index 12e40b51..d1bec6c9 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,7 +1,6 @@ -version: "3.8" services: cognita-db: - image: postgres:13 + image: postgres:13-alpine container_name: cognita-postgres restart: unless-stopped ports: @@ -52,7 +51,7 @@ services: - cognita-docker infinity-server: - image: michaelf34/infinity:0.0.63 + image: michaelf34/infinity:0.0.68 pull_policy: if_not_present restart: unless-stopped container_name: infinity @@ -77,10 +76,10 @@ services: - cognita-docker qdrant-server: - image: qdrant/qdrant:v1.8.4 + image: qdrant/qdrant:v1.12.0 pull_policy: if_not_present restart: unless-stopped - container_name: qdrant + container_name: cognita-qdrant ports: - 6333:6333 - 6334:6334 @@ -134,6 +133,8 @@ services: image: fedirz/faster-whisper-server:latest-cpu pull_policy: if_not_present container_name: faster-whisper + profiles: + - whisper restart: unless-stopped healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health"] diff --git a/frontend/src/components/base/atoms/RadioGroup.tsx b/frontend/src/components/base/atoms/RadioGroup.tsx new file mode 100644 index 00000000..136b8a15 --- /dev/null +++ b/frontend/src/components/base/atoms/RadioGroup.tsx @@ -0,0 +1,46 @@ +import React from 'react' +import classNames from '@/utils/classNames' + +interface RadioGroupProps { + label: string + value: string + onChange: (value: any) => void + options: Array<{ + label: string + value: string + }> +} + +export const RadioGroup: React.FC = ({ + label, + value, + onChange, + options, +}) => { + return ( +
+ +
+ {options.map((option) => ( + + ))} +
+
+ ) +} diff --git a/frontend/src/components/base/atoms/Select.tsx b/frontend/src/components/base/atoms/Select.tsx new file mode 100644 index 00000000..4ffd93b0 --- /dev/null +++ b/frontend/src/components/base/atoms/Select.tsx @@ -0,0 +1,47 @@ +import React from 'react' +import classNames from '@/utils/classNames' + +interface SelectOption { + label: string + value: string +} + +interface SelectProps extends React.SelectHTMLAttributes { + options: SelectOption[] + placeholder?: string + error?: string +} + +const Select = React.forwardRef( + ({ options, placeholder, className, error, ...props }, ref) => { + return ( +
+ + {error &&

{error}

} +
+ ) + } +) + +Select.displayName = 'Select' + +export default Select diff --git a/frontend/src/router/DocsQA.tsx b/frontend/src/router/DocsQA.tsx index 6a9e448b..2351a3f3 100644 --- a/frontend/src/router/DocsQA.tsx +++ b/frontend/src/router/DocsQA.tsx @@ -9,6 +9,9 @@ import Applications from '@/screens/dashboard/docsqa/Applications' const DocsQA = lazy(() => import('@/screens/dashboard/docsqa/main')) const DocsQAChatbot = lazy(() => import('@/screens/dashboard/docsqa/Chatbot')) const DocsQASettings = lazy(() => import('@/screens/dashboard/docsqa/settings')) +const StructuredQA = lazy( + () => import('@/screens/dashboard/docsqa/StructuredQA') +) const FallBack = () => (
@@ -41,6 +44,10 @@ export const routes = (): BreadcrumbsRoute[] => [ path: '/collections', children: [{ index: true, element: }], }, + { + path: '/structured', + children: [{ index: true, element: }], + }, { path: '/data-sources', children: [{ index: true, element: }], diff --git a/frontend/src/screens/dashboard/docsqa/DataSources/FormType.ts b/frontend/src/screens/dashboard/docsqa/DataSources/FormType.ts index 355727b0..0d067120 100644 --- a/frontend/src/screens/dashboard/docsqa/DataSources/FormType.ts +++ b/frontend/src/screens/dashboard/docsqa/DataSources/FormType.ts @@ -1,15 +1,20 @@ -export type FormInputData = { +export interface FormInputData { dataSourceType: string - localdir: { + dataSourceUri?: string + localdir?: { name: string - files: { - id: string - file: File - }[] - uploadedFileIds: string[] + files: FileObject[] } - dataSourceUri: string - webConfig: { + webConfig?: { use_sitemap: boolean } + structured?: { + type: 'file' | 'database' + connectionString?: string + } +} + +export type FileObject = { + id: string + file: File } diff --git a/frontend/src/screens/dashboard/docsqa/DataSources/NewDataSource.tsx b/frontend/src/screens/dashboard/docsqa/DataSources/NewDataSource.tsx index 784fba97..d8d2d5fa 100644 --- a/frontend/src/screens/dashboard/docsqa/DataSources/NewDataSource.tsx +++ b/frontend/src/screens/dashboard/docsqa/DataSources/NewDataSource.tsx @@ -1,5 +1,10 @@ import React, { useEffect, useState } from 'react' -import { useForm, SubmitHandler, Controller, FormProvider } from "react-hook-form" +import { + useForm, + SubmitHandler, + Controller, + FormProvider, +} from 'react-hook-form' import { startCase } from 'lodash' import { uploadArtifactFileWithSignedURI } from '@/api/truefoundry' import Button from '@/components/base/atoms/Button' @@ -11,6 +16,7 @@ import { LOCAL_SOURCE_NAME, TFY_SOURCE_NAME, WEB_SOURCE_NAME, + STRUCTURED_SOURCE_NAME, } from '@/stores/constants' import { customerId, @@ -26,7 +32,7 @@ import { FormInputData } from './FormType' import FileUpload from './FileUpload' import { getFilePath } from '@/utils/artifacts' import { data } from 'autoprefixer' - +import { RadioGroup } from '@/components/base/atoms/RadioGroup' type FileObject = { id: string @@ -45,18 +51,23 @@ const NewDataSource: React.FC = ({ onClose }) => { const { data: dataLoaders } = useGetDataLoadersQuery() - const [uploadDataToDataDirectory] = useUploadDataToDataDirectoryMutation() const [uploadDataToLocalDirectory] = useUploadDataToLocalDirectoryMutation() const [addDataSource] = useAddDataSourceMutation() + const [structuredType, setStructuredType] = useState<'file' | 'database'>( + 'file' + ) const close = () => { setIsNewDataSourceDrawerOpen(false) onClose() } - const uploadDocs = async (uploadName: string, files: FormInputData['localdir']['files']) => { + const uploadDocs = async ( + uploadName: string, + files: FormInputData['localdir']['files'] + ) => { try { const entries: any = files.map((fileObj: FileObject) => [ getFilePath(fileObj.file), @@ -78,19 +89,23 @@ const NewDataSource: React.FC = ({ onClose }) => { const uploadedFileIds = await Promise.all( response.data.map(async ({ path, signed_url }: any) => { try { - await uploadArtifactFileWithSignedURI(signed_url, pathToFileMap[path].file); - return pathToFileMap[path].id; + await uploadArtifactFileWithSignedURI( + signed_url, + pathToFileMap[path].file + ) + return pathToFileMap[path].id } catch (error) { - console.error(`Failed to upload file: ${path}`, error); - return null; + console.error(`Failed to upload file: ${path}`, error) + return null } }) - ); + ) // Update the uploaded file ids state, filtering out null values - setLocalUploadedFileIds( - [...localUploadedFileIds, ...uploadedFileIds.filter(Boolean)] - ); + setLocalUploadedFileIds([ + ...localUploadedFileIds, + ...uploadedFileIds.filter(Boolean), + ]) } return dataDirectoryFqn } catch (err) { @@ -98,8 +113,7 @@ const NewDataSource: React.FC = ({ onClose }) => { } } - - const methods = useForm({ mode: 'onChange'}) + const methods = useForm({ mode: 'onChange' }) const selectedDataSourceType = methods.watch('dataSourceType') @@ -110,17 +124,24 @@ const NewDataSource: React.FC = ({ onClose }) => { 'error', 'Invalid Form!', Object.values(methods.formState?.errors || {}) - .map((e) => e.message).join(', ') - || 'Please fill all required fields' + .map((e) => e.message) + .join(', ') || 'Please fill all required fields' ) } - if (data.dataSourceType === 'localdir' && !data.localdir) { + + // Add validation for structured file upload + if ( + data.dataSourceType === STRUCTURED_SOURCE_NAME && + data.structured?.type === 'file' && + !methods.getValues('localdir')?.files?.length + ) { return notify( 'error', 'Files are required!', 'Please upload files to process' ) } + let fqn let res: { data_source: { fqn: string } } switch (data.dataSourceType) { @@ -131,13 +152,16 @@ const NewDataSource: React.FC = ({ onClose }) => { upload_name: data.localdir.name, }).unwrap() } else { - const ddFqn = await uploadDocs(data.localdir.name, data.localdir.files) + const ddFqn = await uploadDocs( + data.localdir.name, + data.localdir.files + ) res = await addDataSource({ type: TFY_SOURCE_NAME, - uri: ddFqn + uri: ddFqn, }).unwrap() } - break; + break case TFY_SOURCE_NAME: res = await addDataSource({ type: selectedDataSourceType, @@ -146,7 +170,7 @@ const NewDataSource: React.FC = ({ onClose }) => { customerId: customerId, }, }).unwrap() - break; + break case WEB_SOURCE_NAME: res = await addDataSource({ type: WEB_SOURCE_NAME, @@ -155,7 +179,40 @@ const NewDataSource: React.FC = ({ onClose }) => { use_sitemap: data.webConfig.use_sitemap, }, }).unwrap() - break; + break + case STRUCTURED_SOURCE_NAME: + if (data.structured?.type === 'file') { + const localdir = methods.getValues('localdir') + if (!localdir?.name || !localdir?.files?.length) { + throw new Error('Files and source name are required') + } + + if (IS_LOCAL_DEVELOPMENT) { + res = await uploadDataToLocalDirectory({ + files: localdir.files.map((f: FileObject) => f.file), + upload_name: localdir.name, + is_structured: true, + }).unwrap() + } else { + // Handle remote file upload + const ddFqn = await uploadDocs(localdir.name, localdir.files) + res = await addDataSource({ + type: STRUCTURED_SOURCE_NAME, + uri: ddFqn, + }).unwrap() + } + } else { + // Handle database connection + if (!data.structured?.connectionString) { + throw new Error('Database connection string is required') + } + res = await addDataSource({ + type: STRUCTURED_SOURCE_NAME, + uri: data.structured.connectionString, + }).unwrap() + } + break + default: throw new Error('Invalid data source type') } @@ -191,18 +248,83 @@ const NewDataSource: React.FC = ({ onClose }) => { dataSourceType: dataLoaders[0].type, }) setLocalUploadedFileIds([]) + if (dataLoaders[0].type === STRUCTURED_SOURCE_NAME) { + setStructuredType('file') + methods.setValue('structured.type', 'file') + methods.setValue('structured.connectionString', undefined) + methods.clearErrors('structured.connectionString') + } } } + const renderStructuredDataForm = () => { + return ( +
+ { + setStructuredType(value) + methods.setValue('structured.type', value) + // Reset form fields when switching between file and database + if (value === 'database') { + methods.setValue('localdir', undefined) + // Clear file upload validation + methods.clearErrors('localdir') + } else { + methods.setValue('structured.connectionString', undefined) + // Clear database validation + methods.clearErrors('structured.connectionString') + } + }} + options={[ + { label: 'File Upload', value: 'file' }, + { label: 'Database Connect', value: 'database' }, + ]} + /> + + {structuredType === 'file' && ( + + )} + + {structuredType === 'database' && ( +
+ +
+ )} +
+ ) + } + return ( <> - ))} -
- } - />} + onClick={() => { + // Reset form and clear all errors + methods.reset({ + dataSourceType: source.type, + }) + methods.clearErrors() + setLocalUploadedFileIds([]) + // Initialize structured type if selecting structured data source + if (source.type === STRUCTURED_SOURCE_NAME) { + setStructuredType('file') + methods.setValue('structured.type', 'file') + methods.setValue( + 'structured.connectionString', + undefined + ) + } else { + // Clear structured form data when selecting other types + methods.setValue('structured', undefined) + } + }} + type="button" + > +
+
+ {startCase(source.type)} +
+ {source.description && ( +
+ ({source.description}) +
+ )} +
+ + ))} + + )} + /> + )}
{selectedDataSourceType === WEB_SOURCE_NAME && ( )} {selectedDataSourceType === LOCAL_SOURCE_NAME && ( - + )} + {selectedDataSourceType === STRUCTURED_SOURCE_NAME && + renderStructuredDataForm()} {[TFY_SOURCE_NAME].includes(selectedDataSourceType) && (
@@ -320,8 +468,7 @@ const NewDataSource: React.FC = ({ onClose }) => { className="gap-1 btn-sm font-normal btn-neutral" type="submit" disabled={ - methods.formState.isSubmitting || - !methods.formState.isValid + methods.formState.isSubmitting || !methods.formState.isValid } />
diff --git a/frontend/src/screens/dashboard/docsqa/Navbar.tsx b/frontend/src/screens/dashboard/docsqa/Navbar.tsx index 4fbd0325..166ff1f6 100644 --- a/frontend/src/screens/dashboard/docsqa/Navbar.tsx +++ b/frontend/src/screens/dashboard/docsqa/Navbar.tsx @@ -8,6 +8,7 @@ import { faRocket, faGear, faPlay, + faTable, } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { Drawer } from '@mui/material' @@ -31,6 +32,11 @@ function getMenuOptions(): { route: '/', icon: faPlay, }, + { + label: 'StructuredQA', + route: '/structured', + icon: faTable, + }, { label: 'Collections', route: '/collections', @@ -66,7 +72,7 @@ export default function NavBar({ children }: any) { {({ isActive }) => ( <> { return notify( 'error', 'Collection Name is Required!', - 'Please provide a collection name', + 'Please provide a collection name' ) } const embeddingModel = allEmbeddingModels.find( - (model: any) => model.name == selectedEmbeddingModel, + (model: any) => model.name == selectedEmbeddingModel ) const params = { @@ -95,14 +95,14 @@ const NewCollection = ({ open, onClose, onSuccess }: NewCollectionProps) => { }) const allCollectionToJobNames = JSON.parse( - localStorage.getItem('collectionToJob') || '{}', + localStorage.getItem('collectionToJob') || '{}' ) localStorage.setItem( 'collectionToJob', JSON.stringify({ ...allCollectionToJobNames, [collectionName]: res, - }), + }) ) onClose(collectionName) @@ -111,7 +111,7 @@ const NewCollection = ({ open, onClose, onSuccess }: NewCollectionProps) => { notify( 'success', 'Collection is successfully added!', - 'Collection will be available to use after 3-5 minutes.', + 'Collection will be available to use after 3-5 minutes.' ) } catch (err: any) { notifyError('Failed to create the colllection', err) @@ -157,7 +157,7 @@ const NewCollection = ({ open, onClose, onSuccess }: NewCollectionProps) => { { Select a Data Source FQN - {dataSources?.map((source: any) => ( - - {source.fqn} - - ))} + {dataSources?.map( + (source: any) => + !source.fqn.startsWith('structured') && ( + + {source.fqn} + + ) + )} @@ -255,7 +258,7 @@ const NewCollection = ({ open, onClose, onSuccess }: NewCollectionProps) => {
{ dataSources?.filter( - (source) => source.fqn === selectedDataSource, + (source) => source.fqn === selectedDataSource )[0].type }
@@ -266,7 +269,7 @@ const NewCollection = ({ open, onClose, onSuccess }: NewCollectionProps) => {
{ dataSources?.filter( - (source) => source.fqn === selectedDataSource, + (source) => source.fqn === selectedDataSource )[0].uri }
diff --git a/frontend/src/screens/dashboard/docsqa/StructuredQA/StructuredQA.tsx b/frontend/src/screens/dashboard/docsqa/StructuredQA/StructuredQA.tsx new file mode 100644 index 00000000..875c48fa --- /dev/null +++ b/frontend/src/screens/dashboard/docsqa/StructuredQA/StructuredQA.tsx @@ -0,0 +1,32 @@ +import React, { useState } from 'react' + +import Spinner from '@/components/base/atoms/Spinner/Spinner' +import NoCollections from '../NoCollections' +import { useStructuredQAContext } from './context' +import ConfigSidebar from './components/ConfigSidebar' +import Chat from './components/Chat' + +const StructuredQA = () => { + const { selectedDataSource, isDataSourcesLoading } = useStructuredQAContext() + + return ( + <> +
+ {isDataSourcesLoading ? ( +
+ +
+ ) : selectedDataSource ? ( + <> + + + + ) : ( + + )} +
+ + ) +} + +export default StructuredQA diff --git a/frontend/src/screens/dashboard/docsqa/StructuredQA/components/Answer.tsx b/frontend/src/screens/dashboard/docsqa/StructuredQA/components/Answer.tsx new file mode 100644 index 00000000..619e022f --- /dev/null +++ b/frontend/src/screens/dashboard/docsqa/StructuredQA/components/Answer.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import IconProvider from '@/components/assets/IconProvider' +import Markdown from 'react-markdown' +import { useStructuredQAContext } from '../context' + +const Answer = (props: any) => { + const { answer, image_base64 } = useStructuredQAContext() + + return ( +
+
+
+ +
+
+
Answer:
+ {answer} + {image_base64 && ( +
+ answer +
+ )} +
+
+
+ ) +} + +export default Answer diff --git a/frontend/src/screens/dashboard/docsqa/StructuredQA/components/Chat.tsx b/frontend/src/screens/dashboard/docsqa/StructuredQA/components/Chat.tsx new file mode 100644 index 00000000..6c7fd8c8 --- /dev/null +++ b/frontend/src/screens/dashboard/docsqa/StructuredQA/components/Chat.tsx @@ -0,0 +1,37 @@ +import React, { useState } from 'react' + +import Spinner from '@/components/base/atoms/Spinner' +import { useStructuredQAContext } from '../context' +import PromptForm from './PromptForm' +import ErrorAnswer from './ErrorAnswer' +import Answer from './Answer' +import NoAnswer from './NoAnswer' + +const Right = () => { + const { errorMessage, answer } = useStructuredQAContext() + + const [isRunningPrompt, setIsRunningPrompt] = useState(false) + + return ( +
+ + {answer ? ( + + ) : isRunningPrompt ? ( +
+ +
Fetching Answer...
+
+ ) : errorMessage ? ( + + ) : ( + + )} +
+ ) +} + +export default Right diff --git a/frontend/src/screens/dashboard/docsqa/StructuredQA/components/ConfigSelector.tsx b/frontend/src/screens/dashboard/docsqa/StructuredQA/components/ConfigSelector.tsx new file mode 100644 index 00000000..40e5369c --- /dev/null +++ b/frontend/src/screens/dashboard/docsqa/StructuredQA/components/ConfigSelector.tsx @@ -0,0 +1,59 @@ +import React from 'react' + +import { FormControl, MenuItem, Select } from '@mui/material' + +interface ConfigProps { + title: string + placeholder: string + initialValue: string + data: any[] | undefined + handleOnChange: (e: any) => void + renderItem?: (e: any) => React.ReactNode + className?: string +} + +const ConfigSelector = (props: ConfigProps) => { + const { + title, + placeholder, + initialValue, + data, + className, + handleOnChange, + renderItem, + } = props + return ( +
+
{title}:
+ + + +
+ ) +} + +export default ConfigSelector diff --git a/frontend/src/screens/dashboard/docsqa/StructuredQA/components/ConfigSidebar.tsx b/frontend/src/screens/dashboard/docsqa/StructuredQA/components/ConfigSidebar.tsx new file mode 100644 index 00000000..837e3758 --- /dev/null +++ b/frontend/src/screens/dashboard/docsqa/StructuredQA/components/ConfigSidebar.tsx @@ -0,0 +1,121 @@ +import { MenuItem, Switch, TextareaAutosize } from '@mui/material' +import React from 'react' +import SimpleCodeEditor from '@/components/base/molecules/SimpleCodeEditor' +import ConfigSelector from './ConfigSelector' +import { useStructuredQAContext } from '../context' + +const Left = (props: any) => { + const { + selectedDataSource, + selectedQueryModel, + dataSources, + description, + allEnabledModels, + table, + tableConfig, + setSelectedDataSource, + setSelectedQueryModel, + setDescription, + setTable, + setTableConfig, + resetQA, + } = useStructuredQAContext() + + return ( +
+
+ { + resetQA() + setSelectedDataSource(e.target.value) + }} + renderItem={(item) => + item.type === 'structured' && ( + + {/* {item.fqn.split('/').slice(-1)[0]} */} + {item.fqn} + + ) + } + /> + + {selectedDataSource && + (selectedDataSource.includes('postgresql://') || + selectedDataSource.includes('mysql://') || + selectedDataSource.includes('sqlite://')) && ( + <> +
+
Table Name:
+ setTable(e.target.value)} + /> +
+ +
Where Clause (optional):
+
+

+ Note: Where clause is optional and filters the data to reduce + the size of the dataframe. It is a list of dict, with keys: +

+
    +
  • + column: Filter column name +
  • +
  • + operator: Relational condition like '=', + '!=', '<', '>', etc +
  • +
  • + value: Value to filter on +
  • +
+
+ + setTableConfig(updatedConfig ?? '') + } + /> + + )} + + { + resetQA() + setSelectedQueryModel(e.target.value) + }} + renderItem={(item) => ( + + {item.name} + + )} + /> +
+ +
Description (optional):
+ setDescription(e.target.value)} + /> +
+ ) +} + +export default Left diff --git a/frontend/src/screens/dashboard/docsqa/StructuredQA/components/ErrorAnswer.tsx b/frontend/src/screens/dashboard/docsqa/StructuredQA/components/ErrorAnswer.tsx new file mode 100644 index 00000000..7ab5da40 --- /dev/null +++ b/frontend/src/screens/dashboard/docsqa/StructuredQA/components/ErrorAnswer.tsx @@ -0,0 +1,19 @@ +import React from 'react' +import IconProvider from '@/components/assets/IconProvider' + +const ErrorAnswer = () => { + return ( +
+
+ +
+
+
Error
+ We failed to get answer for your query, please try again by resending + query or try again in some time. +
+
+ ) +} + +export default ErrorAnswer diff --git a/frontend/src/screens/dashboard/docsqa/StructuredQA/components/NoAnswer.tsx b/frontend/src/screens/dashboard/docsqa/StructuredQA/components/NoAnswer.tsx new file mode 100644 index 00000000..afa7f74e --- /dev/null +++ b/frontend/src/screens/dashboard/docsqa/StructuredQA/components/NoAnswer.tsx @@ -0,0 +1,25 @@ +import React from 'react' + +import DocsQaInformation from '../../DocsQaInformation' + +const NoAnswer = () => { + return ( +
+
+ +

+ Select a collection from sidebar, +
review all the settings and start asking Questions +

+ + } + /> +
+
+ ) +} + +export default NoAnswer diff --git a/frontend/src/screens/dashboard/docsqa/StructuredQA/components/PromptForm.tsx b/frontend/src/screens/dashboard/docsqa/StructuredQA/components/PromptForm.tsx new file mode 100644 index 00000000..6489487f --- /dev/null +++ b/frontend/src/screens/dashboard/docsqa/StructuredQA/components/PromptForm.tsx @@ -0,0 +1,124 @@ +import React, { useState } from 'react' +// import { SSE } from 'sse.js' + +import { baseQAFoundryPath, CollectionQueryDto } from '@/stores/qafoundry' +import Input from '@/components/base/atoms/Input' +import Button from '@/components/base/atoms/Button' +import { useStructuredQAContext } from '../context' +import { notifyError } from '@/utils/error' + +const Form = (props: any) => { + const { + setErrorMessage, + setImageBase64, + setAnswer, + setPrompt, + setTable, + prompt, + answer, + image_base64, + description, + table, + selectedQueryModel, + allEnabledModels, + selectedDataSource, + selectedQueryController, + tableConfig, + } = useStructuredQAContext() + + const { isRunningPrompt, setIsRunningPrompt } = props + + const handlePromptSubmit = async () => { + setIsRunningPrompt(true) + setAnswer('') + setErrorMessage(false) + try { + const selectedModel = allEnabledModels.find( + (model: any) => model.name == selectedQueryModel + ) + if (!selectedModel) { + throw new Error('Model not found') + } + + try { + JSON.parse(tableConfig) + } catch (err: any) { + throw new Error('Invalid Table Configuration') + } + + const params: CollectionQueryDto = Object.assign({ + data_source_fqn: selectedDataSource, + query: prompt, + model_configuration: { + name: selectedModel.name, + provider: selectedModel.provider, + }, + description: description, + }) + + if ( + selectedDataSource.includes('postgresql://') || + selectedDataSource.includes('mysql://') || + selectedDataSource.includes('sqlite://') + ) { + params.table = table + params.where = JSON.parse(tableConfig) + } + + try { + const response = await fetch( + `${baseQAFoundryPath}/retrievers/structured/answer`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + } + ) + + if (!response.ok) { + const error = await response.json() + throw new Error(error.detail[0].msg) + } + + const data = await response.json() + if (data?.type === 'answer') { + setAnswer(data.content) + setImageBase64(data.image_base64 ?? '') + setIsRunningPrompt(false) + } + } catch (err: any) { + setPrompt('') + setIsRunningPrompt(false) + notifyError('Failed to retrieve answer', { message: err.message }) + } + } catch (err: any) { + setPrompt('') + setIsRunningPrompt(false) + notifyError('Failed to retrieve answer', err) + } + } + + return ( +
+ e.preventDefault()}> + setPrompt(e.target.value)} + /> +
+ ) +} + +export default Form diff --git a/frontend/src/screens/dashboard/docsqa/StructuredQA/context.tsx b/frontend/src/screens/dashboard/docsqa/StructuredQA/context.tsx new file mode 100644 index 00000000..f2dd913c --- /dev/null +++ b/frontend/src/screens/dashboard/docsqa/StructuredQA/context.tsx @@ -0,0 +1,119 @@ +import React, { + createContext, + useState, + useEffect, + ReactNode, + useContext, +} from 'react' + +import { + useGetAllEnabledChatModelsQuery, + useGetDataSourcesQuery, +} from '@/stores/qafoundry' + +import { StructuredQAContextType, SelectedTableConfigType } from './types' + +interface StructuredQAProviderProps { + children: ReactNode +} + +const defaultTableConfig = `[ + { + "column": "", + "operator": "", + "value": "" + } +]` + +const StructuredQAContext = createContext( + undefined +) + +export const StructuredQAProvider: React.FC = ({ + children, +}) => { + const [selectedQueryModel, setSelectedQueryModel] = React.useState('') + const [selectedDataSource, setSelectedDataSource] = useState('') + const [table, setTable] = useState('') + const [tableConfig, setTableConfig] = useState(defaultTableConfig) + const [selectedTableConfig, setSelectedTableConfig] = useState< + SelectedTableConfigType[] | undefined + >([]) + const [selectedQueryController, setSelectedQueryController] = + useState('structured') + + const [errorMessage, setErrorMessage] = useState(false) + const [answer, setAnswer] = useState('') + const [image_base64, setImageBase64] = useState('') + const [prompt, setPrompt] = useState('') + const [description, setDescription] = useState('') + + const { data: dataSources, isLoading: isDataSourcesLoading } = + useGetDataSourcesQuery() + const { data: allEnabledModels } = useGetAllEnabledChatModelsQuery() + + const resetQA = () => { + setAnswer('') + setErrorMessage(false) + setPrompt('') + } + + useEffect(() => { + if (dataSources && dataSources.length) + setSelectedDataSource(dataSources[0].fqn) + }, [dataSources]) + + useEffect(() => { + if (allEnabledModels && allEnabledModels.length) { + setSelectedQueryModel(allEnabledModels[0].name) + } + }, [allEnabledModels]) + + useEffect(() => { + if (tableConfig) setSelectedTableConfig(JSON.parse(tableConfig)) + }, [tableConfig]) + + const value = { + selectedQueryModel, + selectedDataSource, + dataSources, + selectedQueryController, + prompt, + answer, + image_base64, + errorMessage, + description, + isDataSourcesLoading, + allEnabledModels, + table, + tableConfig, + selectedTableConfig, + setSelectedQueryModel, + setAnswer, + setImageBase64, + setPrompt, + setSelectedDataSource, + setTable, + setTableConfig, + setSelectedTableConfig, + setErrorMessage, + resetQA, + setDescription, + } + + return ( + + {children} + + ) +} + +export const useStructuredQAContext = () => { + const context = useContext(StructuredQAContext) + if (!context) { + throw new Error( + 'useStructuredQAContext must be used within a StructuredQAProvider' + ) + } + return context +} diff --git a/frontend/src/screens/dashboard/docsqa/StructuredQA/index.tsx b/frontend/src/screens/dashboard/docsqa/StructuredQA/index.tsx new file mode 100644 index 00000000..29bdc021 --- /dev/null +++ b/frontend/src/screens/dashboard/docsqa/StructuredQA/index.tsx @@ -0,0 +1,13 @@ +import React from 'react' +import { StructuredQAProvider } from './context' +import StructuredQA from './StructuredQA' + +const index = () => { + return ( + + + + ) +} + +export default index diff --git a/frontend/src/screens/dashboard/docsqa/StructuredQA/types.tsx b/frontend/src/screens/dashboard/docsqa/StructuredQA/types.tsx new file mode 100644 index 00000000..90b6a062 --- /dev/null +++ b/frontend/src/screens/dashboard/docsqa/StructuredQA/types.tsx @@ -0,0 +1,37 @@ +export interface SelectedTableConfigType { + column: string + operator: string + value: string +} + +export interface StructuredQAContextType { + selectedQueryModel: string + selectedDataSource: string + // FIXME: add type + dataSources: any + table: string + tableConfig: string + selectedTableConfig: SelectedTableConfigType[] | undefined + selectedQueryController: string + prompt: string + answer: string + image_base64: string + errorMessage: boolean + description: string + isDataSourcesLoading: boolean + allEnabledModels: any + + setSelectedQueryModel: React.Dispatch> + setSelectedDataSource: React.Dispatch> + setAnswer: React.Dispatch> + setPrompt: React.Dispatch> + setImageBase64: React.Dispatch> + setErrorMessage: React.Dispatch> + setDescription: React.Dispatch> + setTable: React.Dispatch> + setTableConfig: React.Dispatch> + setSelectedTableConfig: React.Dispatch< + React.SetStateAction + > + resetQA: () => void +} diff --git a/frontend/src/screens/dashboard/docsqa/main/context.tsx b/frontend/src/screens/dashboard/docsqa/main/context.tsx index eda5f47d..9b38118e 100644 --- a/frontend/src/screens/dashboard/docsqa/main/context.tsx +++ b/frontend/src/screens/dashboard/docsqa/main/context.tsx @@ -64,6 +64,7 @@ export const DocsQAProvider: React.FC = ({ children }) => { if (!openapiSpecs?.paths) return [] return Object.keys(openapiSpecs?.paths) .filter((path) => path.includes('/retrievers/')) + .filter((path) => !path.includes('/structured/')) .map((str) => { var parts = str.split('/') return parts[2] diff --git a/frontend/src/screens/dashboard/docsqa/settings/index.tsx b/frontend/src/screens/dashboard/docsqa/settings/index.tsx index c25b2cda..6a046bfc 100644 --- a/frontend/src/screens/dashboard/docsqa/settings/index.tsx +++ b/frontend/src/screens/dashboard/docsqa/settings/index.tsx @@ -34,7 +34,7 @@ const DocsQASettings = () => { isLoading: isCollectionDetailsLoading, isFetching: isCollectionDetailsFetching, } = useGetCollectionDetailsQuery(selectedCollection ?? '', { - skip: !selectedCollection, + skip: !selectedCollection || selectedCollection === '', }) const associatedDataSourcesRows = useMemo(() => { @@ -97,8 +97,8 @@ const DocsQASettings = () => { return ( <> -
-
Collections
+
+
Collections
{ + uploadDataToLocalDirectory: builder.mutation< + { data_source: { fqn: string } }, + { + files: File[] + upload_name: string + is_structured?: boolean + } + >({ + query: (payload: { + files: File[] + upload_name: string + is_structured?: boolean + }) => { var bodyFormData = new FormData() bodyFormData.append('upload_name', payload.upload_name) payload.files.forEach((file) => { bodyFormData.append('files', file) }) + if (payload.is_structured) { + bodyFormData.append('is_structured', 'true') + } return { url: '/v1/internal/upload-to-local-directory', body: bodyFormData, diff --git a/sample-data/structured/employers.csv b/sample-data/structured/employers.csv new file mode 100644 index 00000000..d2009bc6 --- /dev/null +++ b/sample-data/structured/employers.csv @@ -0,0 +1,51 @@ +id,name,department +1,John Doe,HR +2,Jane Smith,Sales +3,Alice Johnson,IT +4,Bob Brown,HR +5,Charlie Davis,HR +6,Emily Evans,HR +7,Frank Garcia,Sales +8,Grace Harris,HR +9,Henry Johnson,HR +10,Ivy King,HR +11,Jack Lee,HR +12,Kate Miller,Sales +13,Liam Nelson,IT +14,Mia Ortiz,IT +15,Noah Perez,IT +16,Olivia Robinson,IT +17,Paul Smith,Sales +18,Quinn Taylor,IT +19,Rachel Upton,IT +20,Sam Vance,IT +21,Tina White,HR +22,Uma Xu,IT +23,Vincent Young,IT +24,Wendy Zhang,Marketing +25,Xander Adams,IT +26,Yara Brooks,HR +27,Zachary Clark,Sales +28,Ava Davis,IT +29,Benjamin Edwards,IT +30,Chloe Foster,IT +31,Dylan Green,HR +32,Evelyn Hall,Sales +33,Freddie Ingram,IT +34,Georgia James,IT +35,Hugo Knight,Finance +36,Iris Lewis,HR +37,James Martin,Sales +38,Kylie Nguyen,IT +39,Luke O'Brien,IT +40,Maya Patel,Finance +41,Nina Quinn,HR +42,Oscar Reed,Sales +43,Piper Scott,IT +44,Quincy Taylor,IT +45,Ruby Underwood,Finance +46,Sophia Voss,HR +47,Tyler Walker,IT +48,Ursula Xu,IT +49,Violet Yates,IT +50,Wyatt Zane,Finance diff --git a/sample-data/structured/employers_salary.csv b/sample-data/structured/employers_salary.csv new file mode 100644 index 00000000..fc910325 --- /dev/null +++ b/sample-data/structured/employers_salary.csv @@ -0,0 +1,51 @@ +id,salary +1,50000 +2,60000 +3,70000 +4,55000 +5,65000 +6,62000 +7,58000 +8,72000 +9,54000 +10,68000 +11,50000 +12,61000 +13,73000 +14,57000 +15,69000 +16,64000 +17,60000 +18,71000 +19,53000 +20,67000 +21,52000 +22,59000 +23,75000 +24,56000 +25,68000 +26,62000 +27,60000 +28,74000 +29,55000 +30,69000 +31,58000 +32,64000 +33,72000 +34,57000 +35,68000 +36,50000 +37,61000 +38,73000 +39,54000 +40,68000 +41,52000 +42,59000 +43,75000 +44,56000 +45,68000 +46,62000 +47,60000 +48,74000 +49,55000 +50,69000 diff --git a/sample-data/structured/loan_payments.csv b/sample-data/structured/loan_payments.csv new file mode 100644 index 00000000..a94a2395 --- /dev/null +++ b/sample-data/structured/loan_payments.csv @@ -0,0 +1,501 @@ +Loan_ID,loan_status,Principal,terms,effective_date,due_date,paid_off_time,past_due_days,age,education,Gender +xqd20166231,PAIDOFF,1000,30,9/8/2016,10/7/2016,9/14/2016 19:31,,45,High School or Below,male +xqd20168902,PAIDOFF,1000,30,9/8/2016,10/7/2016,10/7/2016 9:00,,50,Bechalor,female +xqd20160003,PAIDOFF,1000,30,9/8/2016,10/7/2016,9/25/2016 16:58,,33,Bechalor,female +xqd20160004,PAIDOFF,1000,15,9/8/2016,9/22/2016,9/22/2016 20:00,,27,college,male +xqd20160005,PAIDOFF,1000,30,9/9/2016,10/8/2016,9/23/2016 21:36,,28,college,female +xqd20160706,PAIDOFF,300,7,9/9/2016,9/15/2016,9/9/2016 13:45,,35,Master or Above,male +xqd20160007,PAIDOFF,1000,30,9/9/2016,10/8/2016,10/7/2016 23:07,,29,college,male +xqd20160008,PAIDOFF,1000,30,9/9/2016,10/8/2016,10/5/2016 20:33,,36,college,male +xqd20160909,PAIDOFF,1000,30,9/9/2016,10/8/2016,10/8/2016 16:00,,28,college,male +xqd20160010,PAIDOFF,800,15,9/10/2016,9/24/2016,9/24/2016 13:00,,26,college,male +xqd20160011,PAIDOFF,300,7,9/10/2016,9/16/2016,9/11/2016 19:11,,29,college,male +xqd20160012,PAIDOFF,1000,15,9/10/2016,10/9/2016,10/9/2016 16:00,,39,High School or Below,male +xqd20160013,PAIDOFF,1000,30,9/10/2016,10/9/2016,10/7/2016 23:32,,26,college,male +xqd20160014,PAIDOFF,900,7,9/10/2016,9/16/2016,9/13/2016 21:57,,26,college,female +xqd20160015,PAIDOFF,1000,7,9/10/2016,9/16/2016,9/15/2016 14:27,,27,High School or Below,male +xqd20160016,PAIDOFF,800,15,9/10/2016,9/24/2016,9/24/2016 16:00,,26,college,male +xqd20160017,PAIDOFF,1000,30,9/10/2016,10/9/2016,9/27/2016 14:21,,40,High School or Below,male +xqd20160018,PAIDOFF,1000,15,9/10/2016,9/24/2016,9/23/2016 18:49,,32,High School or Below,male +xqd20160019,PAIDOFF,1000,30,9/10/2016,10/9/2016,10/5/2016 22:05,,32,High School or Below,male +xqd20160020,PAIDOFF,800,30,9/10/2016,10/9/2016,9/23/2016 7:42,,26,college,male +xqd20160021,PAIDOFF,1000,30,9/10/2016,10/9/2016,10/9/2016 9:00,,26,college,male +xqd20160022,PAIDOFF,1000,30,9/10/2016,10/9/2016,10/8/2016 17:09,,43,High School or Below,female +xqd20160023,PAIDOFF,1000,30,9/10/2016,10/9/2016,10/9/2016 23:00,,25,High School or Below,male +xqd20160024,PAIDOFF,1000,15,9/10/2016,9/24/2016,9/24/2016 13:00,,26,college,male +xqd20160025,PAIDOFF,1000,30,9/10/2016,10/9/2016,10/3/2016 12:50,,26,college,male +xqd20160026,PAIDOFF,1000,30,9/10/2016,10/9/2016,9/29/2016 12:18,,29,High School or Below,male +xqd20160027,PAIDOFF,800,15,9/10/2016,9/24/2016,9/21/2016 20:16,,39,Bechalor,male +xqd20170088,PAIDOFF,1000,15,9/10/2016,9/24/2016,9/23/2016 8:21,,34,Bechalor,male +xqd20160029,PAIDOFF,1000,30,9/11/2016,10/10/2016,9/22/2016 19:17,,31,college,male +xqd20160030,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/9/2016 17:33,,33,college,male +xqd88160031,PAIDOFF,800,15,9/11/2016,9/25/2016,9/24/2016 14:41,,33,High School or Below,male +xqd20160032,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/7/2016 21:48,,37,college,male +xqd20160033,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/9/2016 17:44,,27,college,male +xqd22169034,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/9/2016 7:24,,37,college,male +xqd20160035,PAIDOFF,800,15,9/11/2016,9/25/2016,9/25/2016 21:49,,33,college,male +xqd20160036,PAIDOFF,800,15,9/11/2016,9/25/2016,9/25/2016 9:00,,29,Bechalor,male +xqd20160037,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/10/2016 16:00,,27,High School or Below,male +xqd20160038,PAIDOFF,700,15,9/11/2016,9/25/2016,9/25/2016 13:00,,33,High School or Below,male +xqd20160039,PAIDOFF,1000,15,9/11/2016,9/25/2016,9/25/2016 9:00,,24,college,male +xqd20160040,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/10/2016 11:33,,21,Bechalor,male +xqd20160041,PAIDOFF,1000,15,9/11/2016,9/25/2016,9/25/2016 9:00,,32,college,female +xqd20160042,PAIDOFF,800,15,9/11/2016,9/25/2016,9/25/2016 14:36,,30,college,male +xqd20160043,PAIDOFF,1000,7,9/11/2016,9/24/2016,9/24/2016 9:00,,31,Bechalor,male +xqd20160044,PAIDOFF,1000,15,9/11/2016,9/25/2016,9/20/2016 15:00,,30,college,male +xqd20160045,PAIDOFF,1000,15,9/11/2016,9/25/2016,9/21/2016 22:29,,24,Bechalor,female +xqd20160046,PAIDOFF,800,7,9/11/2016,9/17/2016,9/12/2016 22:17,,35,High School or Below,male +xqd20160047,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/8/2016 14:14,,22,High School or Below,male +xqd20160048,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/9/2016 8:53,,32,college,male +xqd20160049,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/10/2016 9:00,,32,Bechalor,male +xqd20160050,PAIDOFF,800,15,9/11/2016,9/25/2016,9/25/2016 19:21,,50,High School or Below,male +xqd20160051,PAIDOFF,800,15,9/11/2016,9/25/2016,9/25/2016 13:00,,27,college,female +xqd20160052,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/10/2016 9:00,,35,Bechalor,female +xqd20160053,PAIDOFF,800,15,9/11/2016,9/25/2016,9/13/2016 4:34,,35,Bechalor,female +xqd20160054,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/10/2016 9:00,,34,High School or Below,male +xqd20160055,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/10/2016 16:00,,21,High School or Below,male +xqd20160056,PAIDOFF,1000,15,9/11/2016,9/25/2016,9/25/2016 16:00,,25,college,male +xqd20160057,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/10/2016 9:00,,27,High School or Below,male +xqd20160058,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/7/2016 2:33,,26,Bechalor,male +xqd20160059,PAIDOFF,800,15,9/11/2016,9/25/2016,9/24/2016 11:40,,44,High School or Below,female +xqd20160060,PAIDOFF,800,15,9/11/2016,9/25/2016,9/22/2016 6:38,,39,Master or Above,male +xqd20160061,PAIDOFF,1000,30,9/11/2016,10/10/2016,9/30/2016 21:12,,34,Bechalor,male +xqd20160062,PAIDOFF,1000,15,9/11/2016,9/25/2016,9/24/2016 13:42,,37,college,male +xqd20160063,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/8/2016 7:25,,34,High School or Below,male +xqd20160064,PAIDOFF,1000,30,9/11/2016,10/10/2016,9/12/2016 11:40,,45,college,male +xqd20160065,PAIDOFF,800,15,9/11/2016,9/25/2016,9/25/2016 14:38,,24,High School or Below,male +xqd20160066,PAIDOFF,900,15,9/11/2016,9/25/2016,9/25/2016 23:00,,28,college,male +xqd20160067,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/8/2016 12:04,,28,Bechalor,male +xqd20160068,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/10/2016 9:00,,37,High School or Below,male +xqd20160069,PAIDOFF,300,7,9/11/2016,9/17/2016,9/14/2016 22:05,,35,college,male +xqd20160070,PAIDOFF,1000,30,9/11/2016,10/10/2016,9/24/2016 13:27,,43,Bechalor,male +xqd20160071,PAIDOFF,800,15,9/11/2016,9/25/2016,9/22/2016 21:18,,29,college,male +xqd20160072,PAIDOFF,800,15,9/11/2016,9/25/2016,9/24/2016 22:53,,29,High School or Below,male +xqd20160073,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/10/2016 16:00,,33,Bechalor,female +xqd20160074,PAIDOFF,1000,15,9/11/2016,9/25/2016,9/25/2016 9:00,,34,college,male +xqd20160075,PAIDOFF,1000,15,9/11/2016,9/25/2016,9/25/2016 9:00,,25,college,male +xqd20160076,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/8/2016 13:12,,30,High School or Below,male +xqd20160077,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/9/2016 13:49,,31,Bechalor,male +xqd20160078,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/10/2016 9:00,,35,college,male +xqd20160079,PAIDOFF,1000,30,9/11/2016,10/10/2016,9/30/2016 14:29,,37,college,female +xqd20160080,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/10/2016 9:00,,44,High School or Below,female +xqd20160081,PAIDOFF,1000,30,9/11/2016,10/10/2016,9/21/2016 16:18,,28,High School or Below,male +xqd20160082,PAIDOFF,1000,7,9/11/2016,9/17/2016,9/13/2016 14:53,,25,college,male +xqd20160083,PAIDOFF,1000,15,9/11/2016,9/25/2016,9/25/2016 9:00,,29,college,male +xqd20160084,PAIDOFF,800,15,9/11/2016,9/25/2016,9/25/2016 13:00,,33,college,male +xqd20160085,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/10/2016 13:00,,37,High School or Below,female +xqd20160086,PAIDOFF,1000,30,9/11/2016,11/9/2016,11/9/2016 9:00,,33,college,male +xqd20160087,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/10/2016 9:00,,24,High School or Below,female +xqd20160088,PAIDOFF,1000,30,9/11/2016,10/10/2016,9/17/2016 13:01,,27,college,female +xqd20160089,PAIDOFF,800,15,9/11/2016,9/25/2016,9/21/2016 9:35,,43,Bechalor,male +xqd90160090,PAIDOFF,800,15,9/11/2016,9/25/2016,9/24/2016 20:33,,46,High School or Below,female +xqd91160291,PAIDOFF,800,15,9/11/2016,9/25/2016,9/25/2016 9:00,,34,college,female +xqd90160092,PAIDOFF,1000,7,9/11/2016,9/17/2016,9/17/2016 9:00,,32,Bechalor,female +xqd90163093,PAIDOFF,800,15,9/11/2016,9/25/2016,9/24/2016 0:12,,38,High School or Below,male +xqd20160094,PAIDOFF,800,15,9/11/2016,9/25/2016,9/21/2016 12:43,,27,High School or Below,male +xqd20167095,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/10/2016 13:00,,33,High School or Below,male +xqd20160096,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/8/2016 20:49,,36,college,male +xqd20160097,PAIDOFF,1000,15,9/11/2016,9/25/2016,9/20/2016 5:38,,26,High School or Below,male +xqd20160098,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/10/2016 9:01,,34,college,male +xqd20160099,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/10/2016 9:01,,22,High School or Below,male +xqd20160100,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/8/2016 16:55,,31,Bechalor,female +xqd20160101,PAIDOFF,1000,7,9/11/2016,9/17/2016,9/17/2016 9:00,,29,High School or Below,male +xqd20160102,PAIDOFF,800,15,9/11/2016,9/25/2016,9/25/2016 9:00,,38,college,male +xqd20160103,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/10/2016 16:00,,30,college,male +xqd20160104,PAIDOFF,1000,15,9/11/2016,9/25/2016,9/25/2016 23:48,,45,High School or Below,male +xqd20160105,PAIDOFF,1000,15,9/11/2016,9/25/2016,9/22/2016 13:15,,35,college,male +xqd20160106,PAIDOFF,1000,30,9/11/2016,10/10/2016,9/23/2016 13:31,,30,college,male +xqd20160107,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/8/2016 17:19,,31,High School or Below,male +xqd20160108,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/10/2016 9:01,,31,High School or Below,male +xqd20160109,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/9/2016 13:05,,28,college,male +xqd20160110,PAIDOFF,1000,7,9/11/2016,9/24/2016,9/24/2016 13:00,,29,college,male +xqd20160111,PAIDOFF,800,15,9/11/2016,9/25/2016,9/20/2016 20:47,,29,college,male +xqd20160112,PAIDOFF,1000,30,9/11/2016,11/9/2016,11/9/2016 9:00,,27,college,female +xqd20160113,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/10/2016 9:01,,27,college,male +xqd20160114,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/10/2016 13:01,,33,college,male +xqd20160115,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/8/2016 21:39,,28,college,male +xqd20160116,PAIDOFF,1000,15,9/11/2016,9/25/2016,9/25/2016 23:00,,25,High School or Below,male +xqd20160117,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/7/2016 14:23,,40,college,male +xqd20160118,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/6/2016 15:25,,23,High School or Below,male +xqd20160119,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/8/2016 6:56,,35,Bechalor,male +xqd20160120,PAIDOFF,800,15,9/11/2016,9/25/2016,9/16/2016 11:58,,24,college,male +xqd20160121,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/10/2016 16:01,,34,college,male +xqd20160122,PAIDOFF,1000,30,9/11/2016,10/10/2016,9/27/2016 7:02,,22,High School or Below,male +xqd20160123,PAIDOFF,1000,15,9/11/2016,10/25/2016,10/25/2016 9:00,,20,college,male +xqd20160124,PAIDOFF,1000,15,9/11/2016,9/25/2016,9/24/2016 11:02,,23,college,male +xqd20160125,PAIDOFF,1000,30,9/11/2016,10/10/2016,9/29/2016 18:57,,33,college,male +xqd20160126,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/10/2016 13:01,,26,college,male +xqd20160127,PAIDOFF,1000,15,9/11/2016,9/25/2016,9/25/2016 9:00,,28,High School or Below,male +xqd20160128,PAIDOFF,800,15,9/11/2016,9/25/2016,9/25/2016 12:24,,43,High School or Below,male +xqd78160129,PAIDOFF,1000,15,9/11/2016,9/25/2016,9/25/2016 13:00,,34,Bechalor,male +xqd20160130,PAIDOFF,1000,30,9/11/2016,10/10/2016,9/26/2016 4:41,,38,Bechalor,male +xqd20160131,PAIDOFF,1000,15,9/11/2016,9/25/2016,9/22/2016 15:44,,26,High School or Below,male +xqd20160132,PAIDOFF,800,15,9/11/2016,9/25/2016,9/25/2016 16:00,,43,High School or Below,male +xqd20160133,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/10/2016 16:13,,26,High School or Below,male +xqd20160134,PAIDOFF,1000,30,9/11/2016,10/10/2016,9/30/2016 7:12,,33,college,female +xqd20160135,PAIDOFF,800,15,9/11/2016,9/25/2016,9/23/2016 11:26,,24,college,male +xqd20160136,PAIDOFF,1000,30,9/11/2016,10/10/2016,9/12/2016 10:26,,30,High School or Below,male +xqd20160137,PAIDOFF,800,15,9/11/2016,9/25/2016,9/25/2016 13:00,,32,High School or Below,female +xqd20160138,PAIDOFF,1000,15,9/11/2016,10/25/2016,10/25/2016 9:00,,22,college,male +xqd20160139,PAIDOFF,1000,15,9/11/2016,9/25/2016,9/22/2016 21:45,,47,High School or Below,male +xqd56160140,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/9/2016 20:28,,20,High School or Below,male +xqd20160141,PAIDOFF,1000,30,9/11/2016,10/10/2016,10/1/2016 16:48,,28,High School or Below,male +xqd20160142,PAIDOFF,800,15,9/11/2016,9/25/2016,9/25/2016 9:01,,35,college,male +xqd20160143,PAIDOFF,1000,7,9/11/2016,9/17/2016,9/15/2016 20:36,,27,High School or Below,male +xqd20160144,PAIDOFF,800,15,9/11/2016,9/25/2016,9/21/2016 15:33,,33,college,female +xqd20160145,PAIDOFF,1000,30,9/11/2016,10/10/2016,9/29/2016 13:36,,30,High School or Below,male +xqd20160146,PAIDOFF,1000,15,9/11/2016,9/25/2016,9/22/2016 20:51,,31,college,male +xqd20160147,PAIDOFF,1000,30,9/11/2016,11/9/2016,11/9/2016 23:00,,26,college,female +xqd20160148,PAIDOFF,300,7,9/12/2016,9/18/2016,9/18/2016 9:00,,37,Master or Above,male +xqd20160149,PAIDOFF,1000,15,9/12/2016,9/26/2016,9/26/2016 9:00,,26,Bechalor,male +xqd20160150,PAIDOFF,800,15,9/12/2016,9/26/2016,9/24/2016 10:14,,35,Bechalor,male +xqd20160151,PAIDOFF,1000,15,9/12/2016,10/26/2016,10/26/2016 9:00,,29,college,male +xqd34160152,PAIDOFF,800,15,9/12/2016,9/26/2016,9/23/2016 20:30,,23,college,male +xqd20160153,PAIDOFF,500,15,9/12/2016,9/26/2016,9/13/2016 20:17,,23,college,female +xqd20160154,PAIDOFF,1000,15,9/12/2016,9/26/2016,9/26/2016 9:00,,30,college,male +xqd20160155,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/10/2016 7:01,,34,college,male +xqd20160156,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/11/2016 13:00,,36,High School or Below,female +xqd20160157,PAIDOFF,1000,15,9/12/2016,9/26/2016,9/26/2016 9:00,,26,Bechalor,male +xqd20160158,PAIDOFF,800,15,9/12/2016,9/26/2016,9/24/2016 14:55,,29,High School or Below,male +xqd12160159,PAIDOFF,1000,15,9/12/2016,9/26/2016,9/26/2016 9:00,,28,college,female +xqd20160160,PAIDOFF,1000,30,9/12/2016,10/11/2016,9/25/2016 20:56,,27,High School or Below,male +xqd20160161,PAIDOFF,1000,15,9/12/2016,9/26/2016,9/22/2016 10:49,,24,High School or Below,male +xqd20160162,PAIDOFF,800,15,9/12/2016,9/26/2016,9/25/2016 22:09,,31,Bechalor,male +xqd20160163,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/11/2016 9:00,,28,High School or Below,male +xqd28160164,PAIDOFF,1000,15,9/12/2016,9/26/2016,9/26/2016 9:00,,27,college,female +xqd20160165,PAIDOFF,1000,15,9/12/2016,9/26/2016,9/26/2016 19:33,,25,High School or Below,male +xqd20160166,PAIDOFF,1000,30,9/12/2016,11/10/2016,11/10/2016 16:00,,24,High School or Below,male +xqd20160167,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/11/2016 16:00,,28,college,male +xqd20160168,PAIDOFF,800,30,9/12/2016,10/11/2016,10/11/2016 16:00,,28,college,male +xqd20160169,PAIDOFF,1000,15,9/12/2016,9/26/2016,9/26/2016 13:00,,35,High School or Below,male +xqd27160170,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/11/2016 13:00,,38,college,male +xqd20160171,PAIDOFF,1000,15,9/12/2016,9/26/2016,9/26/2016 16:00,,38,High School or Below,male +xqd20160172,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/11/2016 16:00,,29,college,male +xqd20160173,PAIDOFF,800,15,9/12/2016,9/26/2016,9/26/2016 13:00,,35,High School or Below,male +xqd20160174,PAIDOFF,1000,30,9/12/2016,10/11/2016,9/17/2016 7:39,,24,college,male +xqd20160175,PAIDOFF,800,15,9/12/2016,9/26/2016,9/22/2016 10:30,,39,High School or Below,male +xqd20160176,PAIDOFF,800,15,9/12/2016,9/26/2016,9/26/2016 13:00,,25,college,male +xqd20160177,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/11/2016 16:00,,38,High School or Below,male +xqd20160178,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/10/2016 20:41,,30,college,male +xqd20160179,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/11/2016 9:00,,21,High School or Below,male +xqd20160180,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/10/2016 8:04,,46,college,male +xqd20160181,PAIDOFF,1000,15,9/12/2016,9/26/2016,9/24/2016 11:00,,31,High School or Below,female +xqd20160182,PAIDOFF,300,7,9/12/2016,9/18/2016,9/17/2016 9:25,,29,High School or Below,male +xqd20160183,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/7/2016 11:53,,35,High School or Below,male +xqd20160184,PAIDOFF,800,15,9/12/2016,9/26/2016,9/25/2016 8:39,,30,High School or Below,male +xqd20160185,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/11/2016 9:00,,27,High School or Below,male +xqd20160186,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/10/2016 20:28,,31,High School or Below,female +xqd20160187,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/1/2016 10:18,,33,Bechalor,male +xqd20160188,PAIDOFF,1000,15,9/12/2016,9/26/2016,9/26/2016 16:00,,34,High School or Below,male +xqd20160189,PAIDOFF,800,15,9/12/2016,9/26/2016,9/19/2016 21:07,,28,college,male +xqd20160190,PAIDOFF,800,15,9/12/2016,9/26/2016,9/26/2016 9:00,,42,college,male +xqd20160191,PAIDOFF,1000,30,9/12/2016,10/11/2016,9/30/2016 14:38,,32,college,male +xqd20160192,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/11/2016 13:00,,30,High School or Below,male +xqd20160193,PAIDOFF,1000,15,9/12/2016,9/26/2016,9/14/2016 20:31,,25,High School or Below,female +xqd20160194,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/11/2016 9:00,,27,High School or Below,female +xqd20160195,PAIDOFF,800,15,9/12/2016,9/26/2016,9/24/2016 16:15,,21,college,male +xqd20160196,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/10/2016 15:49,,24,college,male +xqd20160197,PAIDOFF,1000,30,9/12/2016,11/10/2016,11/10/2016 13:00,,29,college,male +xqd20160198,PAIDOFF,1000,15,9/12/2016,9/26/2016,9/23/2016 10:32,,40,college,male +xqd20160199,PAIDOFF,1000,30,9/12/2016,10/11/2016,9/30/2016 14:03,,29,High School or Below,male +xqd20160200,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/9/2016 14:17,,29,college,male +xqd20160201,PAIDOFF,1000,15,9/12/2016,9/26/2016,9/20/2016 8:26,,30,college,male +xqd20160202,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/11/2016 23:00,,26,High School or Below,female +xqd20160203,PAIDOFF,1000,15,9/12/2016,9/26/2016,9/24/2016 20:47,,36,High School or Below,male +xqd20160204,PAIDOFF,800,15,9/12/2016,9/26/2016,9/26/2016 16:00,,27,college,male +xqd20160205,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/11/2016 9:01,,20,college,male +xqd20160206,PAIDOFF,1000,7,9/12/2016,9/18/2016,9/16/2016 14:52,,26,Bechalor,female +xqd20160207,PAIDOFF,1000,30,9/12/2016,11/10/2016,11/10/2016 13:00,,26,college,male +xqd20160208,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/9/2016 10:00,,27,college,male +xqd20160209,PAIDOFF,300,7,9/12/2016,9/18/2016,9/12/2016 14:40,,23,High School or Below,male +xqd20160210,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/11/2016 16:00,,39,High School or Below,male +xqd20160211,PAIDOFF,1000,15,9/12/2016,9/26/2016,9/23/2016 21:58,,27,High School or Below,male +xqd20160212,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/8/2016 18:48,,30,High School or Below,male +xqd20160213,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/10/2016 16:41,,33,High School or Below,female +xqd20160214,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/11/2016 13:01,,27,High School or Below,male +xqd20160215,PAIDOFF,1000,30,9/12/2016,10/11/2016,9/16/2016 2:34,,35,High School or Below,male +xqd20160216,PAIDOFF,1000,30,9/12/2016,11/10/2016,11/10/2016 16:00,,29,college,female +xqd20160217,PAIDOFF,1000,15,9/12/2016,9/26/2016,9/21/2016 8:11,,50,High School or Below,male +xqd20160218,PAIDOFF,800,15,9/12/2016,9/26/2016,9/26/2016 9:00,,31,High School or Below,female +xqd20160219,PAIDOFF,1000,15,9/12/2016,9/26/2016,9/26/2016 13:00,,31,High School or Below,male +xqd20160220,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/11/2016 13:01,,29,High School or Below,male +xqd20160221,PAIDOFF,1000,15,9/12/2016,9/26/2016,9/26/2016 23:00,,35,college,male +xqd20160222,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/11/2016 9:01,,39,college,male +xqd20160223,PAIDOFF,1000,30,9/12/2016,11/10/2016,11/10/2016 13:00,,29,college,male +xqd20160224,PAIDOFF,1000,15,9/12/2016,9/26/2016,9/26/2016 23:00,,30,High School or Below,male +xqd20160225,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/9/2016 10:00,,33,Bechalor,male +xqd20160226,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/11/2016 13:01,,26,High School or Below,male +xqd20160227,PAIDOFF,1000,15,9/12/2016,9/26/2016,9/23/2016 14:01,,25,High School or Below,male +xqd20160228,PAIDOFF,800,15,9/12/2016,9/26/2016,9/25/2016 13:29,,37,Bechalor,male +xqd20160229,PAIDOFF,800,15,9/12/2016,9/26/2016,9/25/2016 14:50,,26,High School or Below,male +xqd20160230,PAIDOFF,800,15,9/12/2016,9/26/2016,9/26/2016 9:00,,26,college,male +xqd20160231,PAIDOFF,1000,15,9/12/2016,10/26/2016,10/26/2016 9:00,,27,college,male +xqd20160232,PAIDOFF,1000,7,9/12/2016,9/25/2016,9/25/2016 9:01,,34,college,female +xqd20160233,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/8/2016 15:35,,37,college,male +xqd20160234,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/11/2016 16:01,,36,High School or Below,male +xqd20160235,PAIDOFF,800,15,9/12/2016,9/26/2016,9/26/2016 19:35,,33,High School or Below,male +xqd20160236,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/9/2016 21:28,,30,High School or Below,male +xqd20160237,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/7/2016 16:45,,30,college,male +xqd20160238,PAIDOFF,800,15,9/12/2016,9/26/2016,9/24/2016 12:13,,36,High School or Below,male +xqd20160239,PAIDOFF,1000,15,9/12/2016,10/11/2016,10/11/2016 9:01,,29,college,male +xqd20160240,PAIDOFF,1000,15,9/12/2016,9/26/2016,9/14/2016 23:02,,36,High School or Below,male +xqd20160241,PAIDOFF,1000,30,9/12/2016,10/11/2016,10/8/2016 11:03,,32,High School or Below,male +xqd20160242,PAIDOFF,1000,15,9/12/2016,9/26/2016,9/26/2016 9:00,,29,High School or Below,female +xqd20160243,PAIDOFF,800,15,9/12/2016,9/26/2016,9/26/2016 23:00,,36,Bechalor,male +xqd20160244,PAIDOFF,800,15,9/12/2016,9/26/2016,9/25/2016 19:31,,30,High School or Below,female +xqd20160245,PAIDOFF,1000,7,9/13/2016,9/19/2016,9/14/2016 19:48,,31,college,male +xqd20160246,PAIDOFF,1000,30,9/13/2016,10/12/2016,10/12/2016 23:00,,19,High School or Below,female +xqd20160247,PAIDOFF,800,15,9/13/2016,9/27/2016,9/25/2016 12:48,,26,college,male +xqd20160248,PAIDOFF,800,15,9/13/2016,9/27/2016,9/26/2016 21:18,,34,High School or Below,male +xqd20160249,PAIDOFF,1000,30,9/13/2016,10/12/2016,10/7/2016 10:22,,35,High School or Below,male +xqd20160250,PAIDOFF,1000,15,9/13/2016,9/27/2016,9/26/2016 6:17,,35,Bechalor,female +xqd20160251,PAIDOFF,800,15,9/13/2016,9/27/2016,9/22/2016 16:57,,38,college,male +xqd20160252,PAIDOFF,1000,30,9/13/2016,10/12/2016,10/9/2016 21:57,,29,college,male +xqd20160253,PAIDOFF,1000,30,9/13/2016,10/12/2016,10/4/2016 12:59,,28,High School or Below,male +xqd20160254,PAIDOFF,500,7,9/13/2016,9/19/2016,9/17/2016 20:51,,22,High School or Below,male +xqd20160255,PAIDOFF,1000,30,9/13/2016,10/12/2016,10/12/2016 23:00,,32,college,male +xqd20160256,PAIDOFF,1000,30,9/13/2016,10/12/2016,10/8/2016 15:51,,31,college,male +xqd20160257,PAIDOFF,800,15,9/13/2016,9/27/2016,9/27/2016 9:00,,28,college,male +xqd20160258,PAIDOFF,1000,15,9/13/2016,9/27/2016,9/27/2016 9:00,,37,college,female +xqd20160259,PAIDOFF,1000,7,9/13/2016,9/19/2016,9/16/2016 15:57,,25,college,male +xqd20160260,PAIDOFF,1000,30,9/13/2016,10/12/2016,10/12/2016 9:00,,19,High School or Below,male +xqd20160261,PAIDOFF,800,15,9/13/2016,9/27/2016,9/26/2016 7:48,,51,college,male +xqd20160262,PAIDOFF,1000,15,9/13/2016,9/27/2016,9/21/2016 16:53,,29,High School or Below,male +xqd20160263,PAIDOFF,800,30,9/13/2016,10/12/2016,10/11/2016 0:29,,23,college,female +xqd20160264,PAIDOFF,1000,15,9/13/2016,9/27/2016,9/25/2016 10:37,,30,High School or Below,male +xqd20160265,PAIDOFF,800,15,9/13/2016,9/27/2016,9/27/2016 13:00,,23,college,male +xqd20160266,PAIDOFF,1000,15,9/13/2016,9/27/2016,9/26/2016 15:10,,34,Bechalor,female +xqd20160267,PAIDOFF,800,15,9/13/2016,9/27/2016,9/24/2016 12:46,,31,Bechalor,female +xqd20160268,PAIDOFF,1000,15,9/14/2016,9/28/2016,9/28/2016 9:00,,24,High School or Below,male +xqd20160269,PAIDOFF,1000,30,9/14/2016,10/13/2016,10/13/2016 9:00,,42,High School or Below,male +xqd20160270,PAIDOFF,800,30,9/14/2016,10/13/2016,10/6/2016 12:09,,40,college,female +xqd20160271,PAIDOFF,1000,30,9/14/2016,10/13/2016,10/14/2016 11:03,,29,High School or Below,male +xqd20160272,PAIDOFF,1000,30,9/14/2016,10/13/2016,10/8/2016 17:12,,32,college,female +xqd20160273,PAIDOFF,1000,30,9/14/2016,11/12/2016,11/12/2016 9:00,,28,college,male +xqd20160274,PAIDOFF,1000,30,9/14/2016,10/13/2016,10/13/2016 9:00,,35,High School or Below,male +xqd20160275,PAIDOFF,1000,30,9/14/2016,10/13/2016,10/13/2016 13:00,,30,Bechalor,male +xqd20160276,PAIDOFF,800,15,9/14/2016,9/28/2016,9/27/2016 15:52,,44,college,male +xqd20160277,PAIDOFF,800,15,9/14/2016,9/28/2016,9/28/2016 13:00,,37,High School or Below,male +xqd20160278,PAIDOFF,1000,30,9/14/2016,10/13/2016,10/13/2016 9:00,,31,college,male +xqd20160279,PAIDOFF,800,15,9/14/2016,9/28/2016,9/15/2016 0:43,,36,college,male +xqd20160280,PAIDOFF,800,30,9/14/2016,10/13/2016,10/10/2016 10:25,,31,college,male +xqd20160281,PAIDOFF,800,15,9/14/2016,9/28/2016,9/27/2016 20:41,,42,High School or Below,male +xqd20160282,PAIDOFF,1000,15,9/14/2016,9/28/2016,9/28/2016 9:00,,28,Bechalor,male +xqd20160283,PAIDOFF,1000,30,9/14/2016,10/13/2016,10/6/2016 6:51,,30,college,male +xqd20160284,PAIDOFF,1000,30,9/14/2016,10/13/2016,10/12/2016 6:25,,30,High School or Below,male +xqd20160285,PAIDOFF,1000,15,9/14/2016,9/28/2016,9/27/2016 22:50,,24,Bechalor,male +xqd20160286,PAIDOFF,1000,30,9/14/2016,11/12/2016,11/12/2016 9:00,,34,Bechalor,male +xqd20160287,PAIDOFF,1000,30,9/14/2016,10/13/2016,10/12/2016 12:30,,29,college,male +xqd20160288,PAIDOFF,1000,30,9/14/2016,10/13/2016,10/12/2016 3:49,,38,High School or Below,female +xqd20160289,PAIDOFF,1000,30,9/14/2016,10/13/2016,10/13/2016 13:00,,34,Bechalor,male +xqd20160290,PAIDOFF,800,15,9/14/2016,9/28/2016,9/27/2016 7:48,,28,High School or Below,male +xqd20160291,PAIDOFF,1000,15,9/14/2016,9/28/2016,9/22/2016 9:28,,30,college,female +xqd20160292,PAIDOFF,1000,30,9/14/2016,10/13/2016,10/11/2016 16:33,,41,High School or Below,male +xqd20160293,PAIDOFF,1000,30,9/14/2016,10/13/2016,9/18/2016 16:56,,29,college,male +xqd20160294,PAIDOFF,1000,30,9/14/2016,10/13/2016,10/13/2016 9:00,,37,High School or Below,male +xqd20160295,PAIDOFF,1000,30,9/14/2016,10/13/2016,10/13/2016 13:00,,36,Bechalor,male +xqd20160296,PAIDOFF,1000,30,9/14/2016,10/13/2016,10/13/2016 13:00,,30,college,female +xqd20160297,PAIDOFF,800,15,9/14/2016,9/28/2016,9/21/2016 4:42,,27,college,male +xqd20160298,PAIDOFF,1000,30,9/14/2016,10/13/2016,10/13/2016 9:00,,29,High School or Below,male +xqd20160299,PAIDOFF,1000,30,9/14/2016,10/13/2016,10/13/2016 9:00,,40,High School or Below,male +xqd20160300,PAIDOFF,1000,30,9/14/2016,10/13/2016,10/13/2016 11:00,,28,college,male +xqd20160301,COLLECTION,1000,15,9/9/2016,9/23/2016,,76,29,college,male +xqd20160302,COLLECTION,1000,30,9/9/2016,10/8/2016,,61,37,High School or Below,male +xqd20160303,COLLECTION,1000,30,9/9/2016,10/8/2016,,61,33,High School or Below,male +xqd20160304,COLLECTION,800,15,9/9/2016,9/23/2016,,76,27,college,male +xqd20160305,COLLECTION,800,15,9/9/2016,9/23/2016,,76,24,Bechalor,male +xqd20160306,COLLECTION,1000,15,9/10/2016,9/24/2016,,75,31,High School or Below,female +xqd20160307,COLLECTION,800,15,9/10/2016,10/9/2016,,60,28,college,male +xqd20160308,COLLECTION,1000,30,9/10/2016,10/9/2016,,60,40,High School or Below,male +xqd20160309,COLLECTION,1000,30,9/10/2016,10/9/2016,,60,33,college,male +xqd20160310,COLLECTION,800,15,9/10/2016,9/24/2016,,75,41,college,male +xqd20160311,COLLECTION,1000,30,9/10/2016,10/9/2016,,60,30,college,male +xqd20160312,COLLECTION,800,15,9/10/2016,9/24/2016,,75,26,High School or Below,female +xqd20160313,COLLECTION,1000,30,9/10/2016,10/9/2016,,60,27,High School or Below,male +xqd20160314,COLLECTION,1000,30,9/10/2016,10/9/2016,,60,20,High School or Below,male +xqd20160315,COLLECTION,1000,30,9/10/2016,10/9/2016,,60,24,college,male +xqd20160316,COLLECTION,1000,15,9/10/2016,10/9/2016,,60,26,High School or Below,male +xqd20160317,COLLECTION,1000,30,9/10/2016,10/9/2016,,60,30,High School or Below,male +xqd20160318,COLLECTION,1000,15,9/10/2016,9/24/2016,,75,29,High School or Below,male +xqd20160319,COLLECTION,1000,30,9/10/2016,10/9/2016,,60,22,Bechalor,male +xqd20160320,COLLECTION,1000,15,9/10/2016,9/24/2016,,75,24,Bechalor,male +xqd20160321,COLLECTION,1000,30,9/10/2016,10/9/2016,,60,25,college,male +xqd20160322,COLLECTION,1000,30,9/10/2016,10/9/2016,,60,28,High School or Below,male +xqd20160323,COLLECTION,1000,30,9/10/2016,10/9/2016,,60,37,college,male +xqd20160324,COLLECTION,800,15,9/10/2016,9/24/2016,,75,32,college,male +xqd20160325,COLLECTION,1000,15,9/10/2016,9/24/2016,,75,34,college,male +xqd20160326,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,28,Bechalor,male +xqd20160327,COLLECTION,800,15,9/11/2016,9/25/2016,,74,35,Bechalor,male +xqd20160328,COLLECTION,1000,30,9/11/2016,11/9/2016,,29,27,college,male +xqd20160329,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,24,High School or Below,female +xqd20160330,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,44,Bechalor,male +xqd20160331,COLLECTION,1000,15,9/11/2016,10/25/2016,,44,31,college,male +xqd20160332,COLLECTION,800,15,9/11/2016,9/25/2016,,74,27,college,male +xqd20160333,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,21,High School or Below,male +xqd20160334,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,30,High School or Below,female +xqd20160335,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,38,college,female +xqd20160336,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,34,High School or Below,male +xqd20160337,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,31,college,male +xqd20160338,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,23,High School or Below,male +xqd20160339,COLLECTION,1000,15,9/11/2016,9/25/2016,,74,27,college,female +xqd20160340,COLLECTION,1000,15,9/11/2016,9/25/2016,,74,39,High School or Below,male +xqd20160341,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,30,High School or Below,female +xqd20160342,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,25,college,male +xqd20160343,COLLECTION,1000,15,9/11/2016,9/25/2016,,74,50,Master or Above,male +xqd20160344,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,23,High School or Below,male +xqd20160345,COLLECTION,800,15,9/11/2016,9/25/2016,,74,38,Bechalor,male +xqd20160346,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,27,High School or Below,male +xqd20160347,COLLECTION,1000,30,9/11/2016,11/9/2016,,29,31,High School or Below,male +xqd20160348,COLLECTION,800,15,9/11/2016,9/25/2016,,74,40,college,male +xqd20160349,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,32,High School or Below,male +xqd20160350,COLLECTION,800,15,9/11/2016,9/25/2016,,74,29,college,male +xqd20160351,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,26,High School or Below,male +xqd20160352,COLLECTION,1000,15,9/11/2016,9/25/2016,,74,25,college,male +xqd20160353,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,35,High School or Below,male +xqd20160354,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,41,High School or Below,male +xqd20160355,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,37,High School or Below,male +xqd20160356,COLLECTION,1000,15,9/11/2016,10/10/2016,,59,34,college,male +xqd20160357,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,45,High School or Below,male +xqd20160358,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,26,Bechalor,male +xqd20160359,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,32,college,male +xqd20160360,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,28,High School or Below,male +xqd20160361,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,34,college,male +xqd20160362,COLLECTION,800,15,9/11/2016,9/25/2016,,74,29,High School or Below,male +xqd20160363,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,26,High School or Below,male +xqd20160364,COLLECTION,1000,15,9/11/2016,9/25/2016,,74,26,college,male +xqd20160365,COLLECTION,800,15,9/11/2016,9/25/2016,,74,22,college,male +xqd20160366,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,27,High School or Below,female +xqd20160367,COLLECTION,800,30,9/11/2016,10/10/2016,,59,33,High School or Below,male +xqd20160368,COLLECTION,800,15,9/11/2016,9/25/2016,,74,28,Bechalor,male +xqd20160369,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,24,college,male +xqd20160370,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,37,High School or Below,male +xqd20160371,COLLECTION,800,15,9/11/2016,9/25/2016,,74,36,High School or Below,male +xqd20160372,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,18,college,male +xqd20160373,COLLECTION,800,15,9/11/2016,9/25/2016,,74,25,High School or Below,male +xqd20160374,COLLECTION,1000,15,9/11/2016,9/25/2016,,74,40,High School or Below,male +xqd20182575,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,29,college,male +xqd20160376,COLLECTION,800,15,9/11/2016,9/25/2016,,74,26,High School or Below,female +xqd20151038,COLLECTION,1000,15,9/11/2016,9/25/2016,,74,30,college,male +xqd20160378,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,33,college,male +xqd20197340,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,30,college,male +xqd20160380,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,32,college,male +xqd20160381,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,25,High School or Below,male +xqd20160382,COLLECTION,800,15,9/11/2016,9/25/2016,,74,35,High School or Below,male +xqd20175721,COLLECTION,1000,15,9/11/2016,9/25/2016,,74,30,Bechalor,male +xqd20160384,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,26,High School or Below,male +xqd20160385,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,29,High School or Below,male +xqd20160386,COLLECTION,1000,30,9/11/2016,11/9/2016,,29,26,High School or Below,male +xqd20160387,COLLECTION,800,15,9/11/2016,9/25/2016,,74,46,High School or Below,male +xqd20160388,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,36,High School or Below,male +xqd20160389,COLLECTION,1000,15,9/11/2016,9/25/2016,,74,38,Bechalor,male +xqd20160390,COLLECTION,1000,15,9/11/2016,10/25/2016,,44,32,High School or Below,male +xqd20160391,COLLECTION,1000,15,9/11/2016,9/25/2016,,74,30,college,male +xqd20125284,COLLECTION,800,15,9/11/2016,9/25/2016,,74,35,High School or Below,male +xqd20160393,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,29,college,female +xqd20160394,COLLECTION,1000,30,9/11/2016,11/9/2016,,29,26,college,male +xqd20160395,COLLECTION,800,15,9/11/2016,9/25/2016,,74,32,High School or Below,male +xqd20160396,COLLECTION,1000,30,9/11/2016,10/10/2016,,59,25,High School or Below,male +xqd20160397,COLLECTION,1000,30,9/12/2016,10/11/2016,,58,33,High School or Below,male +xqd20160398,COLLECTION,800,15,9/12/2016,9/26/2016,,73,39,college,male +xqd20160399,COLLECTION,1000,30,9/12/2016,11/10/2016,,28,28,college,male +xqd20160400,COLLECTION,1000,30,9/12/2016,10/11/2016,,58,26,college,male +xqd20160401,COLLECTION_PAIDOFF,1000,30,9/9/2016,10/8/2016,10/10/2016 11:45,2,26,college,male +xqd20160402,COLLECTION_PAIDOFF,1000,15,9/9/2016,9/23/2016,9/27/2016 17:00,4,28,college,male +xqd20320403,COLLECTION_PAIDOFF,1000,30,9/9/2016,11/7/2016,11/20/2016 14:10,13,39,college,male +xqd20160404,COLLECTION_PAIDOFF,1000,15,9/9/2016,9/23/2016,9/28/2016 15:38,5,29,Bechalor,male +xqd20190405,COLLECTION_PAIDOFF,800,15,9/9/2016,9/23/2016,9/26/2016 17:22,3,33,High School or Below,male +xqd20160406,COLLECTION_PAIDOFF,1000,30,9/10/2016,10/9/2016,10/21/2016 14:00,12,27,college,male +xqd20160407,COLLECTION_PAIDOFF,800,15,9/10/2016,9/24/2016,9/26/2016 11:03,2,34,college,male +xqd20160408,COLLECTION_PAIDOFF,1000,30,9/10/2016,10/9/2016,11/5/2016 15:39,27,26,High School or Below,male +xqd20110409,COLLECTION_PAIDOFF,1000,30,9/10/2016,10/9/2016,11/22/2016 15:53,44,28,High School or Below,male +xqd20160410,COLLECTION_PAIDOFF,1000,15,9/10/2016,9/24/2016,9/29/2016 10:30,5,32,Bechalor,male +xqd20160411,COLLECTION_PAIDOFF,800,15,9/10/2016,10/9/2016,10/10/2016 15:18,1,27,college,female +xqd20160412,COLLECTION_PAIDOFF,1000,30,9/10/2016,10/9/2016,11/5/2016 10:49,27,21,college,male +xqd20160413,COLLECTION_PAIDOFF,800,15,9/11/2016,9/25/2016,9/27/2016 17:10,2,39,college,male +xqd20169083,COLLECTION_PAIDOFF,1000,15,9/11/2016,9/25/2016,9/26/2016 11:35,1,38,college,male +xqd20160415,COLLECTION_PAIDOFF,1000,30,9/11/2016,10/10/2016,10/12/2016 9:59,2,36,High School or Below,female +xqd20160416,COLLECTION_PAIDOFF,800,15,9/11/2016,9/25/2016,9/27/2016 17:14,2,33,college,male +xqd20160417,COLLECTION_PAIDOFF,1000,30,9/11/2016,10/10/2016,10/11/2016 12:45,1,21,college,female +xqd20160418,COLLECTION_PAIDOFF,800,15,9/11/2016,9/25/2016,9/28/2016 11:38,3,25,High School or Below,male +xqd20160419,COLLECTION_PAIDOFF,800,15,9/11/2016,9/25/2016,10/7/2016 13:21,12,29,college,male +xqd20160420,COLLECTION_PAIDOFF,1000,30,9/11/2016,10/10/2016,11/4/2016 15:37,25,33,High School or Below,male +xqd20160421,COLLECTION_PAIDOFF,1000,15,9/11/2016,9/25/2016,9/28/2016 17:39,3,47,High School or Below,female +xqd20160422,COLLECTION_PAIDOFF,1000,30,9/11/2016,10/10/2016,10/12/2016 9:52,2,33,college,male +xqd20160423,COLLECTION_PAIDOFF,800,15,9/11/2016,9/25/2016,9/29/2016 15:12,4,23,High School or Below,male +xqd20160424,COLLECTION_PAIDOFF,1000,15,9/11/2016,10/10/2016,10/12/2016 11:17,2,24,college,male +xqd20880425,COLLECTION_PAIDOFF,1000,30,9/11/2016,11/9/2016,11/10/2016 22:58,1,27,High School or Below,male +xqd20160426,COLLECTION_PAIDOFF,1000,30,9/11/2016,10/10/2016,11/3/2016 15:23,24,32,Bechalor,male +xqd20160427,COLLECTION_PAIDOFF,1000,30,9/11/2016,10/10/2016,10/11/2016 16:44,1,33,college,male +xqd20160428,COLLECTION_PAIDOFF,1000,30,9/11/2016,10/10/2016,10/11/2016 11:02,2,27,college,female +xqd20160429,COLLECTION_PAIDOFF,1000,30,9/11/2016,10/10/2016,10/12/2016 13:17,2,35,High School or Below,male +xqd20160430,COLLECTION_PAIDOFF,500,15,9/11/2016,10/10/2016,10/11/2016 17:22,1,37,Bechalor,male +xqd20160431,COLLECTION_PAIDOFF,800,15,9/11/2016,9/25/2016,9/28/2016 14:02,3,28,Bechalor,male +xqd20160432,COLLECTION_PAIDOFF,1000,15,9/11/2016,9/25/2016,9/29/2016 13:42,4,33,college,male +xqd20160433,COLLECTION_PAIDOFF,800,7,9/11/2016,9/17/2016,9/19/2016 15:00,2,34,Bechalor,female +xqd20160434,COLLECTION_PAIDOFF,1000,30,9/11/2016,10/10/2016,10/12/2016 14:32,2,29,college,male +xqd20160435,COLLECTION_PAIDOFF,1000,30,9/11/2016,10/10/2016,10/11/2016 11:33,1,34,Bechalor,male +xqd20160436,COLLECTION_PAIDOFF,1000,30,9/11/2016,10/10/2016,10/11/2016 16:27,1,29,Bechalor,male +xqd20790437,COLLECTION_PAIDOFF,1000,30,9/11/2016,10/10/2016,11/15/2016 15:27,36,24,High School or Below,male +xqd20160438,COLLECTION_PAIDOFF,1000,30,9/11/2016,10/10/2016,10/11/2016 16:13,1,34,High School or Below,male +xqd20160439,COLLECTION_PAIDOFF,1000,30,9/11/2016,10/10/2016,10/17/2016 10:06,7,25,college,female +xqd20160440,COLLECTION_PAIDOFF,1000,30,9/11/2016,11/9/2016,11/14/2016 13:15,5,24,college,male +xqd20160441,COLLECTION_PAIDOFF,1000,30,9/11/2016,10/10/2016,10/24/2016 16:20,14,30,college,male +xqd20160442,COLLECTION_PAIDOFF,1000,15,9/11/2016,9/25/2016,9/27/2016 16:35,2,28,college,male +xqd20160443,COLLECTION_PAIDOFF,1000,30,9/11/2016,10/10/2016,10/11/2016 11:48,1,24,High School or Below,male +xqd20160444,COLLECTION_PAIDOFF,1000,30,9/11/2016,10/10/2016,11/7/2016 19:21,28,26,college,female +xqd20160445,COLLECTION_PAIDOFF,1000,30,9/11/2016,10/10/2016,10/12/2016 16:22,2,24,High School or Below,male +xqd20160446,COLLECTION_PAIDOFF,1000,15,9/11/2016,9/25/2016,9/27/2016 17:24,2,29,college,male +xqd20420447,COLLECTION_PAIDOFF,1000,30,9/11/2016,10/10/2016,11/4/2016 11:07,25,31,college,male +xqd20160448,COLLECTION_PAIDOFF,1000,30,9/11/2016,10/10/2016,11/2/2016 9:39,23,26,college,male +xqd20160449,COLLECTION_PAIDOFF,1000,30,9/11/2016,10/10/2016,10/13/2016 18:18,3,25,High School or Below,male +xqd20160450,COLLECTION_PAIDOFF,1000,30,9/11/2016,10/10/2016,10/11/2016 11:29,1,29,college,male +xqd20160451,COLLECTION_PAIDOFF,1000,30,9/11/2016,10/10/2016,10/13/2016 16:27,3,38,college,male +xqd20160452,COLLECTION_PAIDOFF,800,15,9/11/2016,9/25/2016,9/29/2016 11:19,4,41,college,male +xqd20390453,COLLECTION_PAIDOFF,1000,15,9/11/2016,9/25/2016,9/28/2016 11:17,3,26,High School or Below,male +xqd20160454,COLLECTION_PAIDOFF,1000,30,9/12/2016,10/11/2016,10/14/2016 11:04,3,26,High School or Below,male +xqd20160455,COLLECTION_PAIDOFF,1000,30,9/12/2016,10/11/2016,10/17/2016 17:40,6,35,High School or Below,male +xqd20160456,COLLECTION_PAIDOFF,1000,15,9/12/2016,9/26/2016,9/28/2016 9:42,2,37,college,male +xqd20160457,COLLECTION_PAIDOFF,1000,30,9/12/2016,10/11/2016,11/18/2016 15:52,38,25,college,male +xqd20160458,COLLECTION_PAIDOFF,1000,30,9/12/2016,10/11/2016,10/30/2016 14:19,19,24,college,male +xqd20160459,COLLECTION_PAIDOFF,1000,30,9/12/2016,10/11/2016,10/13/2016 15:10,2,34,college,male +xqd20160460,COLLECTION_PAIDOFF,800,15,9/12/2016,9/26/2016,9/28/2016 13:36,2,33,college,male +xqd20490461,COLLECTION_PAIDOFF,800,15,9/12/2016,9/26/2016,9/28/2016 15:34,2,38,Bechalor,male +xqd20160462,COLLECTION_PAIDOFF,1000,30,9/12/2016,11/10/2016,11/17/2016 11:55,7,38,High School or Below,male +xqd20160463,COLLECTION_PAIDOFF,1000,30,9/12/2016,11/10/2016,11/15/2016 18:51,5,26,college,male +xqd20870464,COLLECTION_PAIDOFF,1000,15,9/12/2016,9/26/2016,9/30/2016 10:23,4,37,Bechalor,male +xqd20160465,COLLECTION_PAIDOFF,1000,30,9/12/2016,11/10/2016,11/11/2016 17:17,1,42,High School or Below,female +xqd20169466,COLLECTION_PAIDOFF,1000,30,9/12/2016,10/11/2016,10/12/2016 12:54,1,49,High School or Below,female +xqd20160467,COLLECTION_PAIDOFF,1000,30,9/12/2016,10/11/2016,10/15/2016 9:48,4,26,High School or Below,male +xqd20160468,COLLECTION_PAIDOFF,1000,15,9/12/2016,10/26/2016,10/27/2016 11:14,1,41,High School or Below,male +xqd20160469,COLLECTION_PAIDOFF,1000,30,9/12/2016,10/11/2016,10/15/2016 14:14,4,38,High School or Below,male +xqd25660470,COLLECTION_PAIDOFF,1000,30,9/12/2016,10/11/2016,12/2/2016 9:45,52,26,High School or Below,male +xqd20160471,COLLECTION_PAIDOFF,1000,15,9/12/2016,9/26/2016,9/28/2016 15:02,2,32,High School or Below,male +xqd20160472,COLLECTION_PAIDOFF,1000,30,9/12/2016,10/11/2016,11/4/2016 14:46,24,27,Bechalor,male +xqd20160473,COLLECTION_PAIDOFF,800,15,9/12/2016,9/26/2016,11/16/2016 12:12,51,33,college,male +xqd20160474,COLLECTION_PAIDOFF,1000,30,9/12/2016,10/11/2016,10/14/2016 19:02,3,30,High School or Below,male +xqd20160475,COLLECTION_PAIDOFF,800,15,9/12/2016,9/26/2016,9/28/2016 11:34,2,26,High School or Below,female +xqd20160476,COLLECTION_PAIDOFF,1000,30,9/12/2016,10/11/2016,11/9/2016 18:12,29,35,college,female +xqd20160477,COLLECTION_PAIDOFF,800,15,9/12/2016,10/26/2016,10/31/2016 13:07,5,46,college,female +xqd20160478,COLLECTION_PAIDOFF,1000,30,9/12/2016,10/11/2016,10/20/2016 17:38,9,27,college,male +xqd20160479,COLLECTION_PAIDOFF,1000,15,9/12/2016,10/11/2016,11/7/2016 8:55,27,22,High School or Below,male +xqd20160480,COLLECTION_PAIDOFF,1000,30,9/12/2016,10/11/2016,10/12/2016 18:26,1,27,Bechalor,male +xqd20160481,COLLECTION_PAIDOFF,1000,15,9/12/2016,9/26/2016,10/25/2016 13:44,29,30,Bechalor,male +xqd20160482,COLLECTION_PAIDOFF,1000,15,9/12/2016,9/26/2016,9/29/2016 15:07,3,27,High School or Below,male +xqd20160483,COLLECTION_PAIDOFF,800,15,9/12/2016,9/26/2016,9/27/2016 11:40,1,47,college,male +xqd20160484,COLLECTION_PAIDOFF,1000,30,9/12/2016,10/11/2016,10/18/2016 19:08,7,30,college,male +xqd20160485,COLLECTION_PAIDOFF,1000,30,9/12/2016,10/11/2016,10/15/2016 9:23,4,26,college,male +xqd20160486,COLLECTION_PAIDOFF,1000,30,9/12/2016,10/11/2016,10/14/2016 10:07,3,38,High School or Below,male +xqd20160487,COLLECTION_PAIDOFF,800,15,9/12/2016,9/26/2016,11/21/2016 11:36,56,46,High School or Below,male +xqd20160488,COLLECTION_PAIDOFF,1000,30,9/12/2016,10/11/2016,10/13/2016 12:02,2,35,Bechalor,male +xqd20160489,COLLECTION_PAIDOFF,1000,15,9/12/2016,9/26/2016,10/9/2016 19:30,13,45,college,male +xqd20160490,COLLECTION_PAIDOFF,1000,30,9/12/2016,10/11/2016,10/12/2016 18:04,1,36,college,male +xqd20160491,COLLECTION_PAIDOFF,1000,30,9/12/2016,10/11/2016,10/17/2016 10:53,6,38,High School or Below,male +xqd20160492,COLLECTION_PAIDOFF,1000,30,9/12/2016,10/11/2016,11/9/2016 13:41,29,27,college,male +xqd20160493,COLLECTION_PAIDOFF,1000,30,9/12/2016,10/11/2016,10/25/2016 17:44,14,27,Bechalor,male +xqd20160494,COLLECTION_PAIDOFF,1000,15,9/12/2016,9/26/2016,9/29/2016 12:45,3,29,college,male +xqd20160495,COLLECTION_PAIDOFF,1000,30,9/12/2016,10/11/2016,10/13/2016 14:45,2,30,High School or Below,male +xqd20160496,COLLECTION_PAIDOFF,1000,30,9/12/2016,10/11/2016,10/14/2016 19:08,3,28,High School or Below,male +xqd20160497,COLLECTION_PAIDOFF,1000,15,9/12/2016,9/26/2016,10/10/2016 20:02,14,26,High School or Below,male +xqd20160498,COLLECTION_PAIDOFF,800,15,9/12/2016,9/26/2016,9/29/2016 11:49,3,30,college,male +xqd20160499,COLLECTION_PAIDOFF,1000,30,9/12/2016,11/10/2016,11/11/2016 22:40,1,38,college,female +xqd20160500,COLLECTION_PAIDOFF,1000,30,9/12/2016,10/11/2016,10/19/2016 11:58,8,28,High School or Below,male