mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-08-16 21:24:09 +00:00
Merge 43de50cabe into 0c828e4e89
This commit is contained in:
commit
7ec1be10fa
51
.github/workflows/Development.yaml
vendored
Normal file
51
.github/workflows/Development.yaml
vendored
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
name: Bankfeed Deployment to Amazon EKS
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy
|
||||||
|
timeout-minutes: 30
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: Development
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v4
|
||||||
|
with:
|
||||||
|
role-to-assume: ${{ secrets.AWS_ROLE }}
|
||||||
|
role-session-name: GitHub_to_AWS_via_FederatedOIDC
|
||||||
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
|
|
||||||
|
|
||||||
|
- name: Login to Amazon ECR Private
|
||||||
|
id: login-ecr
|
||||||
|
uses: aws-actions/amazon-ecr-login@v2.0.1
|
||||||
|
|
||||||
|
- name: Build, tag, and push image to Amazon ECR
|
||||||
|
id: build-image
|
||||||
|
env:
|
||||||
|
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
|
||||||
|
run: |
|
||||||
|
docker build -t 413144972343.dkr.ecr.us-east-1.amazonaws.com/agents-k8-dev:zfunctions-dev$(echo $GITHUB_SHA | head -c7) -f ./task/Dockerfile ./task
|
||||||
|
docker push 413144972343.dkr.ecr.us-east-1.amazonaws.com/agents-k8-dev:zfunctions-dev$(echo $GITHUB_SHA | head -c7)
|
||||||
|
|
||||||
|
- name: Update deployment file
|
||||||
|
run: TAG=$(echo $GITHUB_SHA | head -c7) && sed -i 's|<IMAGE>|413144972343.dkr.ecr.us-east-1.amazonaws.com/agents-k8-dev:zfunctions-dev'${TAG}'|' $GITHUB_WORKSPACE/task/zfunctions.yaml
|
||||||
|
|
||||||
|
- name: Login AWS EKS on EKS Cluster
|
||||||
|
run: aws eks update-kubeconfig --name ${{ secrets.CLUSTER_NAME }} --region ${{ secrets.AWS_REGION }}
|
||||||
|
|
||||||
|
- name: Deploy to AWS EKS
|
||||||
|
run: kubectl apply -f $GITHUB_WORKSPACE/task/zfunctions.yaml
|
||||||
49
.github/workflows/Production.yaml
vendored
Normal file
49
.github/workflows/Production.yaml
vendored
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
name: Bankfeed Deployment to Amazon EKS
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy
|
||||||
|
timeout-minutes: 30
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: Development
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v4
|
||||||
|
with:
|
||||||
|
role-to-assume: ${{ secrets.AWS_ROLE }}
|
||||||
|
role-session-name: GitHub_to_AWS_via_FederatedOIDC
|
||||||
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
|
|
||||||
|
- name: Login to Amazon ECR Private
|
||||||
|
id: login-ecr
|
||||||
|
uses: aws-actions/amazon-ecr-login@v2.0.1
|
||||||
|
|
||||||
|
- name: Build, tag, and push image to Amazon ECR
|
||||||
|
id: build-image
|
||||||
|
env:
|
||||||
|
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
|
||||||
|
run: |
|
||||||
|
docker build -t 413144972343.dkr.ecr.us-east-1.amazonaws.com/agents-k8-dev:zfunctions-prod$(echo $GITHUB_SHA | head -c7) -f ./task/Dockerfile ./task
|
||||||
|
docker push 413144972343.dkr.ecr.us-east-1.amazonaws.com/agents-k8-dev:zfunctions-prod$(echo $GITHUB_SHA | head -c7)
|
||||||
|
|
||||||
|
- name: Update deployment file
|
||||||
|
run: TAG=$(echo $GITHUB_SHA | head -c7) && sed -i 's|<IMAGE>|413144972343.dkr.ecr.us-east-1.amazonaws.com/agents-k8-dev:zfunctions-prod'${TAG}'|' $GITHUB_WORKSPACE/task/zfunctions-prod.yaml
|
||||||
|
|
||||||
|
- name: Login AWS EKS on EKS Cluster
|
||||||
|
run: aws eks update-kubeconfig --name ${{ secrets.CLUSTER_NAME }} --region ${{ secrets.AWS_REGION }}
|
||||||
|
|
||||||
|
- name: Deploy to AWS EKS
|
||||||
|
run: kubectl apply -f $GITHUB_WORKSPACE/task/zfunctions-prod.yaml
|
||||||
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
.env
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Directories
|
||||||
|
venv/
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
45
README.zfunctions.md
Normal file
45
README.zfunctions.md
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# zFunctions
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Use the following instructions to deploy the sample application.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
- [Python 3.12](https://www.python.org/downloads/).
|
||||||
|
Install python 3.12
|
||||||
|
|
||||||
|
- [The AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html)
|
||||||
|
Install the latest version of AWS CLI.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Git clone this repo.
|
||||||
|
```
|
||||||
|
cd zfunctions
|
||||||
|
```
|
||||||
|
|
||||||
|
Install python virtual environment.
|
||||||
|
```
|
||||||
|
pip install virtualenv
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run locally
|
||||||
|
|
||||||
|
Configure env file.
|
||||||
|
|
||||||
|
```
|
||||||
|
cp env .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Add credentials
|
||||||
|
|
||||||
|
```
|
||||||
|
cd function/fa_onboarding
|
||||||
|
python3 lambda_function.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
To deploy the application, run ...
|
||||||
1
manifest.json
Normal file
1
manifest.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
[{"Config":"290e9bd230e751121c05a6b690d98af3fd8cb3071ea42d1050b48a8f0e1b5b95.json","RepoTags":["413144972343.dkr.ecr.us-east-1.amazonaws.com/serverless-fpa-onboarding-dev:fpa-onboardings"],"Layers":["6f1c4d8131b507830019304cf3a00d3c62b091f24c5531d4938c9c90608f8a74/layer.tar","b32028a76a8b26d0d8283ac689268d660d75e71e4d626335cf52099cb362a95d/layer.tar","dbb582ddbebc7074681248e9609c9cfc7dab25d9ec7e8b20c9fc4f71dc2c7183/layer.tar","a32b78699c6eef32c6edaba0ba98ce7c4e46a34e9809e7e2fd02d0779a85fa18/layer.tar","9d21d8586d15f6075ce8af99f8c372bfa600737fb64a3f69df903f02ab876c5e/layer.tar","a27c98cc01a4ead2285115b35470f69da4e4ddf8c3ec34245aa0ef5730ab2871/layer.tar","a0e87182507e5aa8aea1f21d8b3a8abd28f7a1dcef8567327231a42f3c554c8c/layer.tar","04aaf9cfacf692f07def4e0143dc127d3fcf938d604403239c8b9cede93dcf5b/layer.tar","7c9647faac2e8c0dcb5fd729267684ad1d8abe0408caf5e70f49da6011e9c480/layer.tar","04d41fa8c2ef1644a345916b9b1b9c7b3bcf5290f858daa736b0bb3f78ba7e3c/layer.tar","613b72d1e87d3d4611a402c05bb9d1aee94d9810aaa03bdd97d32e4a455c5c82/layer.tar","b2c1d6b777b8b8ef85e9e018ac16e4fe7f8682ff25cfa82ada0a1400d066741e/layer.tar","d7dd4783c680b76ddba436c196ce95f04d984c36658eab4f7a5db3df999f8a3a/layer.tar","ca0cba66bb432dc2cace13dc815bb0f315f917dac8f4c552ed3a1cb1e63b75c2/layer.tar","130a8593783267760265e57ef3a1ea5fa97c689db798578fd92ef3c9c3db6c96/layer.tar","21d71270202e2e82afd5f30586c1811695408b7449708900de9e70a3a4aebd0a/layer.tar","beb36db6ae8ff0dc077729cd974d06de4ba29f5da6924c85c82a89bf9e4eb7f1/layer.tar","e01e882e420c4e2fdf2012dcfa0614d620c2df15ef3c6273c8ff50e9f0d9e0ed/layer.tar","084ab12df9e40bd0d7e466b0c1c2972da82de75e0f9079a18fdca589de4f18a6/layer.tar"]}]
|
||||||
97
serverless.yml
Normal file
97
serverless.yml
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
service: fpa-onboarding
|
||||||
|
|
||||||
|
frameworkVersion: '3'
|
||||||
|
|
||||||
|
provider:
|
||||||
|
name: aws
|
||||||
|
runtime: python3.11
|
||||||
|
ecr:
|
||||||
|
images:
|
||||||
|
fpa-onboardings:
|
||||||
|
path: ./function/task
|
||||||
|
platform: linux/amd64
|
||||||
|
|
||||||
|
functions:
|
||||||
|
handler:
|
||||||
|
image:
|
||||||
|
name: fpa-onboardings
|
||||||
|
role: FPAOnboardingServerLessRole
|
||||||
|
timeout: 300
|
||||||
|
|
||||||
|
resources:
|
||||||
|
Resources:
|
||||||
|
FPAOnboardingServerLessRole:
|
||||||
|
Type: AWS::IAM::Role
|
||||||
|
Properties:
|
||||||
|
RoleName: fpa-onboarding-serverless-role
|
||||||
|
AssumeRolePolicyDocument:
|
||||||
|
Version: '2012-10-17'
|
||||||
|
Statement:
|
||||||
|
- Effect: Allow
|
||||||
|
Principal:
|
||||||
|
Service:
|
||||||
|
- lambda.amazonaws.com
|
||||||
|
Action:
|
||||||
|
- sts:AssumeRole
|
||||||
|
Policies:
|
||||||
|
- PolicyName: FPA-Onboarding-CloudWatchLogsAccess
|
||||||
|
PolicyDocument:
|
||||||
|
Version: '2012-10-17'
|
||||||
|
Statement:
|
||||||
|
- Effect: Allow
|
||||||
|
Action:
|
||||||
|
- logs:CreateLogGroup
|
||||||
|
- logs:CreateLogStream
|
||||||
|
- logs:PutLogEvents
|
||||||
|
Resource: "arn:aws:logs:*:*:*"
|
||||||
|
- PolicyName: FPA-Onboarding-S3Access
|
||||||
|
PolicyDocument:
|
||||||
|
Version: '2012-10-17'
|
||||||
|
Statement:
|
||||||
|
- Effect: Allow
|
||||||
|
Action:
|
||||||
|
- s3:*
|
||||||
|
Resource: "*"
|
||||||
|
- PolicyName: FPA-Onboarding-events
|
||||||
|
PolicyDocument:
|
||||||
|
Version: '2012-10-17'
|
||||||
|
Statement:
|
||||||
|
- Effect: Allow
|
||||||
|
Action:
|
||||||
|
- events:*
|
||||||
|
Resource: "*"
|
||||||
|
- PolicyName: FPA-Onboarding-IAMPolicies
|
||||||
|
PolicyDocument:
|
||||||
|
Version: '2012-10-17'
|
||||||
|
Statement:
|
||||||
|
- Effect: Allow
|
||||||
|
Action:
|
||||||
|
- iam:TagRole
|
||||||
|
Resource: "*"
|
||||||
|
- PolicyName: FPA-Onboarding-SMAccess
|
||||||
|
PolicyDocument:
|
||||||
|
Version: '2012-10-17'
|
||||||
|
Statement:
|
||||||
|
- Effect: Allow
|
||||||
|
Action:
|
||||||
|
- ssm:GetParameter
|
||||||
|
- ssm:GetParameters
|
||||||
|
- ssm:GetParametersByPath
|
||||||
|
Resource: "*"
|
||||||
|
- PolicyName: FPA-Onboarding-SQSFullAccess
|
||||||
|
PolicyDocument:
|
||||||
|
Version: '2012-10-17'
|
||||||
|
Statement:
|
||||||
|
- Effect: Allow
|
||||||
|
Action:
|
||||||
|
- sqs:*
|
||||||
|
Resource: "*"
|
||||||
|
- PolicyName: FPA-Onboarding-SecretManagerAccess
|
||||||
|
PolicyDocument:
|
||||||
|
Version: '2012-10-17'
|
||||||
|
Statement:
|
||||||
|
- Effect: Allow
|
||||||
|
Action:
|
||||||
|
- secretsmanager:GetSecretValue
|
||||||
|
- secretsmanager:PutSecretValue
|
||||||
|
Resource: "*"
|
||||||
33
task/Dockerfile
Normal file
33
task/Dockerfile
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
FROM python:3.13.2-slim-bullseye
|
||||||
|
|
||||||
|
# Install minimal system dependencies and build tools for Python packages
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
|
gcc \
|
||||||
|
g++ \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy only requirements first to leverage Docker cache
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Remove build tools to reduce image size
|
||||||
|
RUN apt-get purge -y --auto-remove \
|
||||||
|
build-essential \
|
||||||
|
gcc \
|
||||||
|
g++
|
||||||
|
|
||||||
|
# Copy rest of the app code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Create a non-root user for security reasons
|
||||||
|
RUN useradd -m appuser
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
# Run the Python application
|
||||||
|
CMD ["python", "sqs_processor.py"]
|
||||||
477
task/backendAPIs.py
Normal file
477
task/backendAPIs.py
Normal file
@ -0,0 +1,477 @@
|
|||||||
|
import requests
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def get_token():
|
||||||
|
user_id = os.getenv('USER_ID')
|
||||||
|
password = os.getenv('PASSWORD')
|
||||||
|
api_endpoint = os.getenv('API_ENDPOINT')
|
||||||
|
# print(f"API end point: {api_endpoint}")
|
||||||
|
|
||||||
|
url = f'{api_endpoint}/api/v1/token'
|
||||||
|
credentials = {
|
||||||
|
"email": user_id,
|
||||||
|
"password": password
|
||||||
|
}
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(url, json=credentials, headers=headers)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
if 'token' in data:
|
||||||
|
token = data['token']
|
||||||
|
os.environ["AUTH_TOKEN"] = token
|
||||||
|
return token
|
||||||
|
else:
|
||||||
|
return "Token not found in response."
|
||||||
|
else:
|
||||||
|
return f"Failed to get token. Status code: {response.status_code}, Error: {response.text}"
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return f"An error occurred: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_role_id(entity_id):
|
||||||
|
api_endpoint = os.getenv('API_ENDPOINT')
|
||||||
|
auth_token = os.getenv('AUTH_TOKEN')
|
||||||
|
|
||||||
|
url = f'{api_endpoint}/api/v1/role_details?entityId={entity_id}'
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': auth_token,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f"*** Calling get role id... for entity {entity_id}")
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
response_data = response.json()
|
||||||
|
if response.status_code == 200 or response.status_code == 201:
|
||||||
|
role_id = response_data["data"]["_id"]
|
||||||
|
print(f"Roleid set to {role_id} in the environment.")
|
||||||
|
return {"data": role_id, "status_code": 200}
|
||||||
|
else:
|
||||||
|
print(f"Failed to get data. Status code: {response.status_code}")
|
||||||
|
print(f"Error: {response.text}")
|
||||||
|
return {"error": f"Failed to get data. Status code: {response.status_code}",
|
||||||
|
"status_code": response.status_code, "details": response.text}
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return {"error": f"An error occurred: {e}", "status_code": 500}
|
||||||
|
|
||||||
|
|
||||||
|
def get_onboarding_status(onboarding_id):
|
||||||
|
api_endpoint = os.getenv('API_ENDPOINT')
|
||||||
|
auth_token = os.getenv('AUTH_TOKEN')
|
||||||
|
# role_id = os.getenv('ROLE_ID')
|
||||||
|
|
||||||
|
# url = f'{api_endpoint}/api/v1/entity/onboarding_status?id={onboarding_id}'
|
||||||
|
# headers = {
|
||||||
|
# 'Content-Type': 'application/json',
|
||||||
|
# 'Authorization': auth_token,
|
||||||
|
# # 'X-Entity-Role': role_id
|
||||||
|
# }
|
||||||
|
url = f'{api_endpoint}/api/v1/entity/onboarding_status?id={onboarding_id}'
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': auth_token,
|
||||||
|
# 'X-Entity-Role': role_id
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
# print(response.json())
|
||||||
|
return {"data": response.json(), "status_code": 200}
|
||||||
|
else:
|
||||||
|
print(f"Failed to get data. Status code: {response.status_code}")
|
||||||
|
print(f"Error: {response.text}")
|
||||||
|
return {"error": f"Failed to get data. Status code: {response.status_code}",
|
||||||
|
"status_code": response.status_code, "details": response.text}
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return {"error": f"An error occurred: {e}", "status_code": 500}
|
||||||
|
|
||||||
|
|
||||||
|
def update_onboarding_status(step_number, status, error_message, summary):
|
||||||
|
api_endpoint = os.getenv('API_ENDPOINT')
|
||||||
|
auth_token = os.getenv('AUTH_TOKEN')
|
||||||
|
role_id = os.getenv('ROLE_ID')
|
||||||
|
onboarding_id = os.getenv('ONBOARDING_ID')
|
||||||
|
print(f"Using onboardinid: {onboarding_id}, roleid:{role_id} to update onBoardingStatus.")
|
||||||
|
|
||||||
|
url = f"{api_endpoint}/api/v1/entity/onboarding_status?id={onboarding_id}"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': auth_token,
|
||||||
|
'X-Entity-Role': role_id
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# -1 to initialize the steps array
|
||||||
|
if step_number == -1:
|
||||||
|
data = {
|
||||||
|
"id": onboarding_id,
|
||||||
|
"steps": [
|
||||||
|
{"number": 1, "stepName": "Validate uploaded files", "status": "NOT-STARTED", "errorMessage": None,
|
||||||
|
"summary": None, "completedAt": None},
|
||||||
|
{"number": 2, "stepName": "Load fund details", "status": "NOT-STARTED", "errorMessage": None,
|
||||||
|
"summary": None, "completedAt": None},
|
||||||
|
{"number": 3, "stepName": "Load partners", "status": "NOT-STARTED", "errorMessage": None,
|
||||||
|
"summary": None, "completedAt": None},
|
||||||
|
{"number": 4, "stepName": "Load portfolio (financials) information", "status": "NOT-STARTED",
|
||||||
|
"errorMessage": None, "summary": None, "completedAt": None},
|
||||||
|
{"number": 5, "stepName": "Load bank transations", "status": "NOT-STARTED", "errorMessage": None,
|
||||||
|
"summary": None, "completedAt": None},
|
||||||
|
{"number": 6, "stepName": "Load journals", "status": "NOT-STARTED", "errorMessage": None,
|
||||||
|
"summary": None, "completedAt": None},
|
||||||
|
{"number": 7, "stepName": "Load fund performance", "status": "NOT-STARTED", "errorMessage": None,
|
||||||
|
"summary": None, "completedAt": None}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
response = get_onboarding_status(onboarding_id)
|
||||||
|
onboarding_status = response["data"]["data"]
|
||||||
|
steps = onboarding_status["steps"]
|
||||||
|
steps[step_number]["status"] = status
|
||||||
|
steps[step_number]["errorMessage"] = error_message
|
||||||
|
steps[step_number]["summary"] = summary
|
||||||
|
data = {
|
||||||
|
"id": onboarding_id,
|
||||||
|
"steps": steps
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.put(url, headers=headers, data=json.dumps(data))
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
return {"data": response.json(), "status_code": 200}
|
||||||
|
else:
|
||||||
|
return {"error": f"Failed to update onboarding status. Status code: {response.status_code}",
|
||||||
|
"status_code": response.status_code, "details": response.text}
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return {"error": f"An error occurred: {e}", "status_code": 500}
|
||||||
|
|
||||||
|
|
||||||
|
def get_entity_details(entity_id):
|
||||||
|
api_endpoint = os.getenv('API_ENDPOINT')
|
||||||
|
auth_token = os.getenv('AUTH_TOKEN')
|
||||||
|
url = f'{api_endpoint}/api/v1/entity/entity_details?entityId={entity_id}'
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': auth_token
|
||||||
|
}
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
# print(response.json())
|
||||||
|
return response.json()
|
||||||
|
else:
|
||||||
|
# Print the status code and error message if the request failed
|
||||||
|
print(f"Failed to get data. Status code: {response.status_code}")
|
||||||
|
print(f"Error: {response.text}")
|
||||||
|
|
||||||
|
|
||||||
|
def add_user(user):
|
||||||
|
api_endpoint = os.getenv('API_ENDPOINT')
|
||||||
|
auth_token = os.getenv('AUTH_TOKEN')
|
||||||
|
# role_id = os.getenv('ROLE_ID')
|
||||||
|
|
||||||
|
url = f"{api_endpoint}/api/v1/user"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': auth_token,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(url, headers=headers, data=json.dumps(user))
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
return {"data": response.json(), "status_code": 200}
|
||||||
|
else:
|
||||||
|
return {"error": f"Failed to add user. Status code: {response.status_code}",
|
||||||
|
"status_code": response.status_code, "details": response.text}
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return {"error": f"An error occurred: {e}", "status_code": 500}
|
||||||
|
|
||||||
|
|
||||||
|
def add_role(role_payload):
|
||||||
|
api_endpoint = os.getenv('API_ENDPOINT')
|
||||||
|
auth_token = os.getenv('AUTH_TOKEN')
|
||||||
|
role_id = os.getenv('ROLE_ID')
|
||||||
|
|
||||||
|
print(f"Role ID going in to add role API: {role_id}")
|
||||||
|
url = f"{api_endpoint}/api/v1/entity/role/partner"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': auth_token,
|
||||||
|
'X-Entity-Role': role_id
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(url, headers=headers, data=json.dumps(role_payload))
|
||||||
|
|
||||||
|
if response.status_code == 200 or response.status_code == 201:
|
||||||
|
return {"data": response.json(), "status_code": 200}
|
||||||
|
else:
|
||||||
|
return {"error": f"Failed to add role/user. Status code: {response.status_code}",
|
||||||
|
"status_code": response.status_code, "details": response.text}
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return {"error": f"An error occurred: {e}", "status_code": 500}
|
||||||
|
|
||||||
|
|
||||||
|
def update_fund(fund_payload):
|
||||||
|
api_endpoint = os.getenv('API_ENDPOINT')
|
||||||
|
auth_token = os.getenv('AUTH_TOKEN')
|
||||||
|
role_id = os.getenv('ROLE_ID')
|
||||||
|
|
||||||
|
print(f"Update fund use role header: {role_id}")
|
||||||
|
url = f"{api_endpoint}/api/v1/entity/fund_info"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': auth_token,
|
||||||
|
'X-Entity-Role': role_id
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.put(url, headers=headers, data=json.dumps(fund_payload))
|
||||||
|
|
||||||
|
if response.status_code == 200 or response.status_code == 201:
|
||||||
|
return {"data": response.json(), "status_code": 200}
|
||||||
|
else:
|
||||||
|
return {"error": f"Failed to add role/user. Status code: {response.status_code}",
|
||||||
|
"status_code": response.status_code, "details": response.text}
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return {"error": f"An error occurred: {e}", "status_code": 500}
|
||||||
|
|
||||||
|
|
||||||
|
def add_portfolio_company(payload):
|
||||||
|
api_endpoint = os.getenv('API_ENDPOINT')
|
||||||
|
auth_token = os.getenv('AUTH_TOKEN')
|
||||||
|
role_id = os.getenv('ROLE_ID')
|
||||||
|
|
||||||
|
print(f"Update fund use role header: {role_id}")
|
||||||
|
url = f"{api_endpoint}/api/v1/portfolio-company/single"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': auth_token,
|
||||||
|
'X-Entity-Role': role_id
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(url, headers=headers, data=json.dumps(payload))
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
return {"data": response.json(), "status_code": 200}
|
||||||
|
else:
|
||||||
|
return {"error": f"Failed to add portfolio company. Status code: {response.status_code}",
|
||||||
|
"status_code": response.status_code, "details": response.text}
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return {"error": f"An error occurred: {e}", "status_code": 500}
|
||||||
|
|
||||||
|
|
||||||
|
def add_portfolio_company_investment(payload):
|
||||||
|
api_endpoint = os.getenv('API_ENDPOINT')
|
||||||
|
auth_token = os.getenv('AUTH_TOKEN')
|
||||||
|
role_id = os.getenv('ROLE_ID')
|
||||||
|
|
||||||
|
print(f"Update fund use role header: {role_id}")
|
||||||
|
url = f"{api_endpoint}/api/v1/portfolio-company/investment"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': auth_token,
|
||||||
|
'X-Entity-Role': role_id
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(url, headers=headers, data=json.dumps(payload))
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
return {"data": response.json(), "status_code": 200}
|
||||||
|
else:
|
||||||
|
return {"error": f"Failed to add portfolio company investment. Status code: {response.status_code}",
|
||||||
|
"status_code": response.status_code, "details": response.text}
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return {"error": f"An error occurred: {e}", "status_code": 500}
|
||||||
|
|
||||||
|
|
||||||
|
def add_journal(payload):
|
||||||
|
api_endpoint = os.getenv('API_ENDPOINT')
|
||||||
|
auth_token = os.getenv('AUTH_TOKEN')
|
||||||
|
role_id = os.getenv('ROLE_ID')
|
||||||
|
|
||||||
|
print(f"Update fund use role header: {role_id}")
|
||||||
|
url = f"{api_endpoint}/api/v1/bank/journal"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': auth_token,
|
||||||
|
'X-Entity-Role': role_id
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(url, headers=headers, data=json.dumps(payload))
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
return {"data": response.json(), "status_code": 200}
|
||||||
|
else:
|
||||||
|
return {"error": f"Failed to add journal. Status code: {response.status_code}",
|
||||||
|
"status_code": response.status_code, "details": response.text}
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return {"error": f"An error occurred: {e}", "status_code": 500}
|
||||||
|
|
||||||
|
|
||||||
|
def add_journal_ledger(payload):
|
||||||
|
print(payload)
|
||||||
|
api_endpoint = os.getenv('API_ENDPOINT')
|
||||||
|
auth_token = os.getenv('AUTH_TOKEN')
|
||||||
|
role_id = os.getenv('ROLE_ID')
|
||||||
|
|
||||||
|
print(f"Update fund use role header: {role_id}")
|
||||||
|
url = f"{api_endpoint}/api/v1/bank/journal-ledger-single"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': auth_token,
|
||||||
|
'X-Entity-Role': role_id
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(url, headers=headers, data=json.dumps(payload))
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
return {"data": response.json(), "status_code": 200}
|
||||||
|
else:
|
||||||
|
return {"error": f"Failed to add journal ledger. Status code: {response.status_code}",
|
||||||
|
"status_code": response.status_code, "details": response.text}
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return {"error": f"An error occurred: {e}", "status_code": 500}
|
||||||
|
|
||||||
|
|
||||||
|
def add_performance_record(payload):
|
||||||
|
api_endpoint = os.getenv('API_ENDPOINT')
|
||||||
|
auth_token = os.getenv('AUTH_TOKEN')
|
||||||
|
role_id = os.getenv('ROLE_ID')
|
||||||
|
|
||||||
|
print(f"Update fund use role header: {role_id}")
|
||||||
|
url = f"{api_endpoint}/api/v1/transaction/name"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': auth_token,
|
||||||
|
'X-Entity-Role': role_id
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(url, headers=headers, data=json.dumps(payload))
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
return {"data": response.json(), "status_code": 200}
|
||||||
|
else:
|
||||||
|
return {"error": f"Failed to add performance. Status code: {response.status_code}",
|
||||||
|
"status_code": response.status_code, "details": response.text}
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return {"error": f"An error occurred: {e}", "status_code": 500}
|
||||||
|
|
||||||
|
|
||||||
|
def add_bank_transaction_records(payload):
|
||||||
|
api_endpoint = os.getenv('API_ENDPOINT')
|
||||||
|
auth_token = os.getenv('AUTH_TOKEN')
|
||||||
|
role_id = os.getenv('ROLE_ID')
|
||||||
|
|
||||||
|
print(f"Update fund use role header: {role_id}")
|
||||||
|
url = f"{api_endpoint}/api/v1/bank"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': auth_token,
|
||||||
|
'X-Entity-Role': role_id
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(url, headers=headers, data=json.dumps(payload))
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
return {"data": response.json(), "status_code": 200}
|
||||||
|
else:
|
||||||
|
return {"error": f"Failed to add bank transaction. Status code: {response.status_code}",
|
||||||
|
"status_code": response.status_code, "details": response.text}
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return {"error": f"An error occurred: {e}", "status_code": 500}
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_role():
|
||||||
|
|
||||||
|
api_endpoint = os.getenv('API_ENDPOINT')
|
||||||
|
|
||||||
|
url = f"{api_endpoint}/api/v1/entity/role/all_roledata"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': get_token(),
|
||||||
|
'X-Entity-Role':os.getenv('ROLE_ID')
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f"*** Calling get role id. for all LPAs")
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
response_data = response.json()
|
||||||
|
if response.status_code == 200 or response.status_code == 201:
|
||||||
|
lpa_role_id_details = response_data["data"]["data"]
|
||||||
|
# print(f"LPA Roleid {lpa_role_id_details} in the environment.")
|
||||||
|
return lpa_role_id_details
|
||||||
|
else:
|
||||||
|
print(f"Failed to get data. Status code: {response.status_code}")
|
||||||
|
print(f"Error: {response.text}")
|
||||||
|
return {"error": f"Failed to get data. Status code: {response.status_code}",
|
||||||
|
"status_code": response.status_code, "details": response.text}
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return {"error": f"An error occurred: {e}", "status_code": 500}
|
||||||
|
|
||||||
|
def load_partner_data(type,entity,role_id,data):
|
||||||
|
|
||||||
|
api_endpoint = os.getenv('API_ENDPOINT')
|
||||||
|
|
||||||
|
url = f"{api_endpoint}/api/v1/entity/role/partner_data"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': get_token(),
|
||||||
|
}
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"entityId":f"{entity}",
|
||||||
|
"roleId":f"{role_id}",
|
||||||
|
"miscdata": json.loads(data),
|
||||||
|
"type":f"{type}"
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
output = requests.post(url, data=json.dumps(data), headers=headers)
|
||||||
|
if output.status_code == 200 or output.status_code == 201:
|
||||||
|
print("Success posted the request")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return False
|
||||||
21
task/file_classifier.py
Normal file
21
task/file_classifier.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import re
|
||||||
|
import os
|
||||||
|
from utils.prompts import DOCUMENT_CLASSIFY_PROMPT
|
||||||
|
from llm_bedrock import model
|
||||||
|
|
||||||
|
|
||||||
|
def document_classifier(record_details):
|
||||||
|
|
||||||
|
FINAL_DOCUMENT_CLASSIFY_PROMPT = DOCUMENT_CLASSIFY_PROMPT.format(document=record_details)
|
||||||
|
|
||||||
|
json_string = model.invoke(FINAL_DOCUMENT_CLASSIFY_PROMPT).content
|
||||||
|
|
||||||
|
print(json_string)
|
||||||
|
|
||||||
|
pattern = r'<output>(.*?)</output>'
|
||||||
|
match = re.search(pattern, json_string, re.DOTALL)
|
||||||
|
|
||||||
|
if match:
|
||||||
|
return match.group(1).strip()
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
231
task/file_processor.py
Normal file
231
task/file_processor.py
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
import os
|
||||||
|
import requests
|
||||||
|
from utils.prompts import LPA_STATEMENT_PROMPT, SIDE_LETTER_PROMPT
|
||||||
|
from backendAPIs import load_partner_data
|
||||||
|
from llm_bedrock import model
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import List, Dict
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def generate_quarterly_management_fees(
|
||||||
|
fund_start_date: str,
|
||||||
|
fund_end_date: str,
|
||||||
|
fee_schedule: List[Dict[str, float]]
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Generates a JSON string of quarterly management fees based on the provided schedule.
|
||||||
|
|
||||||
|
:param fund_start_date: Fund start date in 'YYYY-MM-DD' format.
|
||||||
|
:param fund_end_date: Fund end date in 'YYYY-MM-DD' format.
|
||||||
|
:param fee_schedule: List of fee changes with 'start_date' and 'annual_fee_percent'.
|
||||||
|
:return: JSON string representing quarterly management fees.
|
||||||
|
"""
|
||||||
|
# Convert string dates to datetime objects
|
||||||
|
start_date = datetime.strptime(fund_start_date, "%Y-%m-%d")
|
||||||
|
end_date = datetime.strptime(fund_end_date, "%Y-%m-%d")
|
||||||
|
|
||||||
|
# Sort fee_schedule by start_date
|
||||||
|
fee_schedule_sorted = sorted(fee_schedule, key=lambda x: x['start_date'])
|
||||||
|
|
||||||
|
# Create list of fee periods with start and end dates
|
||||||
|
fee_periods = []
|
||||||
|
for i, fee in enumerate(fee_schedule_sorted):
|
||||||
|
period_start = datetime.strptime(fee['start_date'], "%Y-%m-%d")
|
||||||
|
if i + 1 < len(fee_schedule_sorted):
|
||||||
|
period_end = datetime.strptime(fee_schedule_sorted[i + 1]['start_date'], "%Y-%m-%d") - timedelta(days=1)
|
||||||
|
else:
|
||||||
|
period_end = end_date
|
||||||
|
# Ensure period doesn't start before fund_start_date
|
||||||
|
if period_start < start_date:
|
||||||
|
period_start = start_date
|
||||||
|
# Ensure period_end doesn't exceed fund_end_date
|
||||||
|
if period_end > end_date:
|
||||||
|
period_end = end_date
|
||||||
|
fee_periods.append({
|
||||||
|
'start_date': period_start,
|
||||||
|
'end_date': period_end,
|
||||||
|
'annual_management_fee_percent': fee['annual_management_fee_percent']
|
||||||
|
})
|
||||||
|
|
||||||
|
# Function to get quarter for a given date
|
||||||
|
def get_quarter(date):
|
||||||
|
return (date.month - 1) // 3 + 1
|
||||||
|
|
||||||
|
# Initialize a dictionary to hold yearly fees
|
||||||
|
yearly_fees = {}
|
||||||
|
|
||||||
|
current_date = start_date
|
||||||
|
while current_date <= end_date:
|
||||||
|
year = current_date.year
|
||||||
|
if year not in yearly_fees:
|
||||||
|
yearly_fees[year] = [0.0, 0.0, 0.0, 0.0]
|
||||||
|
|
||||||
|
quarter = get_quarter(current_date)
|
||||||
|
|
||||||
|
# Find the applicable fee for the current_date
|
||||||
|
applicable_fee = 0.0
|
||||||
|
for period in fee_periods:
|
||||||
|
if period['start_date'] <= current_date <= period['end_date']:
|
||||||
|
applicable_fee = round(period['annual_management_fee_percent'] / 4, 6) # Convert to decimal per quarter
|
||||||
|
break
|
||||||
|
|
||||||
|
yearly_fees[year][quarter - 1] = applicable_fee
|
||||||
|
|
||||||
|
# Move to the next quarter
|
||||||
|
if quarter == 4:
|
||||||
|
# Next quarter is Q1 of next year
|
||||||
|
current_date = datetime(year + 1, 1, 1)
|
||||||
|
else:
|
||||||
|
# Next quarter: first month of next quarter
|
||||||
|
next_quarter_month = 3 * quarter + 1
|
||||||
|
current_date = datetime(year, next_quarter_month, 1)
|
||||||
|
|
||||||
|
# Fill in zero fees before start_date and after end_date
|
||||||
|
formatted_yearly_fees = {}
|
||||||
|
for year in range(start_date.year, end_date.year + 1):
|
||||||
|
if year in yearly_fees:
|
||||||
|
formatted_yearly_fees[str(year)] = yearly_fees[year]
|
||||||
|
else:
|
||||||
|
formatted_yearly_fees[str(year)] = [0.0, 0.0, 0.0, 0.0]
|
||||||
|
|
||||||
|
# Convert to JSON string with keys as strings
|
||||||
|
json_output = json.dumps(formatted_yearly_fees, indent=4)
|
||||||
|
return json_output
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def lpa_amendment(file_data):
|
||||||
|
|
||||||
|
|
||||||
|
FINAL_LPA_PROMPT = LPA_STATEMENT_PROMPT.format(document=file_data)
|
||||||
|
|
||||||
|
json_string = model.invoke(FINAL_LPA_PROMPT).content
|
||||||
|
|
||||||
|
pattern = r'<output>(.*?)</output>'
|
||||||
|
match = re.search(pattern, json_string, re.DOTALL)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if match:
|
||||||
|
final_json_data = match.group(1).strip()
|
||||||
|
|
||||||
|
lpa_output = json.loads(final_json_data)
|
||||||
|
|
||||||
|
if 'managementFee' in lpa_output and all(key in lpa_output['managementFee'] for key in
|
||||||
|
['fund_start_date', 'fund_end_date',
|
||||||
|
'management_fee_schedule']):
|
||||||
|
formatted_management_fee= generate_quarterly_management_fees( lpa_output['managementFee']['fund_start_date'],lpa_output['managementFee']['fund_end_date'],lpa_output['managementFee']['management_fee_schedule'])
|
||||||
|
|
||||||
|
lpa_output['managementFee'] = formatted_management_fee
|
||||||
|
|
||||||
|
final_json_data = json.dumps(lpa_output)
|
||||||
|
|
||||||
|
print("Final json")
|
||||||
|
|
||||||
|
print(final_json_data)
|
||||||
|
|
||||||
|
return final_json_data
|
||||||
|
except Exception as e:
|
||||||
|
print("")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def create_xml_string(data):
|
||||||
|
xml_string = '<partners>\n'
|
||||||
|
for item in data:
|
||||||
|
xml_string += f' <partner>\n'
|
||||||
|
xml_string += f' <id>{item.get("_id", "")}</id>\n'
|
||||||
|
xml_string += f' <partnername>{item.get("partnerExactName", "Not specified")}</partnername>\n'
|
||||||
|
xml_string += f' </partner>\n'
|
||||||
|
xml_string += '</partners>'
|
||||||
|
return xml_string
|
||||||
|
|
||||||
|
|
||||||
|
def side_letter_doc(type,entity_id,file_data,role_data):
|
||||||
|
|
||||||
|
output = model.invoke(SIDE_LETTER_PROMPT.format(document=file_data,role_doc=create_xml_string(role_data))).content
|
||||||
|
|
||||||
|
pattern = r'<output>(.*?)</output>'
|
||||||
|
match = re.search(pattern, output, re.DOTALL)
|
||||||
|
|
||||||
|
json_data = None
|
||||||
|
role_id = None
|
||||||
|
|
||||||
|
if match:
|
||||||
|
json_data = match.group(1).strip()
|
||||||
|
## get id
|
||||||
|
pattern_id = r'<id>(.*?)</id>'
|
||||||
|
match = re.search(pattern_id, output, re.DOTALL)
|
||||||
|
if match:
|
||||||
|
role_id = match.group(1).strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
lpa_output = json.loads(json_data)
|
||||||
|
|
||||||
|
if 'managementFee' in lpa_output and all(key in lpa_output['managementFee'] for key in
|
||||||
|
['fund_start_date', 'fund_end_date',
|
||||||
|
'management_fee_schedule']):
|
||||||
|
formatted_management_fee = generate_quarterly_management_fees(
|
||||||
|
lpa_output['managementFee']['fund_start_date'], lpa_output['managementFee']['fund_end_date'],
|
||||||
|
lpa_output['managementFee']['management_fee_schedule'])
|
||||||
|
|
||||||
|
lpa_output['managementFee'] = formatted_management_fee
|
||||||
|
|
||||||
|
final_json_data = json.dumps(lpa_output)
|
||||||
|
|
||||||
|
print("Final json")
|
||||||
|
|
||||||
|
print(final_json_data)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print("")
|
||||||
|
|
||||||
|
if role_id is None:
|
||||||
|
return "Role is Not fund"
|
||||||
|
|
||||||
|
response = load_partner_data(type,entity_id,role_id,final_json_data)
|
||||||
|
|
||||||
|
|
||||||
|
return response , role_id
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
487
task/gemini_client.py
Normal file
487
task/gemini_client.py
Normal file
@ -0,0 +1,487 @@
|
|||||||
|
"""
|
||||||
|
Gemini client — the Gemini side of llm.py's LLM_BACKEND switch.
|
||||||
|
|
||||||
|
Activated when LLM_BACKEND=gemini (auto-selected if GCP_API_KEY is set).
|
||||||
|
Routes Claude-shaped calls through the google-genai SDK with an API key. Exposes:
|
||||||
|
|
||||||
|
- GeminiBedrockClient: .invoke_model() drop-in for boto3 bedrock-runtime.
|
||||||
|
Translates Anthropic-style request body to Gemini and shapes the response
|
||||||
|
back to the Anthropic JSON the rest of the repo expects.
|
||||||
|
- GeminiChatModel: langchain ChatBedrock-style .invoke().
|
||||||
|
- GeminiConverseClient: converse-API wrapper for RLM / tearsheet loops.
|
||||||
|
- gemini_model_call / gemini_invoke_model: functional helpers used by llm.py.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from utils.token_tracker import record_usage as _record_token_usage
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
from task.utils.token_tracker import record_usage as _record_token_usage
|
||||||
|
except ImportError:
|
||||||
|
def _record_token_usage(_model, _input_tokens, _output_tokens): # type: ignore
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
GEMINI_MODEL_ID = os.environ.get("GEMINI_MODEL_ID", "gemini-3.5-flash")
|
||||||
|
GCP_API_KEY = os.environ.get("GCP_API_KEY") or os.environ.get("GEMINI_API_KEY", "")
|
||||||
|
|
||||||
|
|
||||||
|
_client_init_logged = False
|
||||||
|
_gemini_client_singleton = None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_gemini_client():
|
||||||
|
"""Return a google-genai Client authenticated with GCP_API_KEY.
|
||||||
|
|
||||||
|
Lazily constructed and cached: the genai client (and the GCP_API_KEY
|
||||||
|
requirement) is only resolved on the FIRST LLM call, not at module import.
|
||||||
|
This keeps a missing GCP_API_KEY from crashing the whole worker at boot —
|
||||||
|
it surfaces only on the specific request that needs the model.
|
||||||
|
"""
|
||||||
|
global _gemini_client_singleton
|
||||||
|
if _gemini_client_singleton is not None:
|
||||||
|
return _gemini_client_singleton
|
||||||
|
from google import genai
|
||||||
|
if not GCP_API_KEY:
|
||||||
|
raise RuntimeError(
|
||||||
|
"GCP_API_KEY (or GEMINI_API_KEY) is not set. "
|
||||||
|
"Either set it or switch LLM_BACKEND."
|
||||||
|
)
|
||||||
|
global _client_init_logged
|
||||||
|
if not _client_init_logged:
|
||||||
|
_client_init_logged = True
|
||||||
|
logger.info("[gemini_client] init: model=%s (api-key auth)", GEMINI_MODEL_ID)
|
||||||
|
print(f"[gemini_client] init: model={GEMINI_MODEL_ID} (api-key auth)", flush=True)
|
||||||
|
_gemini_client_singleton = genai.Client(api_key=GCP_API_KEY)
|
||||||
|
return _gemini_client_singleton
|
||||||
|
|
||||||
|
|
||||||
|
def _to_gemini_contents(messages: list[dict]):
|
||||||
|
"""Convert Anthropic-style messages to google-genai Content list.
|
||||||
|
|
||||||
|
Anthropic role "assistant" maps to Gemini role "model". Image blocks
|
||||||
|
in Anthropic format ({"type": "image", "source": {"type": "base64",
|
||||||
|
"media_type": ..., "data": ...}}) become inline Parts.
|
||||||
|
"""
|
||||||
|
from google.genai import types
|
||||||
|
|
||||||
|
contents = []
|
||||||
|
for msg in messages:
|
||||||
|
role = "model" if msg.get("role") == "assistant" else "user"
|
||||||
|
content = msg.get("content")
|
||||||
|
parts = []
|
||||||
|
if isinstance(content, str):
|
||||||
|
parts.append(types.Part.from_text(text=content))
|
||||||
|
elif isinstance(content, list):
|
||||||
|
for block in content:
|
||||||
|
btype = block.get("type")
|
||||||
|
if btype == "text":
|
||||||
|
parts.append(types.Part.from_text(text=block.get("text", "")))
|
||||||
|
elif btype == "image":
|
||||||
|
src = block.get("source", {}) or {}
|
||||||
|
if src.get("type") == "base64":
|
||||||
|
parts.append(types.Part.from_bytes(
|
||||||
|
data=base64.b64decode(src.get("data", "")),
|
||||||
|
mime_type=src.get("media_type", "image/png"),
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
# Tearsheet/Bedrock converse-style block — {"text": ...} or {"image": ...}.
|
||||||
|
if "text" in block:
|
||||||
|
parts.append(types.Part.from_text(text=block["text"]))
|
||||||
|
elif "image" in block:
|
||||||
|
img = block["image"]
|
||||||
|
fmt = img.get("format", "png")
|
||||||
|
data = img.get("source", {}).get("bytes", b"")
|
||||||
|
parts.append(types.Part.from_bytes(
|
||||||
|
data=data, mime_type=f"image/{fmt}",
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
parts.append(types.Part.from_text(text=str(content)))
|
||||||
|
contents.append(types.Content(role=role, parts=parts))
|
||||||
|
return contents
|
||||||
|
|
||||||
|
|
||||||
|
def _build_generate_config(*, system=None, max_tokens=4096, temperature=0,
|
||||||
|
use_thinking=False, thinking_budget=None,
|
||||||
|
json_mode=False, response_schema=None,
|
||||||
|
use_search=False):
|
||||||
|
"""Assemble a GenerateContentConfig with optional thinking + system prompt.
|
||||||
|
|
||||||
|
json_mode=True forces structured JSON output via the Gemini decoder
|
||||||
|
(`response_mime_type="application/json"`) — the model cannot emit
|
||||||
|
prose / markdown / table rows even if the prompt is sloppy.
|
||||||
|
|
||||||
|
response_schema (optional) binds the output to a specific shape. Pass
|
||||||
|
a Python type (`list[str]`, a TypedDict, a pydantic BaseModel) or a
|
||||||
|
`types.Schema` dict. Implies json_mode. The decoder will refuse to
|
||||||
|
emit anything that doesn't conform.
|
||||||
|
|
||||||
|
use_search=True attaches Gemini's built-in Google Search grounding tool
|
||||||
|
so the model searches the live web before answering. OFF by default —
|
||||||
|
callers opt in per request (it adds latency + search cost and makes
|
||||||
|
output web-dependent). Verified on gemini-2.5-pro to coexist
|
||||||
|
with json_mode / response_schema and with thinking, so it composes with
|
||||||
|
the other flags here.
|
||||||
|
"""
|
||||||
|
from google.genai import types
|
||||||
|
|
||||||
|
cfg_kwargs = {
|
||||||
|
"temperature": temperature,
|
||||||
|
"max_output_tokens": max_tokens,
|
||||||
|
}
|
||||||
|
if system:
|
||||||
|
cfg_kwargs["system_instruction"] = system
|
||||||
|
if use_thinking or (thinking_budget and thinking_budget > 0):
|
||||||
|
budget = thinking_budget if (thinking_budget and thinking_budget > 0) else 5000
|
||||||
|
cfg_kwargs["thinking_config"] = types.ThinkingConfig(thinking_budget=budget)
|
||||||
|
if json_mode or response_schema is not None:
|
||||||
|
cfg_kwargs["response_mime_type"] = "application/json"
|
||||||
|
if response_schema is not None:
|
||||||
|
cfg_kwargs["response_schema"] = response_schema
|
||||||
|
if use_search:
|
||||||
|
# Gemini 2.0+ grounding uses GoogleSearch(); serializes to the same
|
||||||
|
# {"google_search": {}} tool the vendor-research REST path sends.
|
||||||
|
cfg_kwargs["tools"] = [types.Tool(google_search=types.GoogleSearch())]
|
||||||
|
return types.GenerateContentConfig(**cfg_kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_text(response) -> str:
|
||||||
|
"""Pull the concatenated text from a google-genai GenerateContentResponse."""
|
||||||
|
if getattr(response, "text", None):
|
||||||
|
return response.text
|
||||||
|
out = []
|
||||||
|
for cand in getattr(response, "candidates", []) or []:
|
||||||
|
content = getattr(cand, "content", None)
|
||||||
|
if not content:
|
||||||
|
continue
|
||||||
|
for part in getattr(content, "parts", []) or []:
|
||||||
|
if getattr(part, "text", None):
|
||||||
|
out.append(part.text)
|
||||||
|
return "".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_usage(response) -> tuple[int, int]:
|
||||||
|
"""Pull token counts from response.usage_metadata per Gemini API docs.
|
||||||
|
|
||||||
|
Documented fields (https://ai.google.dev/gemini-api/docs/tokens):
|
||||||
|
- prompt_token_count — input tokens
|
||||||
|
- candidates_token_count — output tokens (excluding thinking)
|
||||||
|
- thoughts_token_count — thinking tokens (billed as OUTPUT per
|
||||||
|
https://ai.google.dev/gemini-api/docs/pricing
|
||||||
|
"Output price (including thinking tokens)")
|
||||||
|
- cached_content_token_count, total_token_count — informational
|
||||||
|
|
||||||
|
Returns (input_tokens, output_tokens) using:
|
||||||
|
input = prompt_token_count
|
||||||
|
output = candidates_token_count + thoughts_token_count
|
||||||
|
"""
|
||||||
|
um = getattr(response, "usage_metadata", None)
|
||||||
|
if um is None:
|
||||||
|
return 0, 0
|
||||||
|
in_toks = int(getattr(um, "prompt_token_count", 0) or 0)
|
||||||
|
out_toks = (
|
||||||
|
int(getattr(um, "candidates_token_count", 0) or 0)
|
||||||
|
+ int(getattr(um, "thoughts_token_count", 0) or 0)
|
||||||
|
)
|
||||||
|
return in_toks, out_toks
|
||||||
|
|
||||||
|
|
||||||
|
def _record_usage(response, model_id: str) -> tuple[int, int]:
|
||||||
|
"""Manual token tracking — read documented fields, record once."""
|
||||||
|
in_toks, out_toks = _extract_usage(response)
|
||||||
|
if in_toks or out_toks:
|
||||||
|
_record_token_usage(model_id, in_toks, out_toks)
|
||||||
|
return in_toks, out_toks
|
||||||
|
|
||||||
|
|
||||||
|
def _stream_generate(client, *, model: str, contents, config):
|
||||||
|
"""Run generate_content_stream and return (full_text, last_chunk).
|
||||||
|
|
||||||
|
Streaming avoids the 10-minute timeout on long requests. The last chunk
|
||||||
|
carries the cumulative ``usage_metadata`` for token tracking.
|
||||||
|
"""
|
||||||
|
text_parts: list[str] = []
|
||||||
|
last_chunk = None
|
||||||
|
for chunk in client.models.generate_content_stream(
|
||||||
|
model=model, contents=contents, config=config,
|
||||||
|
):
|
||||||
|
last_chunk = chunk
|
||||||
|
chunk_text = _extract_text(chunk)
|
||||||
|
if chunk_text:
|
||||||
|
text_parts.append(chunk_text)
|
||||||
|
return "".join(text_parts), last_chunk
|
||||||
|
|
||||||
|
|
||||||
|
class _StreamBody:
|
||||||
|
"""Mimic boto3 StreamingBody so callers can do response['body'].read()."""
|
||||||
|
|
||||||
|
def __init__(self, data: bytes):
|
||||||
|
self._data = data
|
||||||
|
|
||||||
|
def read(self):
|
||||||
|
return self._data
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiBedrockClient:
|
||||||
|
"""Drop-in replacement for boto3 bedrock-runtime client.
|
||||||
|
|
||||||
|
Translates an Anthropic-shaped request body to Gemini, then shapes the
|
||||||
|
response back to Anthropic JSON so the rest of the repo (which parses
|
||||||
|
response_body['content'][i]['text'] / 'usage') keeps working.
|
||||||
|
|
||||||
|
Token tracking — pure shim: returns the real ``usage`` in the response
|
||||||
|
and does NOT record itself. The caller
|
||||||
|
parses ``response_body['usage']`` and records via the documented
|
||||||
|
``record_usage`` API (``task/utils/token_tracker.py:218``). Token counts
|
||||||
|
are computed from the documented ``usage_metadata`` fields per the
|
||||||
|
Gemini docs (``_extract_usage``).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# Client built lazily on first invoke_model() so a missing GCP_API_KEY
|
||||||
|
# does not crash the worker at import/construction time.
|
||||||
|
self._model = GEMINI_MODEL_ID
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _client(self):
|
||||||
|
return _get_gemini_client()
|
||||||
|
|
||||||
|
def invoke_model(self, modelId: str, body: str, **_kwargs):
|
||||||
|
logger.info("[gemini_client.GeminiBedrockClient.invoke_model] model=%s (caller passed modelId=%s)", self._model, modelId)
|
||||||
|
request = json.loads(body) if isinstance(body, str) else body
|
||||||
|
messages = request.get("messages", [])
|
||||||
|
max_tokens = request.get("max_tokens", 16384)
|
||||||
|
temperature = request.get("temperature", 0)
|
||||||
|
system = request.get("system")
|
||||||
|
thinking = request.get("thinking") or {}
|
||||||
|
|
||||||
|
use_thinking = thinking.get("type") == "enabled"
|
||||||
|
budget = thinking.get("budget_tokens") if use_thinking else None
|
||||||
|
# Optional Google Search grounding — set "use_search": true in the
|
||||||
|
# request body (non-Anthropic field; absent → off, behavior unchanged).
|
||||||
|
use_search = bool(request.get("use_search", False))
|
||||||
|
# Unlike Anthropic, Gemini accepts thinking WITH temperature=0 — keep
|
||||||
|
# the caller's temperature (default 0) so thinking calls stay
|
||||||
|
# deterministic. Never force temperature=1 here.
|
||||||
|
|
||||||
|
contents = _to_gemini_contents(messages)
|
||||||
|
config = _build_generate_config(
|
||||||
|
system=system, max_tokens=max_tokens, temperature=temperature,
|
||||||
|
use_thinking=use_thinking, thinking_budget=budget,
|
||||||
|
use_search=use_search,
|
||||||
|
)
|
||||||
|
|
||||||
|
text, last_chunk = _stream_generate(
|
||||||
|
self._client, model=self._model, contents=contents, config=config,
|
||||||
|
)
|
||||||
|
|
||||||
|
in_toks, out_toks = _extract_usage(last_chunk) if last_chunk else (0, 0)
|
||||||
|
|
||||||
|
response_dict = {
|
||||||
|
"id": f"gemini-{uuid.uuid4().hex[:24]}",
|
||||||
|
"type": "message",
|
||||||
|
"role": "assistant",
|
||||||
|
"content": [{"type": "text", "text": text}],
|
||||||
|
"model": self._model,
|
||||||
|
"stop_reason": "end_turn",
|
||||||
|
"usage": {
|
||||||
|
"input_tokens": in_toks,
|
||||||
|
"output_tokens": out_toks,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return {"body": _StreamBody(json.dumps(response_dict).encode("utf-8"))}
|
||||||
|
|
||||||
|
|
||||||
|
class _InvokeResult:
|
||||||
|
def __init__(self, content: str):
|
||||||
|
self.content = content
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiChatModel:
|
||||||
|
"""Drop-in replacement for LangChain ChatBedrock.
|
||||||
|
|
||||||
|
The ``model_id`` argument is accepted for signature compatibility with
|
||||||
|
callers that hardcode a Bedrock id (e.g. ``us.anthropic.claude-sonnet-4-6``)
|
||||||
|
but is **ignored** — under ``LLM_BACKEND=gemini`` we always call
|
||||||
|
``GEMINI_MODEL_ID``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, model_id: str = None, model_kwargs: dict = None):
|
||||||
|
del model_id # accepted for signature compat; we always use GEMINI_MODEL_ID
|
||||||
|
# Client built lazily on first invoke() so a missing GCP_API_KEY does
|
||||||
|
# not crash the worker at import/construction time.
|
||||||
|
self._model = GEMINI_MODEL_ID
|
||||||
|
self._kwargs = model_kwargs or {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _client(self):
|
||||||
|
return _get_gemini_client()
|
||||||
|
|
||||||
|
def invoke(self, prompt):
|
||||||
|
logger.info("[gemini_client.GeminiChatModel.invoke] model=%s", self._model)
|
||||||
|
if isinstance(prompt, str):
|
||||||
|
messages = [{"role": "user", "content": prompt}]
|
||||||
|
else:
|
||||||
|
messages = prompt
|
||||||
|
|
||||||
|
config = _build_generate_config(
|
||||||
|
max_tokens=self._kwargs.get("max_tokens", 16384),
|
||||||
|
temperature=self._kwargs.get("temperature", 0),
|
||||||
|
use_search=bool(self._kwargs.get("use_search", False)),
|
||||||
|
)
|
||||||
|
text, last_chunk = _stream_generate(
|
||||||
|
self._client, model=self._model,
|
||||||
|
contents=_to_gemini_contents(messages), config=config,
|
||||||
|
)
|
||||||
|
if last_chunk is not None:
|
||||||
|
_record_usage(last_chunk, self._model)
|
||||||
|
return _InvokeResult(text)
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiConverseClient:
|
||||||
|
"""Drop-in replacement for pdf_to_xlsx / tearsheets BedrockClient.
|
||||||
|
|
||||||
|
The ``model_id`` and ``region`` arguments are accepted for signature
|
||||||
|
compatibility (RLMBedrockClient callers hardcode Bedrock model ids and
|
||||||
|
AWS regions) but are **ignored** — under ``LLM_BACKEND=gemini`` we always
|
||||||
|
call ``GEMINI_MODEL_ID``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
model_id: str = None,
|
||||||
|
region: str = None,
|
||||||
|
max_tokens: int = 16384,
|
||||||
|
thinking_budget: int = 4096,
|
||||||
|
use_search: bool = False,
|
||||||
|
):
|
||||||
|
del model_id, region # accepted for signature compat; always GEMINI_MODEL_ID
|
||||||
|
self.model_id = GEMINI_MODEL_ID
|
||||||
|
self.max_tokens = max_tokens
|
||||||
|
self.thinking_budget = thinking_budget if (thinking_budget and thinking_budget > 0) else 0
|
||||||
|
self.use_search = use_search
|
||||||
|
# Client built lazily on first completion() so a missing GCP_API_KEY
|
||||||
|
# does not crash the worker at import/construction time.
|
||||||
|
self.total_input_tokens = 0
|
||||||
|
self.total_output_tokens = 0
|
||||||
|
self.call_count = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _client(self):
|
||||||
|
return _get_gemini_client()
|
||||||
|
|
||||||
|
def _generate(self, messages: list[dict], system: str | None) -> str:
|
||||||
|
logger.info(
|
||||||
|
"[gemini_client.GeminiConverseClient] model=%s thinking=%s call#%d",
|
||||||
|
self.model_id, bool(self.thinking_budget), self.call_count + 1,
|
||||||
|
)
|
||||||
|
contents = _to_gemini_contents(messages)
|
||||||
|
config = _build_generate_config(
|
||||||
|
system=system,
|
||||||
|
max_tokens=self.max_tokens,
|
||||||
|
temperature=0, # Gemini allows thinking at temp 0 — deterministic
|
||||||
|
use_thinking=bool(self.thinking_budget),
|
||||||
|
thinking_budget=self.thinking_budget or None,
|
||||||
|
use_search=self.use_search,
|
||||||
|
)
|
||||||
|
text, last_chunk = _stream_generate(
|
||||||
|
self._client, model=self.model_id, contents=contents, config=config,
|
||||||
|
)
|
||||||
|
if last_chunk is not None:
|
||||||
|
in_toks, out_toks = _record_usage(last_chunk, self.model_id)
|
||||||
|
self.total_input_tokens += in_toks
|
||||||
|
self.total_output_tokens += out_toks
|
||||||
|
self.call_count += 1
|
||||||
|
return text
|
||||||
|
|
||||||
|
def completion(self, messages: list[dict], system: str | None = None) -> str:
|
||||||
|
return self._generate(messages, system)
|
||||||
|
|
||||||
|
def completion_with_image(
|
||||||
|
self,
|
||||||
|
image_bytes: bytes,
|
||||||
|
mime_type: str,
|
||||||
|
prompt: str,
|
||||||
|
system: str | None = None,
|
||||||
|
) -> str:
|
||||||
|
media_type = mime_type if "/" in mime_type else f"image/{mime_type}"
|
||||||
|
b64 = base64.standard_b64encode(image_bytes).decode("utf-8")
|
||||||
|
messages = [{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{"type": "image", "source": {
|
||||||
|
"type": "base64", "media_type": media_type, "data": b64,
|
||||||
|
}},
|
||||||
|
{"type": "text", "text": prompt},
|
||||||
|
],
|
||||||
|
}]
|
||||||
|
return self._generate(messages, system)
|
||||||
|
|
||||||
|
def usage_summary(self) -> str:
|
||||||
|
return (
|
||||||
|
f"Calls: {self.call_count} | "
|
||||||
|
f"Input: {self.total_input_tokens:,} | "
|
||||||
|
f"Output: {self.total_output_tokens:,}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def gemini_model_call(prompt, max_tokens=16384, use_thinking=False, system=None,
|
||||||
|
json_mode=False, response_schema=None, use_search=False):
|
||||||
|
"""Single-shot Gemini call. Returns the text response.
|
||||||
|
|
||||||
|
use_search=True grounds the call with Google Search (off by default)."""
|
||||||
|
logger.info("[gemini_client.gemini_model_call] model=%s thinking=%s json_mode=%s schema=%s search=%s", GEMINI_MODEL_ID, use_thinking, json_mode, response_schema is not None, use_search)
|
||||||
|
client = _get_gemini_client()
|
||||||
|
config = _build_generate_config(
|
||||||
|
system=system, max_tokens=max_tokens,
|
||||||
|
temperature=0, # Gemini allows thinking at temp 0 — deterministic
|
||||||
|
use_thinking=use_thinking,
|
||||||
|
json_mode=json_mode,
|
||||||
|
response_schema=response_schema,
|
||||||
|
use_search=use_search,
|
||||||
|
)
|
||||||
|
contents = _to_gemini_contents([{"role": "user", "content": prompt}])
|
||||||
|
try:
|
||||||
|
text, last_chunk = _stream_generate(
|
||||||
|
client, model=GEMINI_MODEL_ID, contents=contents, config=config,
|
||||||
|
)
|
||||||
|
if last_chunk is not None:
|
||||||
|
_record_usage(last_chunk, GEMINI_MODEL_ID)
|
||||||
|
return text
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Gemini model call failed: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def gemini_invoke_model(messages, system=None, max_tokens=16384, temperature=0,
|
||||||
|
use_thinking=False, use_search=False):
|
||||||
|
"""Multi-turn Gemini call. Returns the text response.
|
||||||
|
|
||||||
|
use_search=True grounds the call with Google Search (off by default)."""
|
||||||
|
logger.info("[gemini_client.gemini_invoke_model] model=%s msgs=%d thinking=%s search=%s", GEMINI_MODEL_ID, len(messages), use_thinking, use_search)
|
||||||
|
client = _get_gemini_client()
|
||||||
|
config = _build_generate_config(
|
||||||
|
system=system, max_tokens=max_tokens,
|
||||||
|
temperature=temperature, # Gemini allows thinking at temp 0 — no forcing
|
||||||
|
use_thinking=use_thinking,
|
||||||
|
use_search=use_search,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
text, last_chunk = _stream_generate(
|
||||||
|
client, model=GEMINI_MODEL_ID,
|
||||||
|
contents=_to_gemini_contents(messages), config=config,
|
||||||
|
)
|
||||||
|
if last_chunk is not None:
|
||||||
|
_record_usage(last_chunk, GEMINI_MODEL_ID)
|
||||||
|
return text
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Gemini invoke_model failed: {e}")
|
||||||
|
raise
|
||||||
47
task/initOnboarding.py
Normal file
47
task/initOnboarding.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
from backendAPIs import (
|
||||||
|
get_token,
|
||||||
|
get_role_id,
|
||||||
|
update_onboarding_status,
|
||||||
|
get_onboarding_status
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_onboarding():
|
||||||
|
|
||||||
|
print(f"Getting Auth token...")
|
||||||
|
token = get_token()
|
||||||
|
# print(f"Token {token}")
|
||||||
|
|
||||||
|
onboarding_id = os.getenv('ONBOARDING_ID')
|
||||||
|
print(f"Get onBoardingStatus for {onboarding_id}")
|
||||||
|
response = get_onboarding_status(onboarding_id)
|
||||||
|
if 'error' in response:
|
||||||
|
print(f"Failed to fetch entityid: Error: {response['error']}")
|
||||||
|
print(f"Status Code: {response['status_code']}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
onboarding_status = response['data']['data']
|
||||||
|
print(f"Onboading status {onboarding_status}")
|
||||||
|
entity_id = onboarding_status['entityId']
|
||||||
|
os.environ["ENTITY_ID"] = entity_id
|
||||||
|
print(f"Entityid set to {entity_id} in the environment")
|
||||||
|
|
||||||
|
print(f"Getting role id...")
|
||||||
|
response = get_role_id(entity_id)
|
||||||
|
print(response)
|
||||||
|
if 'error' in response:
|
||||||
|
print(f"Failed to fetch role id: Error: {response['error']}")
|
||||||
|
print(f"Status Code: {response['status_code']}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
role_id = response['data']
|
||||||
|
os.environ["ROLE_ID"] = role_id
|
||||||
|
print(f"Roleid set to {role_id} in the environment")
|
||||||
|
|
||||||
|
response = update_onboarding_status(-1, "", "", "")
|
||||||
|
if 'error' in response:
|
||||||
|
print(f"Failed to fetch onboarding status: Error: {response['error']}")
|
||||||
|
print(f"Status Code: {response['status_code']}")
|
||||||
|
return False
|
||||||
155
task/lambda_function.py
Normal file
155
task/lambda_function.py
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
from initOnboarding import initialize_onboarding
|
||||||
|
from validatedUploadedFiles import run_file_validation
|
||||||
|
from onboardFundsDummy import onboardFunds
|
||||||
|
from onboardPartners import process_all_partners
|
||||||
|
from onboardBankTransactions import process_bank_transactions
|
||||||
|
from onboardFinancials import process_financials
|
||||||
|
from onboardJournals import process_journals
|
||||||
|
from onboardPerformance import process_fund_performance
|
||||||
|
from backendAPIs import get_all_role
|
||||||
|
from file_processor import lpa_amendment , side_letter_doc
|
||||||
|
from backendAPIs import load_partner_data
|
||||||
|
|
||||||
|
|
||||||
|
def lambda_handler(event):
|
||||||
|
|
||||||
|
# Extract the 'id'
|
||||||
|
onboarding_id = json.loads(event)['id']
|
||||||
|
|
||||||
|
print(f"Received onboarding id from Q: {onboarding_id}")
|
||||||
|
if not onboarding_id:
|
||||||
|
return {
|
||||||
|
'statusCode': 400,
|
||||||
|
'body': json.dumps('Missing onboarding_id')
|
||||||
|
}
|
||||||
|
|
||||||
|
os.environ["ONBOARDING_ID"] = onboarding_id
|
||||||
|
print(f"Onboarding id set to {onboarding_id} in the environment")
|
||||||
|
print(f"Received onboarding id from Q: {onboarding_id}")
|
||||||
|
if not onboarding_id:
|
||||||
|
return {
|
||||||
|
'statusCode': 400,
|
||||||
|
'body': json.dumps('Missing onboarding_id')
|
||||||
|
}
|
||||||
|
|
||||||
|
os.environ["ONBOARDING_ID"] = onboarding_id
|
||||||
|
print(f"Onboarding id set to {onboarding_id} in the environment")
|
||||||
|
|
||||||
|
print("0. Initialising...")
|
||||||
|
response = initialize_onboarding()
|
||||||
|
if response is False:
|
||||||
|
print("Initialization failed, terminating onboarding...")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print("1. Validating input files...")
|
||||||
|
found_files ,pdf_files = run_file_validation()
|
||||||
|
if not isinstance(found_files, dict):
|
||||||
|
print(found_files)
|
||||||
|
error_message = found_files
|
||||||
|
return {
|
||||||
|
'statusCode': 400,
|
||||||
|
'body': json.dumps(error_message)
|
||||||
|
}
|
||||||
|
|
||||||
|
print("2. Running fund onboarding...")
|
||||||
|
onboardFunds(found_files["partner"])
|
||||||
|
|
||||||
|
print("3. Running partner onboarding...")
|
||||||
|
process_all_partners(found_files["partner"])
|
||||||
|
|
||||||
|
print("4. Running financial onboarding...")
|
||||||
|
process_financials(found_files["financials"]) # use Roll Forward ->column B->Security type and from rollforward need to make.
|
||||||
|
|
||||||
|
print("5. Running bank transactions onboarding...")
|
||||||
|
process_bank_transactions(found_files["bankTransactions"])
|
||||||
|
|
||||||
|
print("6. Running journal onboarding...")
|
||||||
|
process_journals(found_files["journals"]) # issue with missing account types
|
||||||
|
|
||||||
|
print("7. Running fund performance onboarding...")
|
||||||
|
process_fund_performance(found_files["fund_performance"]) #
|
||||||
|
|
||||||
|
# start processing PDF files related to GP based on the type of the document
|
||||||
|
#
|
||||||
|
# print("Start processing the PDF files related to GP")
|
||||||
|
#
|
||||||
|
# # give list of the files to classifier function to classify them
|
||||||
|
#
|
||||||
|
# role_data = get_all_role()
|
||||||
|
#
|
||||||
|
# print("GET ALL role data")
|
||||||
|
#
|
||||||
|
# print(role_data)
|
||||||
|
#
|
||||||
|
# classified_pdf_files = pdf_classifier(pdf_files)
|
||||||
|
#
|
||||||
|
# print("Processing the LPA and its Amendments")
|
||||||
|
#
|
||||||
|
# side_letter_files = set()
|
||||||
|
#
|
||||||
|
# for file_category , file_names in classified_pdf_files.items():
|
||||||
|
#
|
||||||
|
# for file_name in file_names:
|
||||||
|
#
|
||||||
|
# output_json = None
|
||||||
|
#
|
||||||
|
# pdf_string = get_pdf_details(file_name)
|
||||||
|
#
|
||||||
|
# print(f"Started processing the file : {file_name}")
|
||||||
|
#
|
||||||
|
# for role_id in role_data:
|
||||||
|
#
|
||||||
|
# print(f"Start processing the file :{file_name} for the role id :{role_id['_id']}")
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# if file_category.strip() == "LPA(Limited Partner Agreement)":
|
||||||
|
#
|
||||||
|
# if output_json is None:
|
||||||
|
# output_json = lpa_amendment(pdf_string)
|
||||||
|
#
|
||||||
|
# response = load_partner_data("LPA", os.environ["ENTITY_ID"], role_id['_id'], output_json)
|
||||||
|
#
|
||||||
|
# if not response:
|
||||||
|
# print(
|
||||||
|
# f"Issue encouneted in file processing of {file_name} for the role id : {role_id['_id']}")
|
||||||
|
# else:
|
||||||
|
# print(f"Successfully processed : {file_name} for the role id : {role_id['_id']}")
|
||||||
|
#
|
||||||
|
# if file_category.strip() == "LPA-Amendment(Limited Partner Agreement-Amendment)":
|
||||||
|
# #
|
||||||
|
# if output_json is None:
|
||||||
|
# output_json = lpa_amendment(pdf_string)
|
||||||
|
#
|
||||||
|
# response = load_partner_data("Amendment", os.environ["ENTITY_ID"], role_id['_id'], output_json)
|
||||||
|
#
|
||||||
|
# if not response:
|
||||||
|
# print(
|
||||||
|
# f"Issue encouneted in file processing of {file_name} for the role id : {role_id['_id']}")
|
||||||
|
# else:
|
||||||
|
# print(f"Successfully processed : {file_name} for the role id : {role_id['_id']}")
|
||||||
|
#
|
||||||
|
# if file_category.strip() == "Side Letter":
|
||||||
|
# side_letter_files = file_names
|
||||||
|
#
|
||||||
|
# ### lets start the file processing for side letter because its different for each LP and each role id need to seperated
|
||||||
|
#
|
||||||
|
# for side_letter_file in side_letter_files:
|
||||||
|
#
|
||||||
|
# print(side_letter_file)
|
||||||
|
#
|
||||||
|
# pdf_string = get_pdf_details(side_letter_file)
|
||||||
|
#
|
||||||
|
# status , role_id = side_letter_doc("Side Letter",os.environ["ENTITY_ID"],pdf_string,role_data)
|
||||||
|
#
|
||||||
|
# if not status:
|
||||||
|
# print(f"Issue encouneted in file processing of side letter {side_letter_file} and related role id : {role_id}")
|
||||||
|
#
|
||||||
|
# else:
|
||||||
|
# print(f"Side letter file : {side_letter_file} was processed successfully and related role id : {role_id}")
|
||||||
|
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
130
task/llm.py
Normal file
130
task/llm.py
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
"""
|
||||||
|
Unified LLM interface — Gemini-only build for the Carta onboarding service.
|
||||||
|
|
||||||
|
This service runs exclusively on Google Gemini (google-genai SDK, API-key auth
|
||||||
|
via the GCP_API_KEY env var). The AWS Bedrock and GCP Vertex backends that the
|
||||||
|
shared lambdaLLM module supports have been removed here: there is a single
|
||||||
|
provider and a single code path.
|
||||||
|
|
||||||
|
All call sites import from this module so the provider stays centralized:
|
||||||
|
|
||||||
|
from llm import get_chat_model # chat model with .invoke()
|
||||||
|
from llm import get_llm_client # low-level client with .invoke_model()
|
||||||
|
from llm import model_call, invoke_messages, get_converse_client
|
||||||
|
|
||||||
|
Retry strategy: on rate-limit / quota errors, wait a fixed interval until the
|
||||||
|
provider's limit window resets and retry until the call succeeds (no
|
||||||
|
exponential backoff). Non-retryable errors are raised immediately.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Backend is fixed to Gemini. Exposed as a constant (and written back to the
|
||||||
|
# environment) so any code or dependency that still reads LLM_BACKEND sees the
|
||||||
|
# only supported value.
|
||||||
|
LLM_BACKEND = "gemini"
|
||||||
|
os.environ["LLM_BACKEND"] = LLM_BACKEND
|
||||||
|
|
||||||
|
# Model id is config-driven so ops can flip Gemini models without code changes.
|
||||||
|
GEMINI_MODEL_ID = os.environ.get("GEMINI_MODEL_ID", "gemini-3.5-flash")
|
||||||
|
ACTIVE_MODEL_ID = GEMINI_MODEL_ID
|
||||||
|
|
||||||
|
logger.info("[llm] backend=gemini | active_model=%s", ACTIVE_MODEL_ID)
|
||||||
|
print(f"[llm] backend=gemini | active_model={ACTIVE_MODEL_ID}", flush=True)
|
||||||
|
|
||||||
|
# Retry config. LLM_RETRY_WAIT is the fixed wait (seconds) between retries;
|
||||||
|
# LLM_MAX_RETRIES is a safety cap (default 1000 ~= 16h at 60s wait).
|
||||||
|
LLM_RETRY_WAIT = float(os.environ.get("LLM_RETRY_WAIT", "60.0"))
|
||||||
|
LLM_MAX_RETRIES = int(os.environ.get("LLM_MAX_RETRIES", "1000"))
|
||||||
|
|
||||||
|
|
||||||
|
def _is_rate_limit(exc):
|
||||||
|
"""Return True for throttling / rate-limit / quota errors from google-genai."""
|
||||||
|
msg = str(exc).lower()
|
||||||
|
if "google.genai" in type(exc).__module__:
|
||||||
|
code = getattr(exc, "code", None)
|
||||||
|
if isinstance(code, int) and code in {408, 429, 500, 502, 503, 504}:
|
||||||
|
return True
|
||||||
|
for token in ("throttl", "rate limit", "too many requests", "quota",
|
||||||
|
"overloaded", "resource_exhausted", "429", "503",
|
||||||
|
"service unavailable"):
|
||||||
|
if token in msg:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _with_retry(fn, *args, **kwargs):
|
||||||
|
"""Invoke fn and wait out provider rate-limit windows until it succeeds."""
|
||||||
|
for attempt in range(LLM_MAX_RETRIES + 1):
|
||||||
|
try:
|
||||||
|
return fn(*args, **kwargs)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
if not _is_rate_limit(e) or attempt >= LLM_MAX_RETRIES:
|
||||||
|
raise
|
||||||
|
logger.warning(
|
||||||
|
"LLM rate-limited (%s) on attempt %d; waiting %.0fs for limit window to reset: %s",
|
||||||
|
type(e).__name__, attempt + 1, LLM_RETRY_WAIT, e,
|
||||||
|
)
|
||||||
|
time.sleep(LLM_RETRY_WAIT)
|
||||||
|
raise RuntimeError("LLM retry loop exhausted without success")
|
||||||
|
|
||||||
|
|
||||||
|
def _gemini():
|
||||||
|
"""Import the gemini_client module (top-level or task-package layout)."""
|
||||||
|
try:
|
||||||
|
import gemini_client as gc
|
||||||
|
except ImportError:
|
||||||
|
from task import gemini_client as gc # type: ignore
|
||||||
|
return gc
|
||||||
|
|
||||||
|
|
||||||
|
def get_llm_client():
|
||||||
|
"""Return a low-level client exposing .invoke_model(), backed by
|
||||||
|
google-genai (GeminiBedrockClient)."""
|
||||||
|
return _gemini().GeminiBedrockClient()
|
||||||
|
|
||||||
|
|
||||||
|
def model_call(prompt, max_tokens=16384, use_thinking=False, system=None,
|
||||||
|
json_mode=False, response_schema=None, use_search=False):
|
||||||
|
"""Single-shot Gemini call. Returns the text response. Retries on throttling."""
|
||||||
|
return _with_retry(
|
||||||
|
_gemini().gemini_model_call, prompt,
|
||||||
|
max_tokens=max_tokens, use_thinking=use_thinking, system=system,
|
||||||
|
json_mode=json_mode, response_schema=response_schema, use_search=use_search,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def invoke_messages(messages, system=None, max_tokens=16384, temperature=0,
|
||||||
|
use_thinking=False, use_search=False):
|
||||||
|
"""Multi-turn Gemini call. Returns the text response. Retries on throttling."""
|
||||||
|
return _with_retry(
|
||||||
|
_gemini().gemini_invoke_model, messages=messages, system=system,
|
||||||
|
max_tokens=max_tokens, temperature=temperature,
|
||||||
|
use_thinking=use_thinking, use_search=use_search,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_chat_model(model_id=None, temperature=0, max_tokens=16384,
|
||||||
|
model_kwargs=None, use_search=False):
|
||||||
|
"""Return a chat model with .invoke() (GeminiChatModel).
|
||||||
|
|
||||||
|
model_id is accepted for call-site compatibility but ignored — the active
|
||||||
|
model is always GEMINI_MODEL_ID. GeminiChatModel records token usage
|
||||||
|
internally."""
|
||||||
|
kwargs = dict(model_kwargs) if model_kwargs else {}
|
||||||
|
kwargs.setdefault("temperature", temperature)
|
||||||
|
kwargs.setdefault("max_tokens", max_tokens)
|
||||||
|
if use_search:
|
||||||
|
kwargs["use_search"] = True
|
||||||
|
return _gemini().GeminiChatModel(model_id=model_id or GEMINI_MODEL_ID,
|
||||||
|
model_kwargs=kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def get_converse_client(max_tokens=16384, thinking_budget=1024, use_search=False):
|
||||||
|
"""Return a converse-style Gemini client (completion / completion_with_image)."""
|
||||||
|
return _gemini().GeminiConverseClient(
|
||||||
|
max_tokens=max_tokens, thinking_budget=thinking_budget, use_search=use_search,
|
||||||
|
)
|
||||||
13
task/llm_bedrock.py
Normal file
13
task/llm_bedrock.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
"""Legacy module name kept for import compatibility.
|
||||||
|
|
||||||
|
Bedrock has been removed from this service — `model` is now the Gemini-backed
|
||||||
|
chat model from the unified Gemini-only interface in task/llm.py. Existing
|
||||||
|
`from llm_bedrock import model` call sites keep working unchanged.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from llm import get_chat_model
|
||||||
|
except ImportError:
|
||||||
|
from task.llm import get_chat_model # type: ignore
|
||||||
|
|
||||||
|
model_parameter = {"temperature": 0, "max_tokens": 4096}
|
||||||
|
model = get_chat_model(model_kwargs=model_parameter)
|
||||||
136
task/onboardBankTransactions.py
Normal file
136
task/onboardBankTransactions.py
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import pandas as pd
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
from s3Ops import read_file_from_s3
|
||||||
|
|
||||||
|
from backendAPIs import (
|
||||||
|
update_onboarding_status,
|
||||||
|
add_bank_transaction_records
|
||||||
|
)
|
||||||
|
|
||||||
|
def xlsx_to_df(xlsx_file, sheet_name):
|
||||||
|
df = pd.read_excel(xlsx_file, sheet_name = sheet_name)
|
||||||
|
df.columns = df.iloc[2]
|
||||||
|
df = df.drop(df.index[:3])
|
||||||
|
df = df.reset_index(drop=True)
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: add the remaining asset type mapping
|
||||||
|
def get_security_type(asset_type):
|
||||||
|
asset_to_security_type = {
|
||||||
|
"Common stock":"COMMON",
|
||||||
|
"Preferred stock":"PREFERRED",
|
||||||
|
"Warrants":"WARRANTS",
|
||||||
|
"Convertible promissory note/SAFEs": "POST MONEY SAFE"
|
||||||
|
}
|
||||||
|
|
||||||
|
for asset, security_type in asset_to_security_type.items():
|
||||||
|
if asset_type.lower() in asset.lower(): # Case-insensitive match
|
||||||
|
return security_type
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def process_bank_transaction_records(df):
|
||||||
|
"""
|
||||||
|
Process and insert users from an Excel file into MongoDB.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path (str): Path to the Excel file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
entity_id = os.getenv('ENTITY_ID')
|
||||||
|
fund_id = os.getenv('FUND_ID')
|
||||||
|
|
||||||
|
# Filter out rows with invalid Journal IDs
|
||||||
|
# df = df[~df['Journal ID'].isin(['Journal ID'])]
|
||||||
|
print(df.head())
|
||||||
|
|
||||||
|
transactions = []
|
||||||
|
|
||||||
|
# Iterate through the DataFrame and prepare partner records
|
||||||
|
for index, row in df.iterrows():
|
||||||
|
|
||||||
|
tdate = row['Date']
|
||||||
|
transaction_record = {
|
||||||
|
"fundId": fund_id,
|
||||||
|
"date": tdate.strftime('%m/%d/%Y'),
|
||||||
|
"memo": row['Name'] if pd.notna(row['Name']) else row['Memo'],
|
||||||
|
"credit": row['Credit'] if pd.notna(row['Credit']) else 0.0,
|
||||||
|
"debit": row['Debit'] if pd.notna(row['Debit']) else 0.0,
|
||||||
|
"balance": row['Balance'] if pd.notna(row['Balance']) else 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
response = add_bank_transaction_records(transaction_record)
|
||||||
|
print(f"Adding transaction {index}:\n {transaction_record}\n\n")
|
||||||
|
if 'error' in response:
|
||||||
|
print(f"Failed to fetch onboarding status: Error: {response['error']}")
|
||||||
|
print(f"Status Code: {response['status_code']}")
|
||||||
|
break # Process next company investment record...
|
||||||
|
transactions.append(transaction_record)
|
||||||
|
|
||||||
|
# Test with smaller set of records
|
||||||
|
# if index == 1:
|
||||||
|
# break
|
||||||
|
|
||||||
|
return transactions
|
||||||
|
|
||||||
|
|
||||||
|
def process_bank_transactions(file_path):
|
||||||
|
# Process bank transactions
|
||||||
|
step_number = 5
|
||||||
|
step_number -= 1
|
||||||
|
success_message = ""
|
||||||
|
error_message = ""
|
||||||
|
status = ""
|
||||||
|
|
||||||
|
bucket_name = os.getenv('S3_UPLOAD_BUCKET_NAME')
|
||||||
|
print(f"Processing file: {file_path}")
|
||||||
|
response = update_onboarding_status(step_number, "IN-PROGRESS", "", "")
|
||||||
|
if 'error' in response:
|
||||||
|
print(f"Failed to fetch onboarding status: Error: {response['error']}")
|
||||||
|
print(f"Status Code: {response['status_code']}")
|
||||||
|
return False
|
||||||
|
journal_excel_file = read_file_from_s3(bucket_name, file_path)
|
||||||
|
sheet_name = 'Bank Transactions Report'
|
||||||
|
transaction_df = xlsx_to_df(journal_excel_file, sheet_name)
|
||||||
|
print(transaction_df.head())
|
||||||
|
transactions = process_bank_transaction_records(transaction_df)
|
||||||
|
if transactions:
|
||||||
|
item_count = len(transactions)
|
||||||
|
success_message = f"Onboarded {item_count} bank transactions."
|
||||||
|
status = "COMPLETE"
|
||||||
|
else:
|
||||||
|
error_message = "No transactions were onboaerded."
|
||||||
|
status = "FAILED"
|
||||||
|
|
||||||
|
response = update_onboarding_status(step_number, status, error_message, success_message)
|
||||||
|
if 'error' in response:
|
||||||
|
print(f"Failed to fetch onboarding status: Error: {response['error']}")
|
||||||
|
print(f"Status Code: {response['status_code']}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# print(f"Journals list:\n {transactions}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
onboardingId = "66ea163564e2f97a059160ef"
|
||||||
|
os.environ["ONBOARDING_ID"] = onboardingId
|
||||||
|
os.environ["FUND_ID"] = "66c5e6d89ecbf552a05b84fc"
|
||||||
|
|
||||||
|
from initOnboarding import initialize_onboarding
|
||||||
|
initialize_onboarding()
|
||||||
|
|
||||||
|
found_files = {'lpa': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/0-Please_DocuSign_CerraCap_II_LP_Limited_Partn.pdf', 'partner': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/0-1-cerracap-ii-lp_2024-07_09_short_partner.xlsx', 'financials': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/2-3-cerracap-ii-lp_2024-08-26_financials.xlsx', 'bankTransactions': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/4-CerraCap_II__LP_bank_transactions_2016-01-01-2024-07-03.xlsx', 'journals': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/5-cerracap-ii-lp_2024-07-09_journals-export.xlsx', 'fund_performance': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/6-cerracap-ii-lp_2024-07-09_fund-performance-report.xlsx'}
|
||||||
|
financials_excel_file_path = found_files["bankTransactions"]
|
||||||
|
process_bank_transactions(financials_excel_file_path)
|
||||||
243
task/onboardFinancials.py
Normal file
243
task/onboardFinancials.py
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
import pandas as pd
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
from s3Ops import read_file_from_s3
|
||||||
|
from utils.prompts import ZIVE_SECURITY_TYPE_PROMPT
|
||||||
|
|
||||||
|
from backendAPIs import (
|
||||||
|
update_onboarding_status,
|
||||||
|
add_portfolio_company,
|
||||||
|
add_portfolio_company_investment
|
||||||
|
)
|
||||||
|
|
||||||
|
# LLM via the unified Gemini-only backend (see task/llm.py).
|
||||||
|
from llm import get_chat_model
|
||||||
|
|
||||||
|
model_parameter = {"temperature": 0, "max_tokens": 16384}
|
||||||
|
llm_bedrock = get_chat_model(model_kwargs=model_parameter)
|
||||||
|
|
||||||
|
|
||||||
|
def xlsx_to_df(xlsx_file, sheet_name):
|
||||||
|
# Read the Excel file, skipping the header rows
|
||||||
|
|
||||||
|
df = pd.read_excel(xlsx_file, sheet_name=sheet_name, header=5, na_values=[''])
|
||||||
|
|
||||||
|
print(df.columns)
|
||||||
|
|
||||||
|
investments = {}
|
||||||
|
current_investment = None
|
||||||
|
|
||||||
|
# Iterate through the rows
|
||||||
|
for index, row in df.iterrows():
|
||||||
|
if pd.notna(row['Investment legal name']):
|
||||||
|
# Start a new investment
|
||||||
|
current_investment = row['Investment legal name']
|
||||||
|
print(current_investment)
|
||||||
|
investments[current_investment] = {'securities': [], 'security_count': 0}
|
||||||
|
|
||||||
|
elif pd.notna(row['Security type']) and current_investment is not None:
|
||||||
|
security_type = row['Security type'].strip().lower()
|
||||||
|
|
||||||
|
if security_type != 'totals' and security_type != 'grand totals':
|
||||||
|
# Increment the security count for this investment
|
||||||
|
investments[current_investment]['security_count'] += 1
|
||||||
|
|
||||||
|
# Add security details to the current investment
|
||||||
|
security = {
|
||||||
|
'type': row['Security type'],
|
||||||
|
'investment_date': row['Investment date'],
|
||||||
|
'investment_number': investments[current_investment]['security_count']
|
||||||
|
}
|
||||||
|
investments[current_investment]['securities'].append(security)
|
||||||
|
|
||||||
|
return investments
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: add the remaining asset type mapping
|
||||||
|
# def get_security_type(asset_type):
|
||||||
|
# asset_to_security_type = {
|
||||||
|
# "Common stock":"COMMON",
|
||||||
|
# "Preferred stock":"PREFERRED",
|
||||||
|
# "Warrants":"WARRANTS",
|
||||||
|
# "Convertible promissory note/SAFEs": "POST MONEY SAFE"
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# for asset, security_type in asset_to_security_type.items():
|
||||||
|
# if asset_type.lower() in asset.lower(): # Case-insensitive match
|
||||||
|
# return security_type
|
||||||
|
#
|
||||||
|
# return ""
|
||||||
|
def get_security_type(asset_type):
|
||||||
|
|
||||||
|
print(asset_type)
|
||||||
|
print("Calling LLM to get the security type")
|
||||||
|
|
||||||
|
FINAL_ZIVE_SECURITY_TYPE_PROMPT = ZIVE_SECURITY_TYPE_PROMPT.format(security_type=asset_type)
|
||||||
|
|
||||||
|
output = llm_bedrock.invoke(FINAL_ZIVE_SECURITY_TYPE_PROMPT).content
|
||||||
|
|
||||||
|
pattern = r'<output>(.*?)</output>'
|
||||||
|
match = re.search(pattern, output, re.DOTALL)
|
||||||
|
|
||||||
|
final_data = match.group(1).strip()
|
||||||
|
|
||||||
|
return final_data
|
||||||
|
|
||||||
|
|
||||||
|
def process_financial_records(investments_df):
|
||||||
|
"""
|
||||||
|
Process and insert users from an Excel file into MongoDB.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path (str): Path to the Excel file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
entity_id = os.getenv('ENTITY_ID')
|
||||||
|
fund_id = os.getenv('FUND_ID')
|
||||||
|
|
||||||
|
# print(f"Header:\n {partners_df.head()}")
|
||||||
|
|
||||||
|
investments = []
|
||||||
|
# current_company = None
|
||||||
|
# portfoltio_company_id = None
|
||||||
|
# investment_number = 0
|
||||||
|
|
||||||
|
for investment, details in investments_df.items():
|
||||||
|
|
||||||
|
print(f"Adding company {investment}")
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"fundId": fund_id,
|
||||||
|
"companyName": investment
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f"\nInvestment: {investment}")
|
||||||
|
response = add_portfolio_company(payload)
|
||||||
|
|
||||||
|
if 'error' in response:
|
||||||
|
print(f"Failed to fetch onboarding status: Error: {response['error']}")
|
||||||
|
print(f"Status Code: {response['status_code']}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
portfoltio_company_id = response["data"]["data"]["_id"]
|
||||||
|
|
||||||
|
for security in details['securities']:
|
||||||
|
|
||||||
|
print(f" Security Type: {security['type']}")
|
||||||
|
print(f" Investment Date: {security['investment_date']}")
|
||||||
|
print(f" Investment Number: {security['investment_number']}")
|
||||||
|
|
||||||
|
investment = {
|
||||||
|
"fundId": fund_id,
|
||||||
|
"portfolioId": portfoltio_company_id,
|
||||||
|
"investmentNumber": security['investment_number'],
|
||||||
|
"investmentDate": security['investment_date'],
|
||||||
|
"securityType": get_security_type(security['type']),
|
||||||
|
"assetName": security['type'],
|
||||||
|
"asset": security['type']
|
||||||
|
}
|
||||||
|
|
||||||
|
print(investment)
|
||||||
|
|
||||||
|
response = add_portfolio_company_investment(investment)
|
||||||
|
|
||||||
|
if 'error' in response:
|
||||||
|
print(response)
|
||||||
|
print(f"Failed to fetch onboarding status: Error: {response['error']}")
|
||||||
|
print(f"Status Code: {response['status_code']}")
|
||||||
|
continue # Process next company investment record...
|
||||||
|
investments.append(investment)
|
||||||
|
|
||||||
|
|
||||||
|
# Iterate through the DataFrame and prepare partner records
|
||||||
|
# for index, row in financials_df.iterrows():
|
||||||
|
#
|
||||||
|
# if current_company != row['Investment']:
|
||||||
|
# current_company = row['Investment']
|
||||||
|
# investment_number = 0
|
||||||
|
#
|
||||||
|
# # make the call to add portfolio company
|
||||||
|
# print(f"Adding company {current_company}")
|
||||||
|
# payload = {
|
||||||
|
# "fundId": fund_id,
|
||||||
|
# "companyName": current_company
|
||||||
|
# }
|
||||||
|
# response = add_portfolio_company(payload)
|
||||||
|
# if 'error' in response:
|
||||||
|
# print(f"Failed to fetch onboarding status: Error: {response['error']}")
|
||||||
|
# print(f"Status Code: {response['status_code']}")
|
||||||
|
# break #Process next company record...
|
||||||
|
|
||||||
|
# portfoltio_company_id = response["data"]["data"]["_id"]
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# investment_number += 1
|
||||||
|
# asset_type = row['Asset type'] if pd.notna(row['Asset type']) else None
|
||||||
|
#
|
||||||
|
# investment = {
|
||||||
|
# "fundId": fund_id,
|
||||||
|
# "portfolioId": portfoltio_company_id,
|
||||||
|
# "investmentNumber": investment_number,
|
||||||
|
# "investmentDate": row['Investment date'] if pd.notna(row['Investment date']) else None,
|
||||||
|
# "securityType": get_security_type(asset_type),
|
||||||
|
# "assetName": row['Asset'] if pd.notna(row['Asset']) else None,
|
||||||
|
# "asset": asset_type
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# response = add_portfolio_company_investment(investment)
|
||||||
|
# if 'error' in response:
|
||||||
|
# print(f"Failed to fetch onboarding status: Error: {response['error']}")
|
||||||
|
# print(f"Status Code: {response['status_code']}")
|
||||||
|
# break # Process next company investment record...
|
||||||
|
# investments.append(investment)
|
||||||
|
# print(f"Adding portforlio investment {index} {current_company}:{portfoltio_company_id}:\n {investment}")
|
||||||
|
|
||||||
|
# Test with smaller set of records
|
||||||
|
# if index == 1:
|
||||||
|
# break
|
||||||
|
|
||||||
|
return investments
|
||||||
|
|
||||||
|
|
||||||
|
def process_financials(file_path):
|
||||||
|
# Process partner data
|
||||||
|
step_number = 4
|
||||||
|
step_number -= 1
|
||||||
|
success_message = ""
|
||||||
|
error_message = ""
|
||||||
|
status = "IN-PROGRESS"
|
||||||
|
|
||||||
|
bucket_name = os.getenv('S3_UPLOAD_BUCKET_NAME')
|
||||||
|
print(f"Processing file: {file_path}")
|
||||||
|
response = update_onboarding_status(step_number, status, error_message, success_message)
|
||||||
|
if 'error' in response:
|
||||||
|
print(f"Failed to fetch onboarding status: Error: {response['error']}")
|
||||||
|
print(f"Status Code: {response['status_code']}")
|
||||||
|
return False
|
||||||
|
partner_excel_file = read_file_from_s3(bucket_name, file_path)
|
||||||
|
sheet_name = 'Roll Forward'
|
||||||
|
financial_df = xlsx_to_df(partner_excel_file, sheet_name)
|
||||||
|
# print(financial_df.head())
|
||||||
|
financials = process_financial_records(financial_df)
|
||||||
|
if financials:
|
||||||
|
item_count = len(financials)
|
||||||
|
success_message = f"Onboarded {item_count} portfolio financial records."
|
||||||
|
status = "COMPLETE"
|
||||||
|
else:
|
||||||
|
error_message = "No transactions were onboaerded."
|
||||||
|
status = "FAILED"
|
||||||
|
|
||||||
|
response = update_onboarding_status(step_number, status, error_message, success_message)
|
||||||
|
if 'error' in response:
|
||||||
|
print(f"Failed to fetch onboarding status: Error: {response['error']}")
|
||||||
|
print(f"Status Code: {response['status_code']}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# print(f"Investments list:\n {financials}")
|
||||||
|
# print(json.dumps(partners_dict, indent=2, sort_keys=True, cls=CustomJSONEncoder))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
279
task/onboardFunds.py
Normal file
279
task/onboardFunds.py
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
from PIL import Image
|
||||||
|
import boto3
|
||||||
|
import fitz
|
||||||
|
import re
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from llm import get_llm_client
|
||||||
|
import tiktoken
|
||||||
|
import base64
|
||||||
|
import time
|
||||||
|
|
||||||
|
from s3Ops import (
|
||||||
|
read_file_from_s3
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def pdf_to_image(pdf_key, input_bucket_name, output_bucket_name):
|
||||||
|
s3 = boto3.client('s3')
|
||||||
|
|
||||||
|
print(f"Reading file from {input_bucket_name}, with key {pdf_key}")
|
||||||
|
# Load the PDF file into memory
|
||||||
|
pdf_object = s3.get_object(Bucket=input_bucket_name, Key=pdf_key)
|
||||||
|
pdf_content = pdf_object['Body'].read()
|
||||||
|
|
||||||
|
# Extract file name from pdf_key
|
||||||
|
file_name = pdf_key.split('/')[-1]
|
||||||
|
output_file_name = file_name.rsplit('.', 1)[0] # Remove file extension
|
||||||
|
|
||||||
|
image_list = []
|
||||||
|
|
||||||
|
# Open the PDF
|
||||||
|
pdf_document = fitz.open(stream=pdf_content, filetype="pdf")
|
||||||
|
|
||||||
|
for page_num in range(len(pdf_document)):
|
||||||
|
page = pdf_document.load_page(page_num)
|
||||||
|
|
||||||
|
scale = 1.8
|
||||||
|
pix = page.get_pixmap(matrix=fitz.Matrix(scale, scale))
|
||||||
|
|
||||||
|
# Convert pixmap to PNG bytes directly
|
||||||
|
img_bytes = pix.tobytes("jpg")
|
||||||
|
|
||||||
|
image_key = f"{output_file_name}/{output_file_name}_{page_num + 1}.jpg"
|
||||||
|
image_list.append(image_key)
|
||||||
|
|
||||||
|
# Upload the image to S3
|
||||||
|
s3.put_object(Bucket=output_bucket_name, Key=image_key, Body=img_bytes, ContentType='image/jpg')
|
||||||
|
|
||||||
|
return image_list
|
||||||
|
|
||||||
|
|
||||||
|
def get_images(image_key, bucket):
|
||||||
|
s3 = boto3.client('s3')
|
||||||
|
|
||||||
|
# Get the object from S3
|
||||||
|
response = s3.get_object(Bucket=bucket, Key=image_key)
|
||||||
|
|
||||||
|
# Read the content of the file
|
||||||
|
image_data = response['Body'].read()
|
||||||
|
|
||||||
|
return image_data
|
||||||
|
|
||||||
|
|
||||||
|
# for use with OCR/last pages
|
||||||
|
def create_message(extracted_texts, prompt, json_template, last_images64):
|
||||||
|
message = {
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": f"Prompt:\n{prompt}\n\n"
|
||||||
|
f"JSON Template:\n```json\n{json_template}```\n\n"
|
||||||
|
f"Text to analyze:\n"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add extracted texts
|
||||||
|
for index, text in enumerate(extracted_texts):
|
||||||
|
message["content"].append({
|
||||||
|
"type": "text",
|
||||||
|
"text": f"Page {index + 1} limited partnership agreement document:\n{text}\n"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Add a separator before images
|
||||||
|
message["content"].append({
|
||||||
|
"type": "text",
|
||||||
|
"text": "\nLast ten corresponding images to analyze:\n"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Add the last few images to the message
|
||||||
|
for i, image_data in enumerate(last_images64):
|
||||||
|
# Add the image
|
||||||
|
message["content"].append({
|
||||||
|
"type": "image",
|
||||||
|
"source": {
|
||||||
|
"type": "base64",
|
||||||
|
"media_type": "image/jpeg",
|
||||||
|
"data": image_data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Add a text description for each image
|
||||||
|
message["content"].append({
|
||||||
|
"type": "text",
|
||||||
|
"text": f"Image {i + 1} (corresponds to Page {len(extracted_texts) - len(last_images64) + i + 1})\n"
|
||||||
|
})
|
||||||
|
|
||||||
|
return message
|
||||||
|
|
||||||
|
|
||||||
|
def ask_llm(message):
|
||||||
|
|
||||||
|
# increase timeout
|
||||||
|
# (boto3/Config removed — the Gemini client manages its own timeouts)
|
||||||
|
|
||||||
|
# initialize unified Gemini LLM client (.invoke_model() drop-in; see task/llm.py)
|
||||||
|
bedrock = get_llm_client()
|
||||||
|
|
||||||
|
# construct the request body for bedrock API
|
||||||
|
body = json.dumps({
|
||||||
|
"max_tokens": 16384,
|
||||||
|
"system": "You are a financial advisor specializing in extracting and evaluating key information from financial documents. We are discussing terms within a limited partnership agreement document.",
|
||||||
|
"messages": [
|
||||||
|
message,
|
||||||
|
{"role": "assistant", "content": '```json'} # Prefill here
|
||||||
|
],
|
||||||
|
"anthropic_version": "bedrock-2023-05-31"
|
||||||
|
})
|
||||||
|
|
||||||
|
# invoke the model
|
||||||
|
response = bedrock.invoke_model(body=body, modelId="anthropic.claude-3-5-sonnet-20240620-v1:0")
|
||||||
|
# parse the response
|
||||||
|
response_body = json.loads(response.get("body").read())
|
||||||
|
|
||||||
|
# return the OCR result
|
||||||
|
return response_body['content'][0]['text']
|
||||||
|
|
||||||
|
|
||||||
|
def detailed_token_count(message, model="cl100k_base"):
|
||||||
|
enc = tiktoken.get_encoding(model)
|
||||||
|
total_tokens = 0
|
||||||
|
breakdown = {}
|
||||||
|
|
||||||
|
# Count tokens for the role
|
||||||
|
role_tokens = len(enc.encode(message["role"]))
|
||||||
|
total_tokens += role_tokens
|
||||||
|
breakdown["role"] = role_tokens
|
||||||
|
|
||||||
|
# Count tokens for each content item
|
||||||
|
for i, item in enumerate(message["content"]):
|
||||||
|
if item["type"] == "text":
|
||||||
|
text_tokens = len(enc.encode(item["text"]))
|
||||||
|
total_tokens += text_tokens
|
||||||
|
breakdown[f"content_{i}"] = text_tokens
|
||||||
|
|
||||||
|
# Add tokens for the "type" field itself
|
||||||
|
type_tokens = len(enc.encode(item["type"]))
|
||||||
|
total_tokens += type_tokens
|
||||||
|
breakdown[f"type_{i}"] = type_tokens
|
||||||
|
|
||||||
|
return total_tokens, breakdown
|
||||||
|
|
||||||
|
|
||||||
|
def clean_json(text):
|
||||||
|
# Find the start and end of the JSON object
|
||||||
|
start = output.find('{')
|
||||||
|
end = output.rfind('}') + 1
|
||||||
|
|
||||||
|
# Extract the JSON string
|
||||||
|
json_str = output[start:end]
|
||||||
|
|
||||||
|
# Parse the JSON string to ensure it's valid
|
||||||
|
try:
|
||||||
|
json.loads(json_str)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return None # Return None if the JSON is invalid
|
||||||
|
|
||||||
|
return json_str
|
||||||
|
|
||||||
|
|
||||||
|
def add_backtack(text):
|
||||||
|
# Find the position of the first '{'
|
||||||
|
start_pos = text.find('{')
|
||||||
|
|
||||||
|
# If there's no '{', return the original text
|
||||||
|
if start_pos == -1:
|
||||||
|
return text
|
||||||
|
|
||||||
|
# Find the start of the line containing '{'
|
||||||
|
line_start = text.rfind('\n', 0, start_pos)
|
||||||
|
if line_start == -1:
|
||||||
|
line_start = 0
|
||||||
|
else:
|
||||||
|
line_start += 1 # Move past the newline character
|
||||||
|
|
||||||
|
# Insert '```json\n' before the line containing '{'
|
||||||
|
return text[:line_start] + '```json\n' + text[line_start:]
|
||||||
|
|
||||||
|
|
||||||
|
def onboardFunds(lpa_file_path):
|
||||||
|
|
||||||
|
input_bucket = os.getenv('S3_UPLOAD_BUCKET_NAME')
|
||||||
|
output_bucket = input_bucket
|
||||||
|
|
||||||
|
image_keys = pdf_to_image(lpa_file_path, input_bucket, output_bucket)
|
||||||
|
images = []
|
||||||
|
for image_key in image_keys:
|
||||||
|
image = get_images(image_key, output_bucket)
|
||||||
|
images.append(image)
|
||||||
|
|
||||||
|
print(image_keys[-6:-1])
|
||||||
|
|
||||||
|
os.environ['TESSDATA_PREFIX'] = 'RTC:pdf processing/eng.traineddata'
|
||||||
|
extracted_texts2 = []
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
for i, image in enumerate(images, 1):
|
||||||
|
# Open the image using PIL
|
||||||
|
img = Image.open(io.BytesIO(image)).convert('L')
|
||||||
|
|
||||||
|
# Append the extracted text to the list
|
||||||
|
extracted_texts2.append(text)
|
||||||
|
|
||||||
|
# Print status update every 10 images
|
||||||
|
if i % 10 == 0:
|
||||||
|
elapsed_time = time.time() - start_time
|
||||||
|
print(f"Processed {i} images out of {len(images)} in {elapsed_time:.2f} seconds")
|
||||||
|
|
||||||
|
# Calculate total time
|
||||||
|
total_time = time.time() - start_time
|
||||||
|
|
||||||
|
# Print final status after processing all images
|
||||||
|
print(f"Finished processing all {len(images)} images in {total_time:.2f} seconds")
|
||||||
|
print(f"Average time per image: {total_time/len(images):.2f} seconds")
|
||||||
|
|
||||||
|
last_images64 = []
|
||||||
|
for image in images[-10:]:
|
||||||
|
image = base64.b64encode(image).decode("utf-8")
|
||||||
|
last_images64.append(image)
|
||||||
|
|
||||||
|
print(len(extracted_texts2))
|
||||||
|
print(extracted_texts2[-2])
|
||||||
|
|
||||||
|
message = create_message(extracted_texts2, prompt2, json_template, last_images64)
|
||||||
|
|
||||||
|
#print(f"\nFinal message structure:")
|
||||||
|
#print(f"Number of content items: {len(message['content'])}")
|
||||||
|
#for i, item in enumerate(message['content']):
|
||||||
|
# print(f"Content {i} length: {len(item['text'])}")
|
||||||
|
|
||||||
|
# Use the detailed token count function
|
||||||
|
total_count, breakdown = detailed_token_count(message)
|
||||||
|
|
||||||
|
print(f"\nTotal token count: {total_count}")
|
||||||
|
#print("Token breakdown:")
|
||||||
|
#for key, value in breakdown.items():
|
||||||
|
# print(f" {key}: {value}")
|
||||||
|
|
||||||
|
outputs = ask_llm(message)
|
||||||
|
|
||||||
|
print(outputs)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
onboardingId = "66ea163564e2f97a059160ef"
|
||||||
|
os.environ["ONBOARDING_ID"] = onboardingId
|
||||||
|
|
||||||
|
from initOnboarding import initialize_onboarding
|
||||||
|
initialize_onboarding()
|
||||||
|
|
||||||
|
found_files = {'lpa': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/0-Please_DocuSign_CerraCap_II_LP_Limited_Partn.pdf', 'partner': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/0-1-cerracap-ii-lp_2024-07_09_short_partner.xlsx', 'financials': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/2-3-cerracap-ii-lp_2024-08-26_financials.xlsx', 'bankTransactions': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/4-CerraCap_II__LP_bank_transactions_2016-01-01-2024-07-03.xlsx', 'journals': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/5-cerracap-ii-lp_2024-07-09_journals-export.xlsx', 'fund_performance': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/6-cerracap-ii-lp_2024-07-09_fund-performance-report.xlsx'}
|
||||||
|
lpa_file_path = found_files["lpa"]
|
||||||
|
onboardFunds(lpa_file_path)
|
||||||
132
task/onboardFundsDummy.py
Normal file
132
task/onboardFundsDummy.py
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import io
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
|
import time
|
||||||
|
from s3Ops import read_file_from_s3
|
||||||
|
import pandas as pd
|
||||||
|
import re
|
||||||
|
from backendAPIs import (
|
||||||
|
update_fund,
|
||||||
|
update_onboarding_status
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean_currency(x):
|
||||||
|
try:
|
||||||
|
if isinstance(x, str):
|
||||||
|
return float(re.sub(r'[\$,]', '', x))
|
||||||
|
return float(x)
|
||||||
|
except:
|
||||||
|
return 0 # or np.nan if you prefer
|
||||||
|
|
||||||
|
|
||||||
|
def get_partner_summary(df):
|
||||||
|
# Calculate values
|
||||||
|
total_commitment = df['Commitment'].apply(clean_currency).sum()
|
||||||
|
earliest_date = pd.to_datetime(df['Issue date']).min()
|
||||||
|
general_partner = df[df['Class'] == 'General Partner']['Partner'].iloc[0]
|
||||||
|
|
||||||
|
# Create dictionary with results
|
||||||
|
summary_dict = {
|
||||||
|
'total_commitments': total_commitment,
|
||||||
|
'earliest_issue_date': earliest_date.strftime('%m/%d/%Y'),
|
||||||
|
'general_partner': general_partner
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary_dict
|
||||||
|
|
||||||
|
|
||||||
|
def get_partners_from_excel(xlsx_file):
|
||||||
|
"""
|
||||||
|
Read Excel file containing pa information.
|
||||||
|
Remove headers and return clean df
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path (str): Path to the Excel file.
|
||||||
|
sheet_name (str): Excel sheet name with partner list
|
||||||
|
Returns:
|
||||||
|
pd.DataFrame: DataFrame containing partner information.
|
||||||
|
"""
|
||||||
|
sheet_name = 'Partners'
|
||||||
|
df = pd.read_excel(xlsx_file, sheet_name = sheet_name)
|
||||||
|
df.columns = df.iloc[2]
|
||||||
|
df = df.drop(df.index[:3])
|
||||||
|
df = df.reset_index(drop=True)
|
||||||
|
|
||||||
|
fields_dict = get_partner_summary(df)
|
||||||
|
|
||||||
|
return fields_dict
|
||||||
|
|
||||||
|
def onboardFunds(lpa_file_path):
|
||||||
|
|
||||||
|
|
||||||
|
# input_bucket = os.getenv('S3_UPLOAD_BUCKET_NAME')
|
||||||
|
step_number = 2
|
||||||
|
step_number -= 1
|
||||||
|
success_message = ""
|
||||||
|
error_message = ""
|
||||||
|
status = "IN-PROGRESS"
|
||||||
|
|
||||||
|
print("IN-PROGREE")
|
||||||
|
|
||||||
|
response = update_onboarding_status(step_number, status, error_message, success_message)
|
||||||
|
|
||||||
|
bucket_name = os.getenv('S3_UPLOAD_BUCKET_NAME')
|
||||||
|
print(f"Processing file: {lpa_file_path}")
|
||||||
|
response = update_onboarding_status(step_number, status, error_message, success_message)
|
||||||
|
if 'error' in response:
|
||||||
|
print(f"Failed to fetch onboarding status: Error: {response['error']}")
|
||||||
|
print(f"Status Code: {response['status_code']}")
|
||||||
|
return False
|
||||||
|
partner_excel_file = read_file_from_s3(bucket_name,lpa_file_path)
|
||||||
|
partners_df = get_partners_from_excel(partner_excel_file)
|
||||||
|
print(partners_df)
|
||||||
|
|
||||||
|
fund_data = {
|
||||||
|
"fundSize": partners_df["total_commitments"],
|
||||||
|
"dateFormed": partners_df["earliest_issue_date"],
|
||||||
|
"fundDuration": "10",
|
||||||
|
"gpName": partners_df["general_partner"],
|
||||||
|
"investmentperiodmanagementFee": "2%",
|
||||||
|
"mgmtCoName": "Test Mgt Co",
|
||||||
|
"postInvestmentPeriodmanagementFee": "4%"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = update_fund(fund_data)
|
||||||
|
print(f"Response: \n {response}")
|
||||||
|
fund_id = response["data"]["data"]["_id"]
|
||||||
|
print(f"Setting fund id in the environment {fund_id}")
|
||||||
|
os.environ["FUND_ID"] = fund_id
|
||||||
|
|
||||||
|
|
||||||
|
if fund_id:
|
||||||
|
success_message = f"Onboarded fund {fund_id} journal records."
|
||||||
|
status = "COMPLETE"
|
||||||
|
else:
|
||||||
|
error_message = "No journals were onboaerded."
|
||||||
|
status = "FAILED"
|
||||||
|
|
||||||
|
response = update_onboarding_status(step_number, status, error_message, success_message)
|
||||||
|
if 'error' in response:
|
||||||
|
print(f"Failed to fetch onboarding status: Error: {response['error']}")
|
||||||
|
print(f"Status Code: {response['status_code']}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return fund_id
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
onboardingId = "66ea163564e2f97a059160ef"
|
||||||
|
os.environ["ONBOARDING_ID"] = onboardingId
|
||||||
|
|
||||||
|
from initOnboarding import initialize_onboarding
|
||||||
|
initialize_onboarding()
|
||||||
|
|
||||||
|
found_files = {'lpa': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/0-Please_DocuSign_CerraCap_II_LP_Limited_Partn.pdf', 'partner': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/0-1-cerracap-ii-lp_2024-07_09_short_partner.xlsx', 'financials': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/2-3-cerracap-ii-lp_2024-08-26_financials.xlsx', 'bankTransactions': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/4-CerraCap_II__LP_bank_transactions_2016-01-01-2024-07-03.xlsx', 'journals': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/5-cerracap-ii-lp_2024-07-09_journals-export.xlsx', 'fund_performance': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/6-cerracap-ii-lp_2024-07-09_fund-performance-report.xlsx'}
|
||||||
|
lpa_file_path = found_files["lpa"]
|
||||||
|
onboardFunds(lpa_file_path)
|
||||||
349
task/onboardJournals.py
Normal file
349
task/onboardJournals.py
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
import pandas as pd
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from utils.prompts import ACCOUNT_TYPE_PROMPT
|
||||||
|
from utils.account_type import account_type_data
|
||||||
|
import re
|
||||||
|
|
||||||
|
from s3Ops import read_file_from_s3
|
||||||
|
|
||||||
|
from backendAPIs import (
|
||||||
|
update_onboarding_status,
|
||||||
|
add_journal,
|
||||||
|
add_journal_ledger
|
||||||
|
)
|
||||||
|
|
||||||
|
# LLM via the unified Gemini-only backend (see task/llm.py).
|
||||||
|
from llm import get_chat_model
|
||||||
|
|
||||||
|
model_parameter = {"temperature": 0, "max_tokens": 16384}
|
||||||
|
llm_bedrock = get_chat_model(model_kwargs=model_parameter)
|
||||||
|
|
||||||
|
|
||||||
|
def xlsx_to_df(xlsx_file, sheet_name):
|
||||||
|
df = pd.read_excel(xlsx_file, sheet_name=sheet_name, header=2, skiprows=2)
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
# TODO: add the remaining asset type mapping
|
||||||
|
|
||||||
|
|
||||||
|
def get_security_type(asset_type):
|
||||||
|
asset_to_security_type = {
|
||||||
|
"Common stock": "COMMON",
|
||||||
|
"Preferred stock": "PREFERRED",
|
||||||
|
"Warrants": "WARRANTS",
|
||||||
|
"Convertible promissory note/SAFEs": "POST MONEY SAFE"
|
||||||
|
}
|
||||||
|
|
||||||
|
for asset, security_type in asset_to_security_type.items():
|
||||||
|
if asset_type.lower() in asset.lower(): # Case-insensitive match
|
||||||
|
return security_type
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def get_account_type(account_type):
|
||||||
|
|
||||||
|
print(account_type.split("-")[0])
|
||||||
|
print(account_type.split("-")[1].lower().strip())
|
||||||
|
|
||||||
|
if account_type.split("-")[0].strip() == "1000" and account_type.split("-")[1].lower().strip() == "bank":
|
||||||
|
return "Bank"
|
||||||
|
|
||||||
|
result = account_type.split("-", 1)
|
||||||
|
|
||||||
|
# Strip whitespace from both parts
|
||||||
|
account_lookup_string = [part.strip() for part in result][1]
|
||||||
|
|
||||||
|
print(account_lookup_string)
|
||||||
|
|
||||||
|
if len(account_type_data.get(account_lookup_string.strip(), "")) > 0:
|
||||||
|
print("from dict")
|
||||||
|
return account_type_data.get(account_lookup_string.strip())
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("llm call")
|
||||||
|
FINAL_ZIVE_ACCOUNT_TYPE_PROMPT = ACCOUNT_TYPE_PROMPT.format(
|
||||||
|
account_type=account_type)
|
||||||
|
|
||||||
|
output = llm_bedrock.invoke(FINAL_ZIVE_ACCOUNT_TYPE_PROMPT).content
|
||||||
|
|
||||||
|
pattern = r'<output>(.*?)</output>'
|
||||||
|
match = re.search(pattern, output, re.DOTALL)
|
||||||
|
|
||||||
|
final_data = match.group(1).strip()
|
||||||
|
|
||||||
|
return final_data
|
||||||
|
|
||||||
|
|
||||||
|
def process_journal_records(df):
|
||||||
|
"""
|
||||||
|
Process and insert users from an Excel file into MongoDB.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path (str): Path to the Excel file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
entity_id = os.getenv('ENTITY_ID')
|
||||||
|
fund_id = os.getenv('FUND_ID')
|
||||||
|
|
||||||
|
# Filter out rows with invalid Journal IDs
|
||||||
|
# df = df[~df['Journal ID'].isin(['Journal ID'])]
|
||||||
|
# print(df)
|
||||||
|
journals = []
|
||||||
|
current_journal = None
|
||||||
|
journal_id = None
|
||||||
|
journal_number = 0
|
||||||
|
jcredit = 0
|
||||||
|
jdebit = 0
|
||||||
|
jdate = None
|
||||||
|
jeventType = None
|
||||||
|
jdescription = None
|
||||||
|
jid = None
|
||||||
|
jls = []
|
||||||
|
# Iterate through the DataFrame and prepare partner records
|
||||||
|
for index, row in df.iterrows():
|
||||||
|
if pd.notna(row['Journal ID']):
|
||||||
|
if current_journal is not None and current_journal != row['Journal ID']:
|
||||||
|
current_journal = row['Journal ID']
|
||||||
|
# journal_number = 0
|
||||||
|
|
||||||
|
# make the call to add portfolio company
|
||||||
|
# print(f"Adding company {current_journal}")
|
||||||
|
|
||||||
|
# Handle NaN values in description field
|
||||||
|
if pd.isna(jdescription) or jdescription is None or str(jdescription).lower() == 'nan':
|
||||||
|
jdescription = "Migration"
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"dateOfJournalEntry": jdate,
|
||||||
|
"eventType": jeventType,
|
||||||
|
"entryDescription": jdescription,
|
||||||
|
"debitAmount": jdebit,
|
||||||
|
"creditAmount": jcredit,
|
||||||
|
}
|
||||||
|
# print(payload)
|
||||||
|
print(payload)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = add_journal(payload)
|
||||||
|
print(response)
|
||||||
|
|
||||||
|
# Check if the response was successful
|
||||||
|
if 'error' in response:
|
||||||
|
print(
|
||||||
|
f"ERROR: Failed to add journal entry: {response.get('error', 'Unknown error')}")
|
||||||
|
print(
|
||||||
|
f"Status Code: {response.get('status_code', 'N/A')}")
|
||||||
|
# Only skip THIS journal's ledger entries, not the entire process
|
||||||
|
# The next journal (with different Journal ID) will still be processed
|
||||||
|
print(
|
||||||
|
f"Skipping journal {current_journal}'s ledger entries but continuing with next journal...")
|
||||||
|
else:
|
||||||
|
# Only process ledger entries if journal was successfully created
|
||||||
|
if response and "data" in response and "data" in response["data"] and "_id" in response["data"]["data"]:
|
||||||
|
journal_id = response["data"]["data"]["_id"]
|
||||||
|
journals.append(payload)
|
||||||
|
|
||||||
|
# Process ledger entries for this journal
|
||||||
|
for roww in jls:
|
||||||
|
print(roww['accountType'])
|
||||||
|
try:
|
||||||
|
add_journal_ledger({
|
||||||
|
"accountType": get_account_type(roww['accountType']),
|
||||||
|
"portfolioCompany": roww['portfolioCompany'] if pd.notna(roww['portfolioCompany']) else '',
|
||||||
|
"partner": roww['partner'] if pd.notna(roww['partner']) else '',
|
||||||
|
"investmentInfo": roww['investmentInfo'] if pd.notna(roww['investmentInfo']) else '',
|
||||||
|
"debitAmount": roww['debitAmount'],
|
||||||
|
"creditAmount": roww['creditAmount'],
|
||||||
|
"shares": roww.get('shares', 0),
|
||||||
|
"journalId": journal_id
|
||||||
|
})
|
||||||
|
except Exception as ledger_error:
|
||||||
|
print(
|
||||||
|
f"ERROR: Failed to add journal ledger entry: {ledger_error}")
|
||||||
|
print("Continuing with next ledger entry...")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"WARNING: Journal created but no ID returned. Response: {response}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Exception while adding journal: {e}")
|
||||||
|
print(
|
||||||
|
"Skipping this journal's ledger entries but continuing with next journal...")
|
||||||
|
|
||||||
|
# Reset accumulators for next journal and add current row as first entry of next journal
|
||||||
|
jls = []
|
||||||
|
jcredit = 0
|
||||||
|
jdebit = 0
|
||||||
|
jls.append({
|
||||||
|
"accountType": row['Account'],
|
||||||
|
"portfolioCompany": row['Issuer'],
|
||||||
|
"investmentInfo": row['Asset'],
|
||||||
|
"partner": row['Partner'],
|
||||||
|
"debitAmount": row['Debit'],
|
||||||
|
"creditAmount": row['Credit'],
|
||||||
|
"shares": row['Shares'] if pd.notna(row['Shares']) else 0
|
||||||
|
})
|
||||||
|
jcredit += row['Credit']
|
||||||
|
jdebit += row['Debit']
|
||||||
|
jdate = row['Journal date']
|
||||||
|
jeventType = row['Event type']
|
||||||
|
jdescription = row['Description']
|
||||||
|
# print(response["data"])
|
||||||
|
# Removed the old error handling as it's now handled above
|
||||||
|
|
||||||
|
# journal_id = response["data"]["data"]["_id"]
|
||||||
|
# print(f"Adding journal:\n {payload}")
|
||||||
|
|
||||||
|
# journal_number += 1
|
||||||
|
else:
|
||||||
|
current_journal = row['Journal ID']
|
||||||
|
jcredit += row['Credit']
|
||||||
|
jdebit += row['Debit']
|
||||||
|
jdate = row['Journal date']
|
||||||
|
jeventType = row['Event type']
|
||||||
|
jdescription = row['Description']
|
||||||
|
jls.append({
|
||||||
|
"accountType": row['Account'],
|
||||||
|
"portfolioCompany": row['Issuer'],
|
||||||
|
"investmentInfo": row['Asset'],
|
||||||
|
"partner": row['Partner'],
|
||||||
|
"debitAmount": row['Debit'],
|
||||||
|
"creditAmount": row['Credit'],
|
||||||
|
"shares": row['Shares'] if pd.notna(row['Shares']) else 0
|
||||||
|
})
|
||||||
|
|
||||||
|
# response = add_journal_ledger(journal_ledger)
|
||||||
|
# print(f"Adding joural ledger {index} {current_journal}:{journal_id}:\n {journal_ledger}\n\n")
|
||||||
|
# if 'error' in response:
|
||||||
|
# print(f"Failed to fetch onboarding status: Error: {response['error']}")
|
||||||
|
# print(f"Status Code: {response['status_code']}")
|
||||||
|
# break # Process next company investment record...
|
||||||
|
# journals.append(journal_ledger)
|
||||||
|
|
||||||
|
# # Test with smaller set of records
|
||||||
|
# if index == 1:
|
||||||
|
# break
|
||||||
|
|
||||||
|
# Process the last journal entry after the loop
|
||||||
|
if len(jls) > 0:
|
||||||
|
# Handle NaN values in description field
|
||||||
|
if pd.isna(jdescription) or jdescription is None or str(jdescription).lower() == 'nan':
|
||||||
|
jdescription = ""
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"dateOfJournalEntry": jdate,
|
||||||
|
"eventType": jeventType,
|
||||||
|
"entryDescription": jdescription,
|
||||||
|
"debitAmount": jdebit,
|
||||||
|
"creditAmount": jcredit,
|
||||||
|
}
|
||||||
|
print(payload)
|
||||||
|
try:
|
||||||
|
response = add_journal(payload)
|
||||||
|
print(response)
|
||||||
|
|
||||||
|
# Check if the response was successful
|
||||||
|
if 'error' in response:
|
||||||
|
print(
|
||||||
|
f"ERROR: Failed to add last journal entry: {response.get('error', 'Unknown error')}")
|
||||||
|
print(f"Status Code: {response.get('status_code', 'N/A')}")
|
||||||
|
else:
|
||||||
|
# Only process ledger entries if journal was successfully created
|
||||||
|
if response and "data" in response and "data" in response["data"] and "_id" in response["data"]["data"]:
|
||||||
|
journal_id = response["data"]["data"]["_id"]
|
||||||
|
journals.append(payload)
|
||||||
|
|
||||||
|
# Process ledger entries for this journal
|
||||||
|
for roww in jls:
|
||||||
|
print(roww['accountType'])
|
||||||
|
try:
|
||||||
|
add_journal_ledger({
|
||||||
|
"accountType": get_account_type(roww['accountType']),
|
||||||
|
"portfolioCompany": roww['portfolioCompany'] if pd.notna(roww['portfolioCompany']) else '',
|
||||||
|
"partner": roww['partner'] if pd.notna(roww['partner']) else '',
|
||||||
|
"investmentInfo": roww['investmentInfo'] if pd.notna(roww['investmentInfo']) else '',
|
||||||
|
"debitAmount": roww['debitAmount'],
|
||||||
|
"creditAmount": roww['creditAmount'],
|
||||||
|
"shares": roww.get('shares', 0),
|
||||||
|
"journalId": journal_id
|
||||||
|
})
|
||||||
|
except Exception as ledger_error:
|
||||||
|
print(
|
||||||
|
f"ERROR: Failed to add journal ledger entry: {ledger_error}")
|
||||||
|
print("Continuing with next ledger entry...")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"WARNING: Last journal created but no ID returned. Response: {response}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Exception while adding last journal: {e}")
|
||||||
|
|
||||||
|
return journals
|
||||||
|
|
||||||
|
|
||||||
|
def process_journals(file_path):
|
||||||
|
# Process partner data
|
||||||
|
step_number = 6
|
||||||
|
step_number -= 1
|
||||||
|
success_message = ""
|
||||||
|
error_message = ""
|
||||||
|
status = "IN-PROGRESS"
|
||||||
|
|
||||||
|
bucket_name = os.getenv('S3_UPLOAD_BUCKET_NAME')
|
||||||
|
print(f"Processing file: {file_path}")
|
||||||
|
response = update_onboarding_status(
|
||||||
|
step_number, status, error_message, success_message)
|
||||||
|
if 'error' in response:
|
||||||
|
print(f"Failed to fetch onboarding status: Error: {response['error']}")
|
||||||
|
print(f"Status Code: {response['status_code']}")
|
||||||
|
return False
|
||||||
|
journal_excel_file = read_file_from_s3(bucket_name, file_path)
|
||||||
|
sheet_name = 'Posted journals'
|
||||||
|
financial_df = xlsx_to_df(journal_excel_file, sheet_name)
|
||||||
|
print("financial_df")
|
||||||
|
print(financial_df)
|
||||||
|
print(financial_df.columns)
|
||||||
|
journals = process_journal_records(financial_df)
|
||||||
|
|
||||||
|
if journals:
|
||||||
|
item_count = len(journals)
|
||||||
|
success_message = f"Onboarded {item_count} journal records."
|
||||||
|
status = "COMPLETE"
|
||||||
|
else:
|
||||||
|
error_message = "No journals were onboaerded."
|
||||||
|
status = "FAILED"
|
||||||
|
|
||||||
|
response = update_onboarding_status(
|
||||||
|
step_number, status, error_message, success_message)
|
||||||
|
if 'error' in response:
|
||||||
|
print(f"Failed to fetch onboarding status: Error: {response['error']}")
|
||||||
|
print(f"Status Code: {response['status_code']}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"Journals list:\n {journals}")
|
||||||
|
# print(json.dumps(partners_dict, indent=2, sort_keys=True, cls=CustomJSONEncoder))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
onboardingId = "66ea163564e2f97a059160ef"
|
||||||
|
os.environ["ONBOARDING_ID"] = onboardingId
|
||||||
|
os.environ["FUND_ID"] = "66c5e6d89ecbf552a05b84fc"
|
||||||
|
|
||||||
|
from initOnboarding import initialize_onboarding
|
||||||
|
initialize_onboarding()
|
||||||
|
|
||||||
|
found_files = {'lpa': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/0-Please_DocuSign_CerraCap_II_LP_Limited_Partn.pdf', 'partner': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/0-1-cerracap-ii-lp_2024-07_09_short_partner.xlsx', 'financials': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/2-3-cerracap-ii-lp_2024-08-26_financials.xlsx',
|
||||||
|
'bankTransactions': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/4-CerraCap_II__LP_bank_transactions_2016-01-01-2024-07-03.xlsx', 'journals': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/5-cerracap-ii-lp_2024-07-09_journals-export.xlsx', 'fund_performance': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/6-cerracap-ii-lp_2024-07-09_fund-performance-report.xlsx'}
|
||||||
|
financials_excel_file_path = found_files["journals"]
|
||||||
|
process_journals(financials_excel_file_path)
|
||||||
240
task/onboardPartners.py
Normal file
240
task/onboardPartners.py
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
import pandas as pd
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
# from bson import ObjectId
|
||||||
|
# from pymongo.results import UpdateResult
|
||||||
|
|
||||||
|
from s3Ops import read_file_from_s3
|
||||||
|
|
||||||
|
from backendAPIs import (
|
||||||
|
update_onboarding_status,
|
||||||
|
add_role
|
||||||
|
)
|
||||||
|
|
||||||
|
# class CustomJSONEncoder(json.JSONEncoder):
|
||||||
|
# def default(self, obj):
|
||||||
|
# if isinstance(obj, ObjectId):
|
||||||
|
# return str(obj) # Convert ObjectId to string for JSON serialization
|
||||||
|
# elif isinstance(obj, UpdateResult):
|
||||||
|
# # Convert UpdateResult to a serializable dictionary
|
||||||
|
# return {
|
||||||
|
# "matched_count": obj.matched_count,
|
||||||
|
# "modified_count": obj.modified_count,
|
||||||
|
# "upserted_id": str(obj.upserted_id) if obj.upserted_id else None,
|
||||||
|
# "acknowledged": obj.acknowledged
|
||||||
|
# }
|
||||||
|
# # For other non-serializable objects, use the default behavior
|
||||||
|
# return super().default(obj)
|
||||||
|
|
||||||
|
|
||||||
|
def get_partners_from_excel(xlsx_file):
|
||||||
|
"""
|
||||||
|
Read Excel file containing pa information.
|
||||||
|
Remove headers and return clean df
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path (str): Path to the Excel file.
|
||||||
|
sheet_name (str): Excel sheet name with partner list
|
||||||
|
Returns:
|
||||||
|
pd.DataFrame: DataFrame containing partner information.
|
||||||
|
"""
|
||||||
|
sheet_name = 'Partners'
|
||||||
|
df = pd.read_excel(xlsx_file, sheet_name = sheet_name)
|
||||||
|
df.columns = df.iloc[2]
|
||||||
|
df = df.drop(df.index[:3])
|
||||||
|
df = df.reset_index(drop=True)
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
def pretty_print_files(files_dict):
|
||||||
|
"""
|
||||||
|
Pretty print the dictionary containing file names.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
files_dict (dict): Dictionary where the keys are the file categories, and the values are the file names.
|
||||||
|
"""
|
||||||
|
file_message = ""
|
||||||
|
for key, value in files_dict.items():
|
||||||
|
file_message += f"- {key.replace('_', ' ').capitalize()}: {value} \n"
|
||||||
|
|
||||||
|
return file_message
|
||||||
|
|
||||||
|
|
||||||
|
def process_partners(partners_df):
|
||||||
|
"""
|
||||||
|
Process and insert users from an Excel file into MongoDB.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path (str): Path to the Excel file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
entity_id = os.getenv('ENTITY_ID')
|
||||||
|
|
||||||
|
# print(f"Header:\n {partners_df.head()}")
|
||||||
|
users = []
|
||||||
|
roles = []
|
||||||
|
|
||||||
|
# Iterate through the DataFrame and prepare partner records
|
||||||
|
for index, row in partners_df.iterrows():
|
||||||
|
|
||||||
|
# Skip rows without a valid Partner name
|
||||||
|
if pd.isna(row['Partner']) in ['Partner']:
|
||||||
|
print(f"Skipping row {index}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
partner_dict = {
|
||||||
|
"firstName": row['Primary Contact Name'].split(' ')[0] if pd.notna(row['Primary Contact Name']) else '',
|
||||||
|
"lastName": ' '.join(row['Primary Contact Name'].split(' ')[1:]) if pd.notna(row['Primary Contact Name']) else '',
|
||||||
|
"email": row['Email'] if pd.notna(row['Email']) else ''
|
||||||
|
}
|
||||||
|
|
||||||
|
users.append(partner_dict)
|
||||||
|
|
||||||
|
print(f"Adding user {partner_dict}")
|
||||||
|
# Insert user record into user collection
|
||||||
|
# user_id = add_user(partner_dict)
|
||||||
|
# print(json.dumps(insert_result, indent=2))
|
||||||
|
|
||||||
|
# Adding role automatiocally adds users
|
||||||
|
role, role_id = process_roles(row)
|
||||||
|
roles.append(role)
|
||||||
|
print(role_id)
|
||||||
|
|
||||||
|
return users
|
||||||
|
|
||||||
|
def remove_all_special_chars(text):
|
||||||
|
return re.sub(r'[^a-zA-Z0-9\s]', '', text)
|
||||||
|
|
||||||
|
|
||||||
|
def process_roles(row):
|
||||||
|
"""
|
||||||
|
Process and insert users from an Excel file into MongoDB.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path (str): Path to the Excel file.
|
||||||
|
"""
|
||||||
|
entity_id = os.getenv('ENTITY_ID')
|
||||||
|
accessType, role = "", ""
|
||||||
|
if pd.notna(row['Class']): # Check if the Class value is not NaN
|
||||||
|
if row['Class'] == 'Limited Partner' or row['Class'] == 'Member':
|
||||||
|
accessType = 'USER'
|
||||||
|
role = "LIMITED PARTNER"
|
||||||
|
elif row['Class'] == 'General Partner':
|
||||||
|
accessType = 'ADMIN'
|
||||||
|
role = "GENERAL PARTNER"
|
||||||
|
|
||||||
|
entity_name = row['Partner'] if any(indicator.lower() in row['Partner'].lower() for indicator in ["corp", "trust", "international", "llc", "ltd", "council", "inc", "llp", "sp", "lp", "capital", "fund","foundation","and","&","l.p."]) or any(char.isdigit() for char in row['Partner']) or re.search(r'\d{1,2}/\d{1,2}/\d{2,4}', row['Partner']) else ''
|
||||||
|
|
||||||
|
if len(entity_name) > 0:
|
||||||
|
first_name = row['Primary Contact Name'].split(' ')[0] if pd.notna(row['Primary Contact Name']) else ''
|
||||||
|
last_name = ' '.join(row['Primary Contact Name'].split(' ')[1:]) if pd.notna(row['Primary Contact Name']) else ''
|
||||||
|
else:
|
||||||
|
first_name = row['Partner'].split(' ')[0] if pd.notna(row['Partner']) else ''
|
||||||
|
last_name = ' '.join(row['Partner'].split(' ')[1:]) if pd.notna(row['Partner']) else ''
|
||||||
|
|
||||||
|
|
||||||
|
role_dict = {
|
||||||
|
# "userId": {
|
||||||
|
# "$oid": user_id
|
||||||
|
# },
|
||||||
|
# "entityId": {
|
||||||
|
# "$oid": entity_id
|
||||||
|
# },
|
||||||
|
"entityId": entity_id,
|
||||||
|
"entityName":entity_name,
|
||||||
|
"accessType": accessType,
|
||||||
|
"role": role,
|
||||||
|
"firstName": first_name,
|
||||||
|
"lastName": last_name,
|
||||||
|
# "status": "NOT INVITED",
|
||||||
|
# "isInvitationSent": False,
|
||||||
|
# "dob": None,
|
||||||
|
"phoneNumber": row['Phone'].split(' ')[0] if pd.notna(row['Phone']) else '',
|
||||||
|
"taxID1": row['Tax ID'].split(' ')[0] if pd.notna(row['Tax ID']) else '',
|
||||||
|
"taxID2": row['Tax ID Type'].split(' ')[0] if pd.notna(row['Tax ID Type']) else '',
|
||||||
|
"street": remove_all_special_chars(row['Street Address'].split(' ')[0]) if pd.notna(row['Street Address']) else '',
|
||||||
|
"address": remove_all_special_chars(" ".join(row['Street Address'].split(' ')[1:])) if pd.notna(row['Street Address']) else '',
|
||||||
|
"country":row['Country'].split(' ')[0] if pd.notna(row['Country']) else '',
|
||||||
|
"city": row['City'].split(' ')[0] if pd.notna(row['City']) else '',
|
||||||
|
"state": row['State'].split(' ')[0] if pd.notna(row['State']) else '',
|
||||||
|
"zipcode": row['ZIP'].split(' ')[0] if pd.notna(row['ZIP']) else '',
|
||||||
|
"commitedAmount": row['Commitment'],
|
||||||
|
"calledCapital": row['Called Capital'],
|
||||||
|
"dateOfCommitment": row['Issue date'],
|
||||||
|
"partnerExactName": row['Partner'],
|
||||||
|
"email":row['Email'] if pd.notna(row['Email']) else ''
|
||||||
|
# "gender": "",
|
||||||
|
# "ethnicity": "",
|
||||||
|
# "photoURL": None,
|
||||||
|
# "menuOpen": False
|
||||||
|
}
|
||||||
|
|
||||||
|
print("entityName")
|
||||||
|
|
||||||
|
print(entity_name)
|
||||||
|
|
||||||
|
print(role_dict)
|
||||||
|
|
||||||
|
# Save role into database
|
||||||
|
role_id = add_role(role_dict)
|
||||||
|
print(f"Upserted Role ID: {role_id}")
|
||||||
|
|
||||||
|
return role_dict, role_id
|
||||||
|
|
||||||
|
|
||||||
|
def process_all_partners(partners_file_path):
|
||||||
|
# Process partner data
|
||||||
|
step_number = 3
|
||||||
|
step_number -= 1
|
||||||
|
success_message = ""
|
||||||
|
error_message = ""
|
||||||
|
status = "IN-PROGRESS"
|
||||||
|
|
||||||
|
bucket_name = os.getenv('S3_UPLOAD_BUCKET_NAME')
|
||||||
|
print(f"Processing file: {partners_file_path}")
|
||||||
|
response = update_onboarding_status(step_number, status, error_message, success_message)
|
||||||
|
if 'error' in response:
|
||||||
|
print(f"Failed to fetch onboarding status: Error: {response['error']}")
|
||||||
|
print(f"Status Code: {response['status_code']}")
|
||||||
|
return False
|
||||||
|
partner_excel_file = read_file_from_s3(bucket_name, partners_file_path)
|
||||||
|
partners_df = get_partners_from_excel(partner_excel_file)
|
||||||
|
print(partners_df.head())
|
||||||
|
partners_dict = process_partners(partners_df)
|
||||||
|
|
||||||
|
if partners_dict:
|
||||||
|
item_count = len(partners_dict)
|
||||||
|
success_message = f"Onboarded {item_count} GPs&LPs."
|
||||||
|
status = "COMPLETE"
|
||||||
|
else:
|
||||||
|
error_message = "No users were onboaerded."
|
||||||
|
status = "FAILED"
|
||||||
|
|
||||||
|
print("*"*30)
|
||||||
|
response = update_onboarding_status(step_number, status, error_message, success_message)
|
||||||
|
if 'error' in response:
|
||||||
|
print(f"Failed to fetch onboarding status: Error: {response['error']}")
|
||||||
|
print(f"Status Code: {response['status_code']}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# print("Partner list:")
|
||||||
|
# print(json.dumps(partners_dict, indent=2, sort_keys=True, cls=CustomJSONEncoder))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
onboardingId = "66ea163564e2f97a059160ef"
|
||||||
|
os.environ["ONBOARDING_ID"] = onboardingId
|
||||||
|
|
||||||
|
from initOnboarding import initialize_onboarding
|
||||||
|
initialize_onboarding()
|
||||||
|
|
||||||
|
found_files = {'lpa': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/0-Please_DocuSign_CerraCap_II_LP_Limited_Partn.pdf', 'partner': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/0-1-cerracap-ii-lp_2024-07_09_short_partner.xlsx', 'financials': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/2-3-cerracap-ii-lp_2024-08-26_financials.xlsx', 'bankTransactions': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/4-CerraCap_II__LP_bank_transactions_2016-01-01-2024-07-03.xlsx', 'journals': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/5-cerracap-ii-lp_2024-07-09_journals-export.xlsx', 'fund_performance': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/6-cerracap-ii-lp_2024-07-09_fund-performance-report.xlsx'}
|
||||||
|
partner_excel_file_path = found_files["partner"]
|
||||||
|
process_all_partners(partner_excel_file_path)
|
||||||
125
task/onboardPerformance.py
Normal file
125
task/onboardPerformance.py
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import pandas as pd
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
from s3Ops import read_file_from_s3
|
||||||
|
|
||||||
|
from backendAPIs import (
|
||||||
|
update_onboarding_status,
|
||||||
|
add_performance_record
|
||||||
|
)
|
||||||
|
|
||||||
|
def xlsx_to_df(xlsx_file, sheet_name):
|
||||||
|
df = pd.read_excel(xlsx_file, sheet_name = sheet_name, header=2,skiprows=2)
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def process_performance_records(df):
|
||||||
|
"""
|
||||||
|
Process and insert users from an Excel file into MongoDB.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path (str): Path to the Excel file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
entity_id = os.getenv('ENTITY_ID')
|
||||||
|
fund_id = os.getenv('FUND_ID')
|
||||||
|
role_id = os.getenv('ROLE_ID')
|
||||||
|
|
||||||
|
performance_records = []
|
||||||
|
|
||||||
|
# Iterate through the DataFrame and prepare partner records
|
||||||
|
for index, row in df.iterrows():
|
||||||
|
|
||||||
|
print(row.to_dict())
|
||||||
|
|
||||||
|
if row['Type'].strip().lower() == "contribution":
|
||||||
|
continue
|
||||||
|
|
||||||
|
tdate = row['Date']
|
||||||
|
performance_record = {
|
||||||
|
"fundId": fund_id,
|
||||||
|
"entityId": entity_id,
|
||||||
|
"roleId": role_id,
|
||||||
|
"receivedDate": tdate.strftime('%m/%d/%Y'),
|
||||||
|
"amount": row['Value'] if row['Value'] >= 0 else row['Value']*-1,
|
||||||
|
"gainLoss": "GAIN" if row['Value'] >= 0 else "LOSS",
|
||||||
|
"type": row['Type'],
|
||||||
|
"partnerExactName": row["Partner"]
|
||||||
|
}
|
||||||
|
|
||||||
|
response = add_performance_record(performance_record)
|
||||||
|
print(f"Adding joural ledger {index}\n {performance_record}\n\n")
|
||||||
|
if 'error' in response:
|
||||||
|
print(response)
|
||||||
|
print(f"Failed to fetch onboarding status: Error: {response['error']}")
|
||||||
|
print(f"Status Code: {response['status_code']}")
|
||||||
|
# break # Process next company investment record...
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
performance_records.append(performance_record)
|
||||||
|
print(f"Performance record: \n{performance_record}")
|
||||||
|
|
||||||
|
# Test with smaller set of records
|
||||||
|
# if index == 1:
|
||||||
|
# break
|
||||||
|
|
||||||
|
return performance_records
|
||||||
|
|
||||||
|
|
||||||
|
def process_fund_performance(file_path):
|
||||||
|
# Process partner data
|
||||||
|
step_number = 7
|
||||||
|
step_number -= 1
|
||||||
|
success_message = ""
|
||||||
|
error_message = ""
|
||||||
|
status = "IN-PROGRESS"
|
||||||
|
|
||||||
|
bucket_name = os.getenv('S3_UPLOAD_BUCKET_NAME')
|
||||||
|
print(f"Processing file: {file_path}")
|
||||||
|
response = update_onboarding_status(step_number, status, error_message, success_message)
|
||||||
|
if 'error' in response:
|
||||||
|
print(f"Failed to fetch onboarding status: Error: {response['error']}")
|
||||||
|
print(f"Status Code: {response['status_code']}")
|
||||||
|
return False
|
||||||
|
performance_excel_file = read_file_from_s3(bucket_name, file_path)
|
||||||
|
sheet_name = 'Partner Capital Activity Detail'
|
||||||
|
performance_df = xlsx_to_df(performance_excel_file, sheet_name)
|
||||||
|
print(performance_df)
|
||||||
|
perf_records = process_performance_records(performance_df)
|
||||||
|
if perf_records:
|
||||||
|
item_count = len(perf_records)
|
||||||
|
success_message = f"Onboarded {item_count} fund performance records."
|
||||||
|
status = "COMPLETE"
|
||||||
|
else:
|
||||||
|
error_message = "No transactions were onboaerded."
|
||||||
|
status = "FAILED"
|
||||||
|
|
||||||
|
response = update_onboarding_status(step_number, status, error_message, success_message)
|
||||||
|
if 'error' in response:
|
||||||
|
print(f"Failed to fetch onboarding status: Error: {response['error']}")
|
||||||
|
print(f"Status Code: {response['status_code']}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# print(f"Journals list:\n {perf_records}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
onboardingId = "66ea163564e2f97a059160ef"
|
||||||
|
os.environ["ONBOARDING_ID"] = onboardingId
|
||||||
|
os.environ["FUND_ID"] = "66c5e6d89ecbf552a05b84fc"
|
||||||
|
|
||||||
|
from initOnboarding import initialize_onboarding
|
||||||
|
initialize_onboarding()
|
||||||
|
|
||||||
|
found_files = {'lpa': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/0-Please_DocuSign_CerraCap_II_LP_Limited_Partn.pdf', 'partner': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/0-1-cerracap-ii-lp_2024-07_09_short_partner.xlsx', 'financials': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/2-3-cerracap-ii-lp_2024-08-26_financials.xlsx', 'bankTransactions': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/4-CerraCap_II__LP_bank_transactions_2016-01-01-2024-07-03.xlsx', 'journals': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/5-cerracap-ii-lp_2024-07-09_journals-export.xlsx', 'fund_performance': '66c5e5c99ecbf552a05b84f9/20240917T235220Z/6-cerracap-ii-lp_2024-07-09_fund-performance-report.xlsx'}
|
||||||
|
financials_excel_file_path = found_files["fund_performance"]
|
||||||
|
process_fund_performance(financials_excel_file_path)
|
||||||
177
task/prompt.py
Normal file
177
task/prompt.py
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
|
||||||
|
|
||||||
|
prompt2 = """
|
||||||
|
Analyze the following limited partnership agreement document and update the provided JSON object with relevant information. Apply your expertise in financial document analysis and understanding of limited partnership agreements throughout this task.
|
||||||
|
|
||||||
|
Key Instructions:
|
||||||
|
1. Carefully read and interpret the entire document, including any amendments.
|
||||||
|
2. Extract all relevant information matching the JSON structure fields.
|
||||||
|
3. Update JSON values with newly extracted information, interpreting complex terms and conditions.
|
||||||
|
4. Apply critical thinking to deduce JSON values when not explicitly stated.
|
||||||
|
5. Only update values in the JSON, keeping all original fields.
|
||||||
|
6. Use null values only if information cannot be found explicitly or implicitly.
|
||||||
|
7. Provide a brief summary after the JSON, including your reasoning for deduced values.
|
||||||
|
8. Prioritize information from amendments over the original agreement, especially time related information such as dates of signatures.
|
||||||
|
|
||||||
|
Critical Document Analysis:
|
||||||
|
- Construct a detailed timeline of the fund's lifecycle, including:
|
||||||
|
a) Initial Closing Date
|
||||||
|
b) First Closing Date
|
||||||
|
c) Final Closing Date (and any potential extensions)
|
||||||
|
d) Start and end of Investment Period
|
||||||
|
e) Total fund term and any extensions
|
||||||
|
- For each date or period, identify triggering events/conditions and calculate durations.
|
||||||
|
- Use the most recent applicable dates for calculations, particularly from amendments.
|
||||||
|
- Explain clauses allowing extensions or alterations of key dates/periods and their potential impact.
|
||||||
|
- Ensure all time-related terms in the JSON are filled with specific values, ranges, or calculated estimates.
|
||||||
|
- Break down the total fund term into its component periods (e.g., Investment Period, Follow-On Period, Remainder of Term).
|
||||||
|
- For interdependent dates, provide relationships in the JSON and explain calculations in the summary.
|
||||||
|
|
||||||
|
Document Interpretation Guidelines:
|
||||||
|
- Cross-reference relevant sections when interpreting complex terms or dates.
|
||||||
|
- Use financial expertise to infer logical connections between different terms and dates.
|
||||||
|
- Make reasonable inferences using industry knowledge and standard practices when information isn't explicitly stated.
|
||||||
|
- Quote relevant document sections to support interpretations.
|
||||||
|
- Consider impacts on fund structure and investor relationships.
|
||||||
|
- Ensure logical consistency between provided values.
|
||||||
|
- Pay attention to legal and financial terminology that may not exactly match JSON field names.
|
||||||
|
|
||||||
|
Additional Notes:
|
||||||
|
- The document text is OCR-processed; interpret unclear sections using context.
|
||||||
|
- Important information might be in unexpected places, including footnotes or definitions.
|
||||||
|
- Pay special attention to the last pages of the document which correspond with the images attached. In these images pay special attention to signatures and dates mentioned therein.
|
||||||
|
|
||||||
|
Document Date Hierarchy:
|
||||||
|
1. Always prioritize dates from signature pages over dates mentioned in the preamble or body of the agreement.
|
||||||
|
2. For critical dates like the First Close Date, the most recent date on any signature page typically supersedes earlier dates mentioned in the document.
|
||||||
|
3. Be aware that the date an agreement is drafted (often mentioned at the beginning) may differ significantly from when it's actually signed and executed.
|
||||||
|
|
||||||
|
Key Points for First Close Date:
|
||||||
|
- The First Close Date is the date the document is signed and executed + 1 day.
|
||||||
|
- This is usually found on the signature page.
|
||||||
|
|
||||||
|
Examples of thought processing:
|
||||||
|
If the text reads, "Return of Capital First, 100% to such Limited Partner until such Limited Partner has received distributions pursuant to this Section 7.4(a)(i) equal to such Limited Partner's aggregate Capital Contributions made in respect of the applicable Portfolio Investment," this implies a hurdle of 0%. Note this may not be in this document as this is just an example.
|
||||||
|
|
||||||
|
In your summary:
|
||||||
|
- Provide a clear, chronological explanation of the fund's timeline.
|
||||||
|
- Explain your reasoning for each calculated date and duration, citing relevant sections.
|
||||||
|
- Clarify any unclear or variable dates/periods and factors affecting them.
|
||||||
|
- Ensure consistency between the summary and the JSON.
|
||||||
|
- Explain calculations and assumptions for estimated dates.
|
||||||
|
- Break down the entire fund term, explaining how each period fits into the overall timeline.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
reflection_prompt = """
|
||||||
|
You are tasked with conducting a thorough review and verification of a limited partnership agreement document. A previous analysis has been performed, resulting in a JSON object and summary. Your job is to independently analyze the document, verify the existing information, identify any discrepancies or missing details, and provide a comprehensive, updated analysis.
|
||||||
|
|
||||||
|
Key Instructions:
|
||||||
|
1. Carefully read and interpret the entire limited partnership agreement document, including any amendments and signature pages.
|
||||||
|
2. Review the provided JSON object and summary from the previous analysis.
|
||||||
|
3. Conduct your own independent analysis of the document, paying attention to all relevant information that should be captured in the JSON structure.
|
||||||
|
4. Verify each piece of information in the existing JSON, updating or correcting as necessary.
|
||||||
|
5. Use your expertise in financial document analysis and understanding of limited partnership agreements throughout this task.
|
||||||
|
|
||||||
|
Document Analysis Guidelines:
|
||||||
|
1. Timeline Verification:
|
||||||
|
- Reconstruct the fund's lifecycle, including all key dates (Initial Closing, First Closing, Final Closing, etc.)
|
||||||
|
- Verify the calculation of the Investment Period and total fund term
|
||||||
|
- Ensure consistency between related dates and periods
|
||||||
|
- Pay special attention to signature pages for critical dates like the First Close Date
|
||||||
|
|
||||||
|
2. Financial Terms:
|
||||||
|
- Analyze hurdle rates, management fees, and carried interest calculations
|
||||||
|
- Verify any complex waterfall structures or distribution mechanisms
|
||||||
|
|
||||||
|
3. Investor Rights and Obligations:
|
||||||
|
- Review limited partner commitments and any special rights
|
||||||
|
- Check for key-person clauses or no-fault divorce provisions
|
||||||
|
|
||||||
|
4. Fund Structure and Governance:
|
||||||
|
- Verify details about general partners, management company, advisory boards, and investment committees
|
||||||
|
|
||||||
|
5. Investment Strategy and Restrictions:
|
||||||
|
- Review investment focus, geographical restrictions, and diversification requirements
|
||||||
|
- Check for investment limitations or concentration limits
|
||||||
|
|
||||||
|
6. Document Interpretation:
|
||||||
|
- Cross-reference relevant sections when interpreting complex terms or dates
|
||||||
|
- Make reasonable inferences using industry knowledge when information isn't explicitly stated
|
||||||
|
- Consider impacts on fund structure and investor relationships
|
||||||
|
- Ensure logical consistency between provided values
|
||||||
|
|
||||||
|
Output:
|
||||||
|
1. An updated JSON object with any necessary changes
|
||||||
|
2. A detailed analysis summary that includes:
|
||||||
|
- Confirmation of verified information
|
||||||
|
- Explanations for any changes or additions made to the JSON
|
||||||
|
- Discussion of any discrepancies found between the document and the previous analysis
|
||||||
|
- Identification of any new information not captured in the original JSON
|
||||||
|
- Explanation of any complex interpretations or inferences made
|
||||||
|
- Any ambiguities or unclear information in the document
|
||||||
|
- Suggestions for additional fields that might be valuable for future extractions
|
||||||
|
|
||||||
|
Additional Notes:
|
||||||
|
- The document text is OCR-processed; interpret unclear sections using context
|
||||||
|
- Important information might be in unexpected places, including footnotes or definitions
|
||||||
|
- Prioritize information from amendments over the original agreement, especially for time-related information
|
||||||
|
- Provide clear reasoning for any deduced values or interpretations
|
||||||
|
- Ensure all time-related terms in the JSON are filled with specific values, ranges, or calculated estimates
|
||||||
|
- Break down the total fund term into its component periods in your summary
|
||||||
|
|
||||||
|
Remember, your goal is to provide the most accurate and comprehensive analysis of the limited partnership agreement possible, building upon but not being constrained by the previous analysis.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
json_template = """
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"fundSize": {
|
||||||
|
"reportingCurrency": null,
|
||||||
|
"targetedAmount": null,
|
||||||
|
"LPMinimumCommit": null,
|
||||||
|
"maximumCap": null,
|
||||||
|
"firstCloseAmount": null,
|
||||||
|
"amountRaisedToDate": null,
|
||||||
|
"totalClosingsToDate": null,
|
||||||
|
},
|
||||||
|
"fundTerm": {
|
||||||
|
"firstCloseDate": null,
|
||||||
|
"targetedFinalCloseDate": null,
|
||||||
|
"investmentPeriod": null,
|
||||||
|
"followOnPeriod": null,
|
||||||
|
"remainderOfTerm": null,
|
||||||
|
"extensionPeriod": null,
|
||||||
|
},
|
||||||
|
"managementFees": {
|
||||||
|
"investmentPeriod": {
|
||||||
|
"percentage": null,
|
||||||
|
"basis": null,
|
||||||
|
},
|
||||||
|
"followOnPeriod": {
|
||||||
|
"percentage": null,
|
||||||
|
"basis": null,
|
||||||
|
},
|
||||||
|
"extensionPeriod": {
|
||||||
|
"percentage": null,
|
||||||
|
"basis": null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"investmentFocus": {
|
||||||
|
"strategicFocus": null,
|
||||||
|
"geographicFocus": null,
|
||||||
|
"industryFocus": null,
|
||||||
|
},
|
||||||
|
"miscTerms": {
|
||||||
|
"reportingCurrency": null,
|
||||||
|
"hurdleRate": null,
|
||||||
|
"carriedInterest": null,
|
||||||
|
"carryCatchUp": null,
|
||||||
|
"carryEscrow": null,
|
||||||
|
"gpCommit": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
"""
|
||||||
57
task/requirements.txt
Normal file
57
task/requirements.txt
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
aiohappyeyeballs==2.4.0
|
||||||
|
aiohttp==3.10.5
|
||||||
|
aiosignal==1.3.1
|
||||||
|
annotated-types==0.7.0
|
||||||
|
anyio==4.6.0
|
||||||
|
async-timeout==4.0.3
|
||||||
|
attrs==24.2.0
|
||||||
|
boto3==1.35.11
|
||||||
|
botocore==1.35.11
|
||||||
|
certifi==2024.8.30
|
||||||
|
cffi==1.17.1
|
||||||
|
charset-normalizer==3.3.2
|
||||||
|
cryptography==43.0.1
|
||||||
|
et-xmlfile==1.1.0
|
||||||
|
exceptiongroup==1.2.2
|
||||||
|
frozenlist==1.4.1
|
||||||
|
greenlet==3.1.1
|
||||||
|
h11==0.14.0
|
||||||
|
httpcore==1.0.5
|
||||||
|
httpx==0.27.2
|
||||||
|
idna==3.10
|
||||||
|
jmespath==1.0.1
|
||||||
|
jsonpatch==1.33
|
||||||
|
jsonpointer==3.0.0
|
||||||
|
multidict==6.1.0
|
||||||
|
numpy==1.26.4
|
||||||
|
openpyxl==3.1.5
|
||||||
|
orjson==3.10.7
|
||||||
|
packaging==24.1
|
||||||
|
pandas==2.2.3
|
||||||
|
pdfminer.six==20231228
|
||||||
|
pdfplumber==0.11.4
|
||||||
|
pillow==10.4.0
|
||||||
|
pycparser==2.22
|
||||||
|
pydantic==2.9.2
|
||||||
|
pydantic_core==2.23.4
|
||||||
|
PyPDF2==3.0.1
|
||||||
|
pypdfium2==4.30.0
|
||||||
|
python-dateutil==2.9.0.post0
|
||||||
|
pytz==2024.2
|
||||||
|
PyYAML==6.0.2
|
||||||
|
requests==2.32.3
|
||||||
|
s3transfer==0.10.2
|
||||||
|
six==1.16.0
|
||||||
|
sniffio==1.3.1
|
||||||
|
SQLAlchemy==2.0.35
|
||||||
|
temp==2020.7.2
|
||||||
|
tenacity==8.5.0
|
||||||
|
timedelta==2020.12.3
|
||||||
|
typing_extensions==4.12.2
|
||||||
|
tzdata==2024.1
|
||||||
|
urllib3==2.2.2
|
||||||
|
xlrd==2.0.1
|
||||||
|
yarl==1.11.1
|
||||||
|
|
||||||
|
# Google Gemini
|
||||||
|
google-genai>=1.0.0
|
||||||
63
task/s3Ops.py
Normal file
63
task/s3Ops.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import boto3
|
||||||
|
import pandas as pd
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
# from dotenv import load_dotenv
|
||||||
|
#
|
||||||
|
# load_dotenv()
|
||||||
|
|
||||||
|
def list_files_in_s3_folder(bucket_name, folder_path):
|
||||||
|
"""
|
||||||
|
List all files in a specified S3 folder.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bucket_name (str): The name of the S3 bucket.
|
||||||
|
folder_path (str): The path to the folder in the S3 bucket.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: A list of file names in the specified S3 folder.
|
||||||
|
"""
|
||||||
|
s3_client = boto3.client('s3')
|
||||||
|
|
||||||
|
|
||||||
|
# List objects in the specified S3 folder
|
||||||
|
result = s3_client.list_objects_v2(Bucket=bucket_name, Prefix=folder_path)
|
||||||
|
|
||||||
|
|
||||||
|
if 'Contents' not in result:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Extract the file names
|
||||||
|
file_names = [item['Key'] for item in result['Contents']]
|
||||||
|
|
||||||
|
return file_names
|
||||||
|
|
||||||
|
|
||||||
|
def read_file_from_s3(bucket_name, file_key):
|
||||||
|
"""
|
||||||
|
Read the content of a file from S3 and return it as a byte stream.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bucket_name (str): The name of the S3 bucket.
|
||||||
|
file_key (str): The key (path) to the Excel file in the S3 bucket.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BytesIO: The content of the file as a BytesIO.
|
||||||
|
"""
|
||||||
|
s3_client = boto3.client('s3')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Download the file content from S3
|
||||||
|
response = s3_client.get_object(Bucket=bucket_name, Key=file_key)
|
||||||
|
|
||||||
|
# Read the file content into a pandas DataFrame
|
||||||
|
file_content = response['Body'].read()
|
||||||
|
|
||||||
|
# Use BytesIO to read the file content in-memory
|
||||||
|
s3_file = BytesIO(file_content)
|
||||||
|
|
||||||
|
return s3_file
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading file from S3: {e}")
|
||||||
|
return None
|
||||||
91
task/sqs_processor.py
Normal file
91
task/sqs_processor.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import boto3
|
||||||
|
import logging
|
||||||
|
from botocore.exceptions import ClientError
|
||||||
|
from lambda_function import lambda_handler
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def read_sqs_messages():
|
||||||
|
"""Read messages from SQS queue"""
|
||||||
|
# Get queue URL from environment variable or use a default for testing
|
||||||
|
queue_url = os.environ.get('SQS_QUEUE_URL')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create SQS client
|
||||||
|
sqs = boto3.client('sqs')
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# Receive message from SQS queue
|
||||||
|
response = sqs.receive_message(
|
||||||
|
QueueUrl=queue_url,
|
||||||
|
AttributeNames=['All'],
|
||||||
|
MaxNumberOfMessages=1,
|
||||||
|
MessageAttributeNames=['All'],
|
||||||
|
VisibilityTimeout=30,
|
||||||
|
WaitTimeSeconds=20
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'Messages' in response:
|
||||||
|
messages = response['Messages']
|
||||||
|
print(f"Received {len(messages)} messages")
|
||||||
|
|
||||||
|
for message in messages:
|
||||||
|
|
||||||
|
receipt_handle = message['ReceiptHandle']
|
||||||
|
message_body = message['Body']
|
||||||
|
|
||||||
|
print("Message Body")
|
||||||
|
print(message_body)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Process the message with error handling
|
||||||
|
lambda_handler(message_body)
|
||||||
|
|
||||||
|
# Delete the message from the queue only if processing succeeded
|
||||||
|
sqs.delete_message(
|
||||||
|
QueueUrl=queue_url,
|
||||||
|
ReceiptHandle=receipt_handle
|
||||||
|
)
|
||||||
|
print(f"Successfully processed and deleted message: {receipt_handle}")
|
||||||
|
|
||||||
|
except (KeyError, json.JSONDecodeError, TypeError) as e:
|
||||||
|
# Log the error and skip this record
|
||||||
|
print(f"ERROR: Failed to process message due to data/parsing error: {e}")
|
||||||
|
print(f"Skipping problematic message: {message_body[:500]}") # Log first 500 chars
|
||||||
|
|
||||||
|
# Delete the problematic message to prevent infinite retries
|
||||||
|
sqs.delete_message(
|
||||||
|
QueueUrl=queue_url,
|
||||||
|
ReceiptHandle=receipt_handle
|
||||||
|
)
|
||||||
|
print(f"Deleted problematic message from queue to prevent pod restart")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# For other errors, log but don't delete - let SQS retry later
|
||||||
|
print(f"ERROR: Unexpected error processing message: {e}")
|
||||||
|
print(f"Message will be retried after visibility timeout")
|
||||||
|
# Don't delete the message, let it become visible again for retry
|
||||||
|
else:
|
||||||
|
print("No messages available in the queue")
|
||||||
|
|
||||||
|
except ClientError as e:
|
||||||
|
print(f"Error processing SQS messages: {e}")
|
||||||
|
time.sleep(5) # Wait before retrying
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Fatal error: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("SQS message reader starting")
|
||||||
|
try:
|
||||||
|
read_sqs_messages()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("SQS message reader stopped by user")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"SQS reader failed with error: {e}")
|
||||||
|
|
||||||
243
task/tester.py
Normal file
243
task/tester.py
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
DOCUMENT_CLASSIFY_PROMPT = """
|
||||||
|
You are an expert in document classification where you are given a document under <document> xml tags and you need to classify it based on the data inside <document-type> xml tag.
|
||||||
|
|
||||||
|
Your output should be enclosed inside <output> xml tag and it should be based on one of the <document-type> options mentioned below.Just output <document-type> in the output <output> xml tag.
|
||||||
|
|
||||||
|
### Document
|
||||||
|
<document>
|
||||||
|
{document}
|
||||||
|
</document>
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
## Document Types that need to classified.
|
||||||
|
<document-type>
|
||||||
|
["LPA(Limited Partner Agreement)", "LPA-Amendment(Limited Partner Agreement-Amendment)", "Side Letter"]
|
||||||
|
</document-type>
|
||||||
|
"""
|
||||||
|
|
||||||
|
LPA_STATEMENT_PROMPT = """
|
||||||
|
<SYSTEM>
|
||||||
|
You are an expert in LPA (Limited Partner Agreement) data extraction that uses a Chain of Thought (CoT) approach with reflection to answer queries. Follow these steps:
|
||||||
|
|
||||||
|
1. Think through the problem step by step within the <thinking> tags.
|
||||||
|
2. Reflect on your thinking to check for any errors or improvements within the <reflection> tags.
|
||||||
|
3. Make any necessary adjustments based on your reflection.
|
||||||
|
4. Provide your final, concise answer within the <output> tags.
|
||||||
|
|
||||||
|
Important: The <thinking> and <reflection> sections are for your internal reasoning process only.
|
||||||
|
Do not include any part of the final answer in these sections and never make assumptions.
|
||||||
|
The actual response to the query must be entirely contained within the <output> tags.
|
||||||
|
|
||||||
|
Use the following format for your response:
|
||||||
|
<thinking>
|
||||||
|
[Your step-by-step reasoning goes here. This is your internal thought process, not the final answer. Please explain the reason for selecting each piece of information.]
|
||||||
|
</thinking>
|
||||||
|
<reflection>
|
||||||
|
[Your reflection on your reasoning, checking for errors or improvements]
|
||||||
|
</reflection>
|
||||||
|
<output>
|
||||||
|
[Your final, concise answer to the query in the specified JSON format. This is the only part that will be shown to the user.]
|
||||||
|
</output>
|
||||||
|
|
||||||
|
</SYSTEM>
|
||||||
|
|
||||||
|
<USER>
|
||||||
|
You are given data in the <document> xml tag. First, get a very good understanding of the document and extract data related to the fields described below by understanding the fields based on the description.
|
||||||
|
|
||||||
|
### Document
|
||||||
|
<document>
|
||||||
|
{document}
|
||||||
|
</document>
|
||||||
|
|
||||||
|
Final output to place under <output> xml tag and populate empty values if the data is not present in the document.
|
||||||
|
<output-format>
|
||||||
|
{{
|
||||||
|
"fundName": "name of the fund",
|
||||||
|
"fileInfo":"name of the fund and under which type of the document is it - Date of the agreement"
|
||||||
|
"managementFee": "Extract the following information from the document:
|
||||||
|
1. Annual management fee percentage(s), including any changes over time
|
||||||
|
2. Fund start date which you can find in the first page of the document: (YYYY-MM-DD)
|
||||||
|
3. Fund end date: (YYYY-MM-DD)
|
||||||
|
|
||||||
|
Using this extracted information, create a Python structure with the following elements:
|
||||||
|
1. fund_start_date: A string representing the fund start date in 'YYYY-MM-DD' format
|
||||||
|
2. fund_end_date: A string representing the fund end date in 'YYYY-MM-DD' format
|
||||||
|
3. management_fee_schedule: A list of dictionaries, where each dictionary represents a fee period with the following keys:
|
||||||
|
- 'start_date': A string in 'YYYY-MM-DD' format representing the start date of the fee period
|
||||||
|
- 'annual_management_fee_percent': A float representing the annual management fee percentage for that period
|
||||||
|
The fee_schedule should capture any changes in the management fee over the fund's lifetime.
|
||||||
|
Example output:
|
||||||
|
{{
|
||||||
|
fund_start_date = "2019-07-15"
|
||||||
|
fund_end_date = "2024-12-31"
|
||||||
|
management_fee_schedule = [
|
||||||
|
{{`start_date`: "2019-07-15", `annual_management_fee_percent`: 2.5}},
|
||||||
|
{{`start_date`: "2023-07-01", `annual_management_fee_percent`: 2.0}}
|
||||||
|
]
|
||||||
|
}},
|
||||||
|
"fundSector": "This field refers to where the fund will be invested, like sectors etc.",
|
||||||
|
"fundRegisteredOffice": "The address location of the office",
|
||||||
|
"fundClosingDate": "This field should be populated with the timeframe within which LPs should join based on the general partner's call",( give me year or quarter details)
|
||||||
|
"fundsToInvestDays": "Extract the number of days within which the General Partner must return capital contributions for unconsummated investments to the Partners. Please provide only the numerical value?",
|
||||||
|
"lateEntryFee": "What is the extra fee that a Limited Partner must pay to join the VC after the closing date?",
|
||||||
|
"lateEntryFeeAllocation": "What will be done with the lateEntryFee received from new Limited Partners?",
|
||||||
|
"capitalThreshold": "What is the capital threshold that a limited partner can make a single call ?",
|
||||||
|
"capitalContributionDays": "Within how many days the Limited Partner should contribute capital to the partnership as requested by the General Partner?((Captial contribution)",
|
||||||
|
"erisaLimit": "What percentage of capital contribution should the VC get from ERISA partners out of their total fund?",
|
||||||
|
"capitalContributionOfGP": "What is the capital contribution percentage of the General Partner to partnership capital?",
|
||||||
|
"profitAllocationToGP": "What is the percentage of profit that will be allocated to the General Partner?", # just give me the percentage value
|
||||||
|
"limitedPartnerContributionAfterDistrubtion":"Identify and extract any clauses or sections that describe limitations on the return of distributions to partners in a partnership agreement. Specifically, look for:
|
||||||
|
1. The maximum amount partners may be required to return
|
||||||
|
2. Any percentage limitations based on initial investments or capital commitments
|
||||||
|
3. Distinctions between different types of partners (e.g., Limited Partners vs. General Partners)
|
||||||
|
4. Time limits for requesting the return of distributions
|
||||||
|
5. Conditions under which distributions may be recalled
|
||||||
|
6. Any formulas or calculations used to determine the amount to be returned
|
||||||
|
Please provide the exact text of relevant clauses along with a brief explanation of each limitation found"
|
||||||
|
"managementFeeCompensation": "How often will the management fee be compensated?",
|
||||||
|
"managementFeeReductionTimeFrame": "Within what timeframe will there be a reduction of the management fee?",
|
||||||
|
"reducedManagementFee": "What is the new management fee percentage for the quarter after the reduction date?",
|
||||||
|
"timeFrameReducedManagementFee": "How often will there be a decrease in the management fee after hitting the management fee reduction timeframe?",# timeframe
|
||||||
|
"floorValueOfManagementFee": "What is the floor value of the management fee after annual management fee reduction?",
|
||||||
|
"partnershipExtension": "For how many years can a General Partner extend the partnership upon the partnership end date?",
|
||||||
|
"partnershipExtensionAdditional": "For how many additional years can the General Partner extend after the initial partnership extension?",
|
||||||
|
"Management Expenses":"Extract complete information in any clauses or sections that describe about the expenses related to management and there limitations and i want the section as well",
|
||||||
|
"Fund/Partnership Expenses": "Extract complete information in any clauses or sections that describe about the expenses related to Fund/Partnership and there limitations and i want the section as well",
|
||||||
|
"OnetimeOrganizationExpenses":"Extract complete information in any clauses or sections that describe about the expenses related to OnetimeOrganization and there limitations and i want the section as well"
|
||||||
|
}}
|
||||||
|
</output-format>
|
||||||
|
</user>
|
||||||
|
"""
|
||||||
|
|
||||||
|
SIDE_LETTER_PROMPT = """
|
||||||
|
You are given side letter document related to a limited partner under <document> xml tag.
|
||||||
|
|
||||||
|
First get a very good understanding of the below document under <document> xml tag . Your task is to generate the response in below format <output-format> in <output> xml tags and partner id related to the partner to be written to <id>
|
||||||
|
|
||||||
|
## Document
|
||||||
|
<document>
|
||||||
|
{document}
|
||||||
|
</document>
|
||||||
|
|
||||||
|
## partner id document
|
||||||
|
<partner-id-document>
|
||||||
|
{role_doc}
|
||||||
|
</partner-id-document>
|
||||||
|
|
||||||
|
|
||||||
|
<output-format>
|
||||||
|
{{
|
||||||
|
"fundName":"Name of the fund",
|
||||||
|
"fundDate":"Limited Partner Agreement date in format : YYYY-MM-DD",
|
||||||
|
"partnerName":"Name of the investing partner",
|
||||||
|
"quarter":"",
|
||||||
|
"platform":"Name of the platform which generated the report,if you are not sure return empty",
|
||||||
|
"carriedInterest": " <case:1> : If there is a change in carried interest then execute below one
|
||||||
|
Just give me the percentage value of profit/carry interest that will be allocated to the General Partner.
|
||||||
|
else
|
||||||
|
<case:2>: Just return `No changes` ",
|
||||||
|
"managementFee": "
|
||||||
|
<case:1> : If there is a changing in management fee structure execute below one
|
||||||
|
Extract the following information from the document:
|
||||||
|
1. Annual management fee percentage(s), including any changes over time
|
||||||
|
2. Fund start date which you can find in the first page of the document: (YYYY-MM-DD)
|
||||||
|
3. Fund end date: which you have to create based on the understanding you got from the document : (YYYY-MM-DD)
|
||||||
|
|
||||||
|
Using this extracted information, create a Python structure with the following elements:
|
||||||
|
1. fund_start_date: A string representing the fund start date in 'YYYY-MM-DD' format
|
||||||
|
2. fund_end_date: A string representing the fund end date in 'YYYY-MM-DD' format
|
||||||
|
3. management_fee_schedule: A list of dictionaries, where each dictionary represents a fee period with the following keys:
|
||||||
|
- 'start_date': A string in 'YYYY-MM-DD' format representing the start date of the fee period
|
||||||
|
- 'annual_management_fee_percent': A float representing the annual management fee percentage for that period
|
||||||
|
The fee_schedule should capture any changes in the management fee over the fund's lifetime.
|
||||||
|
Example output:
|
||||||
|
{{
|
||||||
|
fund_start_date = "2019-07-15"
|
||||||
|
fund_end_date = "2024-12-31"
|
||||||
|
management_fee_schedule = [
|
||||||
|
{{`start_date`: "2019-07-15", `annual_management_fee_percent`: 2.5}},
|
||||||
|
{{`start_date`: "2023-07-01", `annual_management_fee_percent`: 2.0}}
|
||||||
|
]
|
||||||
|
}}
|
||||||
|
else
|
||||||
|
<case:2> :Just return `No changes` ",
|
||||||
|
"sideLetterKeyPoints": "Extract key details in the document, Just return it in a string",
|
||||||
|
}}
|
||||||
|
</output-format>
|
||||||
|
|
||||||
|
<id>
|
||||||
|
"Extract value related to partner id name from <role-document> document"
|
||||||
|
</id>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pdfplumber
|
||||||
|
|
||||||
|
with pdfplumber.open('Side_letter_Greylock Partners_062723.pdf') as pdf:
|
||||||
|
list_pages = []
|
||||||
|
|
||||||
|
for page in pdf.pages:
|
||||||
|
text = page.extract_text()
|
||||||
|
if text:
|
||||||
|
list_pages.append(text.strip())
|
||||||
|
pdf_string = "".join(list_pages)
|
||||||
|
|
||||||
|
|
||||||
|
data_string = """"data": [
|
||||||
|
{
|
||||||
|
"label": "FIN CAP INVEST LLC",
|
||||||
|
"value": "66fbfbd920655b335e6d1ccc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Avinash",
|
||||||
|
"value": "6740785c268e6da0e1c746e6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "CODY HEALTHCARE S CORP",
|
||||||
|
"value": "66fbfbd820655b335e6d1ca8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Enterprise International, Inc.",
|
||||||
|
"value": "66fbfbd820655b335e6d1cc0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Pradeepp Kukunuri",
|
||||||
|
"value": "66fbfbc6f78cfc9f3cafb51c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Shresth",
|
||||||
|
"value": "672dd65e6844833570e5b0e2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Support",
|
||||||
|
"value": "672dd6376844833570e5b0be"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Greylock Partners",
|
||||||
|
"value": "66fbfbd820655b335e6d1cb4"
|
||||||
|
}
|
||||||
|
]"""
|
||||||
|
|
||||||
|
from llm_bedrock import model
|
||||||
|
import re
|
||||||
|
|
||||||
|
output = model.invoke(SIDE_LETTER_PROMPT.format(document=pdf_string,role_doc=data_string)).content
|
||||||
|
|
||||||
|
pattern = r'<output>(.*?)</output>'
|
||||||
|
match = re.search(pattern, output, re.DOTALL)
|
||||||
|
|
||||||
|
print(match.group(1).strip())
|
||||||
|
|
||||||
|
id = r'<id>(.*?)</id>'
|
||||||
|
match = re.search(id,output,re.DOTALL)
|
||||||
|
|
||||||
|
print(match.group(1).strip())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
print(match)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
244
task/utils/account_type.py
Normal file
244
task/utils/account_type.py
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
account_type_data = {
|
||||||
|
"Bank": "Bank",
|
||||||
|
"Capital Call Receivable - LP": "Capital Call Receivable - LP",
|
||||||
|
"Portfolio Interest Receivable": "Portfolio Interest Receivable",
|
||||||
|
"Portfolio interest receivable": "Portfolio Interest Receivable",
|
||||||
|
"Portfolio interest income": "Portfolio Interest Income",
|
||||||
|
"Due to related parties": "Due to Related Party",
|
||||||
|
"Due to investment": "Due to investment",
|
||||||
|
"Due to management company": "Due to Management Company",
|
||||||
|
"Investment - unrealized gain/loss": "Investment - Unrealized Gain/Loss",
|
||||||
|
"Unrealized gain/loss": "Unrealized Gain/Loss",
|
||||||
|
"Portfolio company receivable": "Portfolio Company Receivable",
|
||||||
|
"Realized gain/loss": "Realized Gain/Loss",
|
||||||
|
"Distribution payable": "Distribution Payable",
|
||||||
|
"Cash distribution - LP": "Cash Distribution - LP",
|
||||||
|
"Due to general partner": "Due to General Partner",
|
||||||
|
"Other prepaids": "Other Prepaids",
|
||||||
|
"Portfolio Company Receivable": "Portfolio Company Receivable",
|
||||||
|
"Misc Receivable": "Misc Receivable",
|
||||||
|
"Misc receivable": "Misc Receivable",
|
||||||
|
"Temporarily Commingled Cash from Bank Receivable":
|
||||||
|
"Temporarily Commingled Cash from Bank Receivable",
|
||||||
|
"Due from the Management Company": "Due from the Management Company",
|
||||||
|
"Receivable between Master Feeder": "Receivable between Master Feeder",
|
||||||
|
"Receivable from Affiliate": "Receivable from Affiliate",
|
||||||
|
"Payable between Master Feeder": "Payable between Master Feeder",
|
||||||
|
"Payable to Affiliate": "Payable to Affiliate",
|
||||||
|
"Due from Affiliates": "Due from Affiliates",
|
||||||
|
"Due to Affiliates": "Due to Affiliates",
|
||||||
|
"Other Prepaids": "Other Prepaids",
|
||||||
|
"Accelaration Prepaids": "Accelaration Prepaids",
|
||||||
|
"Organization costs asset": "Organization Costs Asset",
|
||||||
|
"Organization Costs Asset": "Organization Costs Asset",
|
||||||
|
"Investment": "Investment",
|
||||||
|
"Investment - Unrealized Gain/Loss": "Investment - Unrealized Gain/Loss",
|
||||||
|
"Due to Management Company": "Due to Management Company",
|
||||||
|
"Management Fee Payable": "Management Fee Payable",
|
||||||
|
"Admission Fee Payable": "Admission Fee Payable",
|
||||||
|
"Admission Fees": "Admission Fees",
|
||||||
|
"Due to General Partner": "Due to General Partner",
|
||||||
|
"Due to Limited Partner": "Due to Limited Partner",
|
||||||
|
"Management Fees Prepaid": "Management Fees Prepaid",
|
||||||
|
"Due to Portfolio Company": "Due to Portfolio Company",
|
||||||
|
"Due to Other": "Due to Other",
|
||||||
|
"Due to Related Party": "Due to Related Party",
|
||||||
|
"Deferred Capital Calls - LP": "Deferred Capital Calls - LP",
|
||||||
|
"Deferred Capital Calls - GP": "Deferred Capital Calls - GP",
|
||||||
|
"Deferred capital calls - GP": "Deferred Capital Calls - GP",
|
||||||
|
"Audit Accrual": "Audit Accrual",
|
||||||
|
"Fund Administration Accrual": "Fund Administration Accrual",
|
||||||
|
"Tax Accrual": "Tax Accrual",
|
||||||
|
"Other Accrual": "Other Accrual",
|
||||||
|
"Syndication Costs - General": "Syndication Costs - General",
|
||||||
|
"Contributed Capital - LP": "Contributed Capital - LP",
|
||||||
|
"Misc Income": "Misc Income",
|
||||||
|
"Portfolio Interest Income": "Portfolio Interest Income",
|
||||||
|
"Unrealized Gain/Loss": "Unrealized Gain/Loss",
|
||||||
|
"Management Fees": "Management Fees",
|
||||||
|
"Audit Fees": "Audit Fees",
|
||||||
|
"Tax Prep Fees": "Tax Prep Fees",
|
||||||
|
"Fund Administration Fees": "Fund Administration Fees",
|
||||||
|
"Other assets": "Other assets",
|
||||||
|
"Due to limited partner": "Due to Limited Partner",
|
||||||
|
"Administration Fees": "Administration Fees",
|
||||||
|
"Administration Fees - Allocated from Master":
|
||||||
|
"Administration Fees - Allocated from Master",
|
||||||
|
"Audit Fees - Allocated from Master": "Audit Fees - Allocated from Master",
|
||||||
|
"Bank Charges - Allocated from Master":
|
||||||
|
"Bank Charges - Allocated from Master",
|
||||||
|
"Commission Expense": "Commission Expense",
|
||||||
|
"Commission Expense - Allocated from Master":
|
||||||
|
"Commission Expense - Allocated from Master",
|
||||||
|
"FATCA Expenses": "FATCA Expenses",
|
||||||
|
"FATCA Expenses - Allocated from Master":
|
||||||
|
"FATCA Expenses - Allocated from Master",
|
||||||
|
"FS Preparation": "FS Preparation",
|
||||||
|
"FS Preparation - Allocated From Master":
|
||||||
|
"FS Preparation - Allocated From Master",
|
||||||
|
"Legal Expenses - Allocated from Master":
|
||||||
|
"Legal Expenses - Allocated from Master",
|
||||||
|
"Marketing Expenses - Allocated from Master":
|
||||||
|
"Marketing Expenses - Allocated from Master",
|
||||||
|
"Offering & Organizational Cost": "Offering & Organizational Cost",
|
||||||
|
"Offering & Organizational Cost - Allocated from Master":
|
||||||
|
"Offering & Organizational Cost - Allocated from Master",
|
||||||
|
"Performance Fees": "Performance Fees",
|
||||||
|
"Prepaid Offering & Organizational Cost":
|
||||||
|
"Prepaid Offering & Organizational Cost",
|
||||||
|
"Travelling Expense": "Travelling Expense",
|
||||||
|
"Travelling Expense - Allocated from Master":
|
||||||
|
"Travelling Expense - Allocated from Master",
|
||||||
|
"Administration Fees Payable": "Administration Fees Payable",
|
||||||
|
"Bank Charges Payable": "Bank Charges Payable",
|
||||||
|
"FATCA Payable": "FATCA Payable",
|
||||||
|
"FS Prepartion Payable": "FS Prepartion Payable",
|
||||||
|
"Marketing Expenses Payable": "Marketing Expenses Payable",
|
||||||
|
"Offering & Organizational Cost Payable":
|
||||||
|
"Offering & Organizational Cost Payable",
|
||||||
|
"Performance Fees Payable": "Performance Fees Payable",
|
||||||
|
"Travelling Expense Payable": "Travelling Expense Payable",
|
||||||
|
"Reimbursement Payable": "Reimbursement Payable",
|
||||||
|
"Audit Fees Payable": "Audit Fees Payable",
|
||||||
|
"Due to/from GP": "Due to/from GP",
|
||||||
|
"Unrealized Gain (Loss) from Private Investment Companies":
|
||||||
|
"Unrealized Gain (Loss) from Private Investment Companies",
|
||||||
|
"Unrealized Gain (Loss) from Private Investment Companies - Allocated from Master":
|
||||||
|
"Unrealized Gain (Loss) from Private Investment Companies - Allocated from Master",
|
||||||
|
"Unrealized Gain (Loss) from Cryptocurrencies (Price) - Allocated from Master":
|
||||||
|
"Unrealized Gain (Loss) from Cryptocurrencies (Price) - Allocated from Master",
|
||||||
|
"Unrealized Gain (Loss) on Investment in Cryptocurrencies (Price)":
|
||||||
|
"Unrealized Gain (Loss) on Investment in Cryptocurrencies (Price)",
|
||||||
|
"Due from Limited Partner": "Due from Limited Partner",
|
||||||
|
"Due/To from Offshore Feeder Fund": "Due/To from Offshore Feeder Fund",
|
||||||
|
"Due/To from Offshore Master Fund": "Due/To from Offshore Master Fund",
|
||||||
|
"Due/To from Onshore Feeder Fund": "Due/To from Onshore Feeder Fund",
|
||||||
|
"Receivable from pending settlement": "Receivable from pending settlement",
|
||||||
|
"Investment in Cryptocurrencies, (At Cost)":
|
||||||
|
"Investment in Cryptocurrencies, (At Cost)",
|
||||||
|
"Investment in Fixed Income Bond": "Investment in Fixed Income Bond",
|
||||||
|
"Investment in Master": "Investment in Master",
|
||||||
|
"Investment in Private Investment Companies":
|
||||||
|
"Investment in Private Investment Companies",
|
||||||
|
"Investment In Safes, (At Cost)": "Investment In Safes, (At Cost)",
|
||||||
|
"Investment In SAFT, (At Cost)": "Investment In SAFT, (At Cost)",
|
||||||
|
"Change in Unrealized Gain (Loss) from Private Investment Companies":
|
||||||
|
"Change in Unrealized Gain (Loss) from Private Investment Companies",
|
||||||
|
"Change in Unrealized Gain (Loss) on Investment in Cryptocurrencies (Price)":
|
||||||
|
"Change in Unrealized Gain (Loss) on Investment in Cryptocurrencies (Price)",
|
||||||
|
"Change in Unrealized Gain (Loss) on Investment in Cryptocurrencies (Price) - Allocated from Master":
|
||||||
|
"Change in Unrealized Gain (Loss) on Investment in Cryptocurrencies (Price) - Allocated from Master",
|
||||||
|
"Income from Master": "Income from Master",
|
||||||
|
"Staking Income": "Staking Income",
|
||||||
|
"Staking Income - Allocated from Master":
|
||||||
|
"Staking Income - Allocated from Master",
|
||||||
|
"Interest Income - Allocated from Master":
|
||||||
|
"Interest Income - Allocated from Master",
|
||||||
|
"Realized Gain (Loss) on Investment in Cryptocurrencies (Price)":
|
||||||
|
"Realized Gain (Loss) on Investment in Cryptocurrencies (Price)",
|
||||||
|
"Realized Gain (Loss) on Investment in Cryptocurrencies (Price) - Allocated from Master":
|
||||||
|
"Realized Gain (Loss) on Investment in Cryptocurrencies (Price) - Allocated from Master",
|
||||||
|
"Legal Fees": "Legal Fees",
|
||||||
|
"Legal Fees Accrual": "Legal Fees Accrual",
|
||||||
|
"Interest Payable": "Interest Payable",
|
||||||
|
"Loan Payable": "Loan Payable",
|
||||||
|
"Organization Costs Expense": "Organization Costs Expense",
|
||||||
|
"Other Professional Fees": "Other Professional Fees",
|
||||||
|
"Filing Fees": "Filing Fees",
|
||||||
|
"Other Fees": "Other Fees",
|
||||||
|
"Office supplies": "Office supplies",
|
||||||
|
"Organization costs amortization": "Organization Costs Amortization",
|
||||||
|
"Distribution Payable": "Distribution Payable",
|
||||||
|
"Distribution Payable - GP": "Distribution Payable - GP",
|
||||||
|
"Tax Preparation Fees Accrual": "Tax Preparation Fees Accrual",
|
||||||
|
"Realized Gain/Loss": "Realized Gain/Loss",
|
||||||
|
"Escrow from portfolio investments sold":
|
||||||
|
"Escrow from portfolio investments sold",
|
||||||
|
"Cash Distribution - LP": "Cash Distribution - LP",
|
||||||
|
"Cash Distribution - GP": "Cash Distribution - GP",
|
||||||
|
"Cash distribution - GP": "Cash Distribution - GP",
|
||||||
|
"Distribution payable - GP": "Distribution Payable - GP",
|
||||||
|
"Cash Carry Distributions": "Cash Carry Distributions",
|
||||||
|
"Carried Interest Distribution - GP": "Carried Interest Distribution - GP",
|
||||||
|
"Bank 1099able": "Bank 1099able",
|
||||||
|
"Audit fees": "Audit Fees",
|
||||||
|
"Bank charges": "Bank Charges",
|
||||||
|
"Bank Charges": "Bank Charges",
|
||||||
|
"Bank interest income": "Bank Interest Income",
|
||||||
|
"Investment - capitalized interest": "Investment",
|
||||||
|
"Capital call receivable - LP": "Capital Call Receivable - LP",
|
||||||
|
"Misc income": "Misc Income",
|
||||||
|
"Prepaid management fees": "Management Fees Prepaid",
|
||||||
|
"Consulting": "Consulting",
|
||||||
|
"Organization expenses": "Organization Expenses",
|
||||||
|
"Organization Expenses": "Organization Expenses",
|
||||||
|
"Due diligence": "Due Diligence",
|
||||||
|
"Due Diligence": "Due Diligence",
|
||||||
|
"Interest expense": "Interest Expense",
|
||||||
|
"Interest Expense": "Interest Expense",
|
||||||
|
"Contributed capital - LP": "Contributed Capital - LP",
|
||||||
|
"Deal fees": "Deal Fees",
|
||||||
|
"Deal Fees": "Deal Fees",
|
||||||
|
"Deferred capital calls - LP": "Deferred Capital Calls - LP",
|
||||||
|
"Due from general partner": "Due from General Partner",
|
||||||
|
"Due from General Partner": "Due from General Partner",
|
||||||
|
"Due from related parties": "Due from Related Parties",
|
||||||
|
"Due from the management company": "Due from the Management Company",
|
||||||
|
"Due to other": "Due to Other",
|
||||||
|
"Due to portfolio company": "Due to Portfolio Company",
|
||||||
|
"Dues and subscriptions": "Dues and Subscriptions",
|
||||||
|
"Filing fees": "Filing Fees",
|
||||||
|
"Fund administration fees": "Fund Administration Fees",
|
||||||
|
"Legal fees": "Legal Fees",
|
||||||
|
"Legal accrual": "Legal Fees Accrual",
|
||||||
|
"Management fee payable": "Management Fee Payable",
|
||||||
|
"Management fees": "Management Fees",
|
||||||
|
"Management fees offset": "Management fees offset",
|
||||||
|
"Other accrual": "Other Accrual",
|
||||||
|
"Taxes": "Taxes",
|
||||||
|
"Conference and trade shows": "Conference and Trade Shows",
|
||||||
|
"Website design": "Website Design",
|
||||||
|
"Tax prep fees": "Tax Prep Fees",
|
||||||
|
"Syndication costs - Placement agent fees":
|
||||||
|
"Syndication Costs - Placement Agent Fees",
|
||||||
|
"Software": "Software",
|
||||||
|
"Reimbursement of partnership expenses":
|
||||||
|
"Reimbursement of Partnership Expenses",
|
||||||
|
"Reimbursement of Partnership Expenses":
|
||||||
|
"Reimbursement of Partnership Expenses",
|
||||||
|
"Other interest income": "Other Interest Income",
|
||||||
|
"Other Interest Income": "Other Interest Income",
|
||||||
|
"Fund administration accrual": "Fund Administration Accrual",
|
||||||
|
"Contributed capital - GP": "Contributed Capital - GP",
|
||||||
|
"Contributed Capital - GP": "Contributed Capital - GP",
|
||||||
|
"Contributed capital - cashless offset - GP":
|
||||||
|
"Contributed capital - cashless offset - GP",
|
||||||
|
"Capital call receivable - GP": "Capital Call Receivable - GP",
|
||||||
|
"Capital Call Receivable - GP": "Capital Call Receivable - GP",
|
||||||
|
"Tax preparation fees accrual": "Tax Preparation Fees Accrual",
|
||||||
|
"Bank - Checking Account": "Bank",
|
||||||
|
"Bank 1099able - Checking Account": "Bank",
|
||||||
|
"Money market funds": "Money market funds",
|
||||||
|
"Expense Payable": "Expense Payable",
|
||||||
|
"Audit accrual": "Audit Accrual",
|
||||||
|
"Other professional fees": "Other Professional Fees",
|
||||||
|
"Bank 1099able - Checking Account - check":
|
||||||
|
"Bank 1099able - Checking Account",
|
||||||
|
"Organization costs expense": "Organization Costs Expense",
|
||||||
|
"Marketing expenses": "Marketing Expenses",
|
||||||
|
"Marketing Expenses": "Marketing Expenses",
|
||||||
|
"Travel": "Travel",
|
||||||
|
"Meal": "Meal",
|
||||||
|
"Syndication costs - general": "Syndication Costs - General",
|
||||||
|
"Other fees": "Other Fees",
|
||||||
|
"Subscription Fee - LP": "Subscription Fee - LP",
|
||||||
|
"Organization Costs Amortization": "Organization Costs Amortization",
|
||||||
|
"Acquisition fee": "Acquisition Fee",
|
||||||
|
"Acquisition Fee": "Acquisition Fee",
|
||||||
|
"Bank Interest Income": "Bank Interest Income",
|
||||||
|
"Interest Income": "Interest Income",
|
||||||
|
"Late Interest Income": "Late Interest Income",
|
||||||
|
"Dividend Income": "Dividend Income",
|
||||||
|
"Other Income": "Other Income",
|
||||||
|
"Other professional Fees": "Other Professional Fees",
|
||||||
|
}
|
||||||
434
task/utils/prompts.py
Normal file
434
task/utils/prompts.py
Normal file
@ -0,0 +1,434 @@
|
|||||||
|
DOCUMENT_CLASSIFY_PROMPT = """
|
||||||
|
You are an expert in document classification where you are given a document under <document> xml tags and you need to classify it based on the data inside <document-type> xml tag.
|
||||||
|
|
||||||
|
Your output should be enclosed inside <output> xml tag and it should be based on one of the <document-type> options mentioned below.Just output <document-type> in the output <output> xml tag.
|
||||||
|
|
||||||
|
### Document
|
||||||
|
<document>
|
||||||
|
{document}
|
||||||
|
</document>
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
## Document Types that need to classified.
|
||||||
|
<document-type>
|
||||||
|
["LPA(Limited Partner Agreement)", "LPA-Amendment(Limited Partner Agreement-Amendment)", "Side Letter"]
|
||||||
|
</document-type>
|
||||||
|
"""
|
||||||
|
|
||||||
|
LPA_STATEMENT_PROMPT = """
|
||||||
|
<SYSTEM>
|
||||||
|
You are an expert in LPA (Limited Partner Agreement) data extraction that uses a Chain of Thought (CoT) approach with reflection to answer queries. Follow these steps:
|
||||||
|
|
||||||
|
1. Think through the problem step by step within the <thinking> tags.
|
||||||
|
2. Reflect on your thinking to check for any errors or improvements within the <reflection> tags.
|
||||||
|
3. Make any necessary adjustments based on your reflection.
|
||||||
|
4. Provide your final, concise answer within the <output> tags.
|
||||||
|
|
||||||
|
Important: The <thinking> and <reflection> sections are for your internal reasoning process only.
|
||||||
|
Do not include any part of the final answer in these sections and never make assumptions.
|
||||||
|
The actual response to the query must be entirely contained within the <output> tags.
|
||||||
|
|
||||||
|
Use the following format for your response:
|
||||||
|
<thinking>
|
||||||
|
[Your step-by-step reasoning goes here. This is your internal thought process, not the final answer. Please explain the reason for selecting each piece of information.]
|
||||||
|
</thinking>
|
||||||
|
<reflection>
|
||||||
|
[Your reflection on your reasoning, checking for errors or improvements]
|
||||||
|
</reflection>
|
||||||
|
<output>
|
||||||
|
[Your final, concise answer to the query in the specified JSON format. This is the only part that will be shown to the user.]
|
||||||
|
</output>
|
||||||
|
|
||||||
|
</SYSTEM>
|
||||||
|
|
||||||
|
<USER>
|
||||||
|
You are given data in the <document> xml tag. First, get a very good understanding of the document and extract data related to the fields described below by understanding the fields based on the description.
|
||||||
|
|
||||||
|
### Document
|
||||||
|
<document>
|
||||||
|
{document}
|
||||||
|
</document>
|
||||||
|
|
||||||
|
Final output to place under <output> xml tag and populate empty values if the data is not present in the document.
|
||||||
|
<output-format>
|
||||||
|
{{
|
||||||
|
"fundName": "name of the fund",
|
||||||
|
"fileInfo":"name of the fund and under which type of the document is it - Date of the agreement"
|
||||||
|
"managementFee": "Extract the following information from the document:
|
||||||
|
1. Annual management fee percentage(s), including any changes over time
|
||||||
|
2. Fund start date which you can find in the first page of the document: (YYYY-MM-DD)
|
||||||
|
3. Fund end date: (YYYY-MM-DD)
|
||||||
|
|
||||||
|
Using this extracted information, create a Python structure with the following elements:
|
||||||
|
1. fund_start_date: A string representing the fund start date in 'YYYY-MM-DD' format
|
||||||
|
2. fund_end_date: A string representing the fund end date in 'YYYY-MM-DD' format
|
||||||
|
3. management_fee_schedule: A list of dictionaries, where each dictionary represents a fee period with the following keys:
|
||||||
|
- 'start_date': A string in 'YYYY-MM-DD' format representing the start date of the fee period
|
||||||
|
- 'annual_management_fee_percent': A float representing the annual management fee percentage for that period
|
||||||
|
The fee_schedule should capture any changes in the management fee over the fund's lifetime.
|
||||||
|
Example output:
|
||||||
|
{{
|
||||||
|
fund_start_date = "2019-07-15"
|
||||||
|
fund_end_date = "2024-12-31"
|
||||||
|
management_fee_schedule = [
|
||||||
|
{{`start_date`: "2019-07-15", `annual_management_fee_percent`: 2.5}},
|
||||||
|
{{`start_date`: "2023-07-01", `annual_management_fee_percent`: 2.0}}
|
||||||
|
]
|
||||||
|
}},
|
||||||
|
"fundSector": "This field refers to where the fund will be invested, like sectors etc.",
|
||||||
|
"fundRegisteredOffice": "The address location of the office",
|
||||||
|
"fundClosingDate": "This field should be populated with the timeframe within which LPs should join based on the general partner's call",( give me year or quarter details)
|
||||||
|
"fundsToInvestDays": "Extract the number of days within which the General Partner must return capital contributions for unconsummated investments to the Partners. Please provide only the numerical value?",
|
||||||
|
"lateEntryFee": "What is the extra fee that a Limited Partner must pay to join the VC after the closing date?",
|
||||||
|
"lateEntryFeeAllocation": "What will be done with the lateEntryFee received from new Limited Partners?",
|
||||||
|
"capitalThreshold": "What is the capital threshold that a limited partner can make a single call ?",
|
||||||
|
"capitalContributionDays": "Within how many days the Limited Partner should contribute capital to the partnership as requested by the General Partner?((Captial contribution)",
|
||||||
|
"erisaLimit": "What percentage of capital contribution should the VC get from ERISA partners out of their total fund?",
|
||||||
|
"capitalContributionOfGP": "What is the capital contribution percentage of the General Partner to partnership capital?",
|
||||||
|
"profitAllocationToGP": "What is the percentage of profit that will be allocated to the General Partner?", # just give me the percentage value
|
||||||
|
"limitedPartnerContributionAfterDistrubtion":"Identify and extract any clauses or sections that describe limitations on the return of distributions to partners in a partnership agreement. Specifically, look for:
|
||||||
|
1. The maximum amount partners may be required to return
|
||||||
|
2. Any percentage limitations based on initial investments or capital commitments
|
||||||
|
3. Distinctions between different types of partners (e.g., Limited Partners vs. General Partners)
|
||||||
|
4. Time limits for requesting the return of distributions
|
||||||
|
5. Conditions under which distributions may be recalled
|
||||||
|
6. Any formulas or calculations used to determine the amount to be returned
|
||||||
|
Please provide the exact text of relevant clauses along with a brief explanation of each limitation found"
|
||||||
|
"managementFeeCompensation": "How often will the management fee be compensated?",
|
||||||
|
"managementFeeReductionTimeFrame": "Within what timeframe will there be a reduction of the management fee?",
|
||||||
|
"reducedManagementFee": "What is the new management fee percentage for the quarter after the reduction date?",
|
||||||
|
"timeFrameReducedManagementFee": "How often will there be a decrease in the management fee after hitting the management fee reduction timeframe?",# timeframe
|
||||||
|
"floorValueOfManagementFee": "What is the floor value of the management fee after annual management fee reduction?",
|
||||||
|
"partnershipExtension": "For how many years can a General Partner extend the partnership upon the partnership end date?",
|
||||||
|
"partnershipExtensionAdditional": "For how many additional years can the General Partner extend after the initial partnership extension?",
|
||||||
|
"Management Expenses":"Extract complete information in any clauses or sections that describe about the expenses related to management and there limitations and i want the section as well",
|
||||||
|
"Fund/Partnership Expenses": "Extract complete information in any clauses or sections that describe about the expenses related to Fund/Partnership and there limitations and i want the section as well",
|
||||||
|
"OnetimeOrganizationExpenses":"Extract complete information in any clauses or sections that describe about the expenses related to OnetimeOrganization and there limitations and i want the section as well"
|
||||||
|
}}
|
||||||
|
</output-format>
|
||||||
|
</user>
|
||||||
|
"""
|
||||||
|
|
||||||
|
SIDE_LETTER_PROMPT = """
|
||||||
|
You are given side letter document related to a limited partner under <document> xml tag.
|
||||||
|
|
||||||
|
First get a very good understanding of the below document under <document> xml tag . Your task is to generate the response in below format <output-format> in <output> xml tags and partner id related to the partner to be written to <id>
|
||||||
|
|
||||||
|
## Document
|
||||||
|
<document>
|
||||||
|
{document}
|
||||||
|
</document>
|
||||||
|
|
||||||
|
## partner id document
|
||||||
|
<partner-id-document>
|
||||||
|
{role_doc}
|
||||||
|
</partner-id-document>
|
||||||
|
|
||||||
|
|
||||||
|
<output-format>
|
||||||
|
{{
|
||||||
|
"fundName":"Name of the fund",
|
||||||
|
"fundDate":"Limited Partner Agreement date in format : YYYY-MM-DD",
|
||||||
|
"partnerName":"Name of the investing partner",
|
||||||
|
"quarter":"",
|
||||||
|
"platform":"Name of the platform which generated the report,if you are not sure return empty",
|
||||||
|
"carriedInterest": " <case:1> : If there is a change in carried interest then execute below one
|
||||||
|
Just give me the percentage value of profit/carry interest that will be allocated to the General Partner.
|
||||||
|
else
|
||||||
|
<case:2>: Just return `No changes` ",
|
||||||
|
"managementFee": "
|
||||||
|
<case:1> : If there is a changing in management fee structure execute below one
|
||||||
|
Extract the following information from the document:
|
||||||
|
1. Annual management fee percentage(s), including any changes over time
|
||||||
|
2. Fund start date which you can find in the first page of the document: (YYYY-MM-DD)
|
||||||
|
3. Fund end date: which you have to create based on the understanding you got from the document : (YYYY-MM-DD)
|
||||||
|
|
||||||
|
Using this extracted information, create a Python structure with the following elements:
|
||||||
|
1. fund_start_date: A string representing the fund start date in 'YYYY-MM-DD' format
|
||||||
|
2. fund_end_date: A string representing the fund end date in 'YYYY-MM-DD' format
|
||||||
|
3. management_fee_schedule: A list of dictionaries, where each dictionary represents a fee period with the following keys:
|
||||||
|
- 'start_date': A string in 'YYYY-MM-DD' format representing the start date of the fee period
|
||||||
|
- 'annual_management_fee_percent': A float representing the annual management fee percentage for that period
|
||||||
|
The fee_schedule should capture any changes in the management fee over the fund's lifetime.
|
||||||
|
Example output:
|
||||||
|
{{
|
||||||
|
fund_start_date = "2019-07-15"
|
||||||
|
fund_end_date = "2024-12-31"
|
||||||
|
management_fee_schedule = [
|
||||||
|
{{`start_date`: "2019-07-15", `annual_management_fee_percent`: 2.5}},
|
||||||
|
{{`start_date`: "2023-07-01", `annual_management_fee_percent`: 2.0}}
|
||||||
|
]
|
||||||
|
}}
|
||||||
|
else
|
||||||
|
<case:2> :Just return `No changes` ",
|
||||||
|
"sideLetterKeyPoints": "Extract key details in the document, Just return it in a string",
|
||||||
|
}}
|
||||||
|
</output-format>
|
||||||
|
|
||||||
|
<id>
|
||||||
|
"Extract value related to partner id name from <role-document> document"
|
||||||
|
</id>
|
||||||
|
"""
|
||||||
|
|
||||||
|
ZIVE_SECURITY_TYPE_PROMPT = """
|
||||||
|
You are Fund AI assistant . Your task is to convert "Security type" fund investment to platform supported security type. If you are unsure about the security type make "PREFERRED" as default security type.
|
||||||
|
|
||||||
|
## Below are the security type that platform supported:
|
||||||
|
<securities>
|
||||||
|
<security>
|
||||||
|
<type>PRICED</type>
|
||||||
|
<description>A security issued during a funding round where the company's valuation has been established. Investors purchase shares at a fixed price based on the agreed pre-money valuation.</description>
|
||||||
|
</security>
|
||||||
|
|
||||||
|
<security>
|
||||||
|
<type>PRE MONEY SAFE</type>
|
||||||
|
<description>A SAFE is a contract that allows investors to provide capital to a company in exchange for the right to receive equity in the future. A pre-money SAFE sets the valuation cap before new investment, not including the incoming SAFE money.</description>
|
||||||
|
</security>
|
||||||
|
|
||||||
|
<security>
|
||||||
|
<type>CONVERTIBLE NOTE</type>
|
||||||
|
<description>A type of debt instrument that converts into equity upon a specific event, such as a future financing round. Convertible notes often include interest and may have a valuation cap or discount for conversion.</description>
|
||||||
|
</security>
|
||||||
|
|
||||||
|
<security>
|
||||||
|
<type>WARRANTS</type>
|
||||||
|
<description>A financial instrument that gives the holder the right to purchase company stock at a specific price before expiration. Warrants are often issued as part of a funding deal or incentive.</description>
|
||||||
|
</security>
|
||||||
|
|
||||||
|
<security>
|
||||||
|
<type>PREFERRED</type>
|
||||||
|
<description>Preferred stock offers investors certain privileges over common stockholders, such as priority in dividends and liquidation preferences. It is commonly issued during VC funding rounds.</description>
|
||||||
|
</security>
|
||||||
|
|
||||||
|
<security>
|
||||||
|
<type>TOKEN</type>
|
||||||
|
<description>A blockchain-based digital asset representing various rights or utilities within a project, such as governance, usage, or financial stake. Tokens are issued during ICOs (Initial Coin Offerings) or similar fundraising efforts.</description>
|
||||||
|
</security>
|
||||||
|
|
||||||
|
<security>
|
||||||
|
<type>COMMON</type>
|
||||||
|
<description>Common stock represents basic ownership in a company, typically held by founders and employees. Common stockholders usually have voting rights but are last in line for payouts during liquidation.</description>
|
||||||
|
</security>
|
||||||
|
|
||||||
|
<security>
|
||||||
|
<type>OPTIONS</type>
|
||||||
|
<description>Stock options grant the holder the right to purchase shares at a pre-determined price within a set time frame. Often used to incentivize employees or early contributors.</description>
|
||||||
|
</security>
|
||||||
|
|
||||||
|
<security>
|
||||||
|
<type>BRIDGE LOAN</type>
|
||||||
|
<description>Short-term debt financing provided to a company to cover immediate capital needs until a more significant funding round or event occurs.</description>
|
||||||
|
</security>
|
||||||
|
|
||||||
|
<security>
|
||||||
|
<type>Advance on Convertible Securities</type> # if its applicable it always be same value
|
||||||
|
<description>A form of bridge financing where investors provide capital upfront with the agreement to receive equity or another security type in a future round.</description>
|
||||||
|
</security>
|
||||||
|
|
||||||
|
<security>
|
||||||
|
<type>Partnership Interest</type> # if its applicable it always be same value
|
||||||
|
<description>An ownership stake in a partnership entity. Holders may receive distributions based on profits and have voting rights in the partnership's governance.</description>
|
||||||
|
</security>
|
||||||
|
|
||||||
|
<security>
|
||||||
|
<type>Membership Interest</type> # if its applicable it always be same value
|
||||||
|
<description>An ownership stake in an LLC (Limited Liability Company). Membership interests may include voting rights, profit distributions, and management participation.</description>
|
||||||
|
</security>
|
||||||
|
|
||||||
|
<security>
|
||||||
|
<type>SAFT</type>
|
||||||
|
<description>Similar to a SAFE but specifically used for token-based projects. Investors provide funds upfront in exchange for rights to receive future tokens once they are issued.</description>
|
||||||
|
</security>
|
||||||
|
|
||||||
|
<security>
|
||||||
|
<type>BOND</type>
|
||||||
|
<description>A fixed-income security representing a loan made by an investor to a borrower. The issuer agrees to repay the principal along with interest over a specified term.</description>
|
||||||
|
</security>
|
||||||
|
|
||||||
|
<security>
|
||||||
|
<type>PRIVATE INVESTMENT</type>
|
||||||
|
<description>An investment made directly into a private company. This can take various forms, including equity, debt, or hybrid instruments.</description>
|
||||||
|
</security>
|
||||||
|
|
||||||
|
<security>
|
||||||
|
<type>ORDINARY</type>
|
||||||
|
<description>Typically refers to ordinary shares, which are similar to common stock. Holders may have voting rights and share in the profits but are last in line for liquidation payouts.</description>
|
||||||
|
</security>
|
||||||
|
</securities>
|
||||||
|
|
||||||
|
## input
|
||||||
|
<input>
|
||||||
|
{security_type}
|
||||||
|
</input>
|
||||||
|
|
||||||
|
## Always output need to exact value and to be enclosed in <output> xml tags. Just give the output not need of any explanation
|
||||||
|
"""
|
||||||
|
|
||||||
|
ACCOUNT_TYPE_PROMPT = """
|
||||||
|
You are a Fund Expert in classifying account types. You are given account type and you need to look for exact match of the <input> into platform supported account type and if not it should match with other category which need to derive based on other category and you need to write into <output> xml tags.
|
||||||
|
|
||||||
|
# platform supported account types
|
||||||
|
<account-type>
|
||||||
|
Bank
|
||||||
|
Money market funds
|
||||||
|
Capital Call Receivable - LP
|
||||||
|
Portfolio Interest Receivable
|
||||||
|
Reimbursement Payable
|
||||||
|
Due to investment
|
||||||
|
Portfolio Interest Income
|
||||||
|
Due to Related Party
|
||||||
|
Due to Management Company
|
||||||
|
Investment - Unrealized Gain/Loss
|
||||||
|
Unrealized Gain/Loss
|
||||||
|
Portfolio Company Receivable
|
||||||
|
Realized Gain/Loss
|
||||||
|
Distribution Payable
|
||||||
|
Cash Distribution - LP
|
||||||
|
Due to General Partner
|
||||||
|
Misc Receivable
|
||||||
|
Temporarily Commingled Cash from Bank Receivable
|
||||||
|
Due from the Management Company
|
||||||
|
Receivable between Master Feeder
|
||||||
|
Receivable from Affiliate
|
||||||
|
Payable between Master Feeder
|
||||||
|
Payable to Affiliate
|
||||||
|
Due from Affiliates
|
||||||
|
Due to Affiliates
|
||||||
|
Accelaration Prepaids
|
||||||
|
Organization Costs Asset
|
||||||
|
Investment
|
||||||
|
Management Fee Payable
|
||||||
|
Admission Fee Payable
|
||||||
|
Admission Fees
|
||||||
|
Due to Limited Partner
|
||||||
|
Management Fees Prepaid
|
||||||
|
Due to Portfolio Company
|
||||||
|
Deferred Capital Calls - LP
|
||||||
|
Deferred Capital Calls - GP
|
||||||
|
Audit Accrual
|
||||||
|
Fund Administration Accrual
|
||||||
|
Tax Accrual
|
||||||
|
Syndication Costs - General
|
||||||
|
Contributed Capital - LP
|
||||||
|
Misc Income
|
||||||
|
Management Fees
|
||||||
|
Management fees offset
|
||||||
|
Audit Fees
|
||||||
|
Tax Prep Fees
|
||||||
|
Fund Administration Fees
|
||||||
|
Administration Fees
|
||||||
|
Administration Fees - Allocated from Master
|
||||||
|
Audit Fees - Allocated from Master
|
||||||
|
Bank Charges - Allocated from Master
|
||||||
|
Commission Expense
|
||||||
|
Commission Expense - Allocated from Master
|
||||||
|
FATCA Expenses
|
||||||
|
FATCA Expenses - Allocated from Master
|
||||||
|
FS Preparation
|
||||||
|
FS Preparation - Allocated From Master
|
||||||
|
Legal Expenses - Allocated from Master
|
||||||
|
Marketing Expenses - Allocated from Master
|
||||||
|
Offering & Organizational Cost
|
||||||
|
Offering & Organizational Cost - Allocated from Master
|
||||||
|
Performance Fees
|
||||||
|
Prepaid Offering & Organizational Cost
|
||||||
|
Travelling Expense
|
||||||
|
Travelling Expense - Allocated from Master
|
||||||
|
Administration Fees Payable
|
||||||
|
Bank Charges Payable
|
||||||
|
FATCA Payable
|
||||||
|
FS Prepartion Payable
|
||||||
|
Marketing Expenses Payable
|
||||||
|
Offering & Organizational Cost Payable
|
||||||
|
Performance Fees Payable
|
||||||
|
Travelling Expense Payable
|
||||||
|
Audit Fees Payable
|
||||||
|
Due to/from GP
|
||||||
|
Unrealized Gain (Loss) from Private Investment Companies
|
||||||
|
Unrealized Gain (Loss) from Private Investment Companies - Allocated from Master
|
||||||
|
Unrealized Gain (Loss) from Cryptocurrencies (Price) - Allocated from Master
|
||||||
|
Unrealized Gain (Loss) on Investment in Cryptocurrencies (Price)
|
||||||
|
Due from Limited Partner
|
||||||
|
Due/To from Offshore Feeder Fund
|
||||||
|
Due/To from Offshore Master Fund
|
||||||
|
Due/To from Onshore Feeder Fund
|
||||||
|
Receivable from pending settlement
|
||||||
|
Investment in Cryptocurrencies, (At Cost)
|
||||||
|
Investment in Fixed Income Bond
|
||||||
|
Investment in Master
|
||||||
|
Investment in Private Investment Companies
|
||||||
|
Investment In Safes, (At Cost)
|
||||||
|
Investment In SAFT, (At Cost)
|
||||||
|
Change in Unrealized Gain (Loss) from Private Investment Companies
|
||||||
|
Change in Unrealized Gain (Loss) on Investment in Cryptocurrencies (Price)
|
||||||
|
Change in Unrealized Gain (Loss) on Investment in Cryptocurrencies (Price) - Allocated from Master
|
||||||
|
Income from Master
|
||||||
|
Staking Income
|
||||||
|
Staking Income - Allocated from Master
|
||||||
|
Interest Income - Allocated from Master
|
||||||
|
Realized Gain (Loss) on Investment in Cryptocurrencies (Price)
|
||||||
|
Realized Gain (Loss) on Investment in Cryptocurrencies (Price) - Allocated from Master
|
||||||
|
Legal Fees
|
||||||
|
Legal Fees Accrual
|
||||||
|
Interest Payable
|
||||||
|
Loan Payable
|
||||||
|
Organization Costs Expense
|
||||||
|
Filing Fees
|
||||||
|
Office supplies
|
||||||
|
Subscription Fee - LP
|
||||||
|
Organization Costs Amortization
|
||||||
|
Distribution Payable - GP
|
||||||
|
Tax Preparation Fees Accrual
|
||||||
|
Escrow from portfolio investments sold
|
||||||
|
Cash Distribution - GP
|
||||||
|
Cash Carry Distributions
|
||||||
|
Carried Interest Distribution - GP
|
||||||
|
Bank Charges
|
||||||
|
Bank Interest Income
|
||||||
|
Consulting
|
||||||
|
Organization Expenses
|
||||||
|
Due Diligence
|
||||||
|
Interest Expense
|
||||||
|
Deal Fees
|
||||||
|
Due from General Partner
|
||||||
|
Dues and Subscriptions
|
||||||
|
Conference and Trade Shows
|
||||||
|
Website Design
|
||||||
|
Syndication Costs - Placement Agent Fees
|
||||||
|
Software
|
||||||
|
Reimbursement of Partnership Expenses
|
||||||
|
Contributed Capital - GP
|
||||||
|
Contributed capital - cashless offset - GP
|
||||||
|
Capital Call Receivable - GP
|
||||||
|
Expense Payable
|
||||||
|
Marketing Expenses
|
||||||
|
Travel
|
||||||
|
Meal
|
||||||
|
Acquisition Fee
|
||||||
|
Interest Income
|
||||||
|
Late Interest Income
|
||||||
|
Dividend Income
|
||||||
|
</account-type>
|
||||||
|
|
||||||
|
## if you are not able to find a exact math in the above <account-type> xml list then you need to pick the applicable value from the <other-type> xml data.
|
||||||
|
<other-type>
|
||||||
|
Due to Other
|
||||||
|
Other Prepaids
|
||||||
|
Other assets
|
||||||
|
Other Accrual
|
||||||
|
Other Professional Fees
|
||||||
|
Other Fees
|
||||||
|
Due from Related Parties
|
||||||
|
Other Interest Income
|
||||||
|
Misc Income
|
||||||
|
</other-type>
|
||||||
|
|
||||||
|
## input account type
|
||||||
|
<input>
|
||||||
|
{account_type}
|
||||||
|
<input>
|
||||||
|
|
||||||
|
Think step by step when deciding the the output and the output should be a exact match not close match with <account-types> and be a close match with <other-types>.
|
||||||
|
"""
|
||||||
|
|
||||||
373
task/utils/token_tracker.py
Normal file
373
task/utils/token_tracker.py
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
"""
|
||||||
|
Token usage tracker for AI file processing.
|
||||||
|
|
||||||
|
Records per-model input/output token counts during a processing session and
|
||||||
|
logs a record per model when the session ends. (Posting usage to the backend
|
||||||
|
``{BASE_URL}/api/v1/compliance/token`` is disabled — usage is logged only.)
|
||||||
|
|
||||||
|
Design — avoid double counting
|
||||||
|
------------------------------
|
||||||
|
LLM calls in this repo flow through several wrappers (GeminiBedrockClient,
|
||||||
|
think_tool, RLM engine). If every layer recorded usage, each API call would be
|
||||||
|
counted multiple times (the 5x bug we hit before).
|
||||||
|
|
||||||
|
Rule: record at the **terminal caller** — the function that issues a single
|
||||||
|
network call to the model. Shim layers (e.g. ``GeminiBedrockClient.invoke_model``
|
||||||
|
which only repackages a response) must NOT record.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
from utils.token_tracker import token_session, record_usage
|
||||||
|
|
||||||
|
with token_session(task_name="doc.pdf", entity_id=eid,
|
||||||
|
reference_id=event_id):
|
||||||
|
# any LLM call inside here will feed record_usage(model, in, out)
|
||||||
|
...
|
||||||
|
|
||||||
|
Thread-safety
|
||||||
|
-------------
|
||||||
|
Uses ``contextvars.ContextVar`` so each request/thread gets an isolated
|
||||||
|
session. The SQS processor's ThreadPoolExecutor calls ``handler()`` per
|
||||||
|
thread; each handler opens its own ``token_session``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import contextvars
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
from collections import defaultdict
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# USD per token. Source: Anthropic/Google published list prices.
|
||||||
|
_M = 1_000_000
|
||||||
|
|
||||||
|
# (input_price_per_token, output_price_per_token)
|
||||||
|
# Fund-upload flow standardizes on Claude Sonnet 4.6 — Bedrock and Vertex
|
||||||
|
# both use the same list price ($3 in / $15 out per 1M). Gemini entries are
|
||||||
|
# kept because image_template_analysis (document-studio flow) uses Gemini.
|
||||||
|
PRICING: dict[str, tuple[float, float]] = {
|
||||||
|
"claude-sonnet-4-6": (3.0 / _M, 15.0 / _M),
|
||||||
|
"anthropic.claude-sonnet-4-6": (3.0 / _M, 15.0 / _M),
|
||||||
|
"us.anthropic.claude-sonnet-4-6": (3.0 / _M, 15.0 / _M),
|
||||||
|
# Gemini 3 Pro family (image processor, separate flow from fund uploads)
|
||||||
|
"gemini-3-pro-preview": (2.0 / _M, 12.0 / _M),
|
||||||
|
"gemini-3-pro-image-preview": (2.0 / _M, 12.0 / _M),
|
||||||
|
# Gemini 2.5 Pro — default for LLM_BACKEND=gemini. List price for ≤ 200k
|
||||||
|
# prompt tokens (per https://ai.google.dev/gemini-api/docs/pricing —
|
||||||
|
# tiered: $1.25/$10 ≤200k, $2.50/$15 >200k incl. thinking). We bill the
|
||||||
|
# lower tier; ops can override via TOKEN_PRICING_OVERRIDES for >200k flows.
|
||||||
|
"gemini-2.5-pro": (1.25 / _M, 10.0 / _M),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fallback for unrecognized models — Sonnet 4.6 list price. This is a safety
|
||||||
|
# net only; unknown models log a WARNING so Ops can add them to PRICING.
|
||||||
|
_DEFAULT_PRICE = (3.0 / _M, 15.0 / _M)
|
||||||
|
|
||||||
|
_COMPLIANCE_PATH = "/api/v1/compliance/token"
|
||||||
|
|
||||||
|
# Ops can override pricing without a deploy by setting TOKEN_PRICING_OVERRIDES
|
||||||
|
# to a JSON object of {"model_id": [input_per_token, output_per_token]} or
|
||||||
|
# {"model_id": {"input_per_M": 3.0, "output_per_M": 15.0}}.
|
||||||
|
def _load_price_overrides() -> dict[str, tuple[float, float]]:
|
||||||
|
raw = os.getenv("TOKEN_PRICING_OVERRIDES", "").strip()
|
||||||
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
data = json.loads(raw)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("TOKEN_PRICING_OVERRIDES is not valid JSON: %s", e)
|
||||||
|
return {}
|
||||||
|
out: dict[str, tuple[float, float]] = {}
|
||||||
|
for model, val in data.items():
|
||||||
|
try:
|
||||||
|
if isinstance(val, (list, tuple)) and len(val) == 2:
|
||||||
|
out[model] = (float(val[0]), float(val[1]))
|
||||||
|
elif isinstance(val, dict):
|
||||||
|
pin = float(val.get("input_per_M", 0)) / _M
|
||||||
|
pout = float(val.get("output_per_M", 0)) / _M
|
||||||
|
out[model] = (pin, pout)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("TOKEN_PRICING_OVERRIDES bad entry for %r: %s", model, e)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
PRICING.update(_load_price_overrides())
|
||||||
|
|
||||||
|
_unknown_models_warned: set[str] = set()
|
||||||
|
|
||||||
|
|
||||||
|
def _price_for(model: str) -> tuple[float, float]:
|
||||||
|
if not model:
|
||||||
|
return _DEFAULT_PRICE
|
||||||
|
if model in PRICING:
|
||||||
|
return PRICING[model]
|
||||||
|
# Prefix match: "us.anthropic.claude-sonnet-4-6:v1" etc.
|
||||||
|
for key, price in PRICING.items():
|
||||||
|
if model.startswith(key):
|
||||||
|
return price
|
||||||
|
if model not in _unknown_models_warned:
|
||||||
|
_unknown_models_warned.add(model)
|
||||||
|
logger.warning(
|
||||||
|
"token_tracker: no PRICING entry for %r — using default $3/$15 per 1M. "
|
||||||
|
"Add to PRICING or set TOKEN_PRICING_OVERRIDES to correct billing.",
|
||||||
|
model,
|
||||||
|
)
|
||||||
|
return _DEFAULT_PRICE
|
||||||
|
|
||||||
|
|
||||||
|
# Status values the backend AiUsageLog schema accepts. Anything else is
|
||||||
|
# rejected with a ValidatorError (served as a 500, so it would retry to
|
||||||
|
# exhaustion and then drop the record).
|
||||||
|
_BACKEND_STATUS_ENUM = {"in_progress", "completed", "failed"}
|
||||||
|
|
||||||
|
|
||||||
|
class _Session:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
task_name: str,
|
||||||
|
type_: str,
|
||||||
|
entity_id: str,
|
||||||
|
reference_id: Optional[str] = None,
|
||||||
|
reference_model: Optional[str] = None,
|
||||||
|
meta: Optional[dict] = None,
|
||||||
|
role_id: Optional[str] = None,
|
||||||
|
):
|
||||||
|
self.task_name = task_name
|
||||||
|
self.type = type_
|
||||||
|
self.entity_id = entity_id
|
||||||
|
self.reference_id = reference_id
|
||||||
|
self.reference_model = reference_model
|
||||||
|
self.meta = meta
|
||||||
|
self.role_id = role_id
|
||||||
|
self.started_at = datetime.now(timezone.utc).isoformat()
|
||||||
|
self.ended_at: Optional[str] = None
|
||||||
|
self.status = "in_progress"
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
# {model: {"input": int, "output": int}}
|
||||||
|
self._usage: dict[str, dict[str, int]] = defaultdict(
|
||||||
|
lambda: {"input": 0, "output": 0}
|
||||||
|
)
|
||||||
|
|
||||||
|
def record(self, model: str, input_tokens: int, output_tokens: int) -> None:
|
||||||
|
if not (input_tokens or output_tokens):
|
||||||
|
return
|
||||||
|
model_key = model or "unknown"
|
||||||
|
with self._lock:
|
||||||
|
u = self._usage[model_key]
|
||||||
|
u["input"] += int(input_tokens or 0)
|
||||||
|
u["output"] += int(output_tokens or 0)
|
||||||
|
|
||||||
|
def set_status(self, status: str) -> None:
|
||||||
|
self.status = status
|
||||||
|
|
||||||
|
def set_meta(self, meta: Optional[dict]) -> None:
|
||||||
|
self.meta = meta
|
||||||
|
|
||||||
|
def to_records(self) -> list[dict]:
|
||||||
|
# The backend AiUsageLog schema only accepts _BACKEND_STATUS_ENUM.
|
||||||
|
# Custom statuses set via set_status (e.g. "duplicate",
|
||||||
|
# "classified_other") 500 with a ValidatorError on EVERY retry, then
|
||||||
|
# drop the record. Post a valid status and keep the semantic one in
|
||||||
|
# meta.statusDetail.
|
||||||
|
status = self.status
|
||||||
|
meta = self.meta
|
||||||
|
if status not in _BACKEND_STATUS_ENUM:
|
||||||
|
meta = {**(meta or {}), "statusDetail": status}
|
||||||
|
status = "completed"
|
||||||
|
records: list[dict] = []
|
||||||
|
with self._lock:
|
||||||
|
items = list(self._usage.items())
|
||||||
|
for model, u in items:
|
||||||
|
p_in, p_out = _price_for(model)
|
||||||
|
price = u["input"] * p_in + u["output"] * p_out
|
||||||
|
records.append({
|
||||||
|
"taskName": self.task_name,
|
||||||
|
"type": self.type,
|
||||||
|
"inputTokens": u["input"],
|
||||||
|
"outputTokens": u["output"],
|
||||||
|
"price": round(price, 6),
|
||||||
|
"startedAt": self.started_at,
|
||||||
|
"endedAt": self.ended_at,
|
||||||
|
"model": model,
|
||||||
|
"status": status,
|
||||||
|
"referenceId": self.reference_id,
|
||||||
|
"referenceModel": self.reference_model,
|
||||||
|
"meta": meta,
|
||||||
|
"entityId": self.entity_id,
|
||||||
|
})
|
||||||
|
if not records:
|
||||||
|
# Post a zero record so the backend still sees the session.
|
||||||
|
records.append({
|
||||||
|
"taskName": self.task_name,
|
||||||
|
"type": self.type,
|
||||||
|
"inputTokens": 0,
|
||||||
|
"outputTokens": 0,
|
||||||
|
"price": 0,
|
||||||
|
"startedAt": self.started_at,
|
||||||
|
"endedAt": self.ended_at,
|
||||||
|
"model": None,
|
||||||
|
"status": status,
|
||||||
|
"referenceId": self.reference_id,
|
||||||
|
"referenceModel": self.reference_model,
|
||||||
|
"meta": meta,
|
||||||
|
"entityId": self.entity_id,
|
||||||
|
})
|
||||||
|
return records
|
||||||
|
|
||||||
|
|
||||||
|
_current: contextvars.ContextVar[Optional[_Session]] = contextvars.ContextVar(
|
||||||
|
"_token_tracker_session", default=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def current_session() -> Optional[_Session]:
|
||||||
|
return _current.get()
|
||||||
|
|
||||||
|
|
||||||
|
_no_session_warned = False
|
||||||
|
|
||||||
|
|
||||||
|
def record_usage(model: str, input_tokens: int, output_tokens: int) -> None:
|
||||||
|
"""Terminal call sites invoke this after parsing a single API response.
|
||||||
|
|
||||||
|
If there is no active token_session we log a WARNING (once per process) so
|
||||||
|
Ops can catch regressions where an entry point forgot to open a session.
|
||||||
|
"""
|
||||||
|
sess = _current.get()
|
||||||
|
if sess is None:
|
||||||
|
if input_tokens or output_tokens:
|
||||||
|
global _no_session_warned
|
||||||
|
if not _no_session_warned:
|
||||||
|
_no_session_warned = True
|
||||||
|
logger.warning(
|
||||||
|
"record_usage(%s, in=%s, out=%s) called with NO active "
|
||||||
|
"token_session — usage NOT billed. Check that handler() / "
|
||||||
|
"process_single_document() opened a session around this path.",
|
||||||
|
model, input_tokens, output_tokens,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
sess.record(model, input_tokens, output_tokens)
|
||||||
|
except Exception as e: # never let tracking break the caller
|
||||||
|
logger.warning("record_usage failed: %s", e)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_auth_token() -> Optional[str]:
|
||||||
|
# Reuse lambda_function.get_token() so we share the same auth flow.
|
||||||
|
try:
|
||||||
|
from lambda_function import get_token # type: ignore
|
||||||
|
return get_token()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
from task.lambda_function import get_token # type: ignore
|
||||||
|
return get_token()
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
_POST_MAX_ATTEMPTS = int(os.getenv("TOKEN_POST_MAX_ATTEMPTS", "5"))
|
||||||
|
_POST_TIMEOUT = float(os.getenv("TOKEN_POST_TIMEOUT", "15"))
|
||||||
|
# HTTP status codes where a retry is worthwhile (transient).
|
||||||
|
_RETRYABLE_STATUS = {408, 425, 429, 500, 502, 503, 504}
|
||||||
|
|
||||||
|
|
||||||
|
def _post_record(record: dict, role_id: Optional[str] = None) -> bool:
|
||||||
|
"""Log one compliance/token usage record.
|
||||||
|
|
||||||
|
Posting usage to the backend ({BASE_URL}/api/v1/compliance/token) has been
|
||||||
|
disabled for this service — token usage is recorded to the logs only, never
|
||||||
|
sent over the network. The ``role_id`` argument is kept for call-site
|
||||||
|
compatibility. Always returns True (the record was handled).
|
||||||
|
"""
|
||||||
|
logger.info(
|
||||||
|
"compliance/token usage model=%s in=%s out=%s price=%s role=%s "
|
||||||
|
"(log-only; not POSTed)",
|
||||||
|
record.get("model"), record.get("inputTokens"),
|
||||||
|
record.get("outputTokens"), record.get("price"), role_id,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def token_session(
|
||||||
|
task_name: str,
|
||||||
|
entity_id: str,
|
||||||
|
type_: str = "ai_upload",
|
||||||
|
reference_id: Optional[str] = None,
|
||||||
|
reference_model: Optional[str] = None,
|
||||||
|
meta: Optional[dict] = None,
|
||||||
|
role_id: Optional[str] = None,
|
||||||
|
):
|
||||||
|
"""Open a tracking session. On exit, POST one record per model used.
|
||||||
|
|
||||||
|
Status is "completed" on clean exit and "failed" if an exception escapes
|
||||||
|
the block. Use ``session.set_status(...)`` inside the block to override
|
||||||
|
(e.g. to mark "duplicate" or "classified_other"). Custom statuses outside
|
||||||
|
the backend enum are POSTed as "completed" with the real value carried in
|
||||||
|
``meta.statusDetail`` — the backend rejects unknown enum values.
|
||||||
|
"""
|
||||||
|
sess = _Session(
|
||||||
|
task_name=task_name,
|
||||||
|
type_=type_,
|
||||||
|
entity_id=entity_id,
|
||||||
|
reference_id=reference_id,
|
||||||
|
reference_model=reference_model,
|
||||||
|
meta=meta,
|
||||||
|
role_id=role_id,
|
||||||
|
)
|
||||||
|
reset_token = _current.set(sess)
|
||||||
|
exc: Optional[BaseException] = None
|
||||||
|
try:
|
||||||
|
yield sess
|
||||||
|
except BaseException as e:
|
||||||
|
exc = e
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
# An explicit set_status (e.g. "duplicate" before a control-flow
|
||||||
|
# raise) outranks the generic exception default.
|
||||||
|
if exc is not None and sess.status == "in_progress":
|
||||||
|
sess.status = "failed"
|
||||||
|
elif sess.status == "in_progress":
|
||||||
|
sess.status = "completed"
|
||||||
|
sess.ended_at = datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
# Emit an audit log line BEFORE POSTing. This gives Ops a
|
||||||
|
# guaranteed CloudWatch record even if every POST retry fails,
|
||||||
|
# so billing can be reconstructed from logs if needed.
|
||||||
|
records = sess.to_records()
|
||||||
|
try:
|
||||||
|
totals_in = sum(r.get("inputTokens", 0) for r in records)
|
||||||
|
totals_out = sum(r.get("outputTokens", 0) for r in records)
|
||||||
|
totals_price = sum(r.get("price", 0) for r in records)
|
||||||
|
logger.info(
|
||||||
|
"[TOKEN_AUDIT] task=%s entity=%s ref=%s type=%s status=%s "
|
||||||
|
"total_in=%d total_out=%d total_price=$%.6f models=%s "
|
||||||
|
"started=%s ended=%s",
|
||||||
|
sess.task_name, sess.entity_id, sess.reference_id,
|
||||||
|
sess.type, sess.status,
|
||||||
|
totals_in, totals_out, totals_price,
|
||||||
|
[r.get("model") for r in records],
|
||||||
|
sess.started_at, sess.ended_at,
|
||||||
|
)
|
||||||
|
except Exception as audit_err:
|
||||||
|
logger.warning("token audit log failed: %s", audit_err)
|
||||||
|
|
||||||
|
try:
|
||||||
|
for rec in records:
|
||||||
|
_post_record(rec, role_id=sess.role_id)
|
||||||
|
except Exception as post_err:
|
||||||
|
logger.error(
|
||||||
|
"token_session flush raised unexpectedly: %s — records were: %s",
|
||||||
|
post_err, json.dumps(records),
|
||||||
|
)
|
||||||
|
_current.reset(reset_token)
|
||||||
250
task/validatedUploadedFiles.py
Normal file
250
task/validatedUploadedFiles.py
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
import os
|
||||||
|
import boto3
|
||||||
|
|
||||||
|
from s3Ops import (
|
||||||
|
list_files_in_s3_folder
|
||||||
|
)
|
||||||
|
|
||||||
|
from utils.prompts import DOCUMENT_CLASSIFY_PROMPT
|
||||||
|
|
||||||
|
from backendAPIs import (
|
||||||
|
get_onboarding_status,
|
||||||
|
update_onboarding_status
|
||||||
|
)
|
||||||
|
|
||||||
|
bucket_name = os.getenv('S3_UPLOAD_BUCKET_NAME')
|
||||||
|
|
||||||
|
def document_classifier(record_details):
|
||||||
|
|
||||||
|
FINAL_DOCUMENT_CLASSIFY_PROMPT = DOCUMENT_CLASSIFY_PROMPT.format(document=record_details)
|
||||||
|
|
||||||
|
json_string = llm_bedrock.invoke(FINAL_DOCUMENT_CLASSIFY_PROMPT).content
|
||||||
|
|
||||||
|
print(json_string)
|
||||||
|
|
||||||
|
pattern = r'<output>(.*?)</output>'
|
||||||
|
match = re.search(pattern, json_string, re.DOTALL)
|
||||||
|
|
||||||
|
if match:
|
||||||
|
return match.group(1).strip()
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def get_pdf_details(key):
|
||||||
|
# Create a temporary file for the PDF
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf', dir='/tmp') as temp_file:
|
||||||
|
local_file_path = temp_file.name
|
||||||
|
# Download the file from S3
|
||||||
|
s3_client.download_file(bucket_name, key, local_file_path)
|
||||||
|
|
||||||
|
with pdfplumber.open(local_file_path) as pdf:
|
||||||
|
list_pages = []
|
||||||
|
|
||||||
|
for page in pdf.pages:
|
||||||
|
text = page.extract_text()
|
||||||
|
if text:
|
||||||
|
list_pages.append(text.strip())
|
||||||
|
|
||||||
|
pdf_string = "".join(list_pages)
|
||||||
|
|
||||||
|
if len(pdf_string.strip()) > 0:
|
||||||
|
# Clean up the temporary PDF file
|
||||||
|
os.remove(local_file_path)
|
||||||
|
return pdf_string
|
||||||
|
else: # start image processing
|
||||||
|
print("Starting image processing")
|
||||||
|
# Create a temporary directory for storing images
|
||||||
|
with tempfile.TemporaryDirectory(dir='/tmp') as temp_dir:
|
||||||
|
pdf_images_list = pdf_to_images(local_file_path, temp_dir)
|
||||||
|
result = read_images(pdf_images_list)
|
||||||
|
|
||||||
|
# Clean up the temporary PDF file
|
||||||
|
os.remove(local_file_path)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def process_single_pdf(pdf_file):
|
||||||
|
pdf_content = get_pdf_details(key)
|
||||||
|
classification = document_classifier(pdf_content)
|
||||||
|
return pdf_file, classification
|
||||||
|
|
||||||
|
|
||||||
|
def pdf_classifier(files_list):
|
||||||
|
|
||||||
|
categories = {
|
||||||
|
"LPA(Limited Partner Agreement)": [],
|
||||||
|
"Capital Statements": [],
|
||||||
|
"Sub Docs(Subscription Document)": [],
|
||||||
|
"Side Letter": [],
|
||||||
|
"Capital Call Notices": [],
|
||||||
|
"Distribution Notices": [],
|
||||||
|
"Fund Reports": [],
|
||||||
|
"K1s": [],
|
||||||
|
"Other": []
|
||||||
|
}
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=10) as executor:
|
||||||
|
future_to_pdf = {executor.submit(process_single_pdf, pdf_file): pdf_file for pdf_file in pdf_files}
|
||||||
|
|
||||||
|
for future in as_completed(future_to_pdf):
|
||||||
|
pdf_file = future_to_pdf[future]
|
||||||
|
try:
|
||||||
|
pdf_file, classification = future.result()
|
||||||
|
|
||||||
|
# Determine which category the PDF belongs to
|
||||||
|
found_category = False
|
||||||
|
for category in categories.keys():
|
||||||
|
if category.lower() in classification.lower():
|
||||||
|
categories[category].append(pdf_file)
|
||||||
|
found_category = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not found_category:
|
||||||
|
categories["Other"].append(pdf_file)
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
print(f'{pdf_file} generated an exception: {exc}')
|
||||||
|
categories["Other"].append(pdf_file)
|
||||||
|
|
||||||
|
return categories
|
||||||
|
|
||||||
|
def validate_input_files(file_list):
|
||||||
|
"""
|
||||||
|
Function to find specific files in a list based on naming patterns.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_list (str): List of files to validate.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary containing file names that match the required patterns.
|
||||||
|
str: Error message if any required file is missing.
|
||||||
|
"""
|
||||||
|
# Patterns to look for
|
||||||
|
required_files = {
|
||||||
|
# "lpa": "_LP",
|
||||||
|
"partner": "_partner",
|
||||||
|
"financials": "_financials",
|
||||||
|
"bankTransactions": "_bank_transactions",
|
||||||
|
"journals": "_journals",
|
||||||
|
"fund_performance": "_fund-performance"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Dictionary to hold found file names
|
||||||
|
found_files = {
|
||||||
|
# "lpa": "",
|
||||||
|
"partner": None,
|
||||||
|
"financials": None,
|
||||||
|
"bankTransactions": None,
|
||||||
|
"journals": None,
|
||||||
|
"fund_performance": None
|
||||||
|
}
|
||||||
|
|
||||||
|
pdf_files = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
if file_list:
|
||||||
|
# Loop through files in the list
|
||||||
|
for file_name in file_list:
|
||||||
|
if required_files['partner'] in file_name:
|
||||||
|
found_files['partner'] = file_name
|
||||||
|
elif required_files['financials'] in file_name:
|
||||||
|
found_files['financials'] = file_name
|
||||||
|
elif required_files['bankTransactions'] in file_name:
|
||||||
|
found_files['bankTransactions'] = file_name
|
||||||
|
elif required_files['journals'] in file_name:
|
||||||
|
found_files['journals'] = file_name
|
||||||
|
elif required_files['fund_performance'] in file_name:
|
||||||
|
found_files['fund_performance'] = file_name
|
||||||
|
elif file_name.lower().endswith('.pdf'):
|
||||||
|
pdf_files.append(file_name)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return(f"Error: No files found in folder.")
|
||||||
|
|
||||||
|
# if len(pdf_files) > 0:
|
||||||
|
# classified_pdf_files = pdf_classifier(pdf_files)
|
||||||
|
#
|
||||||
|
except FileNotFoundError:
|
||||||
|
return f"Error: Unable to find file.",""
|
||||||
|
|
||||||
|
# Check if all required files are found
|
||||||
|
missing_files = [key for key, value in found_files.items() if value is None]
|
||||||
|
|
||||||
|
if missing_files:
|
||||||
|
return f"Error: Missing required files - {', '.join(missing_files)}",""
|
||||||
|
|
||||||
|
# return found_files,classified_pdf_files
|
||||||
|
|
||||||
|
return found_files , pdf_files
|
||||||
|
|
||||||
|
|
||||||
|
def pretty_print_files(files_dict):
|
||||||
|
"""
|
||||||
|
Pretty print the dictionary containing file names.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
files_dict (dict): Dictionary where the keys are the file categories, and the values are the file names.
|
||||||
|
"""
|
||||||
|
file_message = ""
|
||||||
|
for key, value in files_dict.items():
|
||||||
|
file_message += f"- {key.replace('_', ' ').capitalize()}: {value} \n"
|
||||||
|
|
||||||
|
return file_message
|
||||||
|
|
||||||
|
|
||||||
|
def run_file_validation():
|
||||||
|
|
||||||
|
bucket_name = os.getenv("S3_UPLOAD_BUCKET_NAME")
|
||||||
|
step_number = 1
|
||||||
|
# Array index starts from 0
|
||||||
|
step_number -= 1
|
||||||
|
|
||||||
|
|
||||||
|
print(f"Getting files from bucket {bucket_name}")
|
||||||
|
success_message = ""
|
||||||
|
error_message = ""
|
||||||
|
status = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
onboarding_id = os.getenv('ONBOARDING_ID')
|
||||||
|
print(f"Get onBoardingStatus for {onboarding_id}")
|
||||||
|
response = get_onboarding_status(onboarding_id)
|
||||||
|
# Get current onboarding status values.
|
||||||
|
onboarding_status = response['data']['data']
|
||||||
|
success_message = onboarding_status['steps'][step_number]['summary']
|
||||||
|
error_message = onboarding_status['steps'][step_number]['errorMessage']
|
||||||
|
status = onboarding_status['steps'][step_number]['status']
|
||||||
|
folder_path = onboarding_status['path']
|
||||||
|
|
||||||
|
print(f"Pulling files list from {bucket_name}/{folder_path}")
|
||||||
|
file_list = list_files_in_s3_folder(bucket_name, folder_path)
|
||||||
|
print("S3 files:", file_list)
|
||||||
|
found_files ,classified_files = validate_input_files(file_list)
|
||||||
|
|
||||||
|
print("Files found:", found_files)
|
||||||
|
|
||||||
|
if isinstance(found_files, dict):
|
||||||
|
success_message = pretty_print_files(found_files)
|
||||||
|
status = "COMPLETE"
|
||||||
|
#Validation failed
|
||||||
|
else:
|
||||||
|
error_message = found_files
|
||||||
|
status = "FAILED"
|
||||||
|
success_message = ""
|
||||||
|
|
||||||
|
response = update_onboarding_status(step_number, status, error_message, success_message)
|
||||||
|
if 'error' in response:
|
||||||
|
print(f"Failed to fetch onboarding status: Error: {response['error']}")
|
||||||
|
print(f"Status Code: {response['status_code']}")
|
||||||
|
return False , False
|
||||||
|
|
||||||
|
return found_files , classified_files
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return False , False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
52
task/zfunctions-prod.yaml
Normal file
52
task/zfunctions-prod.yaml
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: prod-zfunctions-service
|
||||||
|
labels:
|
||||||
|
app: prod-zfunctions
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: prod-zfunctions
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: prod-zfunctions
|
||||||
|
spec:
|
||||||
|
serviceAccountName: ziveai-sa
|
||||||
|
containers:
|
||||||
|
- name: zfunctions-dev
|
||||||
|
image: <IMAGE>
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: prod-zfunctions-configmap
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "2Gi"
|
||||||
|
cpu: "1"
|
||||||
|
limits:
|
||||||
|
memory: "4Gi"
|
||||||
|
cpu: "2"
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: prod-zfunctions-service
|
||||||
|
labels:
|
||||||
|
app: prod-zfunctions
|
||||||
|
spec:
|
||||||
|
internalTrafficPolicy: Cluster
|
||||||
|
ipFamilies:
|
||||||
|
- IPv4
|
||||||
|
ipFamilyPolicy: SingleStack
|
||||||
|
selector:
|
||||||
|
app: prod-zfunctions
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 8080
|
||||||
|
targetPort: 8080
|
||||||
|
sessionAffinity: None
|
||||||
|
type: ClusterIP
|
||||||
52
task/zfunctions.yaml
Normal file
52
task/zfunctions.yaml
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: dev-zfunctions-service
|
||||||
|
labels:
|
||||||
|
app: dev-zfunctions
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: dev-zfunctions
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: dev-zfunctions
|
||||||
|
spec:
|
||||||
|
serviceAccountName: ziveai-sa
|
||||||
|
containers:
|
||||||
|
- name: zfunctions-dev
|
||||||
|
image: <IMAGE>
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: dev-zfunctions-configmap
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "2Gi"
|
||||||
|
cpu: "1"
|
||||||
|
limits:
|
||||||
|
memory: "4Gi"
|
||||||
|
cpu: "2"
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: dev-zfunctions-service
|
||||||
|
labels:
|
||||||
|
app: dev-zfunctions
|
||||||
|
spec:
|
||||||
|
internalTrafficPolicy: Cluster
|
||||||
|
ipFamilies:
|
||||||
|
- IPv4
|
||||||
|
ipFamilyPolicy: SingleStack
|
||||||
|
selector:
|
||||||
|
app: dev-zfunctions
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 8080
|
||||||
|
targetPort: 8080
|
||||||
|
sessionAffinity: None
|
||||||
|
type: ClusterIP
|
||||||
28
template.yml
Normal file
28
template.yml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
AWSTemplateFormatVersion: '2010-09-09'
|
||||||
|
Transform: 'AWS::Serverless-2016-10-31'
|
||||||
|
Description: An AWS Lambda application that calls the Lambda API.
|
||||||
|
Resources:
|
||||||
|
function:
|
||||||
|
Type: AWS::Serverless::Function
|
||||||
|
Properties:
|
||||||
|
Handler: lambda_function.lambda_handler
|
||||||
|
Runtime: python3.8
|
||||||
|
CodeUri: function/.
|
||||||
|
Description: Call the AWS Lambda API
|
||||||
|
Timeout: 10
|
||||||
|
# Function's execution role
|
||||||
|
Policies:
|
||||||
|
- AWSLambdaBasicExecutionRole
|
||||||
|
- AWSLambda_ReadOnlyAccess
|
||||||
|
- AWSXrayWriteOnlyAccess
|
||||||
|
Tracing: Active
|
||||||
|
Layers:
|
||||||
|
- !Ref libs
|
||||||
|
libs:
|
||||||
|
Type: AWS::Serverless::LayerVersion
|
||||||
|
Properties:
|
||||||
|
LayerName: blank-python-lib
|
||||||
|
Description: Dependencies for the blank-python sample app.
|
||||||
|
ContentUri: package/.
|
||||||
|
CompatibleRuntimes:
|
||||||
|
- python3.8
|
||||||
Loading…
Reference in New Issue
Block a user