Skip to content

Commit faf6f42

Browse files
gem-neo4jBregenzerK
authored andcommitted
Update Chat Bot
1 parent d16cbf4 commit faf6f42

11 files changed

Lines changed: 143 additions & 90 deletions

README.md

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,69 @@
1-
Pink Programming Chatbot (Flask + Neo4j)
1+
# Pink Programming Chatbot (Using Flask + Neo4j)
22

3-
Overview
4-
- Flask web app scaffold for a chatbot that stores users, chats, and messages in Neo4j.
5-
- Participants complete the Cypher TODOs in `db.py` to enable user auth, chat creation, and viewing history.
6-
- OpenAI is used to generate assistant replies; reads config from environment variables.
3+
Welcome to the Pink Programming Chatbot workshop! Today we will build a chatbot that stores users, chats, and messages in Neo4j.
4+
Your goal is to complete the Cypher TODOs in `db.py` to get a working chatbot.
5+
Good Luck!
6+
7+
## Overview
8+
- This code base is a Flask web app scaffold for a chatbot that stores users, chats, and messages in Neo4j.
9+
- Your goal is to complete the Cypher TODOs in `db.py` to enable user auth, chat creation, and viewing history.
10+
- Use OpenAI through the Cypher GenAI Plugin integration.
11+
12+
## Quick Start with Codespaces
13+
- Create an account on https://console.neo4j.io/ and create a free Aura instance (make sure to save the password).
14+
- **Important**: Connect to the database (click query) and run the command: `ALTER DATABASE <db_name> SET DEFAULT LANGUAGE CYPHER 25` in order to get the correct Cypher version.
15+
16+
![use_template_image](images/use_template.png)
717

8-
Quick Start with Codespaces
9-
- Create an account on https://console-preview.neo4j.io/ and create a free Aura instance (make sure to save the password).
1018
- In the GitHub repository click the green "Use this template" button and open the project in a codespace.
19+
20+
![connect info image](images/connection_info.png)
21+
1122
- Copy `.env.example` to `.env` and set values.
23+
- `NEO4J_USERNAME`: This should be the same name as your Aura instance.
1224
- `NEO4J_CONNECTION_URI`: Inspect your Aura instance and copy the connection URI.
13-
- `NEO4J_PASSWORD`: (paste password that you got during the instance creation).
25+
- `NEO4J_PASSWORD`: paste password that you got during the instance creation.
26+
27+
![start_flask_app_image](images/run_flask_app.png)
28+
1429
- Open the Run and Debug menu and start the "Flask: run app.py" configuration.
30+
31+
![open_browser_image](images/open_browser.png)
32+
1533
- It opens a terminal and starts the Flask development server, click on the link to open the app in a new browser tab.
34+
35+
![welcome_page_image](images/welcome_page.png)
36+
1637
- Try registering a new user, you should see Cypher Error page which means you're ready to start the exercises!
1738

18-
Workshop Tasks (Cypher TODOs in `db.py`)
39+
![cypher_error_image](images/cypher_error.png)
40+
41+
## Workshop Tasks (Cypher TODOs in `db.py`)
1942
- Create/Login User
2043
- `create_user(username, password_hash)`: create a `:User` with properties and return it.
2144
- `fetch_user_by_username(username)`: find a `:User` by username and return it.
2245
- Chat With Chatbot
23-
- `create_chat_and_first_message(user_id, message)`: create a `:Chat` and the first user `:Message`.
24-
- `create_message(chat_id, role, content)`: append messages (both user and assistant) to a chat.
46+
- `create_chat_and_first_message(username, message)`: create a `:Chat` and the first user `:Message`.
47+
- `create_message(chat_id, role, content, previous_ai_chat_id)`: append messages (both user and ai) to a chat.
2548
- See Previous Chats
26-
- `list_user_chats(user_id)`: gat all chats for the user, return summary list.
49+
- `list_user_chats(username)`: get all chats for the user, return a summary in a list.
2750
- `fetch_chat(chat_id)`: return full chat with messages ordered by time.
51+
- `get_ai_response(chat_id, previous_chat_id, message)`: get the response from the OpenAI API.
52+
- `get_previous_ai_chat_id(chat_id)`: find the previous AI chat id for a given chat id to send to OpenAI.
2853

2954
Pages
3055
- `/register` and `/login`: basic authentication using Flask sessions.
3156
- `/chat/new`: start a new chat by sending the first message.
3257
- `/chats`: list previous chats.
3358
- `/chats/<chat_id>`: view and continue a chat.
59+
60+
## Clean Database
61+
You may want to reset your database to start fresh, do this in the Aura console and run "MATCH (n) DETACH DELETE n"
62+
63+
## Database Model
64+
65+
![database_model_image](images/chat_bot_model.png)
66+
67+
## Possible Extensions
68+
- Add a delete chat option
69+
- Add a delete User option

ai.py

Lines changed: 0 additions & 42 deletions
This file was deleted.

app.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
QuerySyntaxError,
1313
create_chat_and_first_message,
1414
create_message,
15+
get_previous_ai_chat_id,
16+
get_ai_response
1517
)
16-
from ai import generate_ai_response
1718

1819

1920
def create_app() -> Flask:
@@ -74,7 +75,7 @@ def login():
7475
flash("User not found.", "error")
7576
return render_template("login.html")
7677

77-
stored_hash = user.get("password_hash")
78+
stored_hash = user.get("passwordHash")
7879
if not stored_hash or not check_password_hash(stored_hash, password):
7980
flash("Invalid credentials.", "error")
8081
return render_template("login.html")
@@ -104,7 +105,7 @@ def wrapper(*args, **kwargs):
104105
def chats():
105106
user = session["user"]
106107
user_identifier = user.get("id") or user.get("username")
107-
user_chats = list_user_chats(user_id=user_identifier)
108+
user_chats = list_user_chats(username=user_identifier)
108109
return render_template("chats.html", chats=user_chats)
109110

110111
@app.route("/chats/<chat_id>", methods=["GET", "POST"])
@@ -114,10 +115,11 @@ def chat(chat_id: str):
114115
if request.method == "POST":
115116
user_message = request.form.get("message", "").strip()
116117
if user_message:
117-
create_message(chat_id, role="user", content=user_message)
118-
ai_reply = generate_ai_response(user_message)
119-
if ai_reply:
120-
create_message(chat_id, role="assistant", content=ai_reply)
118+
create_message(chat_id, role="user", content=user_message, previous_ai_chat_id=None)
119+
previous_chat_id = get_previous_ai_chat_id(chat_id)
120+
ai_response = get_ai_response(chat_id=chat_id, previous_chat_id=previous_chat_id, message=user_message)
121+
if ai_response:
122+
create_message(chat_id, role="ai", content=ai_response["message"], previous_ai_chat_id=ai_response["chatId"])
121123
chat_obj = fetch_chat(chat_id)
122124
if not chat_obj:
123125
flash("Chat not found or Cypher TODO incomplete.", "error")
@@ -137,10 +139,10 @@ def chat_new():
137139
user_identifier = user.get("id") or user.get("username")
138140
new_chat = create_chat_and_first_message(user_identifier, user_message)
139141
chat_id = new_chat.get("id") if new_chat else None
140-
# Generate AI reply and save as assistant message
141-
ai_reply = generate_ai_response(user_message)
142+
# Generate AI reply and save as ai message
143+
ai_reply = get_ai_response(chat_id=chat_id, previous_chat_id=None, message=user_message)
142144
if chat_id and ai_reply:
143-
create_message(chat_id, role="assistant", content=ai_reply)
145+
create_message(chat_id, role="ai", content=ai_reply["message"], previous_ai_chat_id=ai_reply["chatId"])
144146
return redirect(url_for("chat", chat_id=chat_id))
145147
flash("Chat created but could not determine chat id. Complete Cypher TODOs.", "warning")
146148
return redirect(url_for("chats"))

db.py

Lines changed: 83 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,14 @@ def _error_handling(cypher: str, e: Neo4jError):
6969

7070
def create_user(username: str, password_hash: str) -> Dict[str, Any]:
7171
"""
72-
TODO: Create a new user node if it doesn't exist.
72+
TODO: Create a new user node if it doesn't exist. The app expects a user object called user.
7373
7474
Suggested shape:
7575
- Label: User
76-
- Properties: {username: $username, password_hash: $password_hash, createdAt: datetime()}
76+
- Properties:
77+
- username: $username
78+
- passwordHash: $password_hash
79+
- createdAt: datetime()
7780
"""
7881
cypher = """
7982
// TODO: Cypher query to create a User node
@@ -84,7 +87,7 @@ def create_user(username: str, password_hash: str) -> Dict[str, Any]:
8487

8588
def fetch_user_by_username(username: str) -> Optional[Dict[str, Any]]:
8689
"""
87-
TODO: Fetch a user node by username.
90+
TODO: Fetch a user node by username; the app expects a user object called user. The app expects a user object called user.
8891
"""
8992
cypher = """
9093
// TODO: Cypher query to fetch a User node by username
@@ -93,66 +96,120 @@ def fetch_user_by_username(username: str) -> Optional[Dict[str, Any]]:
9396
return record["user"] if record else None
9497

9598

96-
def create_chat_and_first_message(user_id: str, message: str) -> Dict[str, Any]:
99+
def create_chat_and_first_message(username: str, message: str) -> Dict[str, Any]:
97100
"""
98-
TODO: Create a new Chat node linked to the User and store the first user message.
101+
TODO: Create a new Chat node linked to the User and store the first user message. The app expects a chat object called chat.
99102
100-
Suggested shape:
101-
- Label: Chat
102-
- Properties: {id: randomUUID(), createdAt: datetime(), updatedAt: datetime()}
103-
- Relationship: (User)-[:STARTED]->(Chat)
104-
"""
103+
Suggested shape:
104+
- Label: Chat
105+
- Properties:
106+
- id: randomUUID()
107+
- createdAt: datetime()
108+
- updatedAt: datetime()
109+
- Relationship: (user)-[:STARTED]->(chat)
110+
"""
105111
cypher = """
106112
// TODO: Cypher query to create chat
107113
"""
108-
chat_record = _run_query_single(cypher, user_id=user_id, message=message)
109-
create_message(chat_record["chat"]["id"], role="user", content=message)
114+
chat_record = _run_query_single(cypher, username=username, message=message)
115+
create_message(chat_record["chat"]["id"], role="user", content=message, previous_ai_chat_id=None)
110116
return chat_record["chat"] if chat_record else {}
111117

112118

113-
def create_message(chat_id: str, role: str, content: str) -> Dict[str, Any]:
119+
def create_message(chat_id: str, role: str, content: str, previous_ai_chat_id: Optional[str]) -> Dict[str, Any]:
114120
"""
115-
TODO: Create a Message node and link it to an existing Chat.
121+
TODO: Create a Message node and link it to an existing Chat. The app expects a message object called message.
116122
117123
Suggested shape:
118124
- Label: Message
119-
- Properties: {id: randomUUID(), role: 'user', content: $message, createdAt: datetime()}
120-
- Relationship: (Chat)-[:HAS_MESSAGE]->(Message)
125+
- Properties:
126+
- id: randomUUID()
127+
- role: $role
128+
- content: $message
129+
- createdAt: datetime()
130+
- previousAIChatId: $previous_ai_chat_id // Note: this comes from the AI and is used for OpenAI to keep track of the conversation
131+
- Relationship: (chat)-[:HAS_MESSAGE]->(message)
121132
"""
122133
cypher = """
123134
// TODO: Cypher query to create a message for a chat
124135
"""
125-
record = _run_query_single(cypher, chat_id=chat_id, role=role, content=content)
136+
record = _run_query_single(cypher, chat_id=chat_id, role=role, content=content,
137+
previous_ai_chat_id=previous_ai_chat_id)
126138
return record["message"] if record else {}
127139

128140

129-
def list_user_chats(user_id: str) -> List[Dict[str, Any]]:
141+
def list_user_chats(username: str) -> List[Dict[str, Any]]:
130142
"""
131-
TODO: Return all chats for the logged-in user with a summary.
143+
TODO: Return all chats for the logged-in user. The app expects a stream of chat objects called chat.
132144
133145
Suggested shape:
134146
- Label: Chat
135-
- Properties: {id, createdAt, updatedAt}
136-
- Relationship: (User)-[:STARTED]->(Chat)
147+
- Properties:
148+
- id
149+
- createdAt
150+
- updatedAt
151+
- Relationship: (user)-[:STARTED]->(chat)
137152
"""
138153
cypher = """
139154
// TODO: cypher query to list user chats
140155
"""
141-
records = _run_query(cypher, user_id=user_id)
156+
records = _run_query(cypher, username=username)
142157
return [r["chat"] for r in records]
143158

144159

145160
def fetch_chat(chat_id: str) -> Optional[Dict[str, Any]]:
146161
"""
147-
TODO: Fetch a full chat with all messages.
162+
TODO: Fetch a full chat with all messages in a list. The app expects a chat object called chat and a list of messages called messages.
163+
164+
Hint: Use collect()
148165
149166
suggested shape:
150167
- Label: Chat
151-
- Properties: {id, createdAt, updatedAt}
152-
- Relationship: (Chat)-[:HAS_MESSAGE]->(Message)
168+
- Properties:
169+
- id
170+
- createdAt
171+
- updatedAt
172+
- Relationship: (chat)-[:HAS_MESSAGE]->(message)
153173
"""
154174
cypher = """
155175
// TODO: Cypher query to fetch a chat by id and its messages
156176
"""
157177
record = _run_query_single(cypher, chat_id=chat_id)
158-
return record["chat"] if record else None
178+
return record if record else None
179+
180+
181+
def get_ai_response(chat_id: str, previous_chat_id: Optional[str], message: str) -> Optional[Dict[str, Any]]:
182+
"""
183+
TODO: Send the message to our GenAI plugin; using the ai.text.chat function. The app expects a Cypher Map called aiResponse.
184+
185+
See the docs for more: https://neo4j.com/docs/genai/plugin/current/generate-text/#chat-new
186+
187+
- Using the ai.text.chat function, send the message and the config to OpenAI
188+
189+
Recommended AI config:
190+
{
191+
token: $openaiToken,
192+
model: 'gpt-5-nano'
193+
}
194+
"""
195+
cypher = """
196+
// TODO: Cypher query to send message to AI plugin
197+
"""
198+
record = _run_query_single(cypher, chat_id=chat_id, previous_chat_id=previous_chat_id, message=message,
199+
openaiToken=os.getenv("OPENAI_API_KEY"))
200+
return record["aiResponse"] if record else None
201+
202+
203+
def get_previous_ai_chat_id(chat_id: str) -> Optional[str]:
204+
"""
205+
TODO: Using the chat id, find the most recent AI ChatID (returned by openAI), in order to continue the conversation
206+
if there is none, then the AI chat will be the first chat in this conversation. The app expects a string called chatId.
207+
208+
Hint: Use an ORDER BY and LIMIT and only search for AI messages
209+
210+
"""
211+
cypher = """
212+
// TODO: Cypher query to find the most recent AI ChatID
213+
"""
214+
record = _run_query_single(cypher, chat_id=chat_id)
215+
return record["chatId"] if record else None

images/chat_bot_model.png

57.7 KB
Loading

images/connection_info.png

206 KB
Loading

images/cypher_error.png

112 KB
Loading

images/open_browser.png

25.5 KB
Loading

images/run_flask_app.png

15 KB
Loading

images/use_template.png

51.7 KB
Loading

0 commit comments

Comments
 (0)