├── .devcontainer └── devcontainer.json ├── .gitignore ├── App_Information.py ├── README.md ├── agent.py ├── img ├── Flowchart-2.jpg ├── Flowchart.jpg ├── story_writing_graph_running.png └── story_writing_screenshot.png ├── langgraph.json ├── pages └── Story_Writing.py ├── requirements.txt ├── test.py └── test_app.py /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Python 3", 3 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 4 | "image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye", 5 | "customizations": { 6 | "codespaces": { 7 | "openFiles": [ 8 | "README.md", 9 | "streamlit_story_app.py" 10 | ] 11 | }, 12 | "vscode": { 13 | "settings": {}, 14 | "extensions": [ 15 | "ms-python.python", 16 | "ms-python.vscode-pylance" 17 | ] 18 | } 19 | }, 20 | "updateContentCommand": "[ -f packages.txt ] && sudo apt update && sudo apt upgrade -y && sudo xargs apt install -y 7 | /* Centering title horizontally */ 8 | .centered-title { 9 | text-align: center; 10 | } 11 | """,unsafe_allow_html=True) 12 | 13 | st.title("App Information") 14 | 15 | st.write("This app is a quick example showing how you can use LangGraph Cloud in your applications. \ 16 | Before using the app, I highly recommend reading through this info guide to gain a better understanding of how it works.") 17 | 18 | st.header("Background") 19 | 20 | st.write("This app was designed to show off some LangGraph Cloud features in a fun, interactive way. This app is designed to allow users to write a story \ 21 | with the help of a LangGraph agent. The app allows users to edit chapters they have written already, or continue the story by writing \ 22 | the next chapter. This means the user can have multiple versions of the same chapter number and can select the \ 23 | one they like most to continue the story from. At the beginning the user provides the graph information on the summary of the story, the writing style \ 24 | they want, and any additional details important to the story. From that point they just need to provide edit and continue instructions to steer the \ 25 | agent in the desired direction. \n \nNote: This app is a prototype and not ready for deployment. There might be bugs/poor results from the agent.") 26 | 27 | st.header("The Graph State") 28 | 29 | st.write("One of the coolest features of LangGraph Cloud is the ability to have a persistent state across many runs of the graph. \ 30 | In this case we are able to retain information about the story as the graph continues to write it. In our case, we keep an overall \ 31 | chapter state graph, which is just a dictionary containing the different chapters written so far. Each time you edit or continue the \ 32 | story a new chapter is added to the graph. Each chapter keeps information about its content, title, and the relationship it has with \ 33 | the other chapters in the story (i.e. what chapters are siblings, children, parents, or cousins to it). Below is an example of what the \ 34 | chapter graph would look like after a user has been using the Story Writing tool for a little bit:") 35 | 36 | st.image('./img/Flowchart.jpg') 37 | 38 | st.write("Let's dive into the graph to understand it a little better. First note that each color represents a different chapter number. In this instance \ 39 | we have two Chapter 1's, two Chapter 2's, three Chapter 3's, and a single Chapter 4. By following the node numbers we can reconstruct how this story \ 40 | was written. First, Node 1 was created when the user clicked on \"New Story\". Then Node 2 was created when the user pressed \"Continue\". The user \ 41 | then created Node 3 by editing the chapter that was contained in Node 1. You can follow the rest of the story creation on your own by tracking the \ 42 | increasing node numbers.") 43 | 44 | st.write("When using the story app, you can navigate between previous chapters, next chapters, current chapters. It can be a little hard to understand \ 45 | what chapters show up where, so let's take a look at an example where the user is currently viewing the chapter in Node 5. The following diagram \ 46 | highlights the relationships Node 5 has with other nodes, and the explanation below dives into how these relationships work and how they inform \ 47 | what previous, next, and current chapter options we have to choose from:") 48 | 49 | st.image('./img/Flowchart-2.jpg') 50 | 51 | 52 | st.write("In this diagram, we draw \ 53 | red arrows representing all of the other nodes the user could move to. \n \nThere is one \"Next Chapter\" option, Node 8, because Node 5 only has \ 54 | one child. If we were to press \"Continue\" again from Node 5 to create another child, there would then be two options for the \"Next Chapter\". \n \nThere\ 55 | are three current chapter options. The first is Node 5 itself (the chapter you are viewing is always an option to be the current chapter!) \ 56 | and then Nodes 6 and 7 are also options. Node 7 is a \"Sibling\" of Node 5 because it was created by editing from Node 5. If we were to make \ 57 | further edits to Node 7, that new node would also be a siblig of Node 5. Any nodes that are direct \"edit descendants\" of a node are considered \"Siblings\" \ 58 | of that node. \n \nNode 6 is what we call a \"Cousin\" node because it originates from the same node as Node 5 (namely Node 4) but is not directly \ 59 | connected to it on our flow chart. Any nodes that originate from the same parent as a particular node are considered \"Cousin\" nodes. To summarize: \ 60 | the \"Current Chapter\" options consist of the current node itself, all of its \"Sibling\" nodes, and all of its \"Cousin\" nodes. \n\ 61 | Lastly, you can (unless you are at a node representing a Chapter 1 - in this case Node 1 and Node 3) go back to the previous chapter. Unlike the \ 62 | current or next chapter options, there is always only one previous chapter to go back to: your direct parent. For Node 5, its direct parent is \ 63 | Node 4, so that would show up as the option for the previous chapter. \n \nOne last important thing to note is that chapter options are displayed by their \ 64 | chapter title, which makes it slightly easier to know where you are going instead of relying on node numbers which don't have much significance to the user.") 65 | 66 | st.header("Using the app") 67 | 68 | st.subheader("While viewing a chapter") 69 | 70 | st.write("There are a variety of different things to navigate through while using the app, let's walk through them by exploring this screenshot:") 71 | 72 | st.image("./img/story_writing_screenshot.png") 73 | 74 | st.write("At the top of the page you will see the title of the story, which is generated by the graph when the user starts the story. When you click on the \"Load Story\" \ 75 | button on the left hand navigation pane, stories are listed by their titles to make it easier for users to remember which story they are loading. \n \nBelow the \ 76 | story title, there are two dropdowns for selecting the previous or next chapter to navigate to. These only show up as dropdowns if there exist chapters \ 77 | to move to, otherwise text saying \"No next/previous chapters!\" will show up. \n \nBelow those dropdowns, you can see the current chapter title. Chapter \ 78 | titles are generated by the graph after the chapter has been written to try and allow the title to match the chapter content as much as possible. \n \nBelow \ 79 | the chapter title is the actual chapter content itself. The chapter content is inside a scrollable element to limit the amount of vertical space it takes up. \n \nBelow \ 80 | the chapter content is the chapter number. Remember that the chapter number does not uniquely identify a chapter since we can create multiple \"Chapter 2's\", but \ 81 | this number serves to give us some reference of where we are in the story. \n \nNext up we have the dropdown for selecting different versions of the current \ 82 | chapter (in this case the different \"Chapter 2's\") \n \nLastly, we have the feedback buttons, which are a cool feature that allows users to send feedback \ 83 | to LangSmith. When a user clicks on the \"Bad Writing\" or \"Good Writing\" button, feedback is automatically sent to the trace in LangSmith that \ 84 | corresponds to the run of the graph that wrote the chapter the user is currently viewing.") 85 | 86 | st.subheader("While the graph is running") 87 | 88 | st.image("./img/story_writing_graph_running.png") 89 | 90 | st.write("When the user navigates to the navigation panel and clicks \"Submit\" a graph run is started, and the app reacts accordingly. The most important thing to \ 91 | notice while the graph is running is that the graph state is streamed back to the user. Specifically you will see 5 messages in the Graph State location \ 92 | and they will appear in the following order: ") 93 | 94 | st.write(''' 95 | 96 | - \"Waiting for user input...\" is when the user has yet to click submit, and the graph is waiting for the input 97 | 98 | - \"Summarixing story so far...\" is written when the graph is summarizing the story up to this point. The graph does this so that it has better context on what to write next. 99 | 100 | - \"Brainstorming ideas for chapter..\" occurs after the graph has summarized the story and is now thinking of ideas for the next chapter. 101 | 102 | - \"Planning outline for chapter...\" comes after the brainstorm phase as the graph narrows down the ideas and creates a rough outline of the chapter. 103 | 104 | - \"Writing the chapter...\" is the last step and is the only step where the LLM output is streamed to the user since this step is outputting the actual chapter content. 105 | ''') 106 | 107 | st.write("After the chapter is done being written, the app will automatically return to the \"Chapter Viewing\" state. You can also return to that state by \ 108 | clicking the \"Back\" button if you change your mind on making an edit or continuing the story.") 109 | 110 | st.header("Ideas for future work") 111 | 112 | st.write("There are a ton of ways you could improve this app to make the ") 113 | st.write(''' 114 | 115 | - Improve the graph (smarter writing process, tool calling for writing style of famous authors) 116 | 117 | - Add graph visualizations 118 | 119 | - Add more functionality (user edits of the chapter) 120 | 121 | ''') 122 | 123 | if __name__ == "__main__": 124 | asyncio.run(main()) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # App Information 2 | 3 | ## Using the website 4 | 5 | To use the website, all you need to do is use streamlit to run the app locally. First, clone the repository and then once you are in the repository directory call `streamlit run App_Information.py`. If you are hosting your own graph (see [this documentation](https://langchain-ai.github.io/langgraph/cloud/deployment/cloud/) for hosting on LangGraph Cloud), make sure to change the url in the `get_client` function call in `pages/Story_Writing.py` to match the url of your deployed graph. 6 | 7 | Once you have called `streamlit run App_Information.py` you can go to `http://localhost:8501` to see the application. 8 | 9 | If you don't wish to host yourself, you can access the website by going to the following URL: https://langchain-story-writing.streamlit.app. 10 | 11 | ## Background 12 | 13 | This app was designed to show off some LangGraph Cloud features in a fun, interactive way. This app is designed to allow users to write a story with the help of a LangGraph agent. The app allows users to edit chapters they have written already, or continue the story by writing the next chapter. This means the user can have multiple versions of the same chapter number and can select the one they like most to continue the story from. At the beginning the user provides the graph information on the summary of the story, the writing style they want, and any additional details important to the story. From that point they just need to provide edit and continue instructions to steer the agent in the desired direction. 14 | 15 | *Note: This app is a prototype and not ready for deployment. There might be bugs/poor results from the agent.* 16 | 17 | ## The Graph State 18 | 19 | One of the coolest features of LangGraph Cloud is the ability to have a persistent state across many runs of the graph. In this case we are able to retain information about the story as the graph continues to write it. In our case, we keep an overall chapter state graph, which is just a dictionary containing the different chapters written so far. Each time you edit or continue the story a new chapter is added to the graph. Each chapter keeps information about its content, title, and the relationship it has with the other chapters in the story (i.e. what chapters are siblings, children, parents, or cousins to it). Below is an example of what the chapter graph would look like after a user has been using the Story Writing tool for a little bit: 20 | 21 | ![Flowchart](./img/Flowchart.jpg) 22 | 23 | Let's dive into the graph to understand it a little better. First note that each color represents a different chapter number. In this instance we have two Chapter 1's, two Chapter 2's, three Chapter 3's, and a single Chapter 4. By following the node numbers we can reconstruct how this story was written. First, Node 1 was created when the user clicked on "New Story". Then Node 2 was created when the user pressed "Continue". The user then created Node 3 by editing the chapter that was contained in Node 1. You can follow the rest of the story creation on your own by tracking the increasing node numbers. 24 | 25 | When using the story app, you can navigate between previous chapters, next chapters, current chapters. It can be a little hard to understand what chapters show up where, so let's take a look at an example where the user is currently viewing the chapter in Node 5. The following diagram highlights the relationships Node 5 has with other nodes, and the explanation below dives into how these relationships work and how they inform what previous, next, and current chapter options we have to choose from: 26 | 27 | ![Flowchart-2](./img/Flowchart-2.jpg) 28 | 29 | In this diagram, we draw red arrows representing all of the other nodes the user could move to. 30 | 31 | - There is one "Next Chapter" option, Node 8, because Node 5 only has one child. If we were to press "Continue" again from Node 5 to create another child, there would then be two options for the "Next Chapter". 32 | - There are three current chapter options. The first is Node 5 itself (the chapter you are viewing is always an option to be the current chapter!) and then Nodes 6 and 7 are also options. Node 7 is a "Sibling" of Node 5 because it was created by editing from Node 5. If we were to make further edits to Node 7, that new node would also be a sibling of Node 5. Any nodes that are direct "edit descendants" of a node are considered "Siblings" of that node. 33 | - Node 6 is what we call a "Cousin" node because it originates from the same node as Node 5 (namely Node 4) but is not directly connected to it on our flow chart. Any nodes that originate from the same parent as a particular node are considered "Cousin" nodes. To summarize: the "Current Chapter" options consist of the current node itself, all of its "Sibling" nodes, and all of its "Cousin" nodes. 34 | - Lastly, you can (unless you are at a node representing a Chapter 1 - in this case Node 1 and Node 3) go back to the previous chapter. Unlike the current or next chapter options, there is always only one previous chapter to go back to: your direct parent. For Node 5, its direct parent is Node 4, so that would show up as the option for the previous chapter. 35 | 36 | One last important thing to note is that chapter options are displayed by their chapter title, which makes it slightly easier to know where you are going instead of relying on node numbers which don't have much significance to the user. 37 | 38 | ## Using the app 39 | 40 | ### While viewing a chapter 41 | 42 | There are a variety of different things to navigate through while using the app, let's walk through them by exploring this screenshot: 43 | 44 | ![Story Writing Screenshot](./img/story_writing_screenshot.png) 45 | 46 | At the top of the page you will see the title of the story, which is generated by the graph when the user starts the story. When you click on the "Load Story" button on the left-hand navigation pane, stories are listed by their titles to make it easier for users to remember which story they are loading. 47 | 48 | Below the story title, there are two dropdowns for selecting the previous or next chapter to navigate to. These only show up as dropdowns if there exist chapters to move to, otherwise text saying "No next/previous chapters!" will show up. 49 | 50 | Below those dropdowns, you can see the current chapter title. Chapter titles are generated by the graph after the chapter has been written to try and allow the title to match the chapter content as much as possible. 51 | 52 | Below the chapter title is the actual chapter content itself. The chapter content is inside a scrollable element to limit the amount of vertical space it takes up. 53 | 54 | Below the chapter content is the chapter number. Remember that the chapter number does not uniquely identify a chapter since we can create multiple "Chapter 2's", but this number serves to give us some reference of where we are in the story. 55 | 56 | Next up we have the dropdown for selecting different versions of the current chapter (in this case the different "Chapter 2's"). 57 | 58 | Lastly, we have the feedback buttons, which are a cool feature that allows users to send feedback to LangSmith. When a user clicks on the "Bad Writing" or "Good Writing" button, feedback is automatically sent to the trace in LangSmith that corresponds to the run of the graph that wrote the chapter the user is currently viewing. 59 | 60 | ### While the graph is running 61 | 62 | ![Story Writing Graph Running](./img/story_writing_graph_running.png) 63 | 64 | When the user navigates to the navigation panel and clicks "Submit" a graph run is started, and the app reacts accordingly. The most important thing to notice while the graph is running is that the graph state is streamed back to the user. Specifically you will see 5 messages in the Graph State location and they will appear in the following order: 65 | 66 | - "Waiting for user input..." is when the user has yet to click submit, and the graph is waiting for the input 67 | - "Summarizing story so far..." is written when the graph is summarizing the story up to this point. The graph does this so that it has better context on what to write next. 68 | - "Brainstorming ideas for chapter.." occurs after the graph has summarized the story and is now thinking of ideas for the next chapter. 69 | - "Planning outline for chapter..." comes after the brainstorm phase as the graph narrows down the ideas and creates a rough outline of the chapter. 70 | - "Writing the chapter..." is the last step and is the only step where the LLM output is streamed to the user since this step is outputting the actual chapter content. 71 | 72 | After the chapter is done being written, the app will automatically return to the "Chapter Viewing" state. You can also return to that state by clicking the "Back" button if you change your mind on making an edit or continuing the story. 73 | 74 | ## Ideas for future work 75 | 76 | There are a ton of ways you could improve this app to make the: 77 | 78 | - Improve the graph (smarter writing process, tool calling for writing style of famous authors) 79 | - Add graph visualizations 80 | - Add more functionality (user edits of the chapter) 81 | -------------------------------------------------------------------------------- /agent.py: -------------------------------------------------------------------------------- 1 | from langchain_anthropic import ChatAnthropic 2 | from langchain_openai import ChatOpenAI 3 | from langgraph.graph import StateGraph, END 4 | from typing import TypedDict, Annotated, List, Dict 5 | from langchain_core.output_parsers import StrOutputParser 6 | from langchain_core.prompts import ChatPromptTemplate 7 | from copy import deepcopy 8 | 9 | title_llm = ChatOpenAI(model="gpt-4o",metadata={"name":"title_llm"}) 10 | chapter_title_llm = ChatOpenAI(model="gpt-4o",metadata={"name":"chapter_title_llm"}) 11 | summary_llm = ChatOpenAI(model="gpt-4o",metadata={"name":"summary_llm"}) 12 | brainstorm_llm = ChatOpenAI(model="gpt-4o",temperature=1,metadata={"name":"brainstorm_llm"}) 13 | plan_llm = ChatAnthropic(model="claude-3-haiku-20240307",metadata={"name":"plan_llm"}) 14 | write_llm = ChatAnthropic(model="claude-3-sonnet-20240229",metadata={"name":"write_llm"}) 15 | 16 | summary_messages = [ 17 | ("system", "You are an assistant solely focused on summarizing books. Your goal \ 18 | is to summarize so that all logical dependencies are captured. It is not important for \ 19 | you to summarize minute details but rather focus on important things like character names, \ 20 | relationships, and the sequence of events that have occured so far. Your summary should contain enough \ 21 | information for a human to read it and reconstruct the book's main plotline accurately."), 22 | ("human", "Please help me summarize the following book: {chapters_str}"), 23 | ] 24 | 25 | brainstorm_messages = [ 26 | ("system", "You are an assistant tasked with brainstorming ideas for brainstorming ideas for \ 27 | a chapter in a story. You should brainstorm ideas relevant to the plotline and in accordance with \ 28 | the users wishes for the next chapter. You should brainstorm multiple ideas for what the chapter could \ 29 | be about, making detailed descriptions of all your ideas. Do not return anything other than a numbered list of ideas."), 30 | ("human", "{summary_request}"), 31 | ("human", "{detail_request}"), 32 | ("human", "{style_request}"), 33 | ("human", "This is the summary of the story up to this point: {story_summary}"), 34 | ("human", "I would like to {action}. Can you please help me brainstorm ideas for that?") 35 | ] 36 | 37 | outline_messages = [ 38 | ("system", "You are an assistant tasked with outlining a new chapter in a story. You will be provided with \ 39 | some potential ideas for the chapter. You should choose one of those ideas, and then write a clear outline \ 40 | for it. Your outline should include a beginning, middle, and end. You should only return the outline of the \ 41 | story, not any other information or text. Here is an example of what you should return: \ 42 | \ 43 | I. Introduction\n- **Setting Description:**\n - The old mansion at the end of Hawthorn Lane, shrouded in mystery and ivy.\n - Historical significance: Passed down through generations in Emma's family.\n- **Character Introduction:**\n - Emma, 27 years old, determined to uncover family secrets.\n - Mention of the secret room rumored by her great-grandmother.\n\n#### II. Emma's Curiosity and Determination\n- **Great-Grandmother's Mention:**\n - Flashback to Emma's childhood memory of great-grandmother hinting at the secret room.\n- **Emma's Motivation:**\n - Transition from childhood curiosity to adult determination to uncover the secret.\n\n#### III. The Stormy Evening\n- **Setting the Scene:**\n - Description of the stormy evening: rain, wind, ancient trees.\n- **Preparation:**\n - Emma armed with a flashlight and an old blueprint found in the attic.\n - Description of the blueprint: yellowed, frayed, delicate.\n\n#### IV. Discovery of the Unmarked Space\n- **Blueprint Examination:**\n - Emma tracing the lines and discovering the unmarked space between the library and the drawing-room.\n- **Realization:**\n - Heart skipping a beat; determination to investigate further.\n\n#### V. The Library\n- **Description of the Library:**\n - Cavernous room, floor-to-ceiling bookshelves, scent of aged paper.\n- **Search for Irregularities:**\n - Emma scanning the walls, finding the worn bookshelf.\n- **The Lost Histories Book:**\n - Discovery of the out-of-place leather-bound volume.\n - Pulling the book to reveal the secret passage.\n\n#### VI. The Hidden Passage\n- **Bookshelf Mechanism:**\n - Bookshelf swinging open to reveal a narrow passage.\n- **Initial Hesitation:**\n - Emma’s breath catching, moment of hesitation.\n- **Descent:**\n - Flashlight beam, steep spiral staircase, mix of fear and excitement.\n\n#### VII. The Secret Room\n- **Room Description:**\n - Small, dimly lit, musty air.\n- **The Wooden Chest:**\n - Intricately carved surface, Emma’s trembling fingers lifting the lid.\n- **Contents of the Chest:**\n - Faded photographs, letters tied with ribbon, ornate key.\n\n#### VIII. Discoveries and Revelations\n- **Photographs:**\n - Black-and-white image of great-grandmother and an unknown man.\n - Noting the secret happiness in their eyes.\n- **Love Letters:**\n - Untying the ribbon, reading the first letter.\n - Story of forbidden love and a promise to protect their secret.\n\n#### IX. Emotional Connection\n- **Emma’s Reaction:**\n - Eyes filling with tears, realization of the room’s significance.\n- **Legacy of Love:**\n - Understanding the room as a sanctuary of love and resilience.\n- **The Ornate Key:**\n - Speculation about what the key might unlock.\n\n#### X. Emma's Resolution\n- **Vow to Uncover the Full Story:**\n - Determination to piece together the past.\n - Honoring the legacy of love and courage.\n- **Emerging from the Secret Room:**\n - Returning up the spiral staircase, storm waning, dawn breaking.\n\n#### XI. Conclusion\n- **Newfound Connection:**\n - Deeper connection to heritage.\n - Understanding the importance of discovering, cherishing, and passing on family secrets.\n- **End of a Mystery:**\n - Mansion holds one less mystery.\n - Walls whispering a new story of love and resilience.\n\n### Themes and Motifs\n- **Heritage and Legacy:**\n - Importance of family history and secrets.\n- **Love and Resilience:**\n - Enduring nature of love and strength across generations.\n- **Curiosity and Discovery:**\n - Emma's journey from curiosity to discovery and understanding.\n \n### Literary Devices\n- **Imagery:**\n - Vivid descriptions of the mansion, storm, and secret room.\n- **Foreshadowing:**\n - Hints from great-grandmother about the secret room.\n- **Symbolism:**\n - The ornate key symbolizing unlocking the past and hidden truths."), 44 | ("human", "{summary_request}"), 45 | ("human", "{detail_request}"), 46 | ("human", "{style_request}"), 47 | ("human", "This is the summary of the story up to this point: {story_summary}"), 48 | ("human", "Here are a list of ideas for the chapter I would like you to outline: {brainstorm_ideas}"), 49 | ("human", "I would like to {action}. Can you please make a clear outline for that chapter?") 50 | ] 51 | 52 | write_messages = [ 53 | ("system", "You are an assistant tasked with writing book chapters. You will receive an outline \ 54 | of the chapter and you should return the content of the chapter only. Do not return the chapter numnber, the chapter title, or any other information \ 55 | related to the chapter. Just write the words that would appear on the page. You should \ 56 | return the content like the following example: \ 57 | \ 58 | \ 59 | The old mansion stood at the end of Hawthorn Lane, shrouded in mystery and ivy. It had been in Emma\'s family for generations, passed down from one enigmatic ancestor to another. Though the house had always been a source of curiosity, none of the family\'s secrets intrigued Emma quite as much as the rumored secret room.\n\nHer great-grandmother had once mentioned it in passing, her eyes twinkling with a mixture of mischief and nostalgia. But Emma had been too young to press for details then. Now, at twenty-seven, her curiosity had matured into a determination.\n\nIt was a stormy evening when Emma decided to search for the room. The rain battered against the windows, and the wind howled through the ancient trees surrounding the mansion. Armed with a flashlight and an old, dusty blueprint of the house she found in the attic, she made her way through the labyrinthine corridors.\n\nThe blueprint had been yellowed with age, its edges frayed and delicate. Emma traced her finger along the lines, noting the familiar rooms and passages. Her heart skipped a beat when she noticed a small, unmarked space between the library and the drawing-room—an area that didn’t correspond with any door or window she knew of.\n\nShe hurried to the library, her footsteps echoing in the vast, empty halls. The library was a cavernous room, filled with floor-to-ceiling bookshelves and the comforting scent of aged paper. She scanned the walls, searching for any irregularities. Her eyes landed on a particular bookshelf, slightly more worn than the others, its wood darker and dustier.\n\nEmma approached it cautiously, running her fingers along the spines of the old books. One of the books, a leather-bound volume titled \"The Lost Histories,\" seemed oddly out of place. She pulled it, and with a soft click, the entire bookshelf swung open to reveal a narrow passage.\n\nHer breath caught in her throat. The flashlight beam sliced through the darkness, revealing a steep, spiral staircase. She hesitated only for a moment before descending, her heart pounding with a mix of fear and excitement.\n\nThe staircase led to a small, dimly lit room. The air was musty, filled with the scent of forgotten memories. In the center of the room stood a wooden chest, its surface intricately carved with symbols Emma didn\'t recognize. She knelt beside it, her fingers trembling as she lifted the lid.\n\nInside the chest were relics of the past: faded photographs, letters tied with ribbon, and a peculiar, ornate key. Emma picked up one of the photographs. It was a black-and-white image of her great-grandmother as a young woman, standing beside a man Emma had never seen before. They were smiling, their eyes filled with a secret happiness.\n\nShe turned her attention to the letters, carefully untying the ribbon. The delicate parchment crackled as she unfolded the first one. It was a love letter, written in elegant, flowing script. As she read, a story unfolded—a tale of forbidden love, hidden meetings, and a promise to protect their secret at all costs.\n\nEmma\'s eyes filled with tears. The secret room was more than just a hidden space; it was a sanctuary of love, a testament to the resilience of her great-grandmother\'s spirit. The ornate key, she realized, must unlock something even more precious.\n\nWith newfound determination, Emma vowed to uncover the full story. She would piece together the fragments of the past, honoring the legacy of love and courage that had been hidden away for so long.\n\nAs she made her way back up the spiral staircase, the storm outside began to wane, the first rays of dawn breaking through the clouds. The secret room had given her more than just answers; it had bestowed upon her a deeper connection to her heritage, a reminder that some secrets are meant to be discovered, cherished, and passed on.\n\nAnd thus, the old mansion at the end of Hawthorn Lane held one less mystery, but its walls whispered a new story—a story of love, hidden away but never forgotten. \ 60 | \ 61 | Please remember to always only return the chapter writing itself. Do not return any other text."), 62 | ("human", "{summary_request}"), 63 | ("human", "{detail_request}"), 64 | ("human", "{style_request}"), 65 | ("human", "This is the summary of the story up to this point: {story_summary}"), 66 | ("human", "Here is the outline I would like you to follow when writing the chapter: {outline}"), 67 | ("human", "I would like to {action}. Can you please write the chapter for me, remebering to follow the outline I just provided? Pleease remember to return the chapter text only, not any commentary to the user or additional text.") 68 | ] 69 | 70 | summary_prompt = ChatPromptTemplate.from_messages(summary_messages) 71 | brainstorm_prompt = ChatPromptTemplate.from_messages(brainstorm_messages) 72 | outline_prompt = ChatPromptTemplate.from_messages(outline_messages) 73 | write_prompt = ChatPromptTemplate.from_messages(write_messages) 74 | 75 | summary_chain = summary_prompt | summary_llm | StrOutputParser() 76 | brainstorm_chain = brainstorm_prompt | brainstorm_llm | StrOutputParser() 77 | outline_chain = outline_prompt | plan_llm | StrOutputParser() 78 | write_chain = write_prompt | write_llm | StrOutputParser() 79 | 80 | def summarize_current_story(state,chapter_id): 81 | if chapter_id == '-1': 82 | return "" 83 | current_chapter_id = chapter_id 84 | chapters_currently_selected_text = [state['chapter_graph'][current_chapter_id]['content']] 85 | while state['chapter_graph'][current_chapter_id]['parent'] != '-1': 86 | chapters_currently_selected_text.append(state['chapter_graph'][current_chapter_id]['content']) 87 | current_chapter_id = state['chapter_graph'][current_chapter_id]['parent'] 88 | chapters_str = "\n\n".join( 89 | [f"Chapter {i}\n\n{chapters_currently_selected_text[i]}" for i in range(len(chapters_currently_selected_text))] 90 | ).strip() 91 | return summary_chain.invoke({"chapters_str": chapters_str}) 92 | 93 | def write_chapter(user_message, chapters_summary, state): 94 | brainstorm_ideas = brainstorm_chain.invoke({'story_summary':chapters_summary,'action':user_message,'summary_request':state['summary_request'], \ 95 | 'detail_request':state['detail_request'],'style_request':state['style_request']}) 96 | outline = outline_chain.invoke({'story_summary':chapters_summary,'action':user_message,'summary_request':state['summary_request'], \ 97 | 'detail_request':state['detail_request'],'style_request':state['style_request'],'brainstorm_ideas':brainstorm_ideas}) 98 | 99 | response = write_chain.invoke({'story_summary':chapters_summary,'action':user_message,'summary_request':state['summary_request'], \ 100 | 'detail_request':state['detail_request'],'style_request':state['style_request'],'outline':outline}) 101 | chapter_content = response 102 | chapter_title = chapter_title_llm.invoke(f"Please come up with a title for the following chapter: {chapter_content}. The title should be 6 words or less.").content.replace("\"","") 103 | return chapter_content, chapter_title 104 | 105 | def get_title(state,first_chapter): 106 | return title_llm.invoke(f"Please come up with a short title, less than 6 words, for a story. The story has the following overall plot {state['summary']}, and here is the first chapter {first_chapter}").content.replace("\"","") 107 | 108 | def write_first_chapter(state): 109 | if state['summary']: 110 | state['summary_request'] = f"I would like the overall plot of the story to be {state['summary']}" 111 | 112 | if state['details']: 113 | state['detail_request'] = f"I would like you to keep the following details in mind when writing {state['details']}" 114 | 115 | if state['style']: 116 | state['style_request'] = f"Please make sure to use the following writing style {state['style']}" 117 | chapter_content, chapter_title = write_chapter("Please write the first chapter of this story.", "no story up to this point, this is the first chapter!", state) 118 | 119 | state['current_chapter_id'] = '1' 120 | state['chapter_id_viewing'] = '1' 121 | state['chapter_graph'] = {'1':Chapter(content=chapter_content,title=chapter_title,children=[],siblings=[],cousins=[],parent='-1')} 122 | state['story_title'] = get_title(state,chapter_content) 123 | return state 124 | 125 | edit_prompt = """Here is the current state of the new chapter: 126 | 127 | 128 | {draft} 129 | 130 | 131 | Here are some edits I want to make to that chapter: 132 | 133 | 134 | {edit} 135 | """ 136 | 137 | def edit_chapter(state): 138 | chapters_summary = summarize_current_story(state,state['chapter_graph'][state["chapter_id_viewing"]]['parent']) 139 | user_message = edit_prompt.format( 140 | chapters_summary=chapters_summary, 141 | draft=state['chapter_graph'][state['chapter_id_viewing']]['content'], 142 | edit=state['rewrite_instructions'] 143 | ) 144 | 145 | chapter_content, chapter_title = write_chapter(user_message, chapters_summary, state) 146 | 147 | #create new chapter 148 | state['chapter_graph'][str(int(state["current_chapter_id"])+1)] = Chapter(content=chapter_content,title=chapter_title,children=[], \ 149 | siblings=deepcopy(state["chapter_graph"][state["chapter_id_viewing"]]['siblings']+[state["chapter_id_viewing"]]), \ 150 | cousins=deepcopy(state["chapter_graph"][state["chapter_id_viewing"]]['cousins']), \ 151 | parent=deepcopy(state["chapter_graph"][state["chapter_id_viewing"]]['parent'])) 152 | #update siblings 153 | for sibling in state["chapter_graph"][str(int(state["chapter_id_viewing"])+1)]['siblings']: 154 | state["chapter_graph"][sibling]['siblings'].append(str(int(state["current_chapter_id"])+1)) 155 | 156 | state["chapter_graph"][state["chapter_id_viewing"]]['siblings'].append(str(int(state["current_chapter_id"])+1)) 157 | state["current_chapter_id"] = str(int(state["current_chapter_id"])+1) 158 | state["chapter_id_viewing"] = deepcopy(state["current_chapter_id"]) 159 | return state 160 | 161 | continue_prompt = """Here is what I want in the next chapter: 162 | 163 | 164 | {instructions} 165 | """ 166 | 167 | def continue_chapter(state): 168 | chapters_summary = summarize_current_story(state,state["chapter_id_viewing"]) 169 | user_message = continue_prompt.format( 170 | chapters_summary=chapters_summary, 171 | instructions=state['continue_instructions'] 172 | ) 173 | 174 | chapter_content, chapter_title = write_chapter(user_message, chapters_summary, state) 175 | #create new chapter 176 | state['chapter_graph'][str(int(state["current_chapter_id"])+1)] = Chapter(content=chapter_content,title=chapter_title,children=[],siblings=[], \ 177 | cousins=deepcopy(state["chapter_graph"][state["chapter_id_viewing"]]['children']),\ 178 | parent=deepcopy(state['chapter_id_viewing'])) 179 | #update cousins 180 | for child in state["chapter_graph"][state["chapter_id_viewing"]]['children']: 181 | state["chapter_graph"][child]['cousins'].append(str(int(state["current_chapter_id"])+1)) 182 | #update children 183 | state["chapter_graph"][state["chapter_id_viewing"]]['children'].append(str(int(state["current_chapter_id"])+1)) 184 | state["current_chapter_id"] = str(int(state["current_chapter_id"])+1) 185 | state["chapter_id_viewing"] = deepcopy(state["current_chapter_id"]) 186 | return state 187 | 188 | # State Definitions 189 | class Chapter(TypedDict): 190 | content: str 191 | """Content of the chapter""" 192 | title: str 193 | """Title of the chapter""" 194 | children: list 195 | """Direct descendants of the chapter - chapters you get by clicking Continue""" 196 | siblings: list 197 | """Direct edit descendants of the chapter - chapters you get by clicking Edit""" 198 | cousins: list 199 | """Chapters that originated from the same parent but are not directly related""" 200 | parent: int 201 | """For chapter number n, it is the node of chapter number n-1 that is directly related""" 202 | 203 | def update_chapter_graph(old_chapter_graph, new_chapter_graph): 204 | # Always add nodes to the overall graph 205 | if isinstance(new_chapter_graph,dict): 206 | old_chapter_graph.update(new_chapter_graph) 207 | return old_chapter_graph 208 | 209 | class State(TypedDict): 210 | summary: str 211 | """Summary user passes in at beginning of story""" 212 | details: str 213 | """Details provided by user at beginning of story""" 214 | style: str 215 | """Desired writing style from the user""" 216 | summary_request: str = "" 217 | """Prompt engineered summary instructions for LLM""" 218 | detail_request: str = "" 219 | """Prompt engineered detial instructions for LLM""" 220 | style_request: str = "" 221 | """Prompt engineered writing style instructions for LLM""" 222 | chapter_graph: Annotated[dict[str, Chapter], update_chapter_graph] 223 | """Graph containing all of our chapter""" 224 | chapter_id_viewing: str 225 | """What node in the graph the user is currently viewing""" 226 | current_chapter_id: str 227 | """What is the current highest node (i.e. how many chapters have been written so far)""" 228 | rewrite_instructions: str 229 | """Instructions to edit a chapter from the user""" 230 | continue_instructions: str 231 | """Instructions to write the next chapter from the suer""" 232 | story_title: str = "" 233 | """Overall title of the story""" 234 | 235 | def router(state): 236 | if len(state.get('chapter_graph', [])) == 0: 237 | return "first" 238 | elif state.get('rewrite_instructions', ''): 239 | return "rewrite" 240 | else: 241 | return "continue" 242 | 243 | graph = StateGraph(State) 244 | graph.set_conditional_entry_point(router) 245 | graph.add_node("first", write_first_chapter) 246 | graph.add_node("rewrite", edit_chapter) 247 | graph.add_node("continue", continue_chapter) 248 | graph.add_edge("first", END) 249 | graph.add_edge("rewrite", END) 250 | graph.add_edge("continue", END) 251 | graph = graph.compile() -------------------------------------------------------------------------------- /img/Flowchart-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/langchain-ai/story-writing/1d06b51c8dc1fc7e24ff83ea3716c5a942f7638b/img/Flowchart-2.jpg -------------------------------------------------------------------------------- /img/Flowchart.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/langchain-ai/story-writing/1d06b51c8dc1fc7e24ff83ea3716c5a942f7638b/img/Flowchart.jpg -------------------------------------------------------------------------------- /img/story_writing_graph_running.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/langchain-ai/story-writing/1d06b51c8dc1fc7e24ff83ea3716c5a942f7638b/img/story_writing_graph_running.png -------------------------------------------------------------------------------- /img/story_writing_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/langchain-ai/story-writing/1d06b51c8dc1fc7e24ff83ea3716c5a942f7638b/img/story_writing_screenshot.png -------------------------------------------------------------------------------- /langgraph.json: -------------------------------------------------------------------------------- 1 | { 2 | "python_version": "3.11", 3 | "dependencies": [ 4 | "." 5 | ], 6 | "graphs": { 7 | "agent": "./agent.py:graph" 8 | }, 9 | "env": ".env" 10 | } -------------------------------------------------------------------------------- /pages/Story_Writing.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | import streamlit as st 4 | from langgraph_sdk import get_client 5 | import asyncio 6 | from langsmith import Client 7 | from streamlit_extras.stylable_container import stylable_container 8 | from copy import deepcopy 9 | st.set_page_config(layout="wide") 10 | 11 | # Langsmith feedback client 12 | feedback_client = Client(api_url="https://beta.api.smith.langchain.com") 13 | 14 | # Find run id for giving feedback 15 | async def get_run_id_corresponding_to_node(client, thread, node_id): 16 | '''Get the run id corresponding to the chapter written''' 17 | runs = await client.runs.list(thread_id=thread['thread_id']) 18 | 19 | for r in runs: 20 | if r['kwargs']['config']['configurable']['node_id'] == node_id: 21 | return r['run_id'] 22 | return None 23 | 24 | # Create the agent 25 | async def start_agent(session_id): 26 | client = get_client(url="https://ht-mundane-strait-97-7d4d2b12ec9c54a4aa34492a954c-g3ps4aazkq-uc.a.run.app") 27 | assistants = await client.assistants.search() 28 | assistants = [a for a in assistants if not a['config']] 29 | thread = await client.threads.create(metadata={"user":session_id}) 30 | assistant = assistants[0] 31 | await asyncio.sleep(5.5) 32 | return [client,thread,assistant] 33 | 34 | # Find a story that we had previously written 35 | async def get_thread_state(client,thread_id): 36 | return await client.threads.get_state(thread_id) 37 | 38 | # Find stories user has written 39 | async def get_user_threads(client,session_id): 40 | threads = await client.threads.search(metadata={"user":session_id}) 41 | 42 | untitled_count = 1 43 | for t in threads: 44 | t_state = await get_thread_state(client,t['thread_id']) 45 | try: 46 | t['story_title'] = t_state['values']['story_title'] 47 | except: 48 | t['story_title'] = f"Untitled story #{untitled_count}" 49 | untitled_count += 1 50 | return threads 51 | 52 | llm_to_title = { 53 | "starting":"Waiting for user input ...", 54 | "brainstorm_llm": "Brainstorming ideas for chapter...", 55 | "plan_llm": "Planning outline for chapter...", 56 | "summary_llm": "Summarizing story so far...", 57 | "write_llm": "Writing the chapter...", 58 | "title_llm": "Generating a title for the story...", 59 | "chapter_title_llm": "Generating title for chapter..." 60 | } 61 | 62 | # Streaming chapter writing 63 | async def generate_answer(placeholder, placeholder_title, input, client, thread, assistant, metadata = {}): 64 | current_llm = "starting" 65 | placeholder_title.markdown(f"

\ 66 | {llm_to_title[current_llm]} \ 67 |

",unsafe_allow_html=True) 68 | current_ind = 0 69 | ans = "" 70 | async for chunk in client.runs.stream( 71 | thread['thread_id'], assistant['assistant_id'], input=input, config={"configurable":metadata}, \ 72 | stream_mode="messages", multitask_strategy="rollback" 73 | ): 74 | if chunk.data and 'run_id' not in chunk.data: 75 | if isinstance(chunk.data,dict): 76 | try: 77 | current_llm = chunk.data[list(chunk.data.keys())[0]]['metadata']['name'] 78 | placeholder_title.markdown(f"

\ 79 | {llm_to_title[current_llm]}

",unsafe_allow_html=True) 80 | except: 81 | pass 82 | elif current_llm == "write_llm" and chunk.data[0]['content']: 83 | ans += chunk.data[0]['content'][current_ind:] 84 | placeholder.info(ans) 85 | current_ind += len(chunk.data[0]['content'][current_ind:]) 86 | 87 | # Update variables after chapter has been written 88 | async def get_current_state(client,thread): 89 | current_state = await client.threads.get_state(thread_id=thread['thread_id']) 90 | return current_state 91 | 92 | # When user selects a different chapter to view 93 | async def update_current_state(client,thread,values): 94 | await client.threads.update_state(thread_id=thread['thread_id'],values=values) 95 | 96 | # Create new thread for new story 97 | async def get_new_thread(client,session_id): 98 | thread = await client.threads.create(metadata={"user":session_id}) 99 | return thread 100 | 101 | # Make sure event loop never closes 102 | async def call_async_function_safely(func,*args): 103 | try: 104 | # Try to run the async function using the existing event loop 105 | result = await func(*args) 106 | except RuntimeError as e: 107 | if "Event loop is closed" in str(e): 108 | # If the event loop is closed, create a new one and run the async function 109 | loop = asyncio.new_event_loop() 110 | asyncio.set_event_loop(loop) 111 | result = await func(*args) 112 | else: 113 | raise e 114 | return result 115 | 116 | # Helper function for chapter options 117 | def transform_titles_into_options(titles): 118 | name_counts = {} 119 | for name in set(titles): 120 | name_counts[name] = titles.count(name) 121 | 122 | # Transform names with numbered suffixes 123 | transformed_titles = [] 124 | for name in titles[::-1]: 125 | count = name_counts[name] 126 | if count > 1 or titles.count(name) > 1: 127 | transformed_titles.append(f"{name} #{count}") 128 | else: 129 | transformed_titles.append(name) 130 | name_counts[name] -= 1 131 | 132 | return transformed_titles[::-1] 133 | 134 | # Update variables after writing 135 | async def update_session_variables(): 136 | current_state = await call_async_function_safely(get_current_state,st.session_state.client,st.session_state.thread) 137 | st.session_state.chapter_graph = current_state['values']['chapter_graph'] 138 | st.session_state.story_title = current_state['values']['story_title'] 139 | st.session_state.currently_selected_chapter = str(current_state['values']['chapter_id_viewing']) 140 | st.session_state.current_node_id = str(int(current_state['values']['current_chapter_id']) + 1) 141 | st.session_state.next_chapter_options = st.session_state.chapter_graph[st.session_state.currently_selected_chapter]['children'] 142 | st.session_state.current_chapter_options = st.session_state.chapter_graph[st.session_state.currently_selected_chapter]['siblings'] \ 143 | + st.session_state.chapter_graph[st.session_state.currently_selected_chapter]['cousins'] + [st.session_state.currently_selected_chapter] 144 | st.session_state.previous_chapter_options = [x for x in [st.session_state.chapter_graph[st.session_state.currently_selected_chapter]['parent']] if x!= '-1'] 145 | 146 | # Reset variables on new story 147 | async def reset_session_variables(): 148 | st.session_state.chapter_graph = {"-1":{'content':"Click Start Story to begin writing!", 'title':"Pre-start Chapter"}} 149 | st.session_state.currently_selected_chapter = "-1" 150 | st.session_state.chapter_number = 0 151 | st.session_state.current_node_id = '1' 152 | st.session_state.story_title = "" 153 | st.session_state.current_chapter_options = ["-1"] 154 | st.session_state.previous_chapter_options, st.session_state.next_chapter_options = [],[] 155 | 156 | async def stream(*args): 157 | await asyncio.gather(call_async_function_safely(generate_answer,*args)) 158 | 159 | async def main(): 160 | st.markdown(""" 161 | 167 | """, unsafe_allow_html=True) 168 | if "story_title" not in st.session_state or st.session_state.story_title == "": 169 | st.markdown("

Story Writing with LangGraph

", unsafe_allow_html=True) 170 | else: 171 | st.markdown(f"

{st.session_state.story_title}

", unsafe_allow_html=True) 172 | 173 | if "page_loaded" not in st.session_state: 174 | st.session_state.page_loaded = False 175 | st.session_state.selected_previous_chapter, st.session_state.selected_next_chapter, st.session_state.selected_current_chapter = None,None,None 176 | st.session_state.story_title = "" 177 | st.session_state.num_selected = 0 178 | st.session_state.writing = False 179 | 180 | if "session_id" not in st.session_state: 181 | st.session_state.session_id = ''.join(random.choices(string.ascii_letters + string.digits, k=10)) 182 | 183 | if st.session_state.page_loaded == False: 184 | st.session_state.client,st.session_state.thread,st.session_state.assistant = await call_async_function_safely(start_agent,st.session_state.session_id) 185 | await reset_session_variables() 186 | st.session_state.page_loaded = True 187 | 188 | if "story_started" not in st.session_state: 189 | st.session_state.story_started = False 190 | 191 | if "show_edit_input" not in st.session_state: 192 | st.session_state.show_edit_input = False 193 | 194 | if "show_start_input" not in st.session_state: 195 | st.session_state.show_start_input = False 196 | 197 | if "show_continue_input" not in st.session_state: 198 | st.session_state.show_continue_input = False 199 | 200 | if "show_load_story" not in st.session_state: 201 | st.session_state.show_load_story = False 202 | 203 | if ('start_submit' in st.session_state and st.session_state.start_submit == True) or \ 204 | ('edit_submit' in st.session_state and st.session_state.edit_submit == True) or \ 205 | ('continue_submit' in st.session_state and st.session_state.continue_submit == True): 206 | st.session_state.running = True 207 | else: 208 | st.session_state.running = False 209 | 210 | # Starting/New story 211 | if st.session_state.show_start_input: 212 | summary_text = st.sidebar.text_area("Summary", disabled=st.session_state.running) 213 | detail_text = st.sidebar.text_area("Details", disabled=st.session_state.running) 214 | style_text = st.sidebar.text_area("Writing Style", disabled=st.session_state.running) 215 | col1, col2 = st.sidebar.columns([1, 1]) 216 | with col1: 217 | if st.button("Back",key="start-back", disabled=st.session_state.running): 218 | st.session_state.show_start_input = False 219 | st.session_state.writing = False 220 | st.rerun() 221 | with col2: 222 | if st.button("Submit",key="start_submit", disabled=st.session_state.running): 223 | if st.session_state.story_started: 224 | st.session_state.thread = await call_async_function_safely(get_new_thread,st.session_state.client,st.session_state.session_id) 225 | await reset_session_variables() 226 | await stream(st.session_state.box,st.session_state.box_title,{'summary':summary_text,'details':detail_text,'style':style_text},st.session_state.client,st.session_state.thread, 227 | st.session_state.assistant,{"node_id":st.session_state.current_node_id}) 228 | st.session_state.story_started = True 229 | await update_session_variables() 230 | 231 | st.session_state.show_start_input = False 232 | st.session_state.writing = False 233 | st.session_state.chapter_number = 1 234 | st.rerun() 235 | # Editing story 236 | elif st.session_state.show_edit_input: 237 | edit_chapter_text = st.sidebar.text_area("Edit Instructions", disabled=st.session_state.running) 238 | col1, col2 = st.sidebar.columns([1, 1]) 239 | with col1: 240 | if st.button("Back",key="edit-back", disabled=st.session_state.running): 241 | st.session_state.show_edit_input = False 242 | st.session_state.writing = False 243 | st.rerun() 244 | with col2: 245 | if st.button("Submit",key="edit_submit", disabled=st.session_state.running): 246 | await stream(st.session_state.box,st.session_state.box_title,{'rewrite_instructions':edit_chapter_text},st.session_state.client,st.session_state.thread, 247 | st.session_state.assistant,{"node_id":st.session_state.current_node_id}) 248 | 249 | await update_session_variables() 250 | st.session_state.show_edit_input = False 251 | st.session_state.writing = False 252 | st.rerun() 253 | # Continuing story 254 | elif st.session_state.show_continue_input: 255 | next_chapter_text = st.sidebar.text_area("Next Chapter Instructions", disabled=st.session_state.running) 256 | col1, col2 = st.sidebar.columns([1, 1]) 257 | with col1: 258 | if st.button("Back",key="continue-back", disabled=st.session_state.running): 259 | st.session_state.show_continue_input = False 260 | st.session_state.writing = False 261 | st.rerun() 262 | with col2: 263 | if st.button("Submit",key="continue_submit", disabled=st.session_state.running): 264 | await stream(st.session_state.box,st.session_state.box_title,{'continue_instructions':next_chapter_text},st.session_state.client,st.session_state.thread, 265 | st.session_state.assistant,{"node_id":st.session_state.current_node_id}) 266 | 267 | await update_session_variables() 268 | st.session_state.show_continue_input = False 269 | st.session_state.writing = False 270 | st.session_state.chapter_number += 1 271 | st.rerun() 272 | # Loading story 273 | elif st.session_state.show_load_story: 274 | col1, col2 = st.sidebar.columns([1, 1]) 275 | threads = await call_async_function_safely(get_user_threads,st.session_state.client,st.session_state.session_id) 276 | threads_without_current = [t for t in threads if t['thread_id'] != st.session_state.thread['thread_id'] and 'Untitled' not in t['story_title']] 277 | options = [t['story_title'] for t in threads_without_current] 278 | 279 | if len(options) > 0: 280 | selected_story = st.sidebar.selectbox("",options,index=None,placeholder="Select story", \ 281 | label_visibility="collapsed",key=f"story_selector") 282 | else: 283 | selected_story = None 284 | st.sidebar.write("No alternate stories!") 285 | 286 | if selected_story is not None: 287 | st.session_state.thread = threads_without_current[options.index(selected_story)] 288 | await update_session_variables() 289 | st.session_state.show_load_story = False 290 | st.session_state.chapter_number = 1 291 | cur_chapter = st.session_state.currently_selected_chapter 292 | while st.session_state.chapter_graph[cur_chapter]['parent'] != '-1': 293 | st.session_state.chapter_number += 1 294 | cur_chapter = st.session_state.chapter_graph[cur_chapter]['parent'] 295 | st.rerun() 296 | with col1: 297 | if st.button("Back",key="load-story-back"): 298 | st.session_state.show_load_story = False 299 | st.rerun() 300 | # Default Navigation pane 301 | else: 302 | st.sidebar.header("Navigation") 303 | if st.sidebar.button("New Story" if st.session_state.story_started else "Start Story"): 304 | st.session_state.writing = True 305 | st.session_state.show_start_input = True 306 | st.rerun() 307 | elif st.sidebar.button("Edit") and st.session_state.story_started and st.session_state.story_started: 308 | st.session_state.show_edit_input = True 309 | st.session_state.writing = True 310 | st.rerun() 311 | elif st.sidebar.button("Continue") and st.session_state.story_started: 312 | st.session_state.show_continue_input = True 313 | st.session_state.writing = True 314 | st.rerun() 315 | elif st.sidebar.button("Load Story"): 316 | st.session_state.show_load_story = True 317 | st.rerun() 318 | 319 | st.sidebar.write(" ") 320 | 321 | st.markdown(""" 322 | 328 | """, unsafe_allow_html=True) 329 | 330 | col1, _, col3 = st.columns([1, 2, 1]) 331 | 332 | if st.session_state.writing == False: 333 | # Previous chapter options 334 | with col1: 335 | options = transform_titles_into_options([st.session_state.chapter_graph[chapter_id]['title'] for chapter_id in st.session_state.previous_chapter_options]) 336 | if len(options) > 0: 337 | st.session_state.selected_previous_chapter = st.selectbox("",options,index=None,placeholder="Select previous chapter", \ 338 | label_visibility="collapsed",key=f"previous_chapter_{st.session_state.num_selected}") 339 | else: 340 | st.session_state.selected_previous_chapter = None 341 | st.markdown(f"

\ 342 | No previous chapters! \ 343 |

",unsafe_allow_html=True) 344 | 345 | 346 | if st.session_state.selected_previous_chapter is not None: 347 | st.session_state.num_selected += 1 348 | new_chapter_selected = st.session_state.previous_chapter_options[options.index(st.session_state.selected_previous_chapter)] 349 | 350 | await call_async_function_safely(update_current_state,st.session_state.client,st.session_state.thread,{'chapter_id_viewing':new_chapter_selected}) 351 | await call_async_function_safely(update_session_variables) 352 | st.session_state.chapter_number -= 1 353 | st.rerun() 354 | # Next chapter options 355 | with col3: 356 | options = transform_titles_into_options([st.session_state.chapter_graph[chapter_id]['title'] for chapter_id in st.session_state.next_chapter_options]) 357 | if len(options) > 0: 358 | st.session_state.selected_next_chapter = st.selectbox("",options,index=None,placeholder="Select next chapter", \ 359 | label_visibility="collapsed",key=f"next_chapter_{st.session_state.num_selected}") 360 | else: 361 | st.session_state.selected_next_chapter = None 362 | st.markdown(f"

\ 363 | No next chapters! \ 364 |

",unsafe_allow_html=True) 365 | 366 | 367 | if st.session_state.selected_next_chapter is not None: 368 | st.session_state.num_selected += 1 369 | new_chapter_selected = st.session_state.next_chapter_options[options.index(st.session_state.selected_next_chapter)] 370 | 371 | await call_async_function_safely(update_current_state,st.session_state.client,st.session_state.thread,{'chapter_id_viewing':new_chapter_selected}) 372 | await call_async_function_safely(update_session_variables) 373 | st.session_state.chapter_number += 1 374 | st.rerun() 375 | 376 | _, col_middle_title, _ = st.columns([1, 6, 1]) 377 | 378 | if "box_title" not in st.session_state or st.session_state.writing == False: 379 | st.session_state.box_title = col_middle_title.empty() 380 | elif st.session_state.writing == True: 381 | with col_middle_title: 382 | st.markdown(f"

\ 383 | Waiting for user input... \ 384 |

",unsafe_allow_html=True) 385 | 386 | st.session_state.chapter_title = st.markdown(f"

\ 387 | {st.session_state.chapter_graph[st.session_state.currently_selected_chapter]['title']} \ 388 |

",unsafe_allow_html=True) 389 | _, col_middle, col_scroll = st.columns([1, 6, 1]) 390 | if "box" not in st.session_state: 391 | st.session_state.box = col_middle.empty() 392 | st.rerun() 393 | else: 394 | st.session_state.box.info(st.session_state.chapter_graph[st.session_state.currently_selected_chapter]['content']) 395 | 396 | with col_scroll: 397 | st.write("🔺 \nScroll \n🔻") 398 | 399 | st.markdown(f"
\ 400 | Chapter {st.session_state.chapter_number} \ 401 |
",unsafe_allow_html=True) 402 | 403 | _, col2, _ = st.columns([1, 2, 1]) 404 | if st.session_state.writing == False: 405 | # Current chapter options 406 | with col2: 407 | options = transform_titles_into_options([st.session_state.chapter_graph[chapter_id]['title'] for chapter_id in st.session_state.current_chapter_options if chapter_id != "-1"]) 408 | if len(options) > 0: 409 | st.session_state.selected_current_chapter = st.selectbox("",options,index=None,placeholder="Select current chapter", \ 410 | label_visibility="collapsed",key=f"current_chapter_{st.session_state.num_selected}") 411 | else: 412 | st.session_state.selected_current_chapter = None 413 | st.markdown(f"

\ 414 | No alternate current chapters! \ 415 |

",unsafe_allow_html=True) 416 | 417 | if st.session_state.selected_current_chapter is not None: 418 | st.session_state.num_selected += 1 419 | new_chapter_selected = st.session_state.current_chapter_options[options.index(st.session_state.selected_current_chapter)] 420 | 421 | await call_async_function_safely(update_current_state,st.session_state.client,st.session_state.thread,{'chapter_id_viewing':new_chapter_selected}) 422 | 423 | await call_async_function_safely(update_session_variables) 424 | st.rerun() 425 | 426 | # Feedback options 427 | if st.session_state.current_node_id != '1': 428 | _, col2a,col2b, _ = st.columns([1, 1,1, 1]) 429 | # Bad writing 430 | with col2a: 431 | with stylable_container( 432 | key="red_button", 433 | css_styles=""" 434 | button { 435 | background-color: red; 436 | color: white; 437 | border-radius: 20px; 438 | margin-left: 80px; 439 | } 440 | """, 441 | ): 442 | if st.button("Bad writing",key = "red_button"): 443 | run_id = await call_async_function_safely(get_run_id_corresponding_to_node,st.session_state.client, \ 444 | st.session_state.thread, str(int(st.session_state.current_node_id)-1)) 445 | feedback_client.create_feedback( 446 | run_id=run_id, 447 | key="feedback-key", 448 | score=0.0, 449 | comment="comment", 450 | ) 451 | # Good writing 452 | with col2b: 453 | with stylable_container( 454 | key="green_button", 455 | css_styles=""" 456 | button { 457 | background-color: green; 458 | color: white; 459 | border-radius: 20px; 460 | margin-left: 80px; 461 | } 462 | """, 463 | ): 464 | if st.button("Good writing",key = "green_button"): 465 | run_id = await call_async_function_safely(get_run_id_corresponding_to_node,st.session_state.client, \ 466 | st.session_state.thread, str(int(st.session_state.current_node_id)-1)) 467 | feedback_client.create_feedback( 468 | run_id=run_id, 469 | key="feedback-key", 470 | score=1.0, 471 | comment="comment", 472 | ) 473 | 474 | if __name__ == "__main__": 475 | asyncio.run(main()) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | langgraph 2 | langchain_anthropic 3 | langchain_openai 4 | langgraph_cli 5 | langsmith 6 | langgraph_sdk 7 | streamlit_extras -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | def transform_titles_into_options(titles): 2 | name_counts = {} 3 | for name in set(titles): 4 | name_counts[name] = titles.count(name) 5 | 6 | # Transform names with numbered suffixes 7 | transformed_titles = [] 8 | for name in titles[::-1]: 9 | count = name_counts[name] 10 | if count > 1 or titles.count(name) > 1: 11 | transformed_titles.append(f"{name} #{count}") 12 | else: 13 | transformed_titles.append(name) 14 | name_counts[name] -= 1 15 | 16 | return transformed_titles[::-1] 17 | 18 | 19 | print(transform_titles_into_options(['a','b','a','c'])) -------------------------------------------------------------------------------- /test_app.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | import streamlit as st 3 | 4 | st.set_page_config(layout="wide") 5 | 6 | def main(): 7 | st.title("Multiple Columns Example") 8 | 9 | # First set of columns 10 | col1, col2 = st.columns([1, 3]) 11 | 12 | with col1: 13 | st.header("Column 1") 14 | st.write("Content for column 1") 15 | 16 | with col2: 17 | st.header("Column 2") 18 | st.write("Content for column 2") 19 | 20 | # Second set of columns 21 | col3, col4, col5 = st.columns([1, 1, 1]) 22 | 23 | with col3: 24 | st.header("Column 3") 25 | st.write("Content for column 3") 26 | 27 | with col4: 28 | st.header("Column 4") 29 | st.write("Content for column 4") 30 | 31 | with col5: 32 | st.header("Column 5") 33 | st.write("Content for column 5") 34 | 35 | if __name__ == "__main__": 36 | main() --------------------------------------------------------------------------------