Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
tcaiger committed Nov 5, 2024
1 parent f85052f commit c6e3865
Show file tree
Hide file tree
Showing 9 changed files with 227 additions and 13 deletions.
1 change: 1 addition & 0 deletions packages/tupaia-web-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"express": "^4.19.2",
"lodash.groupby": "^4.6.0",
"lodash.keyby": "^4.6.0",
"openai": "^4.71.0",
"winston": "^3.3.3"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions packages/tupaia-web-server/src/app/createApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export async function createApp(db: TupaiaDatabase = new TupaiaDatabase()) {
.useSessionModel(TupaiaWebSessionModel)
.useAttachSession(attachSessionIfAvailable)
.attachApiClientToContext(authHandlerProvider)
.post<routes.OpenAIRequest>('openai', handleWith(routes.OpenAIRoute))
.get<routes.ReportRequest>('report/:reportCode', handleWith(routes.ReportRoute))
.get<routes.LegacyDashboardReportRequest>(
'legacyDashboardReport/:reportCode',
Expand Down
47 changes: 47 additions & 0 deletions packages/tupaia-web-server/src/routes/OpenAIRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/
import OpenAI from 'openai';
import { Request } from 'express';
import { Route } from '@tupaia/server-boilerplate';

export interface Params {
entityCode: string;
projectCode: string;
}
export type ResBody = Record<string, string>;
export type ReqBody = Record<string, never>;
export interface ReqQuery {}

export type OpenAIRequest = Request<Params, ResBody, ReqBody, ReqQuery>;

const apiKey = process.env.OPENAI_API_KEY;

export class OpenAIRoute extends Route<OpenAIRequest> {
public async buildResponse() {
const { userPrompt, systemPrompt } = this.req.body;

const openAIClient = new OpenAI({
apiKey,
});

const completion = await openAIClient.chat.completions.create({
messages: [
{
role: 'system',
content: [
{
type: 'text',
text: systemPrompt,
},
],
},
{ role: 'user', content: JSON.stringify(userPrompt) },
],
model: 'gpt-3.5-turbo',
});

return { message: completion.choices[0].message as unknown as string };
}
}
1 change: 1 addition & 0 deletions packages/tupaia-web-server/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ export {
export { ExportMapOverlayRequest, ExportMapOverlayRoute } from './ExportMapOverlayRoute';
export { LoginRoute, LoginRequest } from './LoginRoute';
export { CountriesRequest, CountriesRoute } from './CountriesRoute';
export { OpenAIRoute, OpenAIRequest } from './OpenAIRoute';
1 change: 1 addition & 0 deletions packages/tupaia-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"downloadjs": "1.4.7",
"leaflet": "^1.7.1",
"moment": "^2.24.0",
"openai": "^4.70.3",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-hook-form": "^6.15.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
* Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd
*/

import React from 'react';
import React, { useState, ChangeEvent } from 'react';
import styled from 'styled-components';
import { useParams } from 'react-router';
import { TextField } from '@material-ui/core';
import { Button, LoadingContainer } from '@tupaia/ui-components';
import { useEntity, useProject } from '../../../api/queries';
import { useExportDashboard } from '../../../api/mutations';
Expand All @@ -28,11 +29,23 @@ const ButtonGroup = styled.div`
justify-content: flex-end;
`;

const SystemPrompt = styled.div`
margin-top: 1rem;
button {
display: block;
}
.MuiTextField-root {
width: 100%;
}
`;

const Wrapper = styled.div`
display: flex;
flex-direction: column;
flex-grow: 1;
width 100%;
width: 100%;
align-items: start;
section + section {
margin-top: 1.5rem;
Expand Down Expand Up @@ -119,8 +132,14 @@ interface ExportDashboardProps {
onClose: () => void;
selectedDashboardItems: string[];
}
const defaultSystemPrompt =
'You are writing for a health software app that provides health data to a government department.' +
'The summary will be used for an exported PDF. Summarise the dashboard in a sentence using up to 250' +
'characters and include the title of the dashboard, the project, country and a summary the dashboard items.';

export const ExportConfig = ({ onClose, selectedDashboardItems }: ExportDashboardProps) => {
const [showSystemPrompt, setShowSystemPrompt] = useState(false);
const [systemPrompt, setSystemPrompt] = useState(defaultSystemPrompt);
const { projectCode, entityCode, dashboardName } = useParams();
const { data: project } = useProject(projectCode);
const { data: entity } = useEntity(projectCode, entityCode);
Expand Down Expand Up @@ -152,6 +171,7 @@ export const ExportConfig = ({ onClose, selectedDashboardItems }: ExportDashboar
return item?.config?.type === DashboardItemVizTypes.Chart;
});

const toggleSystemPrompt = () => setShowSystemPrompt(!showSystemPrompt);
return (
<LoadingContainer
heading="Exporting charts to PDF"
Expand All @@ -168,7 +188,10 @@ export const ExportConfig = ({ onClose, selectedDashboardItems }: ExportDashboar
<ExportSetting>
<section>
<ExportSettingsWrapper>
<ExportDescriptionInput />
<ExportDescriptionInput
systemPrompt={systemPrompt}
selectedDashboardItems={selectedDashboardItems}
/>
</ExportSettingsWrapper>
<ExportSettingsWrapper>
<DisplayFormatSettings />
Expand All @@ -189,6 +212,23 @@ export const ExportConfig = ({ onClose, selectedDashboardItems }: ExportDashboar
}}
/>
</ExportSetting>
<SystemPrompt>
{showSystemPrompt && (
<TextField
label="System prompt"
helperText="The AI autocomplete uses chat gpt to generate an export description using provided dashboard data. The system prompt provides instructions on how to intepret the data and format the output. "
style={{ margin: '1rem 0' }}
rows={6}
multiline
variant="outlined"
value={systemPrompt}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
setSystemPrompt(e.target.value);
}}
/>
)}
<Button onClick={toggleSystemPrompt}>AI settings</Button>
</SystemPrompt>
</ExportSettingsContainer>
{!isLoading && (
<Preview
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/

import React from 'react';
import React, { ChangeEvent } from 'react';
import styled from 'styled-components';
import { useExportSettings } from './ExportSettingsContext';
import { ExportSettingLabel } from './ExportSettingLabel';
import { TextField, OutlinedTextFieldProps } from '@material-ui/core';
import { useMutation } from '@tanstack/react-query';
import { Button } from '@tupaia/ui-components';
import { post } from '../../api';
import { useDashboard } from '../Dashboard';
import { useParams } from 'react-router';
import { useEntity, useProject } from '../../api/queries';

const Wrapper = styled.div`
display: flex;
Expand Down Expand Up @@ -51,10 +57,44 @@ const ExportDescription = styled.div<{

const MAX_CHARACTERS = 250;

export const ExportDescriptionInput = () => {
const { exportDescription, updateExportDescription } = useExportSettings();
export const ExportDescriptionInput = ({ systemPrompt, selectedDashboardItems }) => {
const { exportDescription, setExportDescription } = useExportSettings();
const { projectCode, entityCode } = useParams();
const { activeDashboard } = useDashboard();
const { data: project } = useProject(projectCode);
const { data: entity } = useEntity(projectCode, entityCode);

const { mutate } = useMutation(
['openai'],
systemPrompt => {
const userPrompt = {
title: activeDashboard?.name,
project: project?.name,
country: entity?.name,
dashboardItems: activeDashboard?.items.filter(item =>
selectedDashboardItems.includes(item.code),
),
};
return post('openai', { data: { userPrompt, systemPrompt } });
},
{
onSuccess: data => {
if (data.message.content) {
setExportDescription(data.message.content);
}
},
},
);

const showMaxCharsWarning = exportDescription.length >= MAX_CHARACTERS;

const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setExportDescription(e.target.value);
};

const handleClick = async () => {
mutate(systemPrompt);
};
return (
<Wrapper>
<ExportSettingLabel as="legend">Description</ExportSettingLabel>
Expand All @@ -63,7 +103,7 @@ export const ExportDescriptionInput = () => {
multiline
rows={6}
value={exportDescription}
onChange={updateExportDescription}
onChange={handleChange}
variant="outlined"
error={showMaxCharsWarning}
inputProps={{ maxLength: MAX_CHARACTERS }}
Expand All @@ -73,6 +113,7 @@ export const ExportDescriptionInput = () => {
{showMaxCharsWarning && 'Character limit reached: '}
{exportDescription.length}/{MAX_CHARACTERS}
</ExportDescription>
<Button onClick={handleClick}>★ AI Autocomplete</Button>
</Wrapper>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,6 @@ export const useExportSettings = () => {
setExportWithTable(e.target.checked);
};

const updateExportDescription = (e: ChangeEvent<HTMLInputElement>) => {
setExportDescription(e.target.value);
};

const updateSeparatePagePerItem = (e: ChangeEvent<HTMLInputElement>) => {
setSeparatePagePerItem(e.target.value === 'true');
};
Expand All @@ -95,7 +91,7 @@ export const useExportSettings = () => {
updateExportFormat,
updateExportWithLabels,
updateExportWithTable,
updateExportDescription,
setExportDescription,
resetExportSettings,
separatePagePerItem,
updateSeparatePagePerItem,
Expand Down
Loading

0 comments on commit c6e3865

Please sign in to comment.