feat(contacts): events can now have organizers

This commit is contained in:
2025-03-31 18:40:36 +00:00
parent 0c78d741f0
commit 901f214f9d
11 changed files with 284 additions and 70 deletions

View File

@ -19,4 +19,5 @@ type Events struct {
StartDateTime *time.Time StartDateTime *time.Time
EndDateTime *time.Time EndDateTime *time.Time
LocationID *uuid.UUID LocationID *uuid.UUID
OrganizerID *uuid.UUID
} }

View File

@ -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
}

View File

@ -23,6 +23,7 @@ type eventsTable struct {
StartDateTime postgres.ColumnTimestamp StartDateTime postgres.ColumnTimestamp
EndDateTime postgres.ColumnTimestamp EndDateTime postgres.ColumnTimestamp
LocationID postgres.ColumnString LocationID postgres.ColumnString
OrganizerID postgres.ColumnString
AllColumns postgres.ColumnList AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList MutableColumns postgres.ColumnList
@ -69,8 +70,9 @@ func newEventsTableImpl(schemaName, tableName, alias string) eventsTable {
StartDateTimeColumn = postgres.TimestampColumn("start_date_time") StartDateTimeColumn = postgres.TimestampColumn("start_date_time")
EndDateTimeColumn = postgres.TimestampColumn("end_date_time") EndDateTimeColumn = postgres.TimestampColumn("end_date_time")
LocationIDColumn = postgres.StringColumn("location_id") LocationIDColumn = postgres.StringColumn("location_id")
allColumns = postgres.ColumnList{IDColumn, NameColumn, DescriptionColumn, StartDateTimeColumn, EndDateTimeColumn, LocationIDColumn} OrganizerIDColumn = postgres.StringColumn("organizer_id")
mutableColumns = postgres.ColumnList{NameColumn, DescriptionColumn, StartDateTimeColumn, EndDateTimeColumn, LocationIDColumn} allColumns = postgres.ColumnList{IDColumn, NameColumn, DescriptionColumn, StartDateTimeColumn, EndDateTimeColumn, LocationIDColumn, OrganizerIDColumn}
mutableColumns = postgres.ColumnList{NameColumn, DescriptionColumn, StartDateTimeColumn, EndDateTimeColumn, LocationIDColumn, OrganizerIDColumn}
) )
return eventsTable{ return eventsTable{
@ -83,6 +85,7 @@ func newEventsTableImpl(schemaName, tableName, alias string) eventsTable {
StartDateTime: StartDateTimeColumn, StartDateTime: StartDateTimeColumn,
EndDateTime: EndDateTimeColumn, EndDateTime: EndDateTimeColumn,
LocationID: LocationIDColumn, LocationID: LocationIDColumn,
OrganizerID: OrganizerIDColumn,
AllColumns: allColumns, AllColumns: allColumns,
MutableColumns: mutableColumns, MutableColumns: mutableColumns,

View File

@ -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,
}
}

View File

@ -13,6 +13,7 @@ func UseSchema(schema string) {
Contacts = Contacts.FromSchema(schema) Contacts = Contacts.FromSchema(schema)
Events = Events.FromSchema(schema) Events = Events.FromSchema(schema)
Image = Image.FromSchema(schema) Image = Image.FromSchema(schema)
ImageContacts = ImageContacts.FromSchema(schema)
ImageEvents = ImageEvents.FromSchema(schema) ImageEvents = ImageEvents.FromSchema(schema)
ImageLinks = ImageLinks.FromSchema(schema) ImageLinks = ImageLinks.FromSchema(schema)
ImageLocations = ImageLocations.FromSchema(schema) ImageLocations = ImageLocations.FromSchema(schema)

View File

@ -13,25 +13,27 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
// This prompt is probably shit.
const eventLocationPrompt = ` const eventLocationPrompt = `
You are an agent that extracts events and locations from an image. 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:
Your job is to check if an image has an event or a location and use the correct tools to extract this information.
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. Identify and Create Locations:
Only create an event if you see an event on the image, not all locations have an associated event.
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. 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.
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.
` `
// TODO: this should be read directly from a file on load. // TODO: this should be read directly from a file on load.
@ -80,18 +82,22 @@ const TOOLS = `
}, },
"startDateTime": { "startDateTime": {
"type": "string", "type": "string",
"description": "The start time as an ISO string" "description": "The start time as an ISO string"
}, },
"endDateTime": { "endDateTime": {
"type": "string", "type": "string",
"description": "The end time as an ISO string" "description": "The end time as an ISO string"
}, },
"locationId": { "locationId": {
"type": "string", "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", "startDateTime", "endDateTime"] "required": ["name"]
} }
} }
}, },
@ -114,11 +120,13 @@ type EventLocationAgent struct {
eventModel models.EventModel eventModel models.EventModel
locationModel models.LocationModel locationModel models.LocationModel
contactModel models.ContactModel
toolHandler ToolsHandlers toolHandler ToolsHandlers
} }
type ListLocationArguments struct{} type ListLocationArguments struct{}
type ListOrganizerArguments struct{}
type CreateLocationArguments struct { type CreateLocationArguments struct {
Name string `json:"name"` Name string `json:"name"`
@ -126,6 +134,12 @@ type CreateLocationArguments struct {
Coordinates *string `json:"coordinates,omitempty"` 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 { type AttachImageLocationArguments struct {
LocationId string `json:"locationId"` LocationId string `json:"locationId"`
} }
@ -135,6 +149,7 @@ type CreateEventArguments struct {
StartDateTime string `json:"startDateTime"` StartDateTime string `json:"startDateTime"`
EndDateTime string `json:"endDateTime"` EndDateTime string `json:"endDateTime"`
LocationId string `json:"locationId"` LocationId string `json:"locationId"`
OrganizerName string `json:"organizerName"`
} }
func (agent EventLocationAgent) GetLocations(userId uuid.UUID, imageId uuid.UUID, imageName string, imageData []byte) error { 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 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) client, err := CreateAgentClient(eventLocationPrompt)
if err != nil { if err != nil {
return EventLocationAgent{}, err return EventLocationAgent{}, err
@ -261,6 +276,7 @@ func NewLocationEventAgent(locationModel models.LocationModel, eventModel models
client: client, client: client,
locationModel: locationModel, locationModel: locationModel,
eventModel: eventModel, eventModel: eventModel,
contactModel: contactModel,
} }
toolHandler := ToolsHandlers{ toolHandler := ToolsHandlers{
@ -317,7 +333,7 @@ func NewLocationEventAgent(locationModel models.LocationModel, eventModel models
Fn: func(info ToolHandlerInfo, args CreateEventArguments, call ToolCall) (model.Events, error) { Fn: func(info ToolHandlerInfo, args CreateEventArguments, call ToolCall) (model.Events, error) {
ctx := context.Background() ctx := context.Background()
layout := "2006-01-02T15:04:05" layout := "2006-01-02T15:04:05Z"
startTime, err := time.Parse(layout, args.StartDateTime) startTime, err := time.Parse(layout, args.StartDateTime)
if err != nil { if err != nil {
@ -339,6 +355,14 @@ func NewLocationEventAgent(locationModel models.LocationModel, eventModel models
return event, err 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) _, err = agent.eventModel.SaveToImage(ctx, info.imageId, event.ID)
if err != nil { if err != nil {
return event, err return event, err
@ -349,7 +373,12 @@ func NewLocationEventAgent(locationModel models.LocationModel, eventModel models
return event, err 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)
}, },
} }

View File

@ -80,6 +80,7 @@ func main() {
locationModel := models.NewLocationModel(db) locationModel := models.NewLocationModel(db)
eventModel := models.NewEventModel(db) eventModel := models.NewEventModel(db)
userModel := models.NewUserModel(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) { listener := pq.NewListener(os.Getenv("DB_CONNECTION"), time.Second, time.Second, func(event pq.ListenerEventType, err error) {
if err != nil { if err != nil {
@ -108,7 +109,7 @@ func main() {
panic(err) panic(err)
} }
locationAgent, err := agents.NewLocationEventAgent(locationModel, eventModel) locationAgent, err := agents.NewLocationEventAgent(locationModel, eventModel, contactModel)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -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}
}

View File

@ -62,6 +62,19 @@ func (m EventModel) UpdateLocation(ctx context.Context, eventId uuid.UUID, locat
return updatedEvent, err 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 { func NewEventModel(db *sql.DB) EventModel {
return EventModel{dbPool: db} return EventModel{dbPool: db}
} }

View File

@ -69,31 +69,6 @@ CREATE TABLE haystack.user_locations (
user_id UUID NOT NULL REFERENCES haystack.users (id) 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 ( CREATE TABLE haystack.contacts (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), 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) 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 |----- */ /* -----| Indexes |----- */
CREATE INDEX user_tags_index ON haystack.user_tags(tag); CREATE INDEX user_tags_index ON haystack.user_tags(tag);

View File

@ -3,16 +3,13 @@
"type": "function", "type": "function",
"function": { "function": {
"name": "createLocation", "name": "createLocation",
"description": "Creates a location", "description": "Creates a location. No not use if you think an existing location is suitable!",
"parameters": { "parameters": {
"type": "object", "type": "object",
"properties": { "properties": {
"name": { "name": {
"type": "string" "type": "string"
}, },
"coordinates": {
"type": "string"
},
"address": { "address": {
"type": "string" "type": "string"
} }
@ -43,34 +40,27 @@
"name": { "name": {
"type": "string" "type": "string"
}, },
"datetime": { "startDateTime": {
"type": "string" "type": "string",
"description": "The start time as an ISO string"
},
"endDateTime": {
"type": "string",
"description": "The end time as an ISO string"
}, },
"locationId": { "locationId": {
"type": "string", "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"] "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", "type": "function",
"function": { "function": {