feat(contacts): events can now have organizers
This commit is contained in:
@ -19,4 +19,5 @@ type Events struct {
|
||||
StartDateTime *time.Time
|
||||
EndDateTime *time.Time
|
||||
LocationID *uuid.UUID
|
||||
OrganizerID *uuid.UUID
|
||||
}
|
||||
|
18
backend/.gen/haystack/haystack/model/image_contacts.go
Normal file
18
backend/.gen/haystack/haystack/model/image_contacts.go
Normal 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
|
||||
}
|
@ -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,
|
||||
|
81
backend/.gen/haystack/haystack/table/image_contacts.go
Normal file
81
backend/.gen/haystack/haystack/table/image_contacts.go
Normal 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,
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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.
|
||||
@ -89,9 +91,13 @@ const TOOLS = `
|
||||
"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)
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
70
backend/models/contacts.go
Normal file
70
backend/models/contacts.go
Normal 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}
|
||||
}
|
@ -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}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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": {
|
||||
|
Reference in New Issue
Block a user