From f0477ed7206ddd31fef8fb245254495ec5a661d6 Mon Sep 17 00:00:00 2001 From: John Costa Date: Sun, 13 Apr 2025 16:28:40 +0100 Subject: [PATCH 1/6] fix(email) --- backend/email.go | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/email.go b/backend/email.go index 03c6fb9..fc91505 100644 --- a/backend/email.go +++ b/backend/email.go @@ -56,6 +56,7 @@ func CreateMailClient() (Mailer, error) { client, err := mail.NewClient( "smtp.mailbox.org", + mail.WithTLSPortPolicy(mail.TLSMandatory), mail.WithSMTPAuth(mail.SMTPAuthPlain), mail.WithUsername(os.Getenv("EMAIL_USERNAME")), mail.WithPassword(os.Getenv("EMAIL_PASSWORD")), From b57968b9383d22c1df5d9a85c0985afd09ac442f Mon Sep 17 00:00:00 2001 From: John Costa Date: Sun, 13 Apr 2025 15:02:32 +0100 Subject: [PATCH 2/6] feat(location): agent to create locations --- backend/agents/location_agent.go | 179 +++++++++++++++++++++++++++++++ backend/agents/orchestrator.go | 53 +++++---- backend/events.go | 13 ++- backend/models/locations.go | 24 ++++- 4 files changed, 231 insertions(+), 38 deletions(-) create mode 100644 backend/agents/location_agent.go diff --git a/backend/agents/location_agent.go b/backend/agents/location_agent.go new file mode 100644 index 0000000..25da2e0 --- /dev/null +++ b/backend/agents/location_agent.go @@ -0,0 +1,179 @@ +package agents + +import ( + "context" + "encoding/json" + "os" + "screenmark/screenmark/.gen/haystack/haystack/model" + "screenmark/screenmark/agents/client" + "screenmark/screenmark/models" + "time" + + "github.com/charmbracelet/log" + "github.com/google/uuid" +) + +const locationPrompt = ` +You are an agent. + +The user will send you images and you have to identify if they have any location or a place. This could a picture of a real place, an address, or it's name. + +There are various tools you can use to perform this task. + +listLocations +Lists the users already existing locations, you should do this before using createLocation to avoid creating duplicates. + +createLocation +Use this to create a new location. Avoid making duplicates and only create a new location if listLocations doesnt contain the location on the image. + +linkLocation +Links an image to a location. + +finish +Call when there is nothing else to do. +` + +const locationTools = ` +[ + { + "type": "function", + "function": { + "name": "listLocations", + "description": "List the locations the user already has.", + "parameters": { + "type": "object", + "properties": {}, + "required": [] + } + } + }, + { + "type": "function", + "function": { + "name": "createLocation", + "description": "Use to create a new location", + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "address": { + "type": "string" + } + }, + "required": ["name"] + } + } + }, + { + "type": "function", + "function": { + "name": "linkLocation", + "description": "Use to link an already existing location to the image you were sent", + "parameters": { + "type": "object", + "properties": { + "locationId": { + "type": "string" + } + }, + "required": ["locationId"] + } + } + }, + { + "type": "function", + "function": { + "name": "finish", + "description": "Call this when there is nothing left to do.", + "parameters": { + "type": "object", + "properties": {}, + "required": [] + } + } + } +]` + +type LocationAgent struct { + client client.AgentClient + + locationModel models.LocationModel +} + +type listLocationArguments struct{} +type createLocationArguments struct { + Name string `json:"name"` + Address *string `json:"address"` +} +type linkLocationArguments struct { + LocationID string `json:"locationId"` +} + +func NewLocationAgent(locationModel models.LocationModel) (LocationAgent, error) { + agentClient, err := client.CreateAgentClient(log.NewWithOptions(os.Stdout, log.Options{ + ReportTimestamp: true, + TimeFormat: time.Kitchen, + Prefix: "Locations 📍", + })) + + if err != nil { + return LocationAgent{}, err + } + + agent := LocationAgent{ + client: agentClient, + locationModel: locationModel, + } + + agentClient.ToolHandler.AddTool("listLocations", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) { + return agent.locationModel.List(context.Background(), info.UserId) + }) + + agentClient.ToolHandler.AddTool("createLocation", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) { + args := createLocationArguments{} + err := json.Unmarshal([]byte(_args), &args) + if err != nil { + return model.Locations{}, err + } + + ctx := context.Background() + + location, err := agent.locationModel.Save(ctx, info.UserId, model.Locations{ + Name: args.Name, + Address: args.Address, + }) + + if err != nil { + return model.Locations{}, err + } + + _, err = agent.locationModel.SaveToImage(ctx, info.ImageId, location.ID) + if err != nil { + return model.Locations{}, err + } + + return location, nil + }) + + agentClient.ToolHandler.AddTool("linkLocation", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) { + args := linkLocationArguments{} + err := json.Unmarshal([]byte(_args), &args) + if err != nil { + return "", err + } + + ctx := context.Background() + + contactUuid, err := uuid.Parse(args.LocationID) + if err != nil { + return "", err + } + + agent.locationModel.SaveToImage(ctx, info.ImageId, contactUuid) + return "Saved", nil + }) + + return agent, nil +} diff --git a/backend/agents/orchestrator.go b/backend/agents/orchestrator.go index 9868bef..19a68d7 100644 --- a/backend/agents/orchestrator.go +++ b/backend/agents/orchestrator.go @@ -20,17 +20,15 @@ The agents are available as tool calls. Agents available: -eventLocationAgent -Use it when you think the image contains an event or a location of any sort. This can be an event page, a map, an address or a date. -This could also be a conversation describing an event. - noteAgent Use when there is ANY text on the image. contactAgent - Use it when the image contains information relating a person. +locationAgent +Use it when the image contains some address or a place. + noAction When you think there is no more information to extract from the image. @@ -41,18 +39,6 @@ Do not call the agent if you do not think it is relevant for the image. const OrchestratorTools = ` [ - { - "type": "function", - "function": { - "name": "eventLocationAgent", - "description": "Use when there is an event or location on the image. This could be in writing form", - "parameters": { - "type": "object", - "properties": {}, - "required": [] - } - } - }, { "type": "function", "function": { @@ -76,6 +62,18 @@ const OrchestratorTools = ` "required": [] } } + }, + { + "type": "function", + "function": { + "name": "locationAgent", + "description": "Use when then image contains some place, location or address", + "parameters": { + "type": "object", + "properties": {}, + "required": [] + } + } }, { "type": "function", @@ -101,7 +99,7 @@ type Status struct { Ok bool `json:"ok"` } -func NewOrchestratorAgent(eventLocationAgent EventLocationAgent, noteAgent NoteAgent, contactAgent ContactAgent, imageName string, imageData []byte) (OrchestratorAgent, error) { +func NewOrchestratorAgent(noteAgent NoteAgent, contactAgent ContactAgent, locationAgent LocationAgent, imageName string, imageData []byte) (OrchestratorAgent, error) { agent, err := client.CreateAgentClient(log.NewWithOptions(os.Stdout, log.Options{ ReportTimestamp: true, TimeFormat: time.Kitchen, @@ -112,17 +110,6 @@ func NewOrchestratorAgent(eventLocationAgent EventLocationAgent, noteAgent NoteA return OrchestratorAgent{}, err } - agent.ToolHandler.AddTool("eventLocationAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) { - // We need a way to keep track of this async? - // Probably just a DB, because we don't want to wait. The orchistrator shouldnt wait for this stuff to finish. - - go eventLocationAgent.client.RunAgent(eventLocationPrompt, eventLocationTools, "finish", info.UserId, info.ImageId, imageName, imageData) - - return Status{ - Ok: true, - }, nil - }) - agent.ToolHandler.AddTool("noteAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) { go noteAgent.GetNotes(info.UserId, info.ImageId, imageName, imageData) @@ -139,6 +126,14 @@ func NewOrchestratorAgent(eventLocationAgent EventLocationAgent, noteAgent NoteA }, nil }) + agent.ToolHandler.AddTool("locationAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) { + go locationAgent.client.RunAgent(locationPrompt, locationTools, "finish", info.UserId, info.ImageId, imageName, imageData) + + return Status{ + Ok: true, + }, nil + }) + agent.ToolHandler.AddTool("noAction", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) { // To nothing diff --git a/backend/events.go b/backend/events.go index 39edff8..23f496a 100644 --- a/backend/events.go +++ b/backend/events.go @@ -23,7 +23,6 @@ func ListenNewImageEvents(db *sql.DB, eventManager *EventManager) { defer listener.Close() locationModel := models.NewLocationModel(db) - eventModel := models.NewEventModel(db) noteModel := models.NewNoteModel(db) imageModel := models.NewImageModel(db) contactModel := models.NewContactModel(db) @@ -42,11 +41,6 @@ func ListenNewImageEvents(db *sql.DB, eventManager *EventManager) { ctx := context.Background() go func() { - locationAgent, err := agents.NewLocationEventAgent(locationModel, eventModel, contactModel) - if err != nil { - panic(err) - } - noteAgent, err := agents.NewNoteAgent(noteModel) if err != nil { panic(err) @@ -57,6 +51,11 @@ func ListenNewImageEvents(db *sql.DB, eventManager *EventManager) { panic(err) } + locationAgent, err := agents.NewLocationAgent(locationModel) + if err != nil { + panic(err) + } + image, err := imageModel.GetToProcessWithData(ctx, imageId) if err != nil { log.Println("Failed to GetToProcessWithData") @@ -70,7 +69,7 @@ func ListenNewImageEvents(db *sql.DB, eventManager *EventManager) { return } - orchestrator, err := agents.NewOrchestratorAgent(locationAgent, noteAgent, contactAgent, image.Image.ImageName, image.Image.Image) + orchestrator, err := agents.NewOrchestratorAgent(noteAgent, contactAgent, locationAgent, image.Image.ImageName, image.Image.Image) if err != nil { panic(err) } diff --git a/backend/models/locations.go b/backend/models/locations.go index 7f874c8..b3bfd6c 100644 --- a/backend/models/locations.go +++ b/backend/models/locations.go @@ -7,6 +7,7 @@ import ( . "screenmark/screenmark/.gen/haystack/haystack/table" . "github.com/go-jet/jet/v2/postgres" + "github.com/go-jet/jet/v2/qrm" "github.com/google/uuid" ) @@ -51,13 +52,32 @@ func (m LocationModel) Save(ctx context.Context, userId uuid.UUID, location mode } func (m LocationModel) SaveToImage(ctx context.Context, imageId uuid.UUID, locationId uuid.UUID) (model.ImageLocations, error) { + imageLocation := model.ImageLocations{} + + checkExistingStmt := ImageLocations. + SELECT(ImageLocations.AllColumns). + WHERE( + ImageLocations.ImageID.EQ(UUID(imageId)). + AND(ImageLocations.LocationID.EQ(UUID(locationId))), + ) + + err := checkExistingStmt.QueryContext(ctx, m.dbPool, &imageLocation) + if err != nil && err != qrm.ErrNoRows { + // A real error + return model.ImageLocations{}, err + } + + if err == nil { + // Already exists. + return imageLocation, nil + } + insertImageLocationStmt := ImageLocations. INSERT(ImageLocations.ImageID, ImageLocations.LocationID). VALUES(imageId, locationId). RETURNING(ImageLocations.AllColumns) - imageLocation := model.ImageLocations{} - err := insertImageLocationStmt.QueryContext(ctx, m.dbPool, &imageLocation) + err = insertImageLocationStmt.QueryContext(ctx, m.dbPool, &imageLocation) return imageLocation, err } From 17cc12f0c980ef9d0c3f9c8252c3fc9a6152f43f Mon Sep 17 00:00:00 2001 From: John Costa Date: Sun, 13 Apr 2025 15:24:53 +0100 Subject: [PATCH 3/6] feat(event): seperate event agent --- backend/agents/event_agent.go | 200 +++++++++++++++++++++++++++++++++ backend/agents/orchestrator.go | 26 ++++- backend/events.go | 8 +- backend/models/events.go | 14 +++ 4 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 backend/agents/event_agent.go diff --git a/backend/agents/event_agent.go b/backend/agents/event_agent.go new file mode 100644 index 0000000..a375ea1 --- /dev/null +++ b/backend/agents/event_agent.go @@ -0,0 +1,200 @@ +package agents + +import ( + "context" + "encoding/json" + "os" + "screenmark/screenmark/.gen/haystack/haystack/model" + "screenmark/screenmark/agents/client" + "screenmark/screenmark/models" + "time" + + "github.com/charmbracelet/log" + "github.com/google/uuid" +) + +const eventPrompt = ` +You are an agent. + +The user will send you images and you have to identify if they have any events or a place. +This could be a friend suggesting to meet, a conference, or anything that looks like an event. + +There are various tools you can use to perform this task. + +listEvents +Lists the users already existing events, you should do this before using createEvents to avoid creating duplicates. + +createEvent +Use this to create a new events. + +linkEvent +Links an image to a events. + +finish +Call when there is nothing else to do. +` + +const eventTools = ` +[ + { + "type": "function", + "function": { + "name": "listEvents", + "description": "List the events the user already has.", + "parameters": { + "type": "object", + "properties": {}, + "required": [] + } + } + }, + { + "type": "function", + "function": { + "name": "createEvent", + "description": "Use to create a new events", + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "startDateTime": { + "type": "string", + "description": "The start time as an ISO string" + }, + "endDateTime": { + "type": "string", + "description": "The end time as an ISO string" + } + }, + "required": ["name"] + } + } + }, + { + "type": "function", + "function": { + "name": "linkEvent", + "description": "Use to link an already existing events to the image you were sent", + "parameters": { + "type": "object", + "properties": { + "eventId": { + "type": "string" + } + }, + "required": ["eventsId"] + } + } + }, + { + "type": "function", + "function": { + "name": "finish", + "description": "Call this when there is nothing left to do.", + "parameters": { + "type": "object", + "properties": {}, + "required": [] + } + } + } +]` + +type EventAgent struct { + client client.AgentClient + + eventsModel models.EventModel +} + +type listEventArguments struct{} +type createEventArguments struct { + Name string `json:"name"` + StartDateTime *string `json:"startDateTime"` + EndDateTime *string `json:"endDateTime"` + OrganizerName *string `json:"organizerName"` +} +type linkEventArguments struct { + EventID string `json:"eventId"` +} + +func NewEventAgent(eventsModel models.EventModel) (EventAgent, error) { + agentClient, err := client.CreateAgentClient(log.NewWithOptions(os.Stdout, log.Options{ + ReportTimestamp: true, + TimeFormat: time.Kitchen, + Prefix: "Events 📍", + })) + + if err != nil { + return EventAgent{}, err + } + + agent := EventAgent{ + client: agentClient, + eventsModel: eventsModel, + } + + agentClient.ToolHandler.AddTool("listEvents", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) { + return agent.eventsModel.List(context.Background(), info.UserId) + }) + + agentClient.ToolHandler.AddTool("createEvent", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) { + args := createEventArguments{} + err := json.Unmarshal([]byte(_args), &args) + if err != nil { + return model.Events{}, err + } + + ctx := context.Background() + + layout := "2006-01-02T15:04:05Z" + + startTime, err := time.Parse(layout, *args.StartDateTime) + if err != nil { + return model.Events{}, err + } + + endTime, err := time.Parse(layout, *args.EndDateTime) + if err != nil { + return model.Events{}, err + } + + events, err := agent.eventsModel.Save(ctx, info.UserId, model.Events{ + Name: args.Name, + StartDateTime: &startTime, + EndDateTime: &endTime, + }) + + if err != nil { + return model.Events{}, err + } + + _, err = agent.eventsModel.SaveToImage(ctx, info.ImageId, events.ID) + if err != nil { + return model.Events{}, err + } + + return events, nil + }) + + agentClient.ToolHandler.AddTool("linkEvent", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) { + args := linkEventArguments{} + err := json.Unmarshal([]byte(_args), &args) + if err != nil { + return "", err + } + + ctx := context.Background() + + contactUuid, err := uuid.Parse(args.EventID) + if err != nil { + return "", err + } + + agent.eventsModel.SaveToImage(ctx, info.ImageId, contactUuid) + return "Saved", nil + }) + + return agent, nil +} diff --git a/backend/agents/orchestrator.go b/backend/agents/orchestrator.go index 19a68d7..8cede86 100644 --- a/backend/agents/orchestrator.go +++ b/backend/agents/orchestrator.go @@ -2,6 +2,7 @@ package agents import ( "errors" + "fmt" "os" "screenmark/screenmark/agents/client" "time" @@ -29,6 +30,9 @@ Use it when the image contains information relating a person. locationAgent Use it when the image contains some address or a place. +eventAgent +Use it when the image contains an event, this can be a date, a message suggesting an event. + noAction When you think there is no more information to extract from the image. @@ -74,6 +78,18 @@ const OrchestratorTools = ` "required": [] } } + }, + { + "type": "function", + "function": { + "name": "eventAgent", + "description": "Use when then image contains some event", + "parameters": { + "type": "object", + "properties": {}, + "required": [] + } + } }, { "type": "function", @@ -99,7 +115,7 @@ type Status struct { Ok bool `json:"ok"` } -func NewOrchestratorAgent(noteAgent NoteAgent, contactAgent ContactAgent, locationAgent LocationAgent, imageName string, imageData []byte) (OrchestratorAgent, error) { +func NewOrchestratorAgent(noteAgent NoteAgent, contactAgent ContactAgent, locationAgent LocationAgent, eventAgent EventAgent, imageName string, imageData []byte) (OrchestratorAgent, error) { agent, err := client.CreateAgentClient(log.NewWithOptions(os.Stdout, log.Options{ ReportTimestamp: true, TimeFormat: time.Kitchen, @@ -134,6 +150,14 @@ func NewOrchestratorAgent(noteAgent NoteAgent, contactAgent ContactAgent, locati }, nil }) + agent.ToolHandler.AddTool("eventAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) { + go eventAgent.client.RunAgent(eventPrompt, eventTools, "finish", info.UserId, info.ImageId, imageName, imageData) + + return Status{ + Ok: true, + }, nil + }) + agent.ToolHandler.AddTool("noAction", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) { // To nothing diff --git a/backend/events.go b/backend/events.go index 23f496a..9f7aead 100644 --- a/backend/events.go +++ b/backend/events.go @@ -23,6 +23,7 @@ func ListenNewImageEvents(db *sql.DB, eventManager *EventManager) { defer listener.Close() locationModel := models.NewLocationModel(db) + eventModel := models.NewEventModel(db) noteModel := models.NewNoteModel(db) imageModel := models.NewImageModel(db) contactModel := models.NewContactModel(db) @@ -56,6 +57,11 @@ func ListenNewImageEvents(db *sql.DB, eventManager *EventManager) { panic(err) } + eventAgent, err := agents.NewEventAgent(eventModel) + if err != nil { + panic(err) + } + image, err := imageModel.GetToProcessWithData(ctx, imageId) if err != nil { log.Println("Failed to GetToProcessWithData") @@ -69,7 +75,7 @@ func ListenNewImageEvents(db *sql.DB, eventManager *EventManager) { return } - orchestrator, err := agents.NewOrchestratorAgent(noteAgent, contactAgent, locationAgent, image.Image.ImageName, image.Image.Image) + orchestrator, err := agents.NewOrchestratorAgent(noteAgent, contactAgent, locationAgent, eventAgent, image.Image.ImageName, image.Image.Image) if err != nil { panic(err) } diff --git a/backend/models/events.go b/backend/models/events.go index c1ef38a..d473605 100644 --- a/backend/models/events.go +++ b/backend/models/events.go @@ -14,6 +14,20 @@ type EventModel struct { dbPool *sql.DB } +func (m EventModel) List(ctx context.Context, userId uuid.UUID) ([]model.Events, error) { + listEventsStmt := SELECT(Events.AllColumns). + FROM( + Events. + INNER_JOIN(UserEvents, UserEvents.EventID.EQ(Events.ID)), + ). + WHERE(UserEvents.UserID.EQ(UUID(userId))) + + events := []model.Events{} + + err := listEventsStmt.QueryContext(ctx, m.dbPool, &events) + return events, err +} + func (m EventModel) Save(ctx context.Context, userId uuid.UUID, event model.Events) (model.Events, error) { // TODO tx here insertEventStmt := Events. From fca4a6445cbd94ede882762f597dfcd0f257b7ca Mon Sep 17 00:00:00 2001 From: John Costa Date: Sun, 13 Apr 2025 15:25:07 +0100 Subject: [PATCH 4/6] chore: removing old agent that was messy and too coupled chore --- backend/agents/event_location_agent.go | 267 ------------------------- backend/agents/orchestrator.go | 1 - 2 files changed, 268 deletions(-) delete mode 100644 backend/agents/event_location_agent.go diff --git a/backend/agents/event_location_agent.go b/backend/agents/event_location_agent.go deleted file mode 100644 index 68a07b0..0000000 --- a/backend/agents/event_location_agent.go +++ /dev/null @@ -1,267 +0,0 @@ -package agents - -import ( - "context" - "encoding/json" - "os" - "screenmark/screenmark/.gen/haystack/haystack/model" - "screenmark/screenmark/agents/client" - "screenmark/screenmark/models" - "time" - - "github.com/charmbracelet/log" - "github.com/google/uuid" -) - -// This prompt is probably shit. -const eventLocationPrompt = ` -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: - -Identify and Create Locations: - -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. - -Identify and Create Events: - -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: - -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. -const eventLocationTools = ` -[ - { - "type": "function", - "function": { - "name": "createLocation", - "description": "Creates a location. No not use if you think an existing location is suitable!", - "parameters": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "address": { - "type": "string" - } - }, - "required": ["name"] - } - } - }, - { - "type": "function", - "function": { - "name": "listLocations", - "description": "Lists the locations available", - "parameters": { - "type": "object", - "properties": {} - } - } - }, - { - "type": "function", - "function": { - "name": "createEvent", - "description": "Creates a new event", - "parameters": { - "type": "object", - "properties": { - "name": { - "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" - }, - "organizerName": { - "type": "string", - "description": "The name of the organizer" - } - }, - "required": ["name"] - } - } - }, - { - "type": "function", - "function": { - "name": "finish", - "description": "Nothing else to do. call this function.", - "parameters": {} - } - } -] -` - -type EventLocationAgent struct { - client client.AgentClient - - eventModel models.EventModel - locationModel models.LocationModel - contactModel models.ContactModel - - toolHandler client.ToolsHandlers -} - -// TODO make these private - -type ListLocationArguments struct{} -type ListOrganizerArguments struct{} - -type CreateLocationArguments struct { - Name string `json:"name"` - Address *string `json:"address,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 { - LocationId string `json:"locationId"` -} - -type CreateEventArguments struct { - Name string `json:"name"` - StartDateTime string `json:"startDateTime"` - EndDateTime string `json:"endDateTime"` - LocationId string `json:"locationId"` - OrganizerName string `json:"organizerName"` -} - -func NewLocationEventAgent(locationModel models.LocationModel, eventModel models.EventModel, contactModel models.ContactModel) (EventLocationAgent, error) { - agentClient, err := client.CreateAgentClient(log.NewWithOptions(os.Stdout, log.Options{ - ReportTimestamp: true, - TimeFormat: time.Kitchen, - Prefix: "Location & Events 📅", - })) - if err != nil { - return EventLocationAgent{}, err - } - - agent := EventLocationAgent{ - client: agentClient, - locationModel: locationModel, - eventModel: eventModel, - contactModel: contactModel, - } - - agentClient.ToolHandler.AddTool("listLocations", - func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) { - return agent.locationModel.List(context.Background(), info.UserId) - }, - ) - - agentClient.ToolHandler.AddTool("createLocation", - func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) { - args := CreateLocationArguments{} - err := json.Unmarshal([]byte(_args), &args) - if err != nil { - return model.Locations{}, err - } - - ctx := context.Background() - - location, err := agent.locationModel.Save(ctx, info.UserId, model.Locations{ - Name: args.Name, - Address: args.Address, - }) - - if err != nil { - return location, err - } - - _, err = agent.locationModel.SaveToImage(ctx, info.ImageId, location.ID) - - return location, err - }, - ) - - agentClient.ToolHandler.AddTool("createEvent", - func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) { - args := CreateEventArguments{} - err := json.Unmarshal([]byte(_args), &args) - if err != nil { - return model.Locations{}, err - } - - ctx := context.Background() - - layout := "2006-01-02T15:04:05Z" - - startTime, err := time.Parse(layout, args.StartDateTime) - if err != nil { - return model.Events{}, err - } - - endTime, err := time.Parse(layout, args.EndDateTime) - if err != nil { - return model.Events{}, err - } - - event, err := agent.eventModel.Save(ctx, info.UserId, model.Events{ - Name: args.Name, - StartDateTime: &startTime, - EndDateTime: &endTime, - }) - - if err != nil { - 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 - } - - _, err = agent.contactModel.SaveToImage(ctx, info.ImageId, organizer.ID) - if err != nil { - return event, err - } - - locationId, err := uuid.Parse(args.LocationId) - if err != nil { - return event, err - } - - event, err = agent.eventModel.UpdateLocation(ctx, event.ID, locationId) - if err != nil { - return event, err - } - - return agent.eventModel.UpdateOrganizer(ctx, event.ID, organizer.ID) - }, - ) - - return agent, nil -} diff --git a/backend/agents/orchestrator.go b/backend/agents/orchestrator.go index 8cede86..7c4caca 100644 --- a/backend/agents/orchestrator.go +++ b/backend/agents/orchestrator.go @@ -2,7 +2,6 @@ package agents import ( "errors" - "fmt" "os" "screenmark/screenmark/agents/client" "time" From d687e86f86ca32a6c84ce95f9836e1262afef6cc Mon Sep 17 00:00:00 2001 From: John Costa Date: Sun, 13 Apr 2025 19:18:07 +0100 Subject: [PATCH 5/6] fix --- frontend/src/Login.tsx | 31 +++++++++++++++++-------------- frontend/src/network/index.ts | 8 ++++++-- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/frontend/src/Login.tsx b/frontend/src/Login.tsx index 344d8cd..25d3c46 100644 --- a/frontend/src/Login.tsx +++ b/frontend/src/Login.tsx @@ -1,7 +1,7 @@ import { Button } from "@kobalte/core/button"; import { TextField } from "@kobalte/core/text-field"; import { createSignal, Show, type Component } from "solid-js"; -import { postCode, postLogin } from "./network"; +import { base, postCode, postLogin } from "./network"; import { isTokenValid } from "./ProtectedRoute"; import { Navigate } from "@solidjs/router"; @@ -40,20 +40,23 @@ export const Login: Component = () => { const isAuthorized = isTokenValid(); return ( - }> -
- - Email - - - - - Code + <> + {base} + }> + + + Email - - - -
+ + + Code + + + + + +
+ ); }; diff --git a/frontend/src/network/index.ts b/frontend/src/network/index.ts index 067f3e7..f0d29c5 100644 --- a/frontend/src/network/index.ts +++ b/frontend/src/network/index.ts @@ -17,8 +17,12 @@ type BaseRequestParams = Partial<{ method: "GET" | "POST"; }>; +export const base = import.meta.env.DEV + ? "http://localhost:3040" + : "https://haystack.johncosta.tech"; + const getBaseRequest = ({ path, body, method }: BaseRequestParams): Request => { - return new Request(`http://localhost:3040/${path}`, { + return new Request(`${base}/${path}`, { body, method, }); @@ -29,7 +33,7 @@ const getBaseAuthorizedRequest = ({ body, method, }: BaseRequestParams): Request => { - return new Request(`http://localhost:3040/${path}`, { + return new Request(`${base}/${path}`, { headers: { Authorization: `Bearer ${localStorage.getItem("access")?.toString()}`, }, From 7d68f39bab3251d5853930ca41844c79f7197d73 Mon Sep 17 00:00:00 2001 From: John Costa Date: Sun, 13 Apr 2025 19:34:02 +0100 Subject: [PATCH 6/6] refactor: using tauri http client --- frontend/bun.lockb | Bin 118512 -> 118921 bytes frontend/package.json | 1 + frontend/src-tauri/Cargo.lock | 158 ++++++++++++++++++- frontend/src-tauri/Cargo.toml | 1 + frontend/src-tauri/capabilities/default.json | 7 +- frontend/src-tauri/src/lib.rs | 1 + frontend/src/network/index.ts | 2 + 7 files changed, 162 insertions(+), 8 deletions(-) diff --git a/frontend/bun.lockb b/frontend/bun.lockb index 3e7fff770f996de721d726592f7206885747c900..b495d3122fb1b7a745adafb47b963ffafb2e705c 100755 GIT binary patch delta 18820 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 b6eO;g@kX<96e}y5m&KMeD|(%?s