-
Notifications
You must be signed in to change notification settings - Fork 0
2. Model and Engine
TripView's data model is mainly composed of the User, Trip, Stop, and Chatt objects, which are present in the front end as serializable data classes and in the backend as database entities. We will go over how these data objects are used throughout the various features in TripView.
The User Frontend written with Kotlin and Jetpack Compose manages the client-side experience of TripView. To view details about how TripView works in terms of visual organization and navigation, go to our View UI/UX wiki section. The front end is written using the Model-View-ViewModel (MVVM) architectural pattern, which aims to separate user interface (View) from business logic and data representation (Model) using the ViewModel as an intermediary. Almost every screen has state managed by a specific ViewModel, ensuring object lifetimes and state are consistent within screens but remain within scope.
Our frontend communicates with other parts of the architecture such as the database and LLM by going through the Backend, written with Python and Flask, to manage the server-side of TripView. The backend is structured as a Web REST API, handling requests from the frontend and performing business logic leveraging other backend services before sending responses back to the frontend in JSON. This communication is encapsulated in ApiClient.kt. For more details on the how this communication is handled, view the APIs and Controller wiki section.
While the backend handles much of the business logic, it also uses 4 primary services to facilitate features for TripView users. These include the LLM API for providing agentic travel aid via Gemini, the Google Places (New) API for obtaining detailed information for specific stops of an itinerary, the OSRM Routing API for providing driving route information between a trip's stops, and the Database for maintaining persistent state. Additionally, the frontend directly makes use of the Google Maps SDK to provide navigation and mapping for a trip. The processes and interactions of these services on TripView's data model will be the focus of this wiki section.
The backend uses a Postgres database for storing user and trip information. The backend can directly communicate with this database using the psycopg2 package for Python, however we primarily utilize the SQLAlchemy package when interacting with the database to maintain an Object Relational Mapping (ORM) between our data as it is used in the backend and as it is stored in our database. The [database schema](!TODO LINK!) is defined as seen in the above diagram with SQLAlchemy using classes to simplify the declaration of entities & relationships as well as the access, usage, and modification of those objects.
Our data model is centered around [Trip](!TODO LINK!) objects as the main data created and manipulated by users. Trips encode summary information, navigational information, and associated stops and chats. [Stop](!TODO LINK!) objects are individual itinerary items that a trip sequences. Each stop has location, completion, order, and Google Places information. [Chat](!TODO LINK!) objects are messages between a user and the LLM API Gemini Agent, which encode text, sender, timestamp, and role information. A trip must have one or more stops, and a stop must have one or more trips that it belongs to. A trip may have zero or more chats, and a chat must have exactly one trip it belongs to.
TripView users are represented by [User](!TODO LINK!) objects. This object includes basic account information such as first name, last name, email, username, and password, as well as personal information about a user's likes and dislikes with respect to trips. While users and trips are joined by a [PartOf](!TODO LINK!) relationship, trips are defined by this relationship while users can be in a PartOf relationship with many trips. Users are all in a sparse [IsFriends](!TODO LINK!) relationships with one another, where the none relationship state is the default non-relationship. We will discuss this more extensively in the friend management section.
The LoginResult data class is used to encapsulate user authentication.
When the user attempts to log in with a username and password, the [login](!TODO LINK!) function is called and passes the input username and password to the backend. The [input password is then compared with the stored password hash](!TODO LINK!) of the relevant user record using the check_password_hash function from the werkzeug.security module. Successful login then results in the [generation of a JSON Web Token](!TODO LINK!) (JWT) as implemented by the flask_jwt_extended.create_access_token function. The JWT is created, transmitted, and managed by the JWTManager, which is [configured via a server-side secret key](!TODO LINK!). The resulting user id and token are then transmitted to the frontend within a LoginResult to update the [userId and accessToken app states](!TODO LINK!). These states are stored by the client to enable use of features requiring an authorized user, which require an Authorization Header populated by the access token. When the user logs out using the [logout](!TODO LINK!) function, both the userId and accessToken states are set to null.
User signup is not implemented in our MVP, however it is planned that a [signup](!TODO LINK!) function would be called when a user registers with a username, email, password, first name, last name, likes, and dislikes. These would be used by the backend to [generate a password hash](!TODO LINK!) using the generate_password_hash function from the werkzeug.security module and finally [create a user record](!TODO LINK!). The backend would then [generate a JWT token](!TODO LINK!) as described above and return a LoginResult to update the userId and accessToken app states.
Users are able to configure their profile username, first name, last name, likes, and dislikes. User modifications to these fields are passed to the backend, wherein the [user record is updated for whichever fields were sent](!TODO LINK!). The backend then sends back the updated user data to update the user state on the client side. Likes and dislikes are used to represent user preferences. Preferences are are used by the LLM API to tailor the responses of Gemini service, both for [generating trips](!TODO LINK!) (TODO L337) and [providing landmark context](!TODO LINK!) (TODO L1082) in the form of text inserted into prompts.
LLM API refers to the API built off for our LLM of choice ([gemini-2.5-flash](!TODO LINK!)). The backend is responsible for making requests to and handling responses from the LLM.
The primary function for the LLM is to generate itineraries. To do this, a user fills out a form with their trip details on the frontend when they go to create a new trip. The [submitForm](!TODO LINK!) function passes this information to the backend where the [form data is formatted by the backend for the LLM to use in the model.generate_content function](!TODO LINK!) which hits the [/generateContent endpoint from Gemini](!TODO LINK!), [alongside information the backend has about the user's preferences](!TODO LINK!). The LLM then [generates 3 itinerary choices](!TODO LINK!) into a [predefined JSON schema](!TODO LINK!) for data consistency.
After a valid JSON object representing the trip itinerary choices is generated, the LLM is called again using the generated itineraries to provide high level trip information to help guide user choice, including total cost estimate, cost breakdown, transportation summary, and transportation breakdown, achieved through a [second chained invocation of the model.generate_content function](!TODO LINK!). Once again this data is returned into a valid JSON object. Finally the backend returns a JSON object that is used in the front end to construct [TripSuggestions](!TODO LINK!) data objects, which encode a proto-trip without its own ID or owner.
When one of these suggestions is selected, the [chooseTrip](!TODO LINK!) function is called, sending the trip suggestion to the backend. The backend uses this suggestion to [create a trip object with its own trip_id and owner_id](!TODO LINK!). The stops field is then used to [create stop objects for each proto-stop with it's own stop_id and ties them to the created trip_id](!TODO LINK!). Once each stop is created, [each stop triggers a call of get_place_info](!TODO LINK!), which sends requests to the [Google Places API](!TODO LINK!) to obtain detailed stop information. Finally a [call is made to regenerate_driving_polyline](!TODO LINK!) to create navigation information for the trip using the [OSRM API](!TODO LINK!). These last two APIs will be described in more detail in later subsections. Once all trip information is generated, the trip object is finally sent back to update the front end.
The LLM is also used if the user wants to interactively ask the LLM questions about their trip. To do this the user enters a trip specific chat window facilitated via the ChatViewModel::ChatViewModelFactory ViewModel constructor and enters chats which are forward to the backend using the ApiClient:: in which case the backend formats a prompt with their trip information and question to provide context-informed responses.
The LLM also performs landmark identification by taking an image given by the user as well as location information from their phones location sensor. The image of the landmark is saved locally to the user's device and the text returned by the LLM as well as a uuid referring to the stored image is stored in the database for retrieval later.
The engine provides functionality to remove, add, and reorder itinerary stops from a trip. This is encapsulated with batch edits using stop_ids. Since the sequence of stop_ids essentially defines a trip, the engine is able to use the frontend dataclass to process all edits on the client side without any API requests enabling for smooth modification. Then, once the user is done with their modifications, they confirm their changes, which creates a single API request to the backend using the final list of stop_ids after all changes were made. These changes are then algorithmically repeated on the backend by comparing the front end's stop_ids to the backend's exisitng stops for the respective trip, updating indices of moved trips, creating Stop objects for new trips, and erasing old Stops not found in the frontend.
Additionally, the backend enables the addition of friends to a trip party, enabling them to view the itinerary and chat with the trip's LLM on their own separate chat as per their user_id. Friends are added via the invite_to_party function, which sends the trip_id and friend's user_id for the backend to update the backend with the new PartOf relation for the new participant. This enables the friend to view the trip in their "Friend's Trips" list. A similar process is followed for the opposite when a trip owner wishes to remove a trip member. Using the remove_from_party function, the backend removes the PartOf relationship between the friend and the relevant trip, this removing it from their "Friend's Trips" list.
Finally, users are able to check off and on stops on their itinerary using the update_stop_completed function, enabling them to track their progress. This is implemented as a standalone backend request that toggles freely as a stream of on and off from the user for each stop, which is slow than batching but enables the state update from completing trips to be reactive and independent of trip edits. Additionally trips can be archived once completed fully using the archive_trip function, which moves their status to completed and moves them to the "Completed Trips" list.
Getting information about stops such as their rating, price range, hours, address, and Google Maps link is achieved by using the Google Places (New) API. Whenever a stop object is created in the database by a network request, the backend request handling queries this API using the stop's latitude and longitude and receives information about that stop including address, hours, price range, and user ratings. This information is then cached into the database for each stop object and available for the user to view in the frontend. This is done when trips are created and modified, as these actions may modify the stops in a trip.
In our skeletal product, the backend returns a polyline containing straight lines between all stops on the route to be visualized on the map. In the MVP, the driving polyline is tied to a unique trip_id, with a coordinate string being built in the backend composed of the latitude and longitude coordinates of each Stop in a trip's itinerary. The backend then calls the OSRM API to get a polyline consisting of the driving route between all points on the itinerary, which gets cached in the trip object to reduce the number of API calls to OSRM. This polyline is sent to the frontend to update the polyline without needing to trigger a refresh. The polyline is regenerated when trips are created or modified, as these actions can modify the stops on a trip, as well as the order of navigation.
When a user views a trip and receives the list of stops for that trip from the LLM/backend, the stops of the trip encode location information. The frontend can use the Google Maps SDK to plot each stop as a point on the map to show to the user. While the points themselves can be easily used to create a direct polyline, this isn't very useful for navigation. The engine makes a backend API call to the OSRM API route mentioned above to obtain driving route points. These points are then decoded using bit manipulation to produce a driving path which is plottable on the roads between stops. The engine automatically updates the location of stops and thus the polyline when the stops of a trip are modified, enabling up-to-date routing information to users and enabling flexible schedules.