feat(contacts): events can now have organizers

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

View File

@ -19,4 +19,5 @@ type Events struct {
StartDateTime *time.Time
EndDateTime *time.Time
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
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,

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)
Events = Events.FromSchema(schema)
Image = Image.FromSchema(schema)
ImageContacts = ImageContacts.FromSchema(schema)
ImageEvents = ImageEvents.FromSchema(schema)
ImageLinks = ImageLinks.FromSchema(schema)
ImageLocations = ImageLocations.FromSchema(schema)

View File

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

View File

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

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

View File

@ -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);

View File

@ -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": {