Skip to content

POC - AWS Bedrock #2510

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 102 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
102 commits
Select commit Hold shift + click to select a range
ffede3d
First draft of summary
rauboti Oct 14, 2024
67d16cd
First draft with meta description
rauboti Oct 14, 2024
1d62f4d
Added to learning resource, added load spinner
rauboti Oct 14, 2024
fe8cf12
Add button to framed content
rauboti Oct 14, 2024
48b0345
POST route for haiku invocation (#2513)
ekrojo77 Oct 15, 2024
6527a03
Merge branch 'master' into poc-bedrock
rauboti Oct 15, 2024
c5d364e
update metadescription based on generated content
ekrojo77 Oct 15, 2024
d21a31a
force generation to properly replace slate content
Jonas-C Oct 15, 2024
6fc2fcd
Prompt phrases for generation
ekrojo77 Oct 16, 2024
6590509
Pass articletext to metadescription
rauboti Oct 16, 2024
5d18ea1
Merge branch 'poc-bedrock' into metabeskrivelse-ai
ekrojo77 Oct 16, 2024
de77b79
Update to use input article for metadescription generation
ekrojo77 Oct 16, 2024
d9770d7
Merge pull request #2521 from NDLANO/metabeskrivelse-ai
ekrojo77 Oct 16, 2024
effdd4c
Reusable model invocation
rauboti Oct 17, 2024
1ad23a8
Expand to article summary
rauboti Oct 17, 2024
6de48b7
Draft for relfection questions
rauboti Oct 17, 2024
2b77f8a
Merge branch 'master' into poc-bedrock
rauboti Oct 17, 2024
779c445
Disable ArticleSummary in FrontpageArticle until further notice
rauboti Oct 21, 2024
118bc14
Adjust meta prompt, fix reflection question response
rauboti Oct 21, 2024
4534142
Temporary solution to show summary
rauboti Oct 21, 2024
1d34371
Add default parameters & block empty articles from making requests
rauboti Oct 21, 2024
58238b2
Adding language to prompts
rauboti Oct 22, 2024
179036d
Further tuning prompts, fix language in framed content
rauboti Oct 22, 2024
cf990c2
Fix console error
rauboti Oct 22, 2024
12984e2
Reusing serialize methods
rauboti Oct 22, 2024
31a26d4
Get selected text + prompts
rauboti Oct 24, 2024
85ba36f
change rewrite to rephrase
rauboti Oct 24, 2024
22f4f5b
Slightly change prompt, connect to the llm
rauboti Oct 24, 2024
3a82242
Add popup to show the selected text, trigger the query and show response
rauboti Oct 25, 2024
c45f957
Improvements to prompt questions using longer contextualized prompts
ekrojo77 Oct 27, 2024
07c1858
slightly improve popup visuals
rauboti Oct 28, 2024
eb3226e
Merge branch 'master' into poc-bedrock
rauboti Oct 28, 2024
55dbab0
Merge branch 'master' into poc-bedrock-2
rauboti Oct 28, 2024
221f75e
Merge branch 'poc-bedrock' into poc-bedrock-2
rauboti Oct 28, 2024
f533876
Merge branch 'poc-bedrock-2' into prompt-improvements
rauboti Oct 28, 2024
0808d3b
Adjustments to some translations, loader
rauboti Oct 28, 2024
2bc7d91
Insert suggestion in place of original selection
rauboti Oct 28, 2024
eadfb30
Merge branch 'poc-bedrock-2' into prompt-improvements
rauboti Oct 29, 2024
962e833
Translations for articleSummary
rauboti Oct 29, 2024
1f0d21c
Connecting improved prompts to summary
rauboti Oct 29, 2024
9d21f61
Add translations to meta description prompt
rauboti Oct 29, 2024
4181a55
Connecting improved prompt to meta description
rauboti Oct 29, 2024
d7088e2
Adding translations to reflection question prompt
rauboti Oct 29, 2024
39cdb6f
Connecto reflection questions to improved prompt
rauboti Oct 29, 2024
2e94472
Adding translations to the phrasing prompt
rauboti Oct 29, 2024
7105312
Connect prompt to rephrasing modal
rauboti Oct 29, 2024
05c5e3d
Changes to rephrasing prompt
rauboti Oct 29, 2024
0d75612
Merge pull request #2563 from NDLANO/prompt-improvements
rauboti Oct 29, 2024
b42e703
Merge branch 'master' into poc-bedrock
rauboti Oct 29, 2024
abca3c8
Adjusting secret names
rauboti Oct 30, 2024
8d04d97
Merge branch 'master' into poc-bedrock
ekrojo77 Oct 30, 2024
5410995
test commit, remove after
ekrojo77 Oct 30, 2024
299e005
Add button with low level functionality to keep both texts
rauboti Oct 30, 2024
667871e
trigger deployment
ekrojo77 Oct 30, 2024
b0f799c
Merge branch 'poc-bedrock' into poc-bedrock-2
rauboti Oct 31, 2024
b20c001
Keep formatting in original text when inserting after
rauboti Oct 31, 2024
1addb37
Not insert unless we have a rephrased sample
rauboti Oct 31, 2024
1727cfb
Toolbarbutton text
rauboti Nov 5, 2024
5301df3
Merge branch 'master' into poc-bedrock
rauboti Nov 26, 2024
a8f2a9b
Remove unsupported import
rauboti Nov 26, 2024
c976ef5
Merge branch 'poc-bedrock' into poc-bedrock-2
rauboti Nov 26, 2024
a3ef937
lint
rauboti Nov 26, 2024
90b54e2
Merge pull request #2545 from NDLANO/poc-bedrock-2
rauboti Nov 26, 2024
c6975e5
Add button to ImageEmbedForm
rauboti Nov 27, 2024
0df5676
Merged in endpoint + connected button and textfield
rauboti Nov 27, 2024
7492b7b
Add transcribe and get_transcription endpoints
ekrojo77 Nov 29, 2024
04ed47b
Add helper function for polling
ekrojo77 Nov 29, 2024
fdd9411
Remove default test value
ekrojo77 Nov 29, 2024
814a7fa
Move fetching of env variables to config
ekrojo77 Dec 1, 2024
9039230
load env in api file
ekrojo77 Dec 2, 2024
2bdd97e
Remove fallback, add check if environment is set
ekrojo77 Dec 2, 2024
179b0f4
make the polling a get
ekrojo77 Dec 6, 2024
e8629ad
Merge pull request #2758 from NDLANO/transcribe-api
ekrojo77 Dec 6, 2024
835ceb2
Allow generation of alt text when creating image as well
rauboti Dec 13, 2024
c2d4ab8
First UI updates to get transcription
rauboti Dec 13, 2024
cfe42a2
Temporarily added s3 link for audio transcriptions
rauboti Dec 17, 2024
75d4b9b
Adding polling interval
rauboti Dec 17, 2024
031cd2b
Fix alt text for existing images
rauboti Dec 17, 2024
1b5c955
Enabled for podcasts
rauboti Dec 17, 2024
1bb06c2
Return text from endpoint to field + fix button text
rauboti Dec 17, 2024
aed88b2
Merge branch 'master' into poc-bedrock
rauboti Dec 17, 2024
9d4057f
Merge branch 'poc-bedrock' into poc-bedrock-3
rauboti Dec 17, 2024
aea02d1
Fix linting
rauboti Dec 17, 2024
5b22c40
Merge branch 'poc-bedrock' into poc-bedrock-3
rauboti Dec 17, 2024
8046b54
Fix linting
rauboti Dec 17, 2024
64c710e
fix use of backend for transcribe
ekrojo77 Dec 18, 2024
14326b8
Merge branch 'poc-bedrock-3' of github.com:NDLANO/editorial-frontend …
ekrojo77 Dec 18, 2024
4e51332
Fix polling for audio
ekrojo77 Dec 18, 2024
e6780f8
lint
ekrojo77 Dec 18, 2024
23c75e8
update to poll only on ID
ekrojo77 Dec 18, 2024
7bd47aa
check if job has been performed
ekrojo77 Dec 21, 2024
d68e4d5
Fix transcription calls
ekrojo77 Jan 13, 2025
5c90433
Remove console error
ekrojo77 Jan 13, 2025
74e77af
Use correct env variable
ekrojo77 Jan 13, 2025
dcab17a
Merge branch 'master' into poc-bedrock
ekrojo77 Jan 15, 2025
0aa7e9d
Merge branch 'poc-bedrock' into poc-bedrock-3
ekrojo77 Jan 15, 2025
c1610b5
Bump backend
ekrojo77 Jan 15, 2025
a23c99c
Handle undefined status
ekrojo77 Jan 15, 2025
7f9d826
Add vercel
ekrojo77 Jan 15, 2025
b561e2b
Move vercel to dev dependency
ekrojo77 Jan 15, 2025
81660c7
Trigger Build
ekrojo77 Jan 15, 2025
c6f1361
Merge pull request #2736 from NDLANO/poc-bedrock-3
gunnarvelle Mar 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"@babel/core": "^7.26.0",
"@ndla/preset-panda": "^0.0.48",
"@ndla/scripts": "^2.1.3",
"@ndla/types-backend": "^1.0.1",
"@ndla/types-backend": "^1.0.15",
"@ndla/types-embed": "^5.0.6-alpha.0",
"@ndla/types-taxonomy": "^1.0.30",
"@pandacss/dev": "^0.48.0",
Expand Down Expand Up @@ -71,11 +71,14 @@
"tsx": "^4.19.2",
"typescript": "^5.7.2",
"typescript-eslint": "^8.16.0",
"vercel": "^33.6.2",
"vite": "^6.0.1",
"vitest": "^2.1.6"
},
"dependencies": {
"@ark-ui/react": "^4.1.2",
"@aws-sdk/client-bedrock-runtime": "^3.670.0",
"@aws-sdk/client-transcribe": "^3.699.0",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/modifiers": "^6.0.1",
"@dnd-kit/sortable": "^7.0.2",
Expand All @@ -98,6 +101,7 @@
"@ndla/video-search": "^8.0.70-alpha.0",
"@tanstack/react-query": "5.62.3",
"auth0-js": "^9.22.1",
"buffer": "^6.0.3",
"compression": "^1.7.4",
"cross-fetch": "^3.1.5",
"date-fns": "2.30.0",
Expand Down
62 changes: 62 additions & 0 deletions src/components/LLM/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Copyright (c) 2024-present, NDLA.
*
* This source code is licensed under the GPLv3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import { Buffer } from "buffer";

export const claudeHaikuDefaults = { top_p: 0.7, top_k: 100, temperature: 0.9 };

interface modelProps {
prompt: string;
image?: {
base64: string;
fileType: string;
};
max_tokens?: number;
}

export const invokeModel = async ({ prompt, image, max_tokens = 2000, ...rest }: modelProps) => {
if (!prompt) {
// console.error("No prompt provided to invokeModel");
return null;
}

const payload: any = { prompt, max_tokens, ...rest };
if (image) {
payload.image = image;
}

const response = await fetch("/invoke-model", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});

if (!response.ok) {
// console.error("Failed to get a response from the model");
return null;
}

const responseBody = await response.json();
return parseResponse(responseBody.content[0].text);
};

export const getTextFromHTML = (html: string) => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
return doc.body.textContent || "";
};

const parseResponse = (response: string) => {
return response.split("<answer>")[1].split("</answer>")[0].trim();
};

export const convertBufferToBase64 = (buffer: ArrayBuffer) => {
return Buffer.from(buffer).toString("base64");
};
2 changes: 1 addition & 1 deletion src/components/SlateEditor/PlainTextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const PlainTextEditor = forwardRef<HTMLTextAreaElement, Props>(
const { status, setStatus } = useFormikContext<ArticleFormType>();

useEffect(() => {
if (status?.status === "revertVersion") {
if (status?.status === "revertVersion" || status?.status === "acceptGenerated") {
ReactEditor.deselect(editor);
editor.children = value;
setStatus((prevStatus: FormikStatus) => ({
Expand Down
22 changes: 19 additions & 3 deletions src/components/SlateEditor/RichTextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { useFormikContext } from "formik";
import { isKeyHotkey } from "is-hotkey";
import isEqual from "lodash/isEqual";
import { FocusEvent, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { createEditor, Descendant, Editor, NodeEntry, Range, Transforms } from "slate";
import { BaseRange, createEditor, Descendant, Editor, NodeEntry, Range, Transforms } from "slate";
import { withHistory } from "slate-history";
import { Slate, Editable, withReact, RenderElementProps, RenderLeafProps, ReactEditor } from "slate-react";
import { EditableProps } from "slate-react/dist/components/editable";
Expand All @@ -28,6 +28,7 @@ import { onDragOver, onDragStart, onDrop } from "./plugins/DND";
import { TYPE_HEADING } from "./plugins/heading/types";
import { TYPE_LIST } from "./plugins/list/types";
import { TYPE_PARAGRAPH } from "./plugins/paragraph/types";
import RephraseModal from "./plugins/rephrase/RephraseModal";
import { TYPE_TABLE } from "./plugins/table/types";
import { SlateToolbar } from "./plugins/toolbar";
import { AreaFilters, CategoryFilters } from "./plugins/toolbar/toolbarState";
Expand Down Expand Up @@ -93,6 +94,7 @@ const RichTextEditor = ({
}: RichTextEditorProps) => {
const [editor] = useState(() => withPlugins(withReact(withHistory(createEditor())), plugins));
const [isFirstNormalize, setIsFirstNormalize] = useState(true);
const [rephraseSelection, setRephraseSelection] = useState<BaseRange | null>(null);
const [labelledBy, setLabelledBy] = useState<string | undefined>(undefined);
const prevSubmitted = useRef(submitted);
const field = useFieldContext();
Expand Down Expand Up @@ -138,7 +140,11 @@ const RichTextEditor = ({

useEffect(() => {
// When form is submitted or form content has been revert to a previous version, the editor has to be reinitialized.
if ((!submitted && prevSubmitted.current) || status === "revertVersion") {
if (
(!submitted && prevSubmitted.current) ||
status?.status === "revertVersion" ||
status?.status === "acceptGenerated"
) {
if (isFirstNormalize) {
return;
}
Expand Down Expand Up @@ -319,6 +325,10 @@ const RichTextEditor = ({
[additionalOnKeyDown, editor],
);

const selectors = useMemo(() => {
return { rephrase: setRephraseSelection };
}, []);

return (
<article className={noArticleStyling ? undefined : "ndla-article"}>
<ArticleLanguageProvider language={language}>
Expand All @@ -329,7 +339,12 @@ const RichTextEditor = ({
<Spinner />
) : (
<>
<SlateToolbar options={toolbarOptions} areaOptions={toolbarAreaFilters} hideToolbar={hideToolbar} />
<SlateToolbar
options={toolbarOptions}
areaOptions={toolbarAreaFilters}
hideToolbar={hideToolbar}
selectors={selectors}
/>
{!hideBlockPicker && (
<SlateBlockPicker
editor={editor}
Expand All @@ -338,6 +353,7 @@ const RichTextEditor = ({
{...createBlockpickerOptions(blockpickerOptions)}
/>
)}
<RephraseModal selection={rephraseSelection} setSelection={setRephraseSelection} />
<StyledEditable
{...fieldProps}
aria-labelledby={labelledBy}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,22 @@
*
*/

import { useMemo } from "react";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Editor, Element, NodeEntry, Transforms } from "slate";
import { ReactEditor, RenderElementProps } from "slate-react";
import { BrushLine, CopyrightLine } from "@ndla/icons";
import { BrushLine, CopyrightLine, FileListLine } from "@ndla/icons";
import { IconButton } from "@ndla/primitives";
import { styled } from "@ndla/styled-system/jsx";
import { ContentTypeFramedContent, EmbedWrapper } from "@ndla/ui";
import { FramedContentElement } from ".";
import { TYPE_FRAMED_CONTENT } from "./types";
import { editorValueToPlainText } from "../../../../util/articleContentConverter";
import { useArticleContentType } from "../../../ContentTypeProvider";
import DeleteButton from "../../../DeleteButton";
import { claudeHaikuDefaults, invokeModel } from "../../../LLM/helpers";
import MoveContentButton from "../../../MoveContentButton";
import { useArticleLanguage } from "../../ArticleLanguageProvider";
import { TYPE_COPYRIGHT } from "../copyright/types";
import { defaultCopyrightBlock } from "../copyright/utils";
import { StyledFigureButtons } from "../embed/FigureButtons";
Expand All @@ -38,6 +41,8 @@ interface Props extends RenderElementProps {
const SlateFramedContent = (props: Props) => {
const { element, editor, attributes, children } = props;
const { t } = useTranslation();
const language = useArticleLanguage();
const [isLoading, setIsLoading] = useState(false);
const variant = element.data?.variant ?? "neutral";
const contentType = useArticleContentType();
const hasSlateCopyright = useMemo(() => {
Expand Down Expand Up @@ -81,9 +86,45 @@ const SlateFramedContent = (props: Props) => {
Transforms.insertNodes(editor, defaultCopyrightBlock(), { at: path.concat(node.children.length) });
};

const generateQuestions = async () => {
const articleText = editorValueToPlainText(editor.children);
if (!articleText) {
// console.error("No article content provided to generate meta description");
return;
}
setIsLoading(true);
try {
const generatedText = await invokeModel({
prompt: t("textGeneration.reflectionQuestions.prompt", {
article: articleText,
language: t(`languages.${language}`),
}),
...claudeHaikuDefaults,
});
if (generatedText) {
editor.insertText(generatedText);
}
// generatedText ? editor.insertText(generatedText) : console.error("No generated text");
} catch (error) {
// console.error("Error generating reflection questions", error);
} finally {
setIsLoading(false);
}
};

return (
<EmbedWrapper draggable {...attributes}>
<FigureButtons contentEditable={false}>
<IconButton
variant={variant === "colored" ? "primary" : "secondary"}
size="small"
title={t("textGeneration.reflectionQuestions.button")}
aria-label={t("textGeneration.reflectionQuestions.button")}
onClick={generateQuestions}
loading={isLoading}
>
<FileListLine />
</IconButton>
{!hasSlateCopyright && (
<IconButton
variant="tertiary"
Expand Down
53 changes: 49 additions & 4 deletions src/components/SlateEditor/plugins/image/ImageEmbedForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
*/

import { Formik, useFormikContext } from "formik";
import { useMemo } from "react";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Descendant } from "slate";
import { CheckLine } from "@ndla/icons";
import { FileListLine, CheckLine } from "@ndla/icons";
import {
Button,
CheckboxControl,
Expand All @@ -22,10 +22,12 @@ import {
FieldRoot,
FieldErrorMessage,
FieldTextArea,
Spinner,
} from "@ndla/primitives";
import { styled } from "@ndla/styled-system/jsx";
import { IImageMetaInformationV3DTO } from "@ndla/types-backend/image-api";
import { ImageEmbedData } from "@ndla/types-embed";
import { convertBufferToBase64, claudeHaikuDefaults, invokeModel } from "../../../../components/LLM/helpers";
import { InlineField } from "../../../../containers/FormikForm/InlineField";
import ImageEditor from "../../../../containers/ImageEditor/ImageEditor";
import { inlineContentToEditorValue, inlineContentToHTML } from "../../../../util/articleContentConverter";
Expand Down Expand Up @@ -146,6 +148,12 @@ const InputWrapper = styled("div", {
},
});

const StyledButton = styled(Button, {
base: {
alignSelf: "flex-start",
},
});

const EmbedForm = ({
onClose,
language,
Expand All @@ -156,6 +164,31 @@ const EmbedForm = ({
const inGrid = useInGrid();
const { values, initialValues, isValid, setFieldValue, dirty, isSubmitting } =
useFormikContext<ImageEmbedFormValues>();
const [isLoading, setIsLoading] = useState<boolean>(false);

const generateAltText = async () => {
setIsLoading(true);
if (!image?.image.imageUrl) {
return null;
}

const response = await fetch(image?.image.imageUrl);
const responseContentType = response.headers.get("Content-Type");
const buffer = await response.arrayBuffer();
const base64 = convertBufferToBase64(buffer);

const result = await invokeModel({
prompt: t("textGeneration.altText.prompt", { language: t(`languages.${language}`) }),
image: {
base64,
fileType: responseContentType ?? "",
},
max_tokens: 2000,
...claudeHaikuDefaults,
});
setIsLoading(false);
return result;
};

const formIsDirty = isFormikFormDirty({
values,
Expand All @@ -182,13 +215,25 @@ const EmbedForm = ({
</FieldRoot>
)}
</FormField>

{!values.isDecorative && (
<FormField name="alt">
{({ field, meta }) => (
{({ field, meta, helpers }) => (
<FieldRoot invalid={!!meta.error}>
<FieldLabel>{t("form.image.alt.label")}</FieldLabel>
<FieldTextArea {...field} placeholder={t("form.image.alt.placeholder")} />
<StyledButton
onClick={async () => {
const text = await generateAltText();
if (text && text.length > 0) {
helpers.setValue(text);
}
}}
size="small"
title={t("textGeneration.altText.title")}
>
{t("textGeneration.altText.button")}
{isLoading ? <Spinner size="small" /> : <FileListLine />}
</StyledButton>
<FieldErrorMessage>{meta.error}</FieldErrorMessage>
</FieldRoot>
)}
Expand Down
Loading