-
Notifications
You must be signed in to change notification settings - Fork 93
feat: add Research Paper Explainer & Quiz Agent #72
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| # Lamatic.ai Configuration | ||
| # Get these from https://studio.lamatic.ai → your project settings | ||
|
|
||
| LAMATIC_API_KEY=your_lamatic_api_key_here | ||
| LAMATIC_PROJECT_ID=your_project_id_here | ||
|
|
||
| # Flow endpoint URLs (from your Lamatic Studio deployment) | ||
| EXPLAIN_FLOW_URL=https://your-project.lamatic.app/api/explain | ||
| QUIZ_FLOW_URL=https://your-project.lamatic.app/api/quiz | ||
|
|
||
| # Optional: Customize the app title | ||
| NEXT_PUBLIC_APP_TITLE=Research Paper Explainer | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # dependencies | ||
| /node_modules | ||
| /.pnp | ||
| .pnp.js | ||
| .yarn/install-state.gz | ||
|
|
||
| # testing | ||
| /coverage | ||
|
|
||
| # next.js | ||
| /.next/ | ||
| /out/ | ||
|
|
||
| # production | ||
| /build | ||
|
|
||
| # misc | ||
| .DS_Store | ||
| *.pem | ||
|
|
||
| # debug | ||
| npm-debug.log* | ||
| yarn-debug.log* | ||
| yarn-error.log* | ||
|
|
||
| # local env files | ||
| .env | ||
| .env*.local | ||
|
|
||
| # vercel | ||
| .vercel | ||
|
|
||
| # typescript | ||
| *.tsbuildinfo | ||
| next-env.d.ts |
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you add a live demo link to this file? would allow us to view the functionality better, and allow users to see the kit before forking it. otherwise, LGTM. Thanks! |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,199 @@ | ||||||||||||||||||||||||||||||||||
| # 📚 Research Paper Explainer & Quiz Agent | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| > A Next.js kit built on [Lamatic AgentKit](https://github.com/Lamatic/AgentKit) that takes any research paper abstract or full text, explains it at your chosen comprehension level, and generates an interactive multiple-choice quiz to test understanding. | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
| > A Next.js kit built on [Lamatic AgentKit](https://github.com/Lamatic/AgentKit) that takes any research paper abstract or full text, explains it at your chosen comprehension level, and generates an interactive multiple-choice quiz to test understanding. | |
| > A Next.js kit built on [Lamatic AgentKit](https://github.com/Lamatic/AgentKit) that lets you paste any research paper abstract or full text into the app, explains it at your chosen comprehension level, and generates an interactive multiple-choice quiz to test understanding. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add language specifier to fenced code block.
The folder structure code block is missing a language specifier. While this doesn't affect rendering, it helps with accessibility and linting compliance.
Suggested fix
-```
+```text
research-paper-explainer/
├── app/📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ``` | |
| research-paper-explainer/ | |
| ├── app/ | |
| │ ├── page.tsx # Main UI (input + output panels) | |
| │ ├── layout.tsx # Root layout with fonts | |
| │ ├── globals.css # Design system & animations | |
| │ └── api/ | |
| │ ├── explain/route.ts # Calls the Lamatic explain flow | |
| │ └── quiz/route.ts # Calls the Lamatic quiz flow | |
| ├── lamatic-config.json # Flow definitions for Lamatic Studio | |
| ├── .env.example # Required environment variables | |
| ├── package.json | |
| ├── tailwind.config.ts | |
| ├── tsconfig.json | |
| └── README.md | |
| ``` |
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 32-32: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
Mortarion002 marked this conversation as resolved.
Show resolved
Hide resolved
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| "use server" | ||
|
|
||
| import { lamaticClient } from "@/lib/lamatic-client" | ||
|
|
||
| const EXPLAIN_FLOW_ID = process.env.LAMATIC_FLOW_ID ?? "" | ||
| const QUIZ_FLOW_ID = process.env.QUIZ_FLOW_ID ?? "" | ||
|
Comment on lines
+5
to
+6
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reject missing flow IDs before calling Lamatic.
Suggested fix export async function explainPaper(
paperContent: string,
level: string
): Promise<{ success: boolean; data?: string; error?: string }> {
try {
+ if (!EXPLAIN_FLOW_ID) {
+ return { success: false, error: "LAMATIC_FLOW_ID is not configured" };
+ }
+
console.log("[explain] Calling flow:", EXPLAIN_FLOW_ID)
@@
export async function generateQuiz(
paperContent: string,
numQuestions: number
): Promise<{ success: boolean; data?: any; error?: string }> {
try {
+ if (!QUIZ_FLOW_ID) {
+ return { success: false, error: "QUIZ_FLOW_ID is not configured" };
+ }
+
console.log("[quiz] Calling flow:", QUIZ_FLOW_ID)Also applies to: 12-19, 61-67 |
||
|
|
||
| export async function explainPaper( | ||
| paperContent: string, | ||
| level: string | ||
| ): Promise<{ success: boolean; data?: string; error?: string }> { | ||
| try { | ||
| console.log("[explain] Calling flow:", EXPLAIN_FLOW_ID) | ||
|
|
||
| // Step 1 - trigger the flow | ||
| const resData = await lamaticClient.executeFlow(EXPLAIN_FLOW_ID, { | ||
| paperContent, | ||
| level, | ||
| }) as any | ||
|
|
||
| console.log("[explain] Initial response:", JSON.stringify(resData)) | ||
|
|
||
| // Step 2 - if async, poll using checkStatus | ||
| const requestId = resData?.result?.requestId || resData?.requestId | ||
| if (requestId) { | ||
| console.log("[explain] Polling for requestId:", requestId) | ||
| // poll every 3 seconds, timeout after 60 seconds | ||
| const pollData = await lamaticClient.checkStatus(requestId, 3, 60) as any | ||
| console.log("[explain] Poll result:", JSON.stringify(pollData)) | ||
|
Comment on lines
+21
to
+29
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Redact Lamatic payloads from logs and error strings. These Suggested fix- console.log("[explain] Initial response:", JSON.stringify(resData))
+ console.log("[explain] Initial response received", {
+ requestId: resData?.result?.requestId ?? resData?.requestId ?? null,
+ })
@@
- console.log("[explain] Poll result:", JSON.stringify(pollData))
+ console.log("[explain] Poll completed", { requestId })
@@
- throw new Error("No explanation in poll result: " + JSON.stringify(pollData))
+ throw new Error("Explain flow returned no generated response")
@@
- throw new Error("Empty response: " + JSON.stringify(resData))
+ throw new Error("Explain flow returned no generated response")
@@
- console.log("[quiz] Initial response:", JSON.stringify(resData))
+ console.log("[quiz] Initial response received", {
+ requestId: resData?.result?.requestId ?? resData?.requestId ?? null,
+ })
@@
- console.log("[quiz] Poll result:", JSON.stringify(pollData))
+ console.log("[quiz] Poll completed", { requestId })Also applies to: 39-49, 69-74 |
||
|
|
||
| const explanation = | ||
| pollData?.data?.output?.result?.generatedResponse || | ||
| pollData?.data?.output?.generatedResponse || | ||
| pollData?.result?.generatedResponse || | ||
| pollData?.generatedResponse || | ||
| (typeof pollData === "string" && pollData.length > 10 ? pollData : null) | ||
|
|
||
| if (explanation) return { success: true, data: explanation } | ||
| throw new Error("No explanation in poll result: " + JSON.stringify(pollData)) | ||
| } | ||
|
|
||
| // Realtime response | ||
| const explanation = | ||
| resData?.result?.generatedResponse || | ||
| resData?.generatedResponse || | ||
| (typeof resData?.result === "string" ? resData.result : null) | ||
|
|
||
| if (explanation) return { success: true, data: explanation } | ||
| throw new Error("Empty response: " + JSON.stringify(resData)) | ||
|
|
||
| } catch (error) { | ||
| console.error("[explain] Error:", error) | ||
| return { success: false, error: error instanceof Error ? error.message : "Unknown error" } | ||
| } | ||
| } | ||
|
|
||
| export async function generateQuiz( | ||
| paperContent: string, | ||
| numQuestions: number | ||
| ): Promise<{ success: boolean; data?: any; error?: string }> { | ||
| try { | ||
| console.log("[quiz] Calling flow:", QUIZ_FLOW_ID) | ||
|
|
||
| const resData = await lamaticClient.executeFlow(QUIZ_FLOW_ID, { | ||
| paperContent, | ||
| numQuestions: String(numQuestions), | ||
| }) as any | ||
|
|
||
| console.log("[quiz] Initial response:", JSON.stringify(resData)) | ||
|
|
||
| const requestId = resData?.result?.requestId || resData?.requestId | ||
| if (requestId) { | ||
| const pollData = await lamaticClient.checkStatus(requestId, 3, 60) as any | ||
| console.log("[quiz] Poll result:", JSON.stringify(pollData)) | ||
|
|
||
| const raw = | ||
| pollData?.data?.output?.result?.generatedResponse || | ||
| pollData?.data?.output?.generatedResponse || | ||
| pollData?.result?.generatedResponse || | ||
| pollData?.generatedResponse | ||
|
|
||
| if (raw) { | ||
| const match = typeof raw === "string" ? raw.match(/\{[\s\S]*\}/) : null | ||
| const parsed = match ? JSON.parse(match[0]) : raw | ||
| return { success: true, data: parsed } | ||
|
Comment on lines
+82
to
+85
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't return malformed quiz data as a success. If the model returns prose or any non-JSON string, Suggested fix- if (raw) {
- const match = typeof raw === "string" ? raw.match(/\{[\s\S]*\}/) : null
- const parsed = match ? JSON.parse(match[0]) : raw
- return { success: true, data: parsed }
- }
+ if (raw) {
+ const parsed =
+ typeof raw === "string"
+ ? (() => {
+ const match = raw.match(/\{[\s\S]*\}/)
+ if (!match) throw new Error("Quiz response did not contain JSON")
+ return JSON.parse(match[0])
+ })()
+ : raw
+
+ if (!Array.isArray(parsed?.questions)) {
+ throw new Error("Quiz response is missing questions[]")
+ }
+
+ return { success: true, data: parsed }
+ }
@@
- if (raw) {
- const match = typeof raw === "string" ? raw.match(/\{[\s\S]*\}/) : null
- const parsed = match ? JSON.parse(match[0]) : raw
- return { success: true, data: parsed }
- }
+ if (raw) {
+ const parsed =
+ typeof raw === "string"
+ ? (() => {
+ const match = raw.match(/\{[\s\S]*\}/)
+ if (!match) throw new Error("Quiz response did not contain JSON")
+ return JSON.parse(match[0])
+ })()
+ : raw
+
+ if (!Array.isArray(parsed?.questions)) {
+ throw new Error("Quiz response is missing questions[]")
+ }
+
+ return { success: true, data: parsed }
+ }Also applies to: 90-94 |
||
| } | ||
| throw new Error("No quiz in poll result") | ||
| } | ||
|
|
||
| const raw = resData?.result?.generatedResponse || resData?.generatedResponse | ||
| if (raw) { | ||
| const match = typeof raw === "string" ? raw.match(/\{[\s\S]*\}/) : null | ||
| const parsed = match ? JSON.parse(match[0]) : raw | ||
| return { success: true, data: parsed } | ||
| } | ||
|
|
||
| throw new Error("Empty quiz response") | ||
| } catch (error) { | ||
| console.error("[quiz] Error:", error) | ||
| return { success: false, error: error instanceof Error ? error.message : "Unknown error" } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,26 @@ | ||||||||||||||||||||||||||||||||||||||
| import { NextRequest, NextResponse } from "next/server"; | ||||||||||||||||||||||||||||||||||||||
| import { explainPaper } from "@/actions/orchestrate"; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| export async function POST(req: NextRequest) { | ||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||
| const { paperContent, level } = await req.json(); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (!paperContent || paperContent.trim().length < 50) { | ||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||
| { error: "Please provide at least 50 characters of paper content." }, | ||||||||||||||||||||||||||||||||||||||
| { status: 400 } | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+6
to
+13
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard input types before calling At Line 7, non-string Suggested patch- const { paperContent, level } = await req.json();
+ const body = await req.json();
+ const paperContent = typeof body?.paperContent === "string" ? body.paperContent : "";
+ const level = typeof body?.level === "string" ? body.level : undefined;
- if (!paperContent || paperContent.trim().length < 50) {
+ if (!paperContent || paperContent.trim().length < 50) {
return NextResponse.json(
{ error: "Please provide a research paper abstract or content (at least 50 characters)." },
{ status: 400 }
);
}📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const result = await explainPaper(paperContent, level || "undergraduate"); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (!result.success) { | ||||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ error: result.error }, { status: 502 }); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ explanation: result.data }); | ||||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||||
| console.error("Explain route error:", err); | ||||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ error: "Internal server error." }, { status: 500 }); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: Lamatic/AgentKit
Length of output: 892
Quote env values that contain spaces.
At line 12, the unquoted value can cause dotenv tooling inconsistencies; quote it for portability and consistency with similar values elsewhere in the codebase.
Suggested patch
📝 Committable suggestion
🧰 Tools
🪛 dotenv-linter (4.0.0)
[warning] 12-12: [ValueWithoutQuotes] This value needs to be surrounded in quotes
(ValueWithoutQuotes)