From 9f0d4a2255796ea5bdd0438c0cbd97118e54af76 Mon Sep 17 00:00:00 2001 From: seyeong-han <110819238+seyeong-han@users.noreply.github.com> Date: Sun, 24 Nov 2024 11:09:52 -0600 Subject: [PATCH 1/2] Enhance design (#3) * fix: CharacterGraph to get the graphData * fix: BookPage import * feat: integrate server calling * rm: unused files * fix: change next to react javascript * feat: retrieve data from server * feat: add axios and react-force-graph-2d * docs: update README * feat: fetch book data from google books api * docs: clean unused codes --- README.md | 90 +++---- package-lock.json | 46 ++++ package.json | 2 + src/approuter.jsx | 4 +- src/pages/bookPage/components/AISearch.jsx | 88 ++++--- .../bookPage/components/CharacterGraph.jsx | 104 ++++---- src/pages/bookPage/index.jsx | 224 ++++++++++++++++++ src/pages/bookPage/info/page.jsx | 47 ---- src/pages/bookPage/layout.jsx | 29 --- src/pages/bookPage/page.jsx | 26 -- 10 files changed, 411 insertions(+), 249 deletions(-) create mode 100644 src/pages/bookPage/index.jsx delete mode 100644 src/pages/bookPage/info/page.jsx delete mode 100644 src/pages/bookPage/layout.jsx delete mode 100644 src/pages/bookPage/page.jsx diff --git a/README.md b/README.md index 58beeac..ac264da 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,56 @@ -# Getting Started with Create React App +# BookMind -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). +BookMind is a web application that allows users to explore character relationships and storylines in books using AI-powered visualizations. The application provides interactive mind maps, AI chatbots for deep questions, book summaries, and community contributions. -## Available Scripts +## Features -In the project directory, you can run: +- Interactive Mind Maps: Visualize relationships between characters and plot elements. +- AI Chatbot: Ask deep questions about the book and get insightful answers. +- Book Summaries: Get concise overviews of plots and themes. +- Community Contributions: Add and refine maps with fellow book lovers. -### `npm start` +## Prerequisites -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in your browser. +- Node.js +- Python 3.x +- LlamaStack server running locally +- Environment variables: + - LLAMA_STACK_PORT + - INFERENCE_MODEL + - REACT_APP_GOOGLE_BOOKS_API_KEY -The page will reload when you make changes.\ -You may also see any lint errors in the console. +## Getting Started -### `npm test` +### Backend Setup -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. +1. Install dependencies: -### `npm run build` +``` +pip install -r server/requirements.txt +``` -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. +2. Run the server: -The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! +``` +python server/server.py +``` -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. +### Frontend Setup -### `npm run eject` +1. Install dependencies: -**Note: this is a one-way operation. Once you `eject`, you can't go back!** +``` +npm install +``` -If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. +2. Run the application: -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. +``` +npm start +``` -You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. +## Usage -## Learn More - -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). - -To learn React, check out the [React documentation](https://reactjs.org/). - -### Code Splitting - -This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) - -### Analyzing the Bundle Size - -This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) - -### Making a Progressive Web App - -This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) - -### Advanced Configuration - -This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) - -### Deployment - -This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) - -### `npm run build` fails to minify - -This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) +1. Initialize Memory: Upload your book or choose from the library to initialize memory. +2. AI Analysis: The AI analyzes the book and generates a mind map. +3. Explore Insights: Explore relationships, themes, and Q&A insights. diff --git a/package-lock.json b/package-lock.json index a56aabb..9d8c923 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,12 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "axios": "^1.7.7", "lucide-react": "^0.460.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-force-graph": "^1.44.7", + "react-force-graph-2d": "^1.25.8", "react-router-dom": "^7.0.1", "react-scripts": "^5.0.1", "web-vitals": "^2.1.4" @@ -5624,6 +5626,29 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -16024,6 +16049,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.13.0.tgz", @@ -16345,6 +16375,22 @@ "react": "*" } }, + "node_modules/react-force-graph-2d": { + "version": "1.25.8", + "resolved": "https://registry.npmjs.org/react-force-graph-2d/-/react-force-graph-2d-1.25.8.tgz", + "integrity": "sha512-Z1mxbEDRsmFHCDU7ZcFryFzzOin1kYpKOyqQ1SWiyLCUmR6l/dFt/GhOknxQt976E6kK4V84cD1J6F2OlsViYQ==", + "dependencies": { + "force-graph": "1", + "prop-types": "15", + "react-kapsule": "^2.5" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", diff --git a/package.json b/package.json index 6ae8d0e..e9c3431 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,12 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "axios": "^1.7.7", "lucide-react": "^0.460.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-force-graph": "^1.44.7", + "react-force-graph-2d": "^1.25.8", "react-router-dom": "^7.0.1", "react-scripts": "^5.0.1", "web-vitals": "^2.1.4" diff --git a/src/approuter.jsx b/src/approuter.jsx index da4f0ac..7a80f93 100644 --- a/src/approuter.jsx +++ b/src/approuter.jsx @@ -4,14 +4,14 @@ import { Route, BrowserRouter as Router, Routes } from "react-router-dom"; import Home from "./pages/homePage"; -import AISearch from "./pages/bookPage/components/AISearch"; +import BookPage from "./pages/bookPage"; const AppRouter = function () { return ( <> } /> - } /> + } /> ); diff --git a/src/pages/bookPage/components/AISearch.jsx b/src/pages/bookPage/components/AISearch.jsx index 0ae1db1..042198b 100644 --- a/src/pages/bookPage/components/AISearch.jsx +++ b/src/pages/bookPage/components/AISearch.jsx @@ -1,49 +1,71 @@ -'use client' +import { useState } from "react"; +import { FaSearch, FaSpinner } from "react-icons/fa"; +import axios from "axios"; -import { useState } from 'react' -//import { Input } from "@/components/ui/input" -//import { Button } from "@/components/ui/button" - -export default function AISearch() { - const [query, setQuery] = useState('') - const [response, setResponse] = useState('') +export default function AISearch({ bookTitle, onQueryResponse }) { + const [query, setQuery] = useState(""); + const [response, setResponse] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); const handleSearch = async () => { + if (!query.trim()) return; + + setIsLoading(true); + setError(null); + try { - // Simulate AI response - const aiResponse = `AI response for: "${query}"` - setResponse(aiResponse) - // Here you would typically call your backend API to get the AI response - // and update the graph data + const response = await axios.post("http://localhost:5001/query", { + query: `About ${bookTitle}: ${query}`, + }); + + setResponse(response.data.response); + onQueryResponse?.(response.data); } catch (error) { - console.error('Error in AI search:', error) - setResponse('An error occurred while processing your request.') + console.error("Error in AI search:", error); + setError("Failed to get response. Please try again."); + } finally { + setIsLoading(false); } - } + }; return (

AI-Powered Search

- setQuery(e.target.value)} - className="w-full px-4 py-2 rounded-md border-gray-300 focus:border-blue-500 focus:ring focus:ring-blue-200 transition duration-200" - /> - - {response && ( +
+ setQuery(e.target.value)} + className="w-full px-4 py-2 rounded-md border-gray-300 + focus:border-blue-500 focus:ring focus:ring-blue-200 + transition duration-200" + disabled={isLoading} + /> + +
+ + {error && ( +
{error}
+ )} + + {response && !error && (

AI Response:

-

{response}

+

{response}

)}
- ) + ); } - diff --git a/src/pages/bookPage/components/CharacterGraph.jsx b/src/pages/bookPage/components/CharacterGraph.jsx index 7753f7d..09578b5 100644 --- a/src/pages/bookPage/components/CharacterGraph.jsx +++ b/src/pages/bookPage/components/CharacterGraph.jsx @@ -1,82 +1,67 @@ -'use client' +import { useState, useEffect, useRef } from "react"; +import ForceGraph2D from "react-force-graph-2d"; -import { useState, useEffect, useRef } from 'react' -import dynamic from 'next/dynamic' - -const ForceGraph2D = dynamic(() => import('react-force-graph-2d'), { - ssr: false, - loading: () =>

Loading Graph...

-}) - -export default function CharacterGraph() { - const [mounted, setMounted] = useState(false) - const containerRef = useRef(null) - const [dimensions, setDimensions] = useState({ width: 300, height: 300 }) - - const [data] = useState({ - nodes: [ - // { id: "Harry Potter", group: 1 }, - // { id: "Ron Weasley", group: 1 }, - // { id: "Hermione Granger", group: 1 }, - // { id: "Lord Voldemort", group: 2 } - ], - links: [ - // { source: "Harry Potter", target: "Ron Weasley", relationship: "Friends" }, - // { source: "Harry Potter", target: "Hermione Granger", relationship: "Friends" }, - // { source: "Ron Weasley", target: "Hermione Granger", relationship: "Friends" }, - // { source: "Harry Potter", target: "Lord Voldemort", relationship: "Enemy" } - ] - }) +export default function CharacterGraph({ graphData }) { + const containerRef = useRef(null); + const [dimensions, setDimensions] = useState({ width: 300, height: 300 }); useEffect(() => { - setMounted(true) - const updateDimensions = () => { if (containerRef.current) { setDimensions({ width: containerRef.current.offsetWidth, - height: Math.max(300, containerRef.current.offsetHeight) - }) + height: Math.max(300, containerRef.current.offsetHeight), + }); } - } + }; - updateDimensions() - window.addEventListener('resize', updateDimensions) - - return () => window.removeEventListener('resize', updateDimensions) - }, []) - - if (!mounted) return null + updateDimensions(); + window.addEventListener("resize", updateDimensions); + return () => window.removeEventListener("resize", updateDimensions); + }, []); return (
-

Character Relationship Graph

+

+ Character Relationship Graph +

{ - const label = node.id - const fontSize = 12 / globalScale - ctx.font = `${fontSize}px Inter` - const textWidth = ctx.measureText(label).width - const bckgDimensions = [textWidth, fontSize].map(n => n + fontSize * 0.2) + const label = node.name; + const fontSize = 12 / globalScale; + ctx.font = `${fontSize}px Arial`; + const textWidth = ctx.measureText(label).width; + const bckgDimensions = [textWidth, fontSize].map( + (n) => n + fontSize * 0.2 + ); - ctx.fillStyle = 'rgba(255, 255, 255, 0.8)' - ctx.fillRect(node.x - bckgDimensions[0] / 2, node.y - bckgDimensions[1] / 2, ...bckgDimensions) + ctx.fillStyle = "rgba(255, 255, 255, 0.8)"; + ctx.fillRect( + node.x - bckgDimensions[0] / 2, + node.y - bckgDimensions[1] / 2, + ...bckgDimensions + ); - ctx.textAlign = 'center' - ctx.textBaseline = 'middle' - ctx.fillStyle = node.group === 1 ? '#3b82f6' : '#ef4444' - ctx.fillText(label, node.x, node.y) + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillStyle = "#3b82f6"; + ctx.fillText(label, node.x, node.y); - node.__bckgDimensions = bckgDimensions // to re-use in nodePointerAreaPaint + node.__bckgDimensions = bckgDimensions; }} nodePointerAreaPaint={(node, color, ctx) => { - ctx.fillStyle = color - const bckgDimensions = node.__bckgDimensions - bckgDimensions && ctx.fillRect(node.x - bckgDimensions[0] / 2, node.y - bckgDimensions[1] / 2, ...bckgDimensions) + ctx.fillStyle = color; + const bckgDimensions = node.__bckgDimensions; + bckgDimensions && + ctx.fillRect( + node.x - bckgDimensions[0] / 2, + node.y - bckgDimensions[1] / 2, + ...bckgDimensions + ); }} - linkColor={() => '#9ca3af'} + linkColor={() => "#9ca3af"} linkWidth={1} backgroundColor="#ffffff" width={dimensions.width} @@ -84,6 +69,5 @@ export default function CharacterGraph() { />
- ) + ); } - diff --git a/src/pages/bookPage/index.jsx b/src/pages/bookPage/index.jsx new file mode 100644 index 0000000..067b83f --- /dev/null +++ b/src/pages/bookPage/index.jsx @@ -0,0 +1,224 @@ +import React, { useState } from "react"; +import { FaSearch, FaBook } from "react-icons/fa"; +import AISearch from "./components/AISearch"; +import CharacterGraph from "./components/CharacterGraph"; +import axios from "axios"; + +export default function BookPage() { + const [searchTerm, setSearchTerm] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [graphData, setGraphData] = useState(null); + const [bookData, setBookData] = useState(null); + const [searchComplete, setSearchComplete] = useState(false); + + const verificationGraphData = (graphData) => { + try { + // Create Set of valid node IDs + const nodeIds = new Set(graphData.nodes.map((node) => node.id)); + + // Filter links to only include valid node references + const validLinks = graphData.links.filter( + (link) => nodeIds.has(link.source) && nodeIds.has(link.target) + ); + + console.log("validLinks:", validLinks); + + return { + nodes: graphData.nodes, + links: validLinks, + }; + } catch (error) { + console.error("Error validating graph data:", error); + return graphData; // Return original data if validation fails + } + }; + + const initializeMemory = async (title) => { + setIsLoading(true); + try { + const response = await axios.post("http://localhost:5001/initialize", { + title: title, + }); + console.log("Memory initialized:", response.data); + + // Verify and set graph data + const verifiedData = verificationGraphData(response.data); + setGraphData(verifiedData); + + return verifiedData; + } catch (error) { + console.error("Initialization error:", error); + throw error; + } finally { + setIsLoading(false); + } + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + if (!searchTerm.trim()) { + return; + } + + setIsLoading(true); + try { + // Initialize memory and fetch book data in parallel + const [memoryResponse, bookInfo] = await Promise.all([ + initializeMemory(searchTerm), + fetchBookData(searchTerm), + ]); + + setBookData({ + title: bookInfo.title, + subtitle: bookInfo.summary, + posterUrl: bookInfo.coverUrl, + author: bookInfo.author, + publishedDate: bookInfo.publishedDate, + pageCount: bookInfo.pageCount, + }); + + setGraphData(memoryResponse); + setSearchComplete(true); + } catch (error) { + console.error("Search error:", error); + } finally { + setIsLoading(false); + } + }; + // Add new function to fetch book cover + const fetchBookData = async (bookTitle) => { + try { + const response = await axios.get( + `https://www.googleapis.com/books/v1/volumes`, + { + params: { + q: bookTitle, + key: process.env.REACT_APP_GOOGLE_BOOKS_API_KEY, + }, + } + ); + + if (response.data.items && response.data.items[0]) { + const volumeInfo = response.data.items[0].volumeInfo; + const imageLinks = volumeInfo.imageLinks || {}; + + return { + coverUrl: + imageLinks.extraLarge || + imageLinks.large || + imageLinks.medium || + imageLinks.thumbnail || + "/placeholder.jpg", + summary: volumeInfo.description || "No summary available", + title: volumeInfo.title, + author: volumeInfo.authors?.[0] || "Unknown Author", + publishedDate: volumeInfo.publishedDate, + pageCount: volumeInfo.pageCount, + }; + } + + return { + coverUrl: "/placeholder.jpg", + summary: "No summary available", + title: bookTitle, + author: "Unknown Author", + publishedDate: "", + pageCount: 0, + }; + } catch (error) { + console.error("Error fetching book data:", error); + return { + coverUrl: "/placeholder.jpg", + summary: "Failed to load book information", + title: bookTitle, + author: "Unknown Author", + publishedDate: "", + pageCount: 0, + }; + } + }; + + return ( +
+
+ {/* Search Section */} +
+

+ + Character Mind Map + +

+
+
+
+ setSearchTerm(e.target.value)} + placeholder="Enter book or movie title..." + className="w-full px-5 py-3 rounded-lg border-2 border-gray-200 + focus:border-blue-500 focus:ring-2 focus:ring-blue-200 + transition-all duration-200 bg-white/90 + placeholder-gray-400 text-gray-700" + disabled={isLoading} + /> +
+ +
+
+

+ Search for any book or movie to explore character relationships +

+
+ + {/* Info Section - Only show when search is complete */} + {searchComplete && bookData && ( +
+
+
+
+ {bookData.title} +
+
+
+ +

+ {bookData.title} +

+
+

{bookData.subtitle}

+
+
+
+ +
+
+ +
+
+ +
+
+
+ )} +
+
+ ); +} diff --git a/src/pages/bookPage/info/page.jsx b/src/pages/bookPage/info/page.jsx deleted file mode 100644 index f88630a..0000000 --- a/src/pages/bookPage/info/page.jsx +++ /dev/null @@ -1,47 +0,0 @@ -import dynamic from 'next/dynamic' -import Image from 'next/image' - -const CharacterGraph = dynamic(() => import('@/components/CharacterGraph'), { - ssr: false, - loading: () =>
Loading Character Graph...
-}) - -const AISearch = dynamic(() => import('@/components/AISearch'), { - ssr: false, - loading: () =>
Loading AI Search...
-}) - -export default function InfoPage() { - const bookInfo = { - title: "Harry Potter and the Philosopher's Stone", - subtitle: "The boy who lived comes to Hogwarts", - posterUrl: "/placeholder.svg?height=300&width=200" - } - - return ( -
-
-
-
- {bookInfo.title} -
-
-

{bookInfo.title}

-

{bookInfo.subtitle}

-
-
-
-
- - -
-
- ) -} - diff --git a/src/pages/bookPage/layout.jsx b/src/pages/bookPage/layout.jsx deleted file mode 100644 index 09f8b78..0000000 --- a/src/pages/bookPage/layout.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import './globals.css' -import { Inter } from 'next/font/google' -import Link from 'next/link' - -const inter = Inter({ subsets: ['latin'] }) - -export const metadata = { - title: 'Book/Movie Character Graph', - description: 'Explore character relationships in books and movies', -} - -export default function RootLayout({ children }) { - return ( - - - -
- {children} -
- - - ) -} - diff --git a/src/pages/bookPage/page.jsx b/src/pages/bookPage/page.jsx deleted file mode 100644 index 80e28fb..0000000 --- a/src/pages/bookPage/page.jsx +++ /dev/null @@ -1,26 +0,0 @@ -import Link from 'next/link' -import { Input } from "@/components/ui/input" -import { Button } from "@/components/ui/button" - -export default function Home() { - return ( -
-

Search for a Book or Movie

-
-
- - - - -
-
-
- ) -} - From 12f0d4a5d0b082847dc5187fdb298d338aca61e7 Mon Sep 17 00:00:00 2001 From: seyeong-han Date: Sun, 24 Nov 2024 12:43:51 -0600 Subject: [PATCH 2/2] fix: prompt engineering --- server/server.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/server/server.py b/server/server.py index f907a3c..830a2b6 100644 --- a/server/server.py +++ b/server/server.py @@ -53,15 +53,16 @@ async def get_graph_response(text_response, client): Text to convert: {text_response} - Expected format: + Expected format example (return only the JSON object, no additional text): {{ "nodes": [ - {{"id": "id1", "name": "Character Name", "val": 10}}, + {{"id": id1, "name": "Character Name", "val": 10}} ], "links": [ - {{"source": "id1", "target": "id2"}} + {{"source": id1, "target": "id2"}} ] }} + id1 and id2 are example variables and you should not generate those exact values. """ # Get graph structure from LlamaStack @@ -79,6 +80,7 @@ def jsonify_graph_response(response): """Extract and parse JSON content from graph response.""" try: content = response.completion_message.content + print("content: ", content) # Find indices of first { and last } start_idx = content.find('{') end_idx = content.rfind('}') @@ -136,6 +138,8 @@ async def process_book(book_title): graph_response = await get_graph_response(text_response, client) + print("graph_response: ", graph_response) + graph_data = "" try: graph_data = jsonify_graph_response(graph_response)