From 901f214f9dff39dd6881be18b0bd6c2fb5c61bbb Mon Sep 17 00:00:00 2001 From: John Costa Date: Mon, 31 Mar 2025 18:40:36 +0000 Subject: [PATCH] feat(contacts): events can now have organizers --- .../.gen/haystack/haystack/model/events.go | 1 + .../haystack/haystack/model/image_contacts.go | 18 +++++ .../.gen/haystack/haystack/table/events.go | 7 +- .../haystack/haystack/table/image_contacts.go | 81 +++++++++++++++++++ .../haystack/table/table_use_schema.go | 1 + backend/agents/event_location_agent.go | 67 ++++++++++----- backend/main.go | 3 +- backend/models/contacts.go | 70 ++++++++++++++++ backend/models/events.go | 13 +++ backend/schema.sql | 57 +++++++------ backend/tools.json | 36 +++------ 11 files changed, 284 insertions(+), 70 deletions(-) create mode 100644 backend/.gen/haystack/haystack/model/image_contacts.go create mode 100644 backend/.gen/haystack/haystack/table/image_contacts.go create mode 100644 backend/models/contacts.go diff --git a/backend/.gen/haystack/haystack/model/events.go b/backend/.gen/haystack/haystack/model/events.go index 19e9bb6..42ee4d1 100644 --- a/backend/.gen/haystack/haystack/model/events.go +++ b/backend/.gen/haystack/haystack/model/events.go @@ -19,4 +19,5 @@ type Events struct { StartDateTime *time.Time EndDateTime *time.Time LocationID *uuid.UUID + OrganizerID *uuid.UUID } diff --git a/backend/.gen/haystack/haystack/model/image_contacts.go b/backend/.gen/haystack/haystack/model/image_contacts.go new file mode 100644 index 0000000..7139e7f --- /dev/null +++ b/backend/.gen/haystack/haystack/model/image_contacts.go @@ -0,0 +1,18 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package model + +import ( + "github.com/google/uuid" +) + +type ImageContacts struct { + ID uuid.UUID `sql:"primary_key"` + ImageID uuid.UUID + ContactID uuid.UUID +} diff --git a/backend/.gen/haystack/haystack/table/events.go b/backend/.gen/haystack/haystack/table/events.go index 50803f3..2bdb14b 100644 --- a/backend/.gen/haystack/haystack/table/events.go +++ b/backend/.gen/haystack/haystack/table/events.go @@ -23,6 +23,7 @@ type eventsTable struct { StartDateTime postgres.ColumnTimestamp EndDateTime postgres.ColumnTimestamp LocationID postgres.ColumnString + OrganizerID postgres.ColumnString AllColumns postgres.ColumnList MutableColumns postgres.ColumnList @@ -69,8 +70,9 @@ func newEventsTableImpl(schemaName, tableName, alias string) eventsTable { StartDateTimeColumn = postgres.TimestampColumn("start_date_time") EndDateTimeColumn = postgres.TimestampColumn("end_date_time") LocationIDColumn = postgres.StringColumn("location_id") - allColumns = postgres.ColumnList{IDColumn, NameColumn, DescriptionColumn, StartDateTimeColumn, EndDateTimeColumn, LocationIDColumn} - mutableColumns = postgres.ColumnList{NameColumn, DescriptionColumn, StartDateTimeColumn, EndDateTimeColumn, LocationIDColumn} + OrganizerIDColumn = postgres.StringColumn("organizer_id") + allColumns = postgres.ColumnList{IDColumn, NameColumn, DescriptionColumn, StartDateTimeColumn, EndDateTimeColumn, LocationIDColumn, OrganizerIDColumn} + mutableColumns = postgres.ColumnList{NameColumn, DescriptionColumn, StartDateTimeColumn, EndDateTimeColumn, LocationIDColumn, OrganizerIDColumn} ) return eventsTable{ @@ -83,6 +85,7 @@ func newEventsTableImpl(schemaName, tableName, alias string) eventsTable { StartDateTime: StartDateTimeColumn, EndDateTime: EndDateTimeColumn, LocationID: LocationIDColumn, + OrganizerID: OrganizerIDColumn, AllColumns: allColumns, MutableColumns: mutableColumns, diff --git a/backend/.gen/haystack/haystack/table/image_contacts.go b/backend/.gen/haystack/haystack/table/image_contacts.go new file mode 100644 index 0000000..a94589e --- /dev/null +++ b/backend/.gen/haystack/haystack/table/image_contacts.go @@ -0,0 +1,81 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package table + +import ( + "github.com/go-jet/jet/v2/postgres" +) + +var ImageContacts = newImageContactsTable("haystack", "image_contacts", "") + +type imageContactsTable struct { + postgres.Table + + // Columns + ID postgres.ColumnString + ImageID postgres.ColumnString + ContactID postgres.ColumnString + + AllColumns postgres.ColumnList + MutableColumns postgres.ColumnList +} + +type ImageContactsTable struct { + imageContactsTable + + EXCLUDED imageContactsTable +} + +// AS creates new ImageContactsTable with assigned alias +func (a ImageContactsTable) AS(alias string) *ImageContactsTable { + return newImageContactsTable(a.SchemaName(), a.TableName(), alias) +} + +// Schema creates new ImageContactsTable with assigned schema name +func (a ImageContactsTable) FromSchema(schemaName string) *ImageContactsTable { + return newImageContactsTable(schemaName, a.TableName(), a.Alias()) +} + +// WithPrefix creates new ImageContactsTable with assigned table prefix +func (a ImageContactsTable) WithPrefix(prefix string) *ImageContactsTable { + return newImageContactsTable(a.SchemaName(), prefix+a.TableName(), a.TableName()) +} + +// WithSuffix creates new ImageContactsTable with assigned table suffix +func (a ImageContactsTable) WithSuffix(suffix string) *ImageContactsTable { + return newImageContactsTable(a.SchemaName(), a.TableName()+suffix, a.TableName()) +} + +func newImageContactsTable(schemaName, tableName, alias string) *ImageContactsTable { + return &ImageContactsTable{ + imageContactsTable: newImageContactsTableImpl(schemaName, tableName, alias), + EXCLUDED: newImageContactsTableImpl("", "excluded", ""), + } +} + +func newImageContactsTableImpl(schemaName, tableName, alias string) imageContactsTable { + var ( + IDColumn = postgres.StringColumn("id") + ImageIDColumn = postgres.StringColumn("image_id") + ContactIDColumn = postgres.StringColumn("contact_id") + allColumns = postgres.ColumnList{IDColumn, ImageIDColumn, ContactIDColumn} + mutableColumns = postgres.ColumnList{ImageIDColumn, ContactIDColumn} + ) + + return imageContactsTable{ + Table: postgres.NewTable(schemaName, tableName, alias, allColumns...), + + //Columns + ID: IDColumn, + ImageID: ImageIDColumn, + ContactID: ContactIDColumn, + + AllColumns: allColumns, + MutableColumns: mutableColumns, + } +} diff --git a/backend/.gen/haystack/haystack/table/table_use_schema.go b/backend/.gen/haystack/haystack/table/table_use_schema.go index aa53643..424fcc4 100644 --- a/backend/.gen/haystack/haystack/table/table_use_schema.go +++ b/backend/.gen/haystack/haystack/table/table_use_schema.go @@ -13,6 +13,7 @@ func UseSchema(schema string) { Contacts = Contacts.FromSchema(schema) Events = Events.FromSchema(schema) Image = Image.FromSchema(schema) + ImageContacts = ImageContacts.FromSchema(schema) ImageEvents = ImageEvents.FromSchema(schema) ImageLinks = ImageLinks.FromSchema(schema) ImageLocations = ImageLocations.FromSchema(schema) diff --git a/backend/agents/event_location_agent.go b/backend/agents/event_location_agent.go index dabe9cd..6c7f494 100644 --- a/backend/agents/event_location_agent.go +++ b/backend/agents/event_location_agent.go @@ -13,25 +13,27 @@ import ( "github.com/google/uuid" ) +// This prompt is probably shit. const eventLocationPrompt = ` -You are an agent that extracts events and locations from an image. -Your job is to check if an image has an event or a location and use the correct tools to extract this information. +You are an agent that extracts events, locations, and organizers from an image. Your primary tasks are to identify and create locations and organizers before creating events. Follow these steps: -If you find an event, you should look for a location for this event on the image, it is possible an event doesn't have a location. -Only create an event if you see an event on the image, not all locations have an associated event. +Identify and Create Locations: -It is possible that there is no location or event on an image. +Check if the image contains a location. +If a location is found, check if it exists in the listLocations. +If the location does not exist, create it first. +Always reuse existing locations from listLocations to avoid duplicates. -You should ask for a list of locations, as the user is likely to have this location saved. Reuse existing locations where possible. +Identify and Create Events: -Always reuse existing locations from listLocations. Do not create duplicates. +Check if the image contains an event. An event should have a name and a date. +If an event is found, ensure you have a location (from step 1) and an organizer (from step 2) before creating the event. +Events must have an associated location and organizer. Do not create an event without these. +If possible, return a start time and an end time as ISO datetime strings. +Handling Images Without Events or Locations: -Do not create an event if you don't see any dates, or a name indicating an event. - -Events can have an associated location, if you think there is a location, then you must either use a location from listLocations or you must create it first. -Wherever possible, find the location in the image. - -When possible return a start time as a ISO datetime string, as well as an end date time. +It is possible that the image does not contain an event or a location. In such cases, do not create an event. +Always prioritize the creation of locations and organizers before events. Ensure that all events have an associated location and organizer. ` // TODO: this should be read directly from a file on load. @@ -80,18 +82,22 @@ const TOOLS = ` }, "startDateTime": { "type": "string", - "description": "The start time as an ISO string" + "description": "The start time as an ISO string" }, "endDateTime": { "type": "string", - "description": "The end time as an ISO string" + "description": "The end time as an ISO string" }, "locationId": { "type": "string", "description": "The ID of the location, available by listLocations" + }, + "organizerName": { + "type": "string", + "description": "The name of the organizer" } }, - "required": ["name", "startDateTime", "endDateTime"] + "required": ["name"] } } }, @@ -114,11 +120,13 @@ type EventLocationAgent struct { eventModel models.EventModel locationModel models.LocationModel + contactModel models.ContactModel toolHandler ToolsHandlers } type ListLocationArguments struct{} +type ListOrganizerArguments struct{} type CreateLocationArguments struct { Name string `json:"name"` @@ -126,6 +134,12 @@ type CreateLocationArguments struct { Coordinates *string `json:"coordinates,omitempty"` } +type CreateOrganizerArguments struct { + Name string `json:"name"` + PhoneNumber *string `json:"phoneNumber,omitempty"` + Email *string `json:"email,omitempty"` +} + type AttachImageLocationArguments struct { LocationId string `json:"locationId"` } @@ -135,6 +149,7 @@ type CreateEventArguments struct { StartDateTime string `json:"startDateTime"` EndDateTime string `json:"endDateTime"` LocationId string `json:"locationId"` + OrganizerName string `json:"organizerName"` } func (agent EventLocationAgent) GetLocations(userId uuid.UUID, imageId uuid.UUID, imageName string, imageData []byte) error { @@ -251,7 +266,7 @@ func (handler ToolsHandlers) Handle(info ToolHandlerInfo, request *AgentRequestB return string(stringResponse), nil } -func NewLocationEventAgent(locationModel models.LocationModel, eventModel models.EventModel) (EventLocationAgent, error) { +func NewLocationEventAgent(locationModel models.LocationModel, eventModel models.EventModel, contactModel models.ContactModel) (EventLocationAgent, error) { client, err := CreateAgentClient(eventLocationPrompt) if err != nil { return EventLocationAgent{}, err @@ -261,6 +276,7 @@ func NewLocationEventAgent(locationModel models.LocationModel, eventModel models client: client, locationModel: locationModel, eventModel: eventModel, + contactModel: contactModel, } toolHandler := ToolsHandlers{ @@ -317,7 +333,7 @@ func NewLocationEventAgent(locationModel models.LocationModel, eventModel models Fn: func(info ToolHandlerInfo, args CreateEventArguments, call ToolCall) (model.Events, error) { ctx := context.Background() - layout := "2006-01-02T15:04:05" + layout := "2006-01-02T15:04:05Z" startTime, err := time.Parse(layout, args.StartDateTime) if err != nil { @@ -339,6 +355,14 @@ func NewLocationEventAgent(locationModel models.LocationModel, eventModel models return event, err } + organizer, err := agent.contactModel.Save(ctx, info.userId, model.Contacts{ + Name: args.Name, + }) + + if err != nil { + return event, err + } + _, err = agent.eventModel.SaveToImage(ctx, info.imageId, event.ID) if err != nil { return event, err @@ -349,7 +373,12 @@ func NewLocationEventAgent(locationModel models.LocationModel, eventModel models return event, err } - return agent.eventModel.UpdateLocation(ctx, event.ID, locationId) + event, err = agent.eventModel.UpdateLocation(ctx, event.ID, locationId) + if err != nil { + return event, err + } + + return agent.eventModel.UpdateOrganizer(ctx, event.ID, organizer.ID) }, } diff --git a/backend/main.go b/backend/main.go index e2b3d84..dd19f59 100644 --- a/backend/main.go +++ b/backend/main.go @@ -80,6 +80,7 @@ func main() { locationModel := models.NewLocationModel(db) eventModel := models.NewEventModel(db) userModel := models.NewUserModel(db) + contactModel := models.NewContactModel(db) listener := pq.NewListener(os.Getenv("DB_CONNECTION"), time.Second, time.Second, func(event pq.ListenerEventType, err error) { if err != nil { @@ -108,7 +109,7 @@ func main() { panic(err) } - locationAgent, err := agents.NewLocationEventAgent(locationModel, eventModel) + locationAgent, err := agents.NewLocationEventAgent(locationModel, eventModel, contactModel) if err != nil { panic(err) } diff --git a/backend/models/contacts.go b/backend/models/contacts.go new file mode 100644 index 0000000..5684479 --- /dev/null +++ b/backend/models/contacts.go @@ -0,0 +1,70 @@ +package models + +import ( + "context" + "database/sql" + "screenmark/screenmark/.gen/haystack/haystack/model" + . "screenmark/screenmark/.gen/haystack/haystack/table" + + . "github.com/go-jet/jet/v2/postgres" + + "github.com/google/uuid" +) + +type ContactModel struct { + dbPool *sql.DB +} + +func (m ContactModel) List(ctx context.Context, userId uuid.UUID) ([]model.Contacts, error) { + listContactsStmt := SELECT(Contacts.AllColumns). + FROM( + Contacts. + INNER_JOIN(UserContacts, UserContacts.ContactID.EQ(Contacts.ID)), + ). + WHERE(UserContacts.UserID.EQ(UUID(userId))) + + locations := []model.Contacts{} + + err := listContactsStmt.QueryContext(ctx, m.dbPool, &locations) + return locations, err +} + +func (m ContactModel) Save(ctx context.Context, userId uuid.UUID, contact model.Contacts) (model.Contacts, error) { + // TODO: make this a transaction + + insertContactStmt := Contacts. + INSERT(Contacts.Name, Contacts.Description, Contacts.PhoneNumber, Contacts.Email). + VALUES(contact.Name, contact.Description, contact.PhoneNumber, contact.Email). + RETURNING(Contacts.AllColumns) + + insertedContact := model.Contacts{} + err := insertContactStmt.QueryContext(ctx, m.dbPool, &insertedContact) + + if err != nil { + return insertedContact, err + } + + insertUserContactStmt := UserContacts. + INSERT(UserContacts.UserID, UserContacts.ContactID). + VALUES(userId, insertedContact.ID) + + _, err = insertUserContactStmt.ExecContext(ctx, m.dbPool) + + return insertedContact, err +} + +func (m ContactModel) SaveToImage(ctx context.Context, imageId uuid.UUID, contactId uuid.UUID) (model.ImageContacts, error) { + insertImageContactStmt := ImageLocations. + INSERT(ImageContacts.ImageID, ImageContacts.ContactID). + VALUES(imageId, contactId). + RETURNING(ImageContacts.AllColumns) + + imageContact := model.ImageContacts{} + err := insertImageContactStmt.QueryContext(ctx, m.dbPool, &imageContact) + + return imageContact, err +} + +func NewContactModel(db *sql.DB) ContactModel { + return ContactModel{dbPool: db} +} diff --git a/backend/models/events.go b/backend/models/events.go index a4dc427..c1ef38a 100644 --- a/backend/models/events.go +++ b/backend/models/events.go @@ -62,6 +62,19 @@ func (m EventModel) UpdateLocation(ctx context.Context, eventId uuid.UUID, locat return updatedEvent, err } +func (m EventModel) UpdateOrganizer(ctx context.Context, eventId uuid.UUID, organizerId uuid.UUID) (model.Events, error) { + updateEventContactStmt := Events. + UPDATE(Events.OrganizerID). + SET(organizerId). + WHERE(Events.ID.EQ(UUID(eventId))). + RETURNING(Events.AllColumns) + + updatedEvent := model.Events{} + err := updateEventContactStmt.QueryContext(ctx, m.dbPool, &updatedEvent) + + return updatedEvent, err +} + func NewEventModel(db *sql.DB) EventModel { return EventModel{dbPool: db} } diff --git a/backend/schema.sql b/backend/schema.sql index 8dd4637..9d6f7d7 100644 --- a/backend/schema.sql +++ b/backend/schema.sql @@ -69,31 +69,6 @@ CREATE TABLE haystack.user_locations ( user_id UUID NOT NULL REFERENCES haystack.users (id) ); -CREATE TABLE haystack.events ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - - -- It seems name and description are frequent. We could use table inheritance. - name TEXT NOT NULL, - description TEXT, - - start_date_time TIMESTAMP, - end_date_time TIMESTAMP, - - location_id UUID REFERENCES haystack.locations (id) -); - -CREATE TABLE haystack.image_events ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - event_id UUID NOT NULL REFERENCES haystack.events (id), - image_id UUID NOT NULL REFERENCES haystack.image (id) -); - -CREATE TABLE haystack.user_events ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - event_id UUID NOT NULL REFERENCES haystack.events (id), - user_id UUID NOT NULL REFERENCES haystack.users (id) -); - CREATE TABLE haystack.contacts ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), @@ -111,6 +86,38 @@ CREATE TABLE haystack.user_contacts ( contact_id UUID NOT NULL REFERENCES haystack.contacts (id) ); +CREATE TABLE haystack.image_contacts ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + image_id UUID NOT NULL REFERENCES haystack.image (id), + contact_id UUID NOT NULL REFERENCES haystack.contacts (id) +); + +CREATE TABLE haystack.events ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + + -- It seems name and description are frequent. We could use table inheritance. + name TEXT NOT NULL, + description TEXT, + + start_date_time TIMESTAMP, + end_date_time TIMESTAMP, + + location_id UUID REFERENCES haystack.locations (id), + organizer_id UUID REFERENCES haystack.contacts (id) +); + +CREATE TABLE haystack.image_events ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + event_id UUID NOT NULL REFERENCES haystack.events (id), + image_id UUID NOT NULL REFERENCES haystack.image (id) +); + +CREATE TABLE haystack.user_events ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + event_id UUID NOT NULL REFERENCES haystack.events (id), + user_id UUID NOT NULL REFERENCES haystack.users (id) +); + /* -----| Indexes |----- */ CREATE INDEX user_tags_index ON haystack.user_tags(tag); diff --git a/backend/tools.json b/backend/tools.json index cdd0ec5..6867057 100644 --- a/backend/tools.json +++ b/backend/tools.json @@ -3,16 +3,13 @@ "type": "function", "function": { "name": "createLocation", - "description": "Creates a location", + "description": "Creates a location. No not use if you think an existing location is suitable!", "parameters": { "type": "object", "properties": { "name": { "type": "string" }, - "coordinates": { - "type": "string" - }, "address": { "type": "string" } @@ -43,34 +40,27 @@ "name": { "type": "string" }, - "datetime": { - "type": "string" + "startDateTime": { + "type": "string", + "description": "The start time as an ISO string" + }, + "endDateTime": { + "type": "string", + "description": "The end time as an ISO string" }, "locationId": { "type": "string", - "description": "The ID of the location, available by `listLocations`" + "description": "The ID of the location, available by listLocations" + }, + "organizerName": { + "type": "string", + "description": "The name of the organizer" } }, "required": ["name"] } } }, - { - "type": "function", - "function": { - "name": "attachImageLocation", - "description": "Add a location to an image", - "parameters": { - "type": "object", - "properties": { - "locationId": { - "type": "string" - } - }, - "required": ["locationId"] - } - } - }, { "type": "function", "function": {