Skip to content

Commit

Permalink
Merge pull request #11 from CambioML/jojo-branch
Browse files Browse the repository at this point in the history
[feat] And portfolio risk v1 and sustainability report v1
  • Loading branch information
Cambio ML authored Jul 31, 2024
2 parents f315294 + 8f6be0f commit cea0f47
Show file tree
Hide file tree
Showing 52 changed files with 40,249 additions and 820 deletions.
1 change: 1 addition & 0 deletions .github/workflows/nextjs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ jobs:
NEXT_PUBLIC_AWS_SECRET_ACCESS_KEY: ${{ secrets.NEXT_PUBLIC_AWS_SECRET_ACCESS_KEY }}
NEXT_PUBLIC_AWS_S3_BUCKET_NAME: ${{ secrets.NEXT_PUBLIC_AWS_S3_BUCKET_NAME }}
NEXT_PUBLIC_AWS_REGION: ${{ secrets.NEXT_PUBLIC_AWS_REGION }}
NEXT_PUBLIC_OPENAI_API_KEY: ${{ secrets.NEXT_PUBLIC_OPENAI_API_KEY }}

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
Expand Down
5 changes: 5 additions & 0 deletions app/actions/sustainabilityReport/delay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const delay = (ms: number): Promise<void> => {
return new Promise((resolve) => setTimeout(resolve, ms));
};

export default delay;
36 changes: 36 additions & 0 deletions app/actions/sustainabilityReport/extractQAPairs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Company, ExtractQAResult, SustainabilityMetric } from '@/app/types/SustainabilityTypes';

interface IParams {
company: Company;
metrics: SustainabilityMetric[];
}

const extractQAPairs = async ({ company, metrics }: IParams): Promise<ExtractQAResult> => {
console.log('Extracting QA for company:', company.companyName);
console.log('Metrics:', metrics);
const qaPairs: Record<string, string>[] = [];
const fileResponse = await fetch(`/sustainability-reports/${company.sustainabilityReport}`);
const fileContent = await fileResponse.text();
for (const metric of metrics) {
let answerStartIndex = fileContent.indexOf(metric.question);
if (answerStartIndex === -1) {
console.log('no answer found for:', metric.question);
continue;
}
answerStartIndex += metric.question.length;
const answerContent = fileContent.slice(answerStartIndex);
// Regular expression to match the question pattern
const pattern: RegExp = /^C\d{1,2}\.\d{1,2}[a-z]?/m;

// Split the answer content into segments based on the pattern
const segments: string[] = answerContent.split(pattern);
const answer = segments[0].trim();
console.log(`answer found for ${metric.question}:`, answer);
qaPairs.push({ [metric.name]: answer });
}
const response: ExtractQAResult = { status: 200, error: null, result: { qaPairs } };

return response;
};

export default extractQAPairs;
252 changes: 252 additions & 0 deletions app/actions/sustainabilityReport/scoreProcess.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
import {
FeedbackResult,
MetricFeedback,
ScoreProcessResult,
SustainabilityMetric,
} from '@/app/types/SustainabilityTypes';
import OpenAI from 'openai';

const openai = new OpenAI({
apiKey: process.env.NEXT_PUBLIC_OPENAI_API_KEY || '',
dangerouslyAllowBrowser: true,
});

const SCORE_DICT: { [key: number]: string } = {
0: 'Non-Compliant',
1: 'Low-Compliant',
2: 'Partially Compliant',
3: 'Baseline Compliant',
4: 'Compliant',
5: 'Beyond Compliant',
6: 'Unknown',
};

// Function to remove specific characters from a string
function removeCharacters(inputString: string): string {
const charactersToRemove = ['"', '{', '}', '\\', "'", '#', '*'];
charactersToRemove.forEach((char) => {
inputString = inputString.split(char).join('');
});
return inputString;
}

// Function to extract score from feedback
function extractScore(feedback: string): number | null {
const pattern = /\s*(\d+)\/5/;
const match = feedback.match(pattern);
return match ? Number(match[1]) : 6;
}

// Function to extract QA sections from feedback
function extractQA(feedback: string): FeedbackResult {
feedback = removeCharacters(feedback);
console.log(feedback);

function extractSectionByKeywords(startKeyword: string, endKeyword: string, text: string): string | null {
try {
const startIndex = text.indexOf(startKeyword) + startKeyword.length;
const endIndex = text.indexOf(endKeyword, startIndex);
return text.substring(startIndex, endIndex).trim();
} catch (error) {
return null;
}
}

function extractScoreRationale(section: string | null): [string | null, string | null] {
if (!section) return [null, null];
const scoreMatch = section.match(/Score:\s*(\d+\/\d+)/);
const rationaleMatch = section.match(/Rationale\s*:\s*(.*?)(?:$|[\r\n])/);
const score = scoreMatch ? scoreMatch[1] : null;
const rationale = rationaleMatch ? rationaleMatch[1].replace('\n', ' ').trim() : null;
return [score, rationale];
}

function extractSuggestion(section: string | null): string | null {
if (!section) return null;
const suggestionMatch = section.match(/Suggestion\s*:\s*(.*?)(?:$|[\r\n])/);
const suggestion = suggestionMatch ? suggestionMatch[1].replace('\n', ' ').trim() : null;
return suggestion;
}

const tcfdScoringCriteria = extractSectionByKeywords('TCFD Scoring Criteria:', 'TCFD Evaluation:', feedback);
const tcfdEvaluationSection = extractSectionByKeywords('TCFD Evaluation:', 'TCFD Revision Suggestions:', feedback);
const tcfdRevisionSuggestionsSection = extractSectionByKeywords(
'TCFD Revision Suggestions:',
'IFRS S2 Scoring Criteria:',
feedback
);
const ifrsScoringCriteria = extractSectionByKeywords('IFRS S2 Scoring Criteria:', 'IFRS S2 Evaluation:', feedback);
const ifrsEvaluationSection = extractSectionByKeywords(
'IFRS S2 Evaluation:',
'IFRS S2 Revision Suggestions:',
feedback
);
const ifrsRevisionSuggestionsSection = extractSectionByKeywords(
'IFRS S2 Revision Suggestions:',
'Finish Response.',
feedback
);

const [tcfdScore, tcfdRationale] = extractScoreRationale(tcfdEvaluationSection);
const [ifrsScore, ifrsRationale] = extractScoreRationale(ifrsEvaluationSection);

const tcfdSuggestion = extractSuggestion(tcfdRevisionSuggestionsSection);
const ifrsSuggestion = extractSuggestion(ifrsRevisionSuggestionsSection);

const result: FeedbackResult = {
'TCFD Scoring Criteria': tcfdScoringCriteria || '',
'TCFD Evaluation': {
Score: tcfdScore || '',
Rationale: tcfdRationale || '',
},
'TCFD Revision Suggestions': {
Suggestion: tcfdSuggestion || '',
},
'IFRS S2 Scoring Criteria': ifrsScoringCriteria || '',
'IFRS S2 Evaluation': {
Score: ifrsScore || '',
Rationale: ifrsRationale || '',
},
'IFRS S2 Revision Suggestions': {
Suggestion: ifrsSuggestion || '',
},
};

return result;
}

interface IParams {
qaPairs: Record<string, string>[];
metrics: SustainabilityMetric[];
}

const scoreProcess = async ({ qaPairs, metrics }: IParams): Promise<ScoreProcessResult> => {
console.log('scoring process...', qaPairs);
const metricEvaluations: { [key: string]: MetricFeedback } = {};

for (const qaPair of qaPairs) {
const [questionName, answer] = Object.entries(qaPair)[0];
const metric = metrics.find((metric) => metric.name === questionName);
if (!metric) {
return {
status: 400,
error: `Metric not found for question: ${questionName}`,
result: null,
};
}

console.log(`Question: ${metric.question}, Answer: ${answer}, Metric: ${metric.name}`);

const prompt = `Input Content:
Question Description: ${metric.question}
Baseline Response: ${metric.baselineResponse}
Best Practice: ${metric.bestPractice}
TCFD Disclosure Requirements: ${metric.TCFDDisclosureRequirements}
IFRS S2 Disclosure Requirements: ${metric.IFRSS2DisclosureRequirements}
Company Response: ${answer}
Output Content:
TCFD Scoring Criteria:
Based on the given Question Description, Baseline Response (worst case), and Best Practice (best case), provide a 0-5 points scoring criteria specific to the TCFD requirements.
Response Format:
TCFD Scoring Criteria: 0-5 points based on the following:
0 -
1 -
2 -
3 -
4 -
5 -
TCFD Evaluation:
Evaluate the Company's response quality and depth based on the specified TCFD disclosure requirements. Please return the following in bullet point format:
1. Determine if the response meets the specified TCFD disclosure requirements.
2. Provide a TCFD score (0-5) based on the TCFD Scoring Criteria.
3. If the TCFD score is under 4,provide at least three detailed reasons in the Rationale.
Response Format:
TCFD Evaluation:
Score: x/5
Rationale: "
1. xxx
2. xxx
"
TCFD Revision Suggestions:
Based on the TCFD score, please return the following in bullet point format:
1. Identify deficiencies for unmet disclosure requirements.
2. Provide revision suggestions for each deficiency.
3. If the TCFD score is under 4, provide at least three detailed suggestions, highlighting specific actions and examples for improvement.
Response Format:
TCFD Revision Suggestions:
Suggestion: "
1. xxx
2. xxx
"
IFRS S2 Scoring Criteria:
Based on the given Question Description, Baseline Response (worst case), and Best Practice (best case), provide a 0-5 points scoring criteria specific to the IFRS S2 requirements.
Response Format:
IFRS S2 Scoring Criteria:0-5 points based on the following:
0 -
1 -
2 -
3 -
4 -
5 -
IFRS S2 Evaluation:
Based on the IFRS S2 requirements, please return the following in bullet point format:
1. Re-evaluate the Company's response.
2. Provide a new score (0-5) based on the IFRS S2 Scoring Criteria.
3. If the IFRS S2 score is under 4, should provide at least three detailed reasons in the Rationale.
Response Format:
IFRS S2 Evaluation:
Score: x/5
Rationale: "
1. xxx
2. xxx
"
IFRS S2 Revision Suggestions:
Based on the IFRS S2 Evaluation score, please return the following in bullet point format:
1. Deficiencies for unmet disclosure requirements.
2. Revision suggestions for each deficiency.
3. If the IFRS S2 score is under 4, provide at least three detailed suggestions, highlighting specific actions and examples for improvement.
Response Format:
IFRS S2 Revision Suggestions:
Suggestion: "
1. xxx
2. xxx
"
`;
const params: OpenAI.Chat.ChatCompletionCreateParams = {
max_tokens: 4096,
messages: [
{ role: 'user', content: prompt },
{ role: 'assistant', content: '{' },
],
model: 'gpt-4o',
};
const chatCompletion: OpenAI.Chat.ChatCompletion = await openai.chat.completions.create(params);
const initResult = chatCompletion.choices[0].message.content + 'Finish Response.';

const feedback: FeedbackResult = extractQA(initResult);

const currOutput: MetricFeedback = {
'TCFD Scoring Criteria': feedback['TCFD Scoring Criteria'],
'TCFD Response Score': extractScore(feedback['TCFD Evaluation']['Score']),
'TCFD Response Level': SCORE_DICT[extractScore(feedback['TCFD Evaluation']['Score']) || 6],
'TCFD Response Summary': feedback['TCFD Evaluation'].Rationale,
'TCFD Response Recommendation': feedback['TCFD Revision Suggestions'].Suggestion,

'IFRS S2 Scoring Criteria': feedback['IFRS S2 Scoring Criteria'],
'IFRS S2 Response Score': extractScore(feedback['IFRS S2 Evaluation'].Score),
'IFRS S2 Response Level': SCORE_DICT[extractScore(feedback['IFRS S2 Evaluation'].Score) || 6],
'IFRS S2 Response Summary': feedback['IFRS S2 Evaluation'].Rationale,
'IFRS S2 Response Recommendation': feedback['IFRS S2 Revision Suggestions'].Suggestion,
};
metricEvaluations[questionName] = currOutput;
}

const response: ScoreProcessResult = {
status: 200,
error: null,
result: metricEvaluations,
};
return response;
};

export default scoreProcess;
4 changes: 2 additions & 2 deletions app/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ const Button = ({ label, onClick, disabled, outline, small, icon: Icon, labelIco
transition
w-full
${outline ? 'bg-inherit' : 'bg-sky-300'}
${outline ? 'border-neutral-200' : 'border-neutral-500'}
${outline ? 'text-black' : 'text-neutral-800'}
${outline ? 'border-gray-200' : 'border-gray-500'}
${outline ? 'text-black' : 'text-gray-800'}
${small ? 'py-3' : 'py-4'}
${small ? 'text-lg' : 'text-3xl'}
${small ? 'border-[1px]' : 'border-2'}
Expand Down
4 changes: 2 additions & 2 deletions app/components/Heading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ interface HeadingProps {
const Heading = ({ title, subtitle, small, center }: HeadingProps) => {
return (
<div className={center ? 'text-center' : 'text-start'}>
<div className={`${small ? 'text-2xl' : 'text-4xl'} text-neutral-800 font-semibold whitespace-pre-line`}>
<div className={`${small ? 'text-2xl' : 'text-4xl'} text-gray-800 font-semibold whitespace-pre-line`}>
{title}
</div>
<div className={`font-light text-neutral-500 ${small ? 'text-xl mt-2 ' : 'text-2xl mt-5 '}`}>{subtitle}</div>
<div className={`font-light text-gray-500 ${small ? 'text-xl mt-2 ' : 'text-2xl mt-5 '}`}>{subtitle}</div>
</div>
);
};
Expand Down
2 changes: 1 addition & 1 deletion app/components/IconButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interface IconButtonProps {
const IconButton = ({ icon: Icon, onClick }: IconButtonProps) => {
return (
<div
className="w-full h-full flex items-center justify-center cursor-pointer p-4 hover:bg-neutral-200 hover:text-neutral-800 rounded-lg"
className="w-full h-full flex items-center justify-center cursor-pointer p-4 hover:bg-gray-200 hover:text-gray-800 rounded-lg"
onClick={onClick}
>
<Icon size={16} className="shrink-0" />
Expand Down
19 changes: 19 additions & 0 deletions app/components/PortfolioRiskV1/ReportWorkspace.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use client';
import React from 'react';
import Heading from '../Heading';
import ModuleContainer from '@/app/pages/portfolioriskv1/ModuleContainer';

const ReportWorkspace = () => {
return (
<div className="p-8 bg-gray-100 overflow-y-scroll max-h-[100vh] pb-10">
<Heading title={'Report Generator'} />
<div className="h-fit max-h-[900px] min-w-[800px] grid grid-cols-1 auto-rows-auto 2xl:grid-cols-2 gap-4">
<div className="col-span-2">
<ModuleContainer title={'Report Generator'}>Report GenerateModal</ModuleContainer>
</div>
</div>
</div>
);
};

export default ReportWorkspace;
16 changes: 16 additions & 0 deletions app/components/PortfolioRiskV1/RiskGenerator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Event, Stock } from '@/app/types/ScenarioTypes';

interface RiskGeneratorProps {
event: Event;
stock: Stock;
}

const RiskGenerator = ({ event, stock }: RiskGeneratorProps) => {
return (
<div>
RiskGenerator, {event.title}, {stock.id}
</div>
);
};

export default RiskGenerator;
Loading

0 comments on commit cea0f47

Please sign in to comment.