Skip to content
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

POC - AWS Bedrock #2510

Draft
wants to merge 66 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
66 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
aed88b2
Merge branch 'master' into poc-bedrock
rauboti Dec 17, 2024
aea02d1
Fix linting
rauboti Dec 17, 2024
dcab17a
Merge branch 'master' into poc-bedrock
ekrojo77 Jan 15, 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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
},
"dependencies": {
"@ark-ui/react": "^4.1.2",
"@aws-sdk/client-bedrock-runtime": "^3.670.0",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/modifiers": "^6.0.1",
"@dnd-kit/sortable": "^7.0.2",
Expand Down
50 changes: 50 additions & 0 deletions src/components/LLM/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* 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.
*
*/

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

interface modelProps {
prompt: string;
max_tokens?: number;
}

export const invokeModel = async ({ prompt, max_tokens = 2000, ...rest }: modelProps) => {
if (!prompt) {
console.error("No prompt provided to invokeModel");
return null;
}
const response = await fetch("/invoke-model", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
prompt: prompt,
max_tokens: max_tokens,
...rest,
}),
});

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

const responseBody = await response.json();
return removeConfirmationSentence(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 removeConfirmationSentence = (text: string) => {
return text.includes(":") ? text.split(/:\s*\n/)[1] : text;
};
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,21 @@
*
*/

import { useMemo } from "react";
import escapeHtml from "escape-html";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Editor, Element, NodeEntry, Transforms } from "slate";
import { Editor, Element, NodeEntry, Text, Transforms } from "slate";
import { ReactEditor, RenderElementProps } from "slate-react";
import styled from "@emotion/styled";
import { spacing } from "@ndla/core";
import { BrushLine, Copyright } from "@ndla/icons/editor";
import { BlogPost, BrushLine, Copyright } from "@ndla/icons/editor";
import { IconButton } from "@ndla/primitives";
import { ContentTypeFramedContent, EmbedWrapper } from "@ndla/ui";
import { FramedContentElement } from ".";
import { TYPE_FRAMED_CONTENT } from "./types";
import { useArticleContentType } from "../../../ContentTypeProvider";
import DeleteButton from "../../../DeleteButton";
import { claudeHaikuDefaults, getTextFromHTML, invokeModel } from "../../../LLM/helpers";
import MoveContentButton from "../../../MoveContentButton";
import { TYPE_COPYRIGHT } from "../copyright/types";
import { defaultCopyrightBlock } from "../copyright/utils";
Expand All @@ -40,6 +42,7 @@ interface Props extends RenderElementProps {
const SlateFramedContent = (props: Props) => {
const { element, editor, attributes, children } = props;
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const variant = element.data?.variant ?? "neutral";
const contentType = useArticleContentType();
const hasSlateCopyright = useMemo(() => {
Expand Down Expand Up @@ -83,9 +86,64 @@ const SlateFramedContent = (props: Props) => {
Transforms.insertNodes(editor, defaultCopyrightBlock(), { at: path.concat(node.children.length) });
};

const serialize = (node: any) => {
if (Text.isText(node)) {
let string = escapeHtml(node.text);
if (node.bold) {
string = `<strong>${string}</strong>`;
}
return string;
}

const children = node.children.map((n: any) => serialize(n)).join("");

switch (node.type) {
case "quote":
return `<blockquote><p>${children}</p></blockquote>`;
case "paragraph":
return `<p>${children}</p>`;
case "link":
return `<a href="${escapeHtml(node.url)}">${children}</a>`;
default:
return children;
}
};

const generateQuestions = async () => {
const articleHTML = await serialize(editor.children[0]);
const articleText = getTextFromHTML(articleHTML);
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", { language: t(`languages.NO`) }) + articleText,
rauboti marked this conversation as resolved.
Show resolved Hide resolved
// t("textGeneration.reflectionQuestions.prompt", { language: t(`languages.${articleLanguage}`) }) + articleText,
...claudeHaikuDefaults,
});
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}
>
<BlogPost />
</IconButton>
{!hasSlateCopyright && (
<IconButton
variant="tertiary"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { toCreateFrontPageArticle, toEditMarkup } from "../../../../util/routeHe
import { IngressField, TitleField, SlugField } from "../../../FormikForm";
import { FrontpageArticleFormType } from "../../../FormikForm/articleFormHooks";
import { useSession } from "../../../Session/SessionProvider";
import ArticleSummary from "../../components/summary/ArticleSummary";

const StyledDiv = styled.div`
display: flex;
Expand Down Expand Up @@ -168,6 +169,7 @@ const FrontpageArticleFormContent = ({ articleLanguage }: Props) => {
onCancel={() => setIsNormalizedOnLoad(false)}
severity="warning"
/>
{/* <ArticleSummary /> */}
<StyledContentDiv name="content" label={t("form.content.label")} noBorder>
{({ field: { value, name, onChange }, form: { isSubmitting } }) => (
<ContentTypeProvider value="subject-material">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { findNodesByType } from "../../../../util/slateHelpers";
import { IngressField, TitleField } from "../../../FormikForm";
import { HandleSubmitFunc, LearningResourceFormType } from "../../../FormikForm/articleFormHooks";
import { useSession } from "../../../Session/SessionProvider";
import ArticleSummary from "../../components/summary/ArticleSummary";

const StyledContentDiv = styled(FormikField)`
position: static;
Expand Down Expand Up @@ -74,11 +75,17 @@ const toolbarAreaFilters = createToolbarAreaOptions();
// Plugins are checked from last to first
interface Props {
articleLanguage: string;
articleContent?: string;
articleId?: number;
handleSubmit: HandleSubmitFunc<LearningResourceFormType>;
}

const LearningResourceContent = ({ articleLanguage, articleId, handleSubmit: _handleSubmit }: Props) => {
const LearningResourceContent = ({
articleContent,
articleLanguage,
articleId,
handleSubmit: _handleSubmit,
}: Props) => {
const { t } = useTranslation();
const [creatorsField] = useField<IAuthor[]>("creators");

Expand Down Expand Up @@ -139,6 +146,7 @@ const LearningResourceContent = ({ articleLanguage, articleId, handleSubmit: _ha
onCancel={() => setIsNormalizedOnLoad(false)}
severity="warning"
/>
<ArticleSummary articleContent={articleContent} articleLanguage={articleLanguage} />
<StyledContentDiv name="content" label={t("form.content.label")} noBorder key={values.revision}>
{(fieldProps) => <ContentField articleLanguage={articleLanguage} articleId={articleId} {...fieldProps} />}
</StyledContentDiv>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import LearningResourceContent from "./LearningResourceContent";
import LearningResourceTaxonomy from "./LearningResourceTaxonomy";
import FormAccordion from "../../../../components/Accordion/FormAccordion";
import FormAccordionsWithComments from "../../../../components/Accordion/FormAccordionsWithComments";
import { getTextFromHTML } from "../../../../components/LLM/helpers";
import { IsNewArticleLanguageProvider } from "../../../../components/SlateEditor/IsNewArticleLanguageProvider";
import config from "../../../../config";
import { TAXONOMY_WRITE_SCOPE } from "../../../../constants";
Expand Down Expand Up @@ -58,6 +59,11 @@ const LearningResourcePanels = ({
);
const copyrightFields = useMemo<FlatArticleKeys[]>(() => ["copyright"], []);

const articleText = useMemo(() => {
if (!article?.content) return " ";
return getTextFromHTML(article.content.content);
}, [article?.content]);

return (
<FormAccordionsWithComments
defaultOpen={defaultOpen}
Expand All @@ -80,6 +86,7 @@ const LearningResourcePanels = ({
<IsNewArticleLanguageProvider locale={articleLanguage} article={article}>
<PageContent variant="content">
<LearningResourceContent
articleContent={articleText}
articleLanguage={articleLanguage}
articleId={article?.id}
handleSubmit={handleSubmit}
Expand Down Expand Up @@ -116,7 +123,7 @@ const LearningResourcePanels = ({
title={t("form.metadataSection")}
hasError={!!(errors.metaDescription || errors.metaImageAlt || errors.tags)}
>
<MetaDataField articleLanguage={articleLanguage} />
<MetaDataField articleContent={articleText} articleLanguage={articleLanguage} />
</FormAccordion>
<FormAccordion id={"learning-resource-grepCodes"} title={t("form.name.grepCodes")} hasError={!!errors.grepCodes}>
<GrepCodesField />
Expand Down
110 changes: 110 additions & 0 deletions src/containers/ArticlePage/components/summary/ArticleSummary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* 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 { FieldHelperProps, useFormikContext } from "formik";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Descendant } from "slate";

import { BlogPost } from "@ndla/icons/editor";
import { Button, FieldHelper, FieldLabel, FieldRoot, Spinner, TextArea } from "@ndla/primitives";
import { styled } from "@ndla/styled-system/jsx";
import FieldHeader from "../../../../components/Field/FieldHeader";
import { FormField } from "../../../../components/FormField";
import { claudeHaikuDefaults, invokeModel } from "../../../../components/LLM/helpers";
import PlainTextEditor from "../../../../components/SlateEditor/PlainTextEditor";
import { inlineContentToEditorValue } from "../../../../util/articleContentConverter";
import { ArticleFormType } from "../../../FormikForm/articleFormHooks";

interface Props {
articleContent?: string;
articleLanguage?: string;
}

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

const ComponentRoot = styled("div", {
base: {
display: "flex",
flexDirection: "column",
gap: "xsmall",
},
});

const ArticleSummary = ({ articleContent, articleLanguage }: Props) => {
const { t } = useTranslation();
const [generatedSummary, setGeneratedSummary] = useState("");
const [isLoading, setIsLoading] = useState(false);
const { setStatus } = useFormikContext<ArticleFormType>();

// const generateSummary = async (helpers: FieldHelperProps<Descendant[]>) => {
// const inputQuery = articleContent ?? "";
// setIsLoading(true);
// try {
// const generatedText = await invokeModel(t("textGeneration.articleSummary.prompt") + inputQuery);
// await helpers.setValue(inlineContentToEditorValue(generatedText, true), true);
// // We have to invalidate slate children. We do this with status.
// setStatus({ status: "acceptGenerated" });
// } catch (error) {
// console.error("Error genetating summary", error);
// } finally {
// setIsLoading(false);
// }
// };

const generate = async () => {
if (!articleContent) {
console.error("No article content provided to generate meta description");
return;
}
setIsLoading(true);
try {
const generatedText = await invokeModel({
prompt:
t("textGeneration.articleSummary.prompt", { language: t(`languages.${articleLanguage}`) }) + articleContent,
...claudeHaikuDefaults,
});
generatedText ? setGeneratedSummary(generatedText) : console.error("No generated text");
} catch (error) {
console.error("Error genetating summary", error);
} finally {
setIsLoading(false);
}
};

return (
<ComponentRoot>
<FieldHeader title={t("textGeneration.articleSummary.title")}></FieldHeader>
<TextArea value={generatedSummary} />
<StyledButton size="small" onClick={generate}>
{t("textGeneration.articleSummary.button")} {isLoading ? <Spinner size="small" /> : <BlogPost />}
</StyledButton>

{/* <FormField name={t("textGeneration.articleSummary.title")}>
{({ field, helpers, meta }) => {
return (
<FieldRoot required invalid={!!meta.error}>
<FieldLabel>{t("textGeneration.articleSummary.title")}</FieldLabel>
<FieldHelper>{t("textGeneration.articleSummary.title")}</FieldHelper>
<PlainTextEditor key={field.value} id={field.name} placeholder={t("textGeneration.articleSummary.title")} {...field} />
<StyledButton size="small" onClick={() => generateSummary(helpers)}>
{t("textGeneration.articleSummary.button")} {isLoading ? <Spinner size="small" /> : <BlogPost />}
</StyledButton>
</FieldRoot>
);
}}
</FormField> */}
</ComponentRoot>
);
};

export default ArticleSummary;
11 changes: 9 additions & 2 deletions src/containers/FormikForm/IngressField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ interface Props {
maxLength?: number;
type?: string;
placeholder?: string;
showMaxLength?: boolean;
}

const toolbarOptions = createToolbarDefaultValues({
Expand Down Expand Up @@ -88,10 +89,16 @@ const ingressRenderers: SlatePlugin[] = [

const plugins = ingressPlugins.concat(ingressRenderers);

const IngressField = ({ name = "introduction", maxLength = 300, placeholder }: Props) => {
const IngressField = ({ name = "introduction", maxLength = 300, placeholder, showMaxLength = true }: Props) => {
const { t } = useTranslation();
return (
<FormikField noBorder label={t("form.introduction.label")} name={name} showMaxLength maxLength={maxLength}>
<FormikField
noBorder
label={t("form.introduction.label")}
name={name}
showMaxLength={showMaxLength}
maxLength={maxLength}
>
{({ field, form: { isSubmitting } }) => (
<StyledRichTextEditor
{...field}
Expand Down
Loading
Loading