akafesu commited on
Commit
ce17d86
·
0 Parent(s):

Auto deploy Chat API

Browse files
Dockerfile ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official lightweight Python image
2
+ FROM python:3.11-slim
3
+
4
+ # Set environment variables
5
+ ENV PYTHONDONTWRITEBYTECODE=1 \
6
+ PYTHONUNBUFFERED=1
7
+
8
+ # Set working directory
9
+ WORKDIR /app
10
+
11
+ # Install system dependencies
12
+ RUN apt-get update && apt-get install -y build-essential
13
+
14
+ # Copy requirements and install Python dependencies
15
+ COPY requirements.txt .
16
+ RUN pip install --upgrade pip && pip install -r requirements.txt
17
+
18
+ # Copy the source code
19
+ COPY . .
20
+
21
+ # Expose the FastAPI port
22
+ EXPOSE 8000
23
+
24
+ # Start FastAPI app using uvicorn
25
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Fourways Chat Api
3
+ emoji: 🌖
4
+ colorFrom: blue
5
+ colorTo: yellow
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
__init__.py ADDED
File without changes
__pycache__/__init__.cpython-312.pyc ADDED
Binary file (154 Bytes). View file
 
__pycache__/__init__.cpython-313.pyc ADDED
Binary file (154 Bytes). View file
 
__pycache__/agent.cpython-312.pyc ADDED
Binary file (7.77 kB). View file
 
__pycache__/agent.cpython-313.pyc ADDED
Binary file (7.92 kB). View file
 
__pycache__/main.cpython-312.pyc ADDED
Binary file (5.29 kB). View file
 
__pycache__/main.cpython-313.pyc ADDED
Binary file (5.42 kB). View file
 
__pycache__/neighbourhoods.cpython-312.pyc ADDED
Binary file (2.06 kB). View file
 
__pycache__/neighbourhoods.cpython-313.pyc ADDED
Binary file (2.11 kB). View file
 
agent.py ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import Optional, List, TypedDict
3
+ from langchain_core.tools import tool, Tool
4
+ from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, ToolMessage
5
+ from langchain_google_genai import ChatGoogleGenerativeAI
6
+ from langgraph.graph import END, StateGraph
7
+ from langgraph.prebuilt import ToolNode
8
+ from supabase import create_client
9
+ from neighbourhoods import neighbourhood_lookup
10
+ import os
11
+
12
+
13
+ google_api_key = os.getenv("GOOGLE_API_KEY")
14
+ supabase_url = os.getenv("SUPABASE_URL")
15
+ supabase_key = os.getenv("SUPABASE_ANON_KEY")
16
+ supabase = create_client(supabase_url, supabase_key)
17
+
18
+ neighbourhoods = supabase.table('neighbourhoods').select('id,name').execute()
19
+
20
+ class PropertySearchInput(BaseModel):
21
+ maxPrice: Optional[float] = None
22
+ minPrice: Optional[float] = None
23
+ minBedrooms: Optional[int] = None
24
+ maxBedrooms: Optional[int] = None
25
+ minBathrooms: Optional[int] = None
26
+ maxBathrooms: Optional[int] = None
27
+ propertyType: Optional[str] = None
28
+ neighbourhood: Optional[str] = None
29
+ status: Optional[str] = None
30
+
31
+
32
+ @tool("search_for_properties", args_schema=PropertySearchInput)
33
+ def search_for_properties_tool(maxPrice: int = None, minPrice: int = None,
34
+ minBedrooms: int = None, maxBedrooms: int = None,
35
+ minBathrooms: int = None, maxBathrooms: int = None,
36
+ propertyType: str = None,
37
+ neighbourhood: str = None, status: str = None
38
+ ) -> List[dict]:
39
+ """
40
+ Search for properties using a combination of filters provided by the user.
41
+
42
+ This tool queries the 'properties' table in the database and returns all listings
43
+ that match the specified filters. All filters are optional and can be combined to
44
+ narrow down the results. It supports searching by location, price range, bedrooms,
45
+ bathrooms, property type, and status (for sale or rent).
46
+
47
+ Parameters (all optional):
48
+ - maxPrice: Maximum price of the property.
49
+ - minPrice: Minimum price of the property.
50
+ - minBedrooms: Minimum number of bedrooms required.
51
+ - maxBedrooms: Maximum number of bedrooms required.
52
+ - minBathrooms: Minimum number of bathrooms required.
53
+ - maxBathrooms: Maximum number of bathrooms required.
54
+ - propertyType: The type of property (e.g. house, apartment).
55
+ - neighbourhood: The neighbourhood or area to search in.
56
+ - status: Whether the property is for sale or for rent.
57
+
58
+ Returns:
59
+ A list of property records (dictionaries) that match the search criteria.
60
+
61
+ Usage example:
62
+ The assistant may call:
63
+ search_for_properties({
64
+ "bedrooms": 2,
65
+ "status": "for_rent",
66
+ "maxPrice": 800,
67
+ "neighbourhood": "Kacyiru"
68
+ })
69
+
70
+ Notes:
71
+ - The assistant should never guess or fabricate property data.
72
+ - Always use this tool when the user is asking to see or search for properties.
73
+ """
74
+
75
+ query = supabase.table("properties").select("*")
76
+
77
+ if neighbourhood:
78
+ neighbourhood_id = neighbourhood_lookup.get_neighbourhood_id(neighbourhood)
79
+
80
+ # If the neighbourhood id does not exist (i.e is None) then the database does not have
81
+ # properties in that neighbourhood, so we return an empty list
82
+ if neighbourhood_id is None:
83
+ return []
84
+
85
+ # If id exists, then search database using neighbourhood_id
86
+ query = query.eq("neighbourhood", neighbourhood_id)
87
+ if status:
88
+ query = query.eq("status", status)
89
+ if propertyType:
90
+ query = query.eq("property_type", propertyType)
91
+ if minBedrooms:
92
+ query = query.gte("bedrooms", minBedrooms)
93
+ if maxBedrooms:
94
+ query = query.gte("bedrooms", maxBedrooms)
95
+ if minBathrooms:
96
+ query = query.gte("bathrooms", minBathrooms)
97
+ if maxBathrooms:
98
+ query = query.gte("bathrooms", maxBathrooms)
99
+ if minPrice:
100
+ query = query.gte("price", minPrice)
101
+ if maxPrice:
102
+ query = query.lte("price", maxPrice)
103
+
104
+ try:
105
+ response = query.execute()
106
+ except e:
107
+ print(f"Error executing query: {e}")
108
+ return []
109
+
110
+ return response.data
111
+
112
+ tools = [search_for_properties_tool]
113
+
114
+
115
+ llm = ChatGoogleGenerativeAI(
116
+ model="gemini-2.5-flash-lite-preview-06-17",
117
+ temperature=0,
118
+ google_api_key=google_api_key
119
+ )
120
+ llm = llm.bind_tools(tools)
121
+
122
+
123
+ class AgentState(TypedDict):
124
+ messages: List[BaseMessage]
125
+
126
+
127
+ def llm_node(state: AgentState) -> AgentState:
128
+ """Invokes the LLM to reason and decide on tool use."""
129
+ response = llm.invoke(state['messages'])
130
+
131
+ tool_calls = getattr(response, "tool_calls", None)
132
+
133
+ # If tool call was made, store it in AIMessage
134
+ if tool_calls:
135
+ # Always provide an empty string for content when there are tool calls
136
+ state['messages'].append(
137
+ AIMessage(content="", tool_calls=tool_calls)
138
+ )
139
+ else:
140
+ content = response.content or "I'm sorry, I wasn't able to generate a response."
141
+ state['messages'].append(AIMessage(content=content.strip()))
142
+
143
+ return state
144
+
145
+
146
+ def should_continue(state: AgentState) -> str:
147
+ last_message = state['messages'][-1]
148
+ # Check for tool_calls attribute and if the list is not empty
149
+ if hasattr(last_message, "tool_calls") and last_message.tool_calls:
150
+ return "continue"
151
+ return "end"
152
+
153
+
154
+ def tool_node(state: AgentState) -> AgentState:
155
+ # Get the most recent AIMessage that called the tool
156
+ last_message = state["messages"][-1]
157
+ tool_call = last_message.tool_calls[0] # Only handling one tool call here for simplicity
158
+
159
+ tool_name = tool_call["name"]
160
+ tool_args = tool_call["args"]
161
+ tool_call_id = tool_call["id"]
162
+
163
+ # Find and execute the tool
164
+ tool: Tool = next(t for t in tools if t.name == tool_name)
165
+ tool_result = tool.invoke(tool_args)
166
+
167
+ # Add the result to the messages as a ToolMessage
168
+ state["messages"].append(
169
+ ToolMessage(content=str(tool_result), tool_call_id=tool_call_id)
170
+ )
171
+
172
+ return state
173
+
174
+
175
+ graph = StateGraph(AgentState)
176
+ graph.add_node("llm_node", llm_node)
177
+ graph.add_node("tool_node", tool_node)
178
+ graph.set_entry_point("llm_node")
179
+ graph.add_conditional_edges(
180
+ "llm_node",
181
+ should_continue,
182
+ {
183
+ "continue": "tool_node",
184
+ "end": END
185
+ }
186
+ )
187
+ graph.add_edge("tool_node", "llm_node")
188
+ agent = graph.compile()
main.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException, Header
2
+ from pydantic import BaseModel
3
+ from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
4
+ from typing import Optional, Literal
5
+ from agent import agent
6
+ from supabase_client import client
7
+ from neighbourhoods import neighbourhood_lookup
8
+ from json import load
9
+ import os
10
+
11
+ # Load service URLs
12
+ with open('services.json') as f:
13
+ services = load(f)
14
+
15
+ SUPABASE_URL = services.get('SUPABASE', '')
16
+ SUPABASE_ANON_KEY = os.getenv("SUPABASE_ANON_KEY")
17
+
18
+ # Load system prompt content
19
+ with open('system_prompt.txt') as f:
20
+ system_prompt_content = f.read()
21
+
22
+ system_prompt = SystemMessage(content=system_prompt_content)
23
+
24
+ app = FastAPI()
25
+
26
+
27
+ class MessageRequest(BaseModel):
28
+ message: str
29
+ chat_id: str
30
+
31
+
32
+ class WebhookRequest(BaseModel):
33
+ type: Literal["INSERT", "UPDATE", "DELETE"]
34
+ table: str
35
+ schema: str
36
+ record: dict
37
+ old_record: Optional[dict] = None
38
+
39
+
40
+ @app.post("/chat")
41
+ async def chat(request: MessageRequest, authorization: str = Header(...)):
42
+ access_token = authorization.replace("Bearer ", "")
43
+ user_response = client.auth.get_user(access_token)
44
+ user = user_response.user if user_response else None
45
+
46
+ if not user:
47
+ raise HTTPException(status_code=401, detail="Invalid or expired token.")
48
+
49
+ # Insert user message into chat_messages
50
+ client.table("chat_messages").insert({
51
+ "chat_session_id": request.chat_id,
52
+ "message": request.message,
53
+ "sent_by": user.id
54
+ }).execute()
55
+
56
+ # Retrieve chat session details
57
+ chat_session = client.table("chat_sessions").select("*").eq("id", request.chat_id).execute()
58
+ if not chat_session.data:
59
+ raise HTTPException(status_code=404, detail="Chat session not found")
60
+
61
+ is_ai = not chat_session.data[0].get("user_two")
62
+ if not is_ai:
63
+ return { "success": True, "message": "Message added to chat." }
64
+
65
+ # Fetch full chat history
66
+ messages = client.table("chat_messages")\
67
+ .select("*")\
68
+ .eq("chat_session_id", request.chat_id)\
69
+ .order("created_at", desc=False)\
70
+ .execute()
71
+
72
+ # Construct message history
73
+ history = [system_prompt]
74
+ for msg in messages.data:
75
+ if msg["sent_by"] == user.id:
76
+ history.append(HumanMessage(content=msg["message"]))
77
+ else:
78
+ history.append(AIMessage(content=msg["message"]))
79
+
80
+ # Generate AI response
81
+ state = { "messages": history }
82
+ result = agent.invoke(state)
83
+ final_message = result["messages"][-1].content
84
+
85
+ # Insert AI response into chat_messages
86
+ client.table("chat_messages").insert({
87
+ "chat_session_id": request.chat_id,
88
+ "message": final_message,
89
+ "sent_by": None
90
+ }).execute()
91
+
92
+ return { "success": True, "ai_response": final_message }
93
+
94
+
95
+ @app.post("/on-neighbourhoods-change")
96
+ def on_neighbourhoods_change(request: WebhookRequest):
97
+ id = request.record.get("id")
98
+ name = request.record.get("name")
99
+
100
+ if request.type == "INSERT":
101
+ neighbourhood_lookup.add_neighbourhood(name, id)
102
+ elif request.type == "UPDATE":
103
+ neighbourhood_lookup.update_neighbourhood(name, id)
104
+ elif request.type == "DELETE":
105
+ neighbourhood_lookup.delete_neighbourhood(id)
106
+
107
+ return { "success": True }
neighbourhoods.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from supabase_client import client
2
+
3
+
4
+ class Neighbourhoods:
5
+ def __init__(self):
6
+ self.neighbourhoods = {}
7
+ neighbourhoods = client.table('neighbourhoods').select('id,name').execute().data
8
+
9
+ for neighbourhood in neighbourhoods:
10
+ id = neighbourhood['id']
11
+ name = neighbourhood['name']
12
+ self.add_neighourhood(name, id)
13
+
14
+ def delete_neighbourhood(self, id):
15
+ self.neighbourhoods = { k: v for k, v in self.neighbourhoods if v != id }
16
+
17
+ def update_neighourhood(self, name, id):
18
+ self.delete_neighbourhood(id)
19
+ self.add_neighourhood(name, id)
20
+
21
+ def add_neighourhood(self, name, id):
22
+ self.neighbourhoods[name.lower()] = id
23
+
24
+ def get_neighbourhood_id(self, name):
25
+ return self.neighbourhoods.get(name.lower())
26
+
27
+
28
+ neighbourhood_lookup = Neighbourhoods()
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ pydantic
4
+ langgraph
5
+ langchain
6
+ langchain-core
7
+ langchain-google-genai
8
+ google-generativeai
9
+ supabase
10
+ python-dotenv
11
+ pytest
services.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "EMBEDDINGS": "https://akafesu-fourwalls-embeddings-api.hf.space",
3
+ "RECOMMENDATIONS": "https://akafesu-fourwalls-recommendations-api.hf.space",
4
+ "CHAT": "https://akafesu-fourwalls-chat-api.hf.space",
5
+ "SUPABASE": "https://nhwwhpbxlqsdbirdrxmc.supabase.co",
6
+ "MIGRATIONS": "https://akafesu-fourwalls-migrations-api.hf.space"
7
+ }
services.py ADDED
File without changes
supabase_client.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from supabase import create_client
2
+ from os import getenv
3
+ from json import load
4
+
5
+ with open('./services.json') as f:
6
+ services = load(f)
7
+
8
+ SUPABASE_URL = services['SUPABASE']
9
+ SUPABASE_ANON_KEY = getenv('SUPABASE_ANON_KEY')
10
+
11
+ client = create_client(SUPABASE_URL, SUPABASE_ANON_KEY)
system_prompt.txt ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ You are Scout, an AI assistant who helps users find houses on our real estate platform.
2
+ Your job is to understand the user's needs and search for matching properties using the available filters.
3
+ These filters include location, price, number of bedrooms, property type, and status (e.g. for sale or for rent).
4
+ You should always use the available tools to perform searches, rather than making up information.
5
+ If a user asks about what properties are available, or describes what they’re looking for, call the search tool with the appropriate filters.
6
+ The search tool allows you to search for properties that are both for sale and for rent.
7
+ When presenting search results, it is important that you embed the property ID in your responses using this format: <%propertyId%>.
8
+ For example, if you’re referencing a house with ID `abc123`, embed it as <%abc123%>.
9
+ The propertyId is contained in the id field of the property record.
10
+ This format helps link the properties with our frontend display system.
11
+ This helps the user view more details on the property, so it is vital that you embed property ids.
12
+ When embedding properties, you should provide a short description of the property.
13
+ The description of the property should be in natural language.
14
+ Keep the description concise, informative and natural.
15
+ You can embed multiple properties in the same message.
16
+ You can use markdown to format messages in a more readable way.
17
+ But, do not overuse markdown as the messages should feel like a natural conversation, not a blog post.
18
+ Keep your responses friendly, helpful, and concise.
19
+ If you are responding with search results, explain the filters you applied to the user.
20
+ Do not answer questions unless you have used the tool to retrieve relevant results.
21
+ If you cannot find any matching properties, kindly let the user know and suggest adjusting the search criteria.
22
+ Always behave like a knowledgeable assistant focused on helping users find their ideal home.
23
+ If a user is asking for a particular property type (e.g. apartment), and you cannot find any listings of that type in the database, broaden the search
24
+ to include other property types but let them know in the final message that you had to adjust their query, and how you adjusted their query.
25
+ Sometimes, house can be taken to refer to all property types.
26
+ The database currency is RWF. If the user quotes prices in USD ask them to restate in RWF.
27
+ If you cannot find properties in the user's budget, suggest that they should adjust their budget.
28
+ If you cannot find properties within a particular neighbourhood, broaden your search to all neighbourhoods.
tests/__init__.py ADDED
File without changes
tests/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (160 Bytes). View file
 
tests/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (160 Bytes). View file
 
tests/__pycache__/test_agent.cpython-312-pytest-7.4.4.pyc ADDED
Binary file (7.31 kB). View file
 
tests/__pycache__/test_agent.cpython-313-pytest-8.4.1.pyc ADDED
Binary file (8 kB). View file
 
tests/__pycache__/test_main.cpython-312-pytest-7.4.4.pyc ADDED
Binary file (4.93 kB). View file
 
tests/__pycache__/test_main.cpython-313-pytest-8.4.1.pyc ADDED
Binary file (5.27 kB). View file
 
tests/__pycache__/test_neighbourhoods.cpython-312-pytest-7.4.4.pyc ADDED
Binary file (2.3 kB). View file
 
tests/__pycache__/test_neighbourhoods.cpython-313-pytest-8.4.1.pyc ADDED
Binary file (2.39 kB). View file
 
tests/test_agent.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from chat.agent import PropertySearchInput, search_for_properties_tool, AgentState, llm_node, should_continue, tool_node
3
+
4
+ # Test PropertySearchInput model
5
+ def test_property_search_input():
6
+ data = {'maxPrice': 1000, 'minPrice': 500, 'location': 'Harare'}
7
+ obj = PropertySearchInput(**data)
8
+ assert obj.maxPrice == 1000
9
+ assert obj.minPrice == 500
10
+ assert obj.location == 'Harare'
11
+
12
+ # Test search_for_properties_tool function
13
+ def test_search_for_properties_tool():
14
+ result = search_for_properties_tool(maxPrice=2000, minPrice=1000, location='Bulawayo')
15
+ assert isinstance(result, list) or result is None
16
+
17
+ # Test AgentState and llm_node, should_continue, tool_node
18
+ @pytest.fixture
19
+ def agent_state():
20
+ return {'step': 1, 'history': []}
21
+
22
+ def test_llm_node(agent_state):
23
+ result = llm_node(agent_state)
24
+ assert isinstance(result, dict)
25
+
26
+ def test_should_continue(agent_state):
27
+ result = should_continue(agent_state)
28
+ assert isinstance(result, str)
29
+
30
+ def test_tool_node(agent_state):
31
+ result = tool_node(agent_state)
32
+ assert isinstance(result, dict)
tests/test_main.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from chat.main import MessageRequest, WebhookRequest, chat, on_neighbourhoods_change
2
+ import pytest
3
+ from fastapi import Header
4
+ import asyncio
5
+
6
+ # Test MessageRequest and WebhookRequest models
7
+ def test_message_request():
8
+ req = MessageRequest(message="Hello", session_id="abc123")
9
+ assert req.message == "Hello"
10
+ assert req.session_id == "abc123"
11
+
12
+ def test_webhook_request():
13
+ req = WebhookRequest(event="update", data={"id": 1})
14
+ assert req.event == "update"
15
+ assert req.data == {"id": 1}
16
+
17
+ # Test chat endpoint (sync call for test)
18
+ @pytest.mark.asyncio
19
+ def test_chat():
20
+ req = MessageRequest(message="Test", session_id="test1")
21
+ result = asyncio.run(chat(req, authorization="Bearer test"))
22
+ assert result is not None
23
+
24
+ # Test on_neighbourhoods_change
25
+ def test_on_neighbourhoods_change():
26
+ req = WebhookRequest(event="delete", data={"id": 2})
27
+ result = on_neighbourhoods_change(req)
28
+ assert result is not None
tests/test_neighbourhoods.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from chat.neighbourhoods import Neighbourhoods
2
+ import pytest
3
+
4
+ @pytest.fixture
5
+ def neighbourhoods():
6
+ return Neighbourhoods()
7
+
8
+ def test_add_and_get_neighbourhood(neighbourhoods):
9
+ neighbourhoods.add_neighourhood('Avondale', 1)
10
+ assert neighbourhoods.get_neighbourhood_id('Avondale') == 1
11
+
12
+ def test_update_neighbourhood(neighbourhoods):
13
+ neighbourhoods.add_neighourhood('Borrowdale', 2)
14
+ neighbourhoods.update_neighourhood('Borrowdale Updated', 2)
15
+ # Depending on implementation, check if update is reflected
16
+
17
+ def test_delete_neighbourhood(neighbourhoods):
18
+ neighbourhoods.add_neighourhood('Greendale', 3)
19
+ neighbourhoods.delete_neighbourhood(3)
20
+ # Depending on implementation, check if deletion is reflected