creating stacks using a user request

This commit is contained in:
2025-08-20 21:38:55 +01:00
parent f5e65524aa
commit 10cea769bf
23 changed files with 598 additions and 76 deletions

View File

@ -0,0 +1,22 @@
//
// 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"
"time"
)
type ProcessingLists struct {
ID uuid.UUID `sql:"primary_key"`
UserID uuid.UUID
Title string
Fields string
Status Progress
CreatedAt *time.Time
}

View File

@ -20,10 +20,11 @@ type imageTable struct {
ID postgres.ColumnString ID postgres.ColumnString
ImageName postgres.ColumnString ImageName postgres.ColumnString
Description postgres.ColumnString Description postgres.ColumnString
Image postgres.ColumnString Image postgres.ColumnBytea
AllColumns postgres.ColumnList AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
} }
type ImageTable struct { type ImageTable struct {
@ -64,9 +65,10 @@ func newImageTableImpl(schemaName, tableName, alias string) imageTable {
IDColumn = postgres.StringColumn("id") IDColumn = postgres.StringColumn("id")
ImageNameColumn = postgres.StringColumn("image_name") ImageNameColumn = postgres.StringColumn("image_name")
DescriptionColumn = postgres.StringColumn("description") DescriptionColumn = postgres.StringColumn("description")
ImageColumn = postgres.StringColumn("image") ImageColumn = postgres.ByteaColumn("image")
allColumns = postgres.ColumnList{IDColumn, ImageNameColumn, DescriptionColumn, ImageColumn} allColumns = postgres.ColumnList{IDColumn, ImageNameColumn, DescriptionColumn, ImageColumn}
mutableColumns = postgres.ColumnList{ImageNameColumn, DescriptionColumn, ImageColumn} mutableColumns = postgres.ColumnList{ImageNameColumn, DescriptionColumn, ImageColumn}
defaultColumns = postgres.ColumnList{IDColumn}
) )
return imageTable{ return imageTable{
@ -80,5 +82,6 @@ func newImageTableImpl(schemaName, tableName, alias string) imageTable {
AllColumns: allColumns, AllColumns: allColumns,
MutableColumns: mutableColumns, MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
} }
} }

View File

@ -23,6 +23,7 @@ type imageListsTable struct {
AllColumns postgres.ColumnList AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
} }
type ImageListsTable struct { type ImageListsTable struct {
@ -65,6 +66,7 @@ func newImageListsTableImpl(schemaName, tableName, alias string) imageListsTable
ListIDColumn = postgres.StringColumn("list_id") ListIDColumn = postgres.StringColumn("list_id")
allColumns = postgres.ColumnList{IDColumn, ImageIDColumn, ListIDColumn} allColumns = postgres.ColumnList{IDColumn, ImageIDColumn, ListIDColumn}
mutableColumns = postgres.ColumnList{ImageIDColumn, ListIDColumn} mutableColumns = postgres.ColumnList{ImageIDColumn, ListIDColumn}
defaultColumns = postgres.ColumnList{IDColumn}
) )
return imageListsTable{ return imageListsTable{
@ -77,5 +79,6 @@ func newImageListsTableImpl(schemaName, tableName, alias string) imageListsTable
AllColumns: allColumns, AllColumns: allColumns,
MutableColumns: mutableColumns, MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
} }
} }

View File

@ -24,6 +24,7 @@ type imageSchemaItemsTable struct {
AllColumns postgres.ColumnList AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
} }
type ImageSchemaItemsTable struct { type ImageSchemaItemsTable struct {
@ -67,6 +68,7 @@ func newImageSchemaItemsTableImpl(schemaName, tableName, alias string) imageSche
ImageIDColumn = postgres.StringColumn("image_id") ImageIDColumn = postgres.StringColumn("image_id")
allColumns = postgres.ColumnList{IDColumn, ValueColumn, SchemaItemIDColumn, ImageIDColumn} allColumns = postgres.ColumnList{IDColumn, ValueColumn, SchemaItemIDColumn, ImageIDColumn}
mutableColumns = postgres.ColumnList{ValueColumn, SchemaItemIDColumn, ImageIDColumn} mutableColumns = postgres.ColumnList{ValueColumn, SchemaItemIDColumn, ImageIDColumn}
defaultColumns = postgres.ColumnList{IDColumn}
) )
return imageSchemaItemsTable{ return imageSchemaItemsTable{
@ -80,5 +82,6 @@ func newImageSchemaItemsTableImpl(schemaName, tableName, alias string) imageSche
AllColumns: allColumns, AllColumns: allColumns,
MutableColumns: mutableColumns, MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
} }
} }

View File

@ -25,6 +25,7 @@ type listsTable struct {
AllColumns postgres.ColumnList AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
} }
type ListsTable struct { type ListsTable struct {
@ -69,6 +70,7 @@ func newListsTableImpl(schemaName, tableName, alias string) listsTable {
CreatedAtColumn = postgres.TimestampzColumn("created_at") CreatedAtColumn = postgres.TimestampzColumn("created_at")
allColumns = postgres.ColumnList{IDColumn, UserIDColumn, NameColumn, DescriptionColumn, CreatedAtColumn} allColumns = postgres.ColumnList{IDColumn, UserIDColumn, NameColumn, DescriptionColumn, CreatedAtColumn}
mutableColumns = postgres.ColumnList{UserIDColumn, NameColumn, DescriptionColumn, CreatedAtColumn} mutableColumns = postgres.ColumnList{UserIDColumn, NameColumn, DescriptionColumn, CreatedAtColumn}
defaultColumns = postgres.ColumnList{IDColumn, CreatedAtColumn}
) )
return listsTable{ return listsTable{
@ -83,5 +85,6 @@ func newListsTableImpl(schemaName, tableName, alias string) listsTable {
AllColumns: allColumns, AllColumns: allColumns,
MutableColumns: mutableColumns, MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
} }
} }

View File

@ -23,6 +23,7 @@ type logsTable struct {
AllColumns postgres.ColumnList AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
} }
type LogsTable struct { type LogsTable struct {
@ -65,6 +66,7 @@ func newLogsTableImpl(schemaName, tableName, alias string) logsTable {
CreatedAtColumn = postgres.TimestampzColumn("created_at") CreatedAtColumn = postgres.TimestampzColumn("created_at")
allColumns = postgres.ColumnList{LogColumn, ImageIDColumn, CreatedAtColumn} allColumns = postgres.ColumnList{LogColumn, ImageIDColumn, CreatedAtColumn}
mutableColumns = postgres.ColumnList{LogColumn, ImageIDColumn, CreatedAtColumn} mutableColumns = postgres.ColumnList{LogColumn, ImageIDColumn, CreatedAtColumn}
defaultColumns = postgres.ColumnList{CreatedAtColumn}
) )
return logsTable{ return logsTable{
@ -77,5 +79,6 @@ func newLogsTableImpl(schemaName, tableName, alias string) logsTable {
AllColumns: allColumns, AllColumns: allColumns,
MutableColumns: mutableColumns, MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
} }
} }

View File

@ -0,0 +1,93 @@
//
// 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 ProcessingLists = newProcessingListsTable("haystack", "processing_lists", "")
type processingListsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
UserID postgres.ColumnString
Title postgres.ColumnString
Fields postgres.ColumnString
Status postgres.ColumnString
CreatedAt postgres.ColumnTimestampz
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
}
type ProcessingListsTable struct {
processingListsTable
EXCLUDED processingListsTable
}
// AS creates new ProcessingListsTable with assigned alias
func (a ProcessingListsTable) AS(alias string) *ProcessingListsTable {
return newProcessingListsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ProcessingListsTable with assigned schema name
func (a ProcessingListsTable) FromSchema(schemaName string) *ProcessingListsTable {
return newProcessingListsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ProcessingListsTable with assigned table prefix
func (a ProcessingListsTable) WithPrefix(prefix string) *ProcessingListsTable {
return newProcessingListsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ProcessingListsTable with assigned table suffix
func (a ProcessingListsTable) WithSuffix(suffix string) *ProcessingListsTable {
return newProcessingListsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newProcessingListsTable(schemaName, tableName, alias string) *ProcessingListsTable {
return &ProcessingListsTable{
processingListsTable: newProcessingListsTableImpl(schemaName, tableName, alias),
EXCLUDED: newProcessingListsTableImpl("", "excluded", ""),
}
}
func newProcessingListsTableImpl(schemaName, tableName, alias string) processingListsTable {
var (
IDColumn = postgres.StringColumn("id")
UserIDColumn = postgres.StringColumn("user_id")
TitleColumn = postgres.StringColumn("title")
FieldsColumn = postgres.StringColumn("fields")
StatusColumn = postgres.StringColumn("status")
CreatedAtColumn = postgres.TimestampzColumn("created_at")
allColumns = postgres.ColumnList{IDColumn, UserIDColumn, TitleColumn, FieldsColumn, StatusColumn, CreatedAtColumn}
mutableColumns = postgres.ColumnList{UserIDColumn, TitleColumn, FieldsColumn, StatusColumn, CreatedAtColumn}
defaultColumns = postgres.ColumnList{IDColumn, StatusColumn, CreatedAtColumn}
)
return processingListsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
UserID: UserIDColumn,
Title: TitleColumn,
Fields: FieldsColumn,
Status: StatusColumn,
CreatedAt: CreatedAtColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
}
}

View File

@ -25,6 +25,7 @@ type schemaItemsTable struct {
AllColumns postgres.ColumnList AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
} }
type SchemaItemsTable struct { type SchemaItemsTable struct {
@ -69,6 +70,7 @@ func newSchemaItemsTableImpl(schemaName, tableName, alias string) schemaItemsTab
SchemaIDColumn = postgres.StringColumn("schema_id") SchemaIDColumn = postgres.StringColumn("schema_id")
allColumns = postgres.ColumnList{IDColumn, ItemColumn, ValueColumn, DescriptionColumn, SchemaIDColumn} allColumns = postgres.ColumnList{IDColumn, ItemColumn, ValueColumn, DescriptionColumn, SchemaIDColumn}
mutableColumns = postgres.ColumnList{ItemColumn, ValueColumn, DescriptionColumn, SchemaIDColumn} mutableColumns = postgres.ColumnList{ItemColumn, ValueColumn, DescriptionColumn, SchemaIDColumn}
defaultColumns = postgres.ColumnList{IDColumn}
) )
return schemaItemsTable{ return schemaItemsTable{
@ -83,5 +85,6 @@ func newSchemaItemsTableImpl(schemaName, tableName, alias string) schemaItemsTab
AllColumns: allColumns, AllColumns: allColumns,
MutableColumns: mutableColumns, MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
} }
} }

View File

@ -22,6 +22,7 @@ type schemasTable struct {
AllColumns postgres.ColumnList AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
} }
type SchemasTable struct { type SchemasTable struct {
@ -63,6 +64,7 @@ func newSchemasTableImpl(schemaName, tableName, alias string) schemasTable {
ListIDColumn = postgres.StringColumn("list_id") ListIDColumn = postgres.StringColumn("list_id")
allColumns = postgres.ColumnList{IDColumn, ListIDColumn} allColumns = postgres.ColumnList{IDColumn, ListIDColumn}
mutableColumns = postgres.ColumnList{ListIDColumn} mutableColumns = postgres.ColumnList{ListIDColumn}
defaultColumns = postgres.ColumnList{IDColumn}
) )
return schemasTable{ return schemasTable{
@ -74,5 +76,6 @@ func newSchemasTableImpl(schemaName, tableName, alias string) schemasTable {
AllColumns: allColumns, AllColumns: allColumns,
MutableColumns: mutableColumns, MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
} }
} }

View File

@ -15,6 +15,7 @@ func UseSchema(schema string) {
ImageSchemaItems = ImageSchemaItems.FromSchema(schema) ImageSchemaItems = ImageSchemaItems.FromSchema(schema)
Lists = Lists.FromSchema(schema) Lists = Lists.FromSchema(schema)
Logs = Logs.FromSchema(schema) Logs = Logs.FromSchema(schema)
ProcessingLists = ProcessingLists.FromSchema(schema)
SchemaItems = SchemaItems.FromSchema(schema) SchemaItems = SchemaItems.FromSchema(schema)
Schemas = Schemas.FromSchema(schema) Schemas = Schemas.FromSchema(schema)
UserImages = UserImages.FromSchema(schema) UserImages = UserImages.FromSchema(schema)

View File

@ -24,6 +24,7 @@ type userImagesTable struct {
AllColumns postgres.ColumnList AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
} }
type UserImagesTable struct { type UserImagesTable struct {
@ -67,6 +68,7 @@ func newUserImagesTableImpl(schemaName, tableName, alias string) userImagesTable
CreatedAtColumn = postgres.TimestampzColumn("created_at") CreatedAtColumn = postgres.TimestampzColumn("created_at")
allColumns = postgres.ColumnList{IDColumn, ImageIDColumn, UserIDColumn, CreatedAtColumn} allColumns = postgres.ColumnList{IDColumn, ImageIDColumn, UserIDColumn, CreatedAtColumn}
mutableColumns = postgres.ColumnList{ImageIDColumn, UserIDColumn, CreatedAtColumn} mutableColumns = postgres.ColumnList{ImageIDColumn, UserIDColumn, CreatedAtColumn}
defaultColumns = postgres.ColumnList{IDColumn, CreatedAtColumn}
) )
return userImagesTable{ return userImagesTable{
@ -80,5 +82,6 @@ func newUserImagesTableImpl(schemaName, tableName, alias string) userImagesTable
AllColumns: allColumns, AllColumns: allColumns,
MutableColumns: mutableColumns, MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
} }
} }

View File

@ -24,6 +24,7 @@ type userImagesToProcessTable struct {
AllColumns postgres.ColumnList AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
} }
type UserImagesToProcessTable struct { type UserImagesToProcessTable struct {
@ -67,6 +68,7 @@ func newUserImagesToProcessTableImpl(schemaName, tableName, alias string) userIm
UserIDColumn = postgres.StringColumn("user_id") UserIDColumn = postgres.StringColumn("user_id")
allColumns = postgres.ColumnList{IDColumn, StatusColumn, ImageIDColumn, UserIDColumn} allColumns = postgres.ColumnList{IDColumn, StatusColumn, ImageIDColumn, UserIDColumn}
mutableColumns = postgres.ColumnList{StatusColumn, ImageIDColumn, UserIDColumn} mutableColumns = postgres.ColumnList{StatusColumn, ImageIDColumn, UserIDColumn}
defaultColumns = postgres.ColumnList{IDColumn, StatusColumn}
) )
return userImagesToProcessTable{ return userImagesToProcessTable{
@ -80,5 +82,6 @@ func newUserImagesToProcessTableImpl(schemaName, tableName, alias string) userIm
AllColumns: allColumns, AllColumns: allColumns,
MutableColumns: mutableColumns, MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
} }
} }

View File

@ -22,6 +22,7 @@ type usersTable struct {
AllColumns postgres.ColumnList AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
} }
type UsersTable struct { type UsersTable struct {
@ -63,6 +64,7 @@ func newUsersTableImpl(schemaName, tableName, alias string) usersTable {
EmailColumn = postgres.StringColumn("email") EmailColumn = postgres.StringColumn("email")
allColumns = postgres.ColumnList{IDColumn, EmailColumn} allColumns = postgres.ColumnList{IDColumn, EmailColumn}
mutableColumns = postgres.ColumnList{EmailColumn} mutableColumns = postgres.ColumnList{EmailColumn}
defaultColumns = postgres.ColumnList{IDColumn}
) )
return usersTable{ return usersTable{
@ -74,5 +76,6 @@ func newUsersTableImpl(schemaName, tableName, alias string) usersTable {
AllColumns: allColumns, AllColumns: allColumns,
MutableColumns: mutableColumns, MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
} }
} }

View File

@ -187,6 +187,15 @@ func (chat *Chat) AddSystem(prompt string) {
}) })
} }
func (chat *Chat) AddUser(msg string) {
chat.Messages = append(chat.Messages, ChatUserMessage{
Role: User,
MessageContent: SingleMessage{
Content: msg,
},
})
}
func (chat *Chat) AddImage(imageName string, image []byte, query *string) error { func (chat *Chat) AddImage(imageName string, image []byte, query *string) error {
extension := filepath.Ext(imageName) extension := filepath.Ext(imageName)
if len(extension) == 0 { if len(extension) == 0 {

View File

@ -270,3 +270,38 @@ func (client *AgentClient) RunAgent(userId uuid.UUID, imageId uuid.UUID, imageNa
return client.ToolLoop(toolHandlerInfo, &request) return client.ToolLoop(toolHandlerInfo, &request)
} }
func (client *AgentClient) RunAgentAlone(userID uuid.UUID, userReq string) error {
var tools any
err := json.Unmarshal([]byte(client.Options.JsonTools), &tools)
if err != nil {
return err
}
toolChoice := "auto"
seed := 42
request := AgentRequestBody{
Tools: &tools,
ToolChoice: &toolChoice,
Model: "google/gemini-2.5-flash",
RandomSeed: &seed,
Temperature: 0.3,
EndToolCall: client.Options.EndToolCall,
ResponseFormat: ResponseFormat{
Type: "text",
},
Chat: &Chat{
Messages: make([]ChatMessage, 0),
},
}
request.Chat.AddSystem(client.Options.SystemPrompt)
request.Chat.AddUser(userReq)
toolHandlerInfo := ToolHandlerInfo{
UserId: userID,
}
return client.ToolLoop(toolHandlerInfo, &request)
}

View File

@ -0,0 +1,140 @@
package agents
import (
"context"
"encoding/json"
"fmt"
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/agents/client"
"screenmark/screenmark/models"
"github.com/charmbracelet/log"
"github.com/google/uuid"
)
const createListAgentPrompt = `
You are an agent who's job is to produce a reasonable output for an unstructured input.
Your job is to create lists for the user, the user will give you a title and some fields they want
as part of the list. Your job is to take these fields, adjust their names so they have good names,
and add a good description for each one.
You can add fields if you think they make a lot of sense.
You can remove fields if they are not correct, but be sure before you do this.
`
const listJsonSchema = `
{
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "the title of the list"
},
"description": {
"type": "string",
"description": "the description of the list"
},
"fields": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the field."
},
"description": {
"type": "string",
"description": "A description of the field."
}
},
"required": [
"name",
"description"
]
},
"description": "An array of field objects."
}
},
"required": [
"fields"
]
}
`
type createNewListArguments struct {
Title string `json:"title"`
Description string `json:"description"`
Fields []struct {
Name string `json:"name"`
Description string `json:"description"`
} `json:"fields"`
}
type CreateListAgent struct {
client client.AgentClient
listModel models.ListModel
}
func (agent *CreateListAgent) CreateList(log *log.Logger, userID uuid.UUID, userReq string) error {
request := client.AgentRequestBody{
Model: "google/gemini-2.5-flash",
Temperature: 0.3,
ResponseFormat: client.ResponseFormat{
Type: "json_object",
JsonSchema: listJsonSchema,
},
Chat: &client.Chat{
Messages: make([]client.ChatMessage, 0),
},
}
request.Chat.AddSystem(agent.client.Options.SystemPrompt)
request.Chat.AddUser(userReq)
resp, err := agent.client.Request(&request)
if err != nil {
return fmt.Errorf("request: %w", err)
}
ctx := context.Background()
structuredOutput := resp.Choices[0].Message.Content
var createListArgs createNewListArguments
err = json.Unmarshal([]byte(structuredOutput), &createListArgs)
if err != nil {
return err
}
schemaItems := make([]model.SchemaItems, 0)
for _, field := range createListArgs.Fields {
schemaItems = append(schemaItems, model.SchemaItems{
Item: field.Name,
Description: field.Description,
Value: "string", // keep it simple for now.
})
}
agent.listModel.Save(ctx, userID, createListArgs.Title, createListArgs.Description, schemaItems)
return nil
}
func NewCreateListAgent(log *log.Logger, listModel models.ListModel) CreateListAgent {
client := client.CreateAgentClient(client.CreateAgentClientOptions{
SystemPrompt: createListAgentPrompt,
Log: log,
})
agent := CreateListAgent{
client,
listModel,
}
return agent
}

View File

@ -186,10 +186,6 @@ func NewListAgent(log *log.Logger, listModel models.ListModel) client.AgentClien
return "Thought", nil return "Thought", nil
}) })
agentClient.ToolHandler.AddTool("listLists", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
return listModel.List(context.Background(), info.UserId)
})
agentClient.ToolHandler.AddTool("createList", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) { agentClient.ToolHandler.AddTool("createList", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
args := createListArguments{} args := createListArguments{}
err := json.Unmarshal([]byte(_args), &args) err := json.Unmarshal([]byte(_args), &args)
@ -210,6 +206,10 @@ func NewListAgent(log *log.Logger, listModel models.ListModel) client.AgentClien
return savedList, nil return savedList, nil
}) })
agentClient.ToolHandler.AddTool("listLists", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
return listModel.List(context.Background(), info.UserId)
})
agentClient.ToolHandler.AddTool("addToList", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) { agentClient.ToolHandler.AddTool("addToList", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
args := addToListArguments{} args := addToListArguments{}
err := json.Unmarshal([]byte(_args), &args) err := json.Unmarshal([]byte(_args), &args)

View File

@ -140,6 +140,57 @@ func ListenProcessingImageStatus(db *sql.DB, images models.ImageModel, notifier
} }
} }
func ListenNewStackEvents(db *sql.DB) {
listener := pq.NewListener(os.Getenv("DB_CONNECTION"), time.Second, time.Second, func(event pq.ListenerEventType, err error) {
if err != nil {
panic(err)
}
})
defer listener.Close()
stackModel := models.NewListModel(db)
newStacksLogger := createLogger("New Stacks 🤖", os.Stdout)
newStacksLogger.SetLevel(log.DebugLevel)
err := listener.Listen("new_stack")
if err != nil {
panic(err)
}
for parameters := range listener.Notify {
stackID := uuid.MustParse(parameters.Extra)
newStacksLogger.Debug("Starting processing stack", "StackID", stackID)
ctx := context.Background()
go func() {
stack, err := stackModel.GetProcessing(ctx, stackID)
if err != nil {
newStacksLogger.Error("failed to get processing", "error", err)
return
}
if err := stackModel.StartProcessing(ctx, stackID); err != nil {
newStacksLogger.Error("failed to start processing", "error", err)
return
}
listAgent := agents.NewCreateListAgent(newStacksLogger, stackModel)
userListRequest := fmt.Sprintf("title=%s,fields=%s", stack.Title, stack.Fields)
err = listAgent.CreateList(newStacksLogger, stack.UserID, userListRequest)
if err != nil {
newStacksLogger.Error("running agent", "err", err)
return
}
newStacksLogger.Debug("Finished processing stack", "StackID", stackID)
}()
}
}
/* /*
* TODO: We have channels open every a user sends an image. * TODO: We have channels open every a user sends an image.
* We never close these channels. * We never close these channels.

View File

@ -57,6 +57,7 @@ func main() {
go ListenNewImageEvents(db, &notifier) go ListenNewImageEvents(db, &notifier)
go ListenProcessingImageStatus(db, imageModel, &notifier) go ListenProcessingImageStatus(db, imageModel, &notifier)
go ListenNewStackEvents(db)
r := chi.NewRouter() r := chi.NewRouter()

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"github.com/charmbracelet/log"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -15,7 +16,7 @@ func CorsMiddleware(next http.Handler) http.Handler {
w.Header().Add("Access-Control-Allow-Headers", "*") w.Header().Add("Access-Control-Allow-Headers", "*")
// Access-Control-Allow-Methods is often needed for preflight OPTIONS requests // Access-Control-Allow-Methods is often needed for preflight OPTIONS requests
w.Header().Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Add("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
// The client makes an OPTIONS preflight request before a complex request. // The client makes an OPTIONS preflight request before a complex request.
// We must handle this and respond with the appropriate headers. // We must handle this and respond with the appropriate headers.
@ -30,15 +31,19 @@ func CorsMiddleware(next http.Handler) http.Handler {
const USER_ID = "UserID" const USER_ID = "UserID"
func GetUserID(ctx context.Context) (uuid.UUID, error) { func GetUserID(ctx context.Context, logger *log.Logger, w http.ResponseWriter) (uuid.UUID, error) {
userId := ctx.Value(USER_ID) userId := ctx.Value(USER_ID)
if userId == nil { if userId == nil {
w.WriteHeader(http.StatusUnauthorized)
logger.Warn("UserID not present in request")
return uuid.Nil, errors.New("context does not contain a user id") return uuid.Nil, errors.New("context does not contain a user id")
} }
userIdUuid, ok := userId.(uuid.UUID) userIdUuid, ok := userId.(uuid.UUID)
if !ok { if !ok {
w.WriteHeader(http.StatusUnauthorized)
logger.Warn("UserID not of correct type")
return uuid.Nil, fmt.Errorf("context user id is not of type uuid, got: %t", userId) return uuid.Nil, fmt.Errorf("context user id is not of type uuid, got: %t", userId)
} }
@ -87,3 +92,25 @@ func GetUserIdFromUrl(next http.Handler) http.Handler {
next.ServeHTTP(w, newR) next.ServeHTTP(w, newR)
}) })
} }
func GetPathParamID(logger *log.Logger, param string, w http.ResponseWriter, r *http.Request) (uuid.UUID, error) {
pathParam := r.PathValue(param)
if len(pathParam) == 0 {
w.WriteHeader(http.StatusBadRequest)
err := fmt.Errorf("%s was not present", param)
logger.Warn(err)
return uuid.Nil, err
}
uuidParam, err := uuid.Parse(pathParam)
if len(pathParam) == 0 {
w.WriteHeader(http.StatusBadRequest)
err := fmt.Errorf("could not parse param: %w", err)
logger.Warn(err)
return uuid.Nil, err
}
return uuidParam, nil
}

View File

@ -26,6 +26,90 @@ type ListWithItems struct {
} }
} }
type ImageWithSchema struct {
model.ImageLists
Items []model.ImageSchemaItems
}
type IDValue struct {
ID string `json:"id"`
Value string `json:"value"`
}
// ========================================
// SELECT for lists
// ========================================
func (m ListModel) List(ctx context.Context, userId uuid.UUID) ([]ListWithItems, error) {
getListsWithItems := SELECT(
Lists.AllColumns,
Schemas.AllColumns,
SchemaItems.AllColumns,
).
FROM(
Lists.
INNER_JOIN(Schemas, Schemas.ListID.EQ(Lists.ID)).
INNER_JOIN(SchemaItems, SchemaItems.SchemaID.EQ(Schemas.ID)),
).
WHERE(Lists.UserID.EQ(UUID(userId)))
lists := []ListWithItems{}
err := getListsWithItems.QueryContext(ctx, m.dbPool, &lists)
return lists, err
}
func (m ListModel) ListItems(ctx context.Context, listID uuid.UUID) ([]ImageWithSchema, error) {
getListItems := SELECT(
ImageLists.AllColumns,
ImageSchemaItems.AllColumns,
).
FROM(
ImageLists.
INNER_JOIN(ImageSchemaItems, ImageSchemaItems.ImageID.EQ(ImageLists.ImageID)),
).
WHERE(ImageLists.ListID.EQ(UUID(listID)))
listItems := make([]ImageWithSchema, 0)
err := getListItems.QueryContext(ctx, m.dbPool, &listItems)
return listItems, err
}
// ========================================
// SELECT for specific items
// ========================================
func (m ListModel) GetProcessing(ctx context.Context, processingListID uuid.UUID) (model.ProcessingLists, error) {
getProcessingListStmt := ProcessingLists.
SELECT(ProcessingLists.AllColumns).
WHERE(ProcessingLists.ID.EQ(UUID(processingListID)))
list := model.ProcessingLists{}
err := getProcessingListStmt.QueryContext(ctx, m.dbPool, &list)
return list, err
}
// ========================================
// UPDATE
// ========================================
func (m ListModel) StartProcessing(ctx context.Context, processingListID uuid.UUID) error {
startProcessingStmt := ProcessingLists.
UPDATE(ProcessingLists.Status).
SET(model.Progress_InProgress).
WHERE(ProcessingLists.ID.EQ(UUID(processingListID)))
_, err := startProcessingStmt.ExecContext(ctx, m.dbPool)
return err
}
// ========================================
// INSERT methods
// ========================================
func (m ListModel) Save(ctx context.Context, userId uuid.UUID, name string, description string, schemaItems []model.SchemaItems) (ListWithItems, error) { func (m ListModel) Save(ctx context.Context, userId uuid.UUID, name string, description string, schemaItems []model.SchemaItems) (ListWithItems, error) {
tx, err := m.dbPool.BeginTx(ctx, nil) tx, err := m.dbPool.BeginTx(ctx, nil)
@ -86,53 +170,6 @@ func (m ListModel) Save(ctx context.Context, userId uuid.UUID, name string, desc
return listWithItems, err return listWithItems, err
} }
func (m ListModel) List(ctx context.Context, userId uuid.UUID) ([]ListWithItems, error) {
getListsWithItems := SELECT(
Lists.AllColumns,
Schemas.AllColumns,
SchemaItems.AllColumns,
).
FROM(
Lists.
INNER_JOIN(Schemas, Schemas.ListID.EQ(Lists.ID)).
INNER_JOIN(SchemaItems, SchemaItems.SchemaID.EQ(Schemas.ID)),
).
WHERE(Lists.UserID.EQ(UUID(userId)))
lists := []ListWithItems{}
err := getListsWithItems.QueryContext(ctx, m.dbPool, &lists)
return lists, err
}
type ImageWithSchema struct {
model.ImageLists
Items []model.ImageSchemaItems
}
func (m ListModel) ListItems(ctx context.Context, listID uuid.UUID) ([]ImageWithSchema, error) {
getListItems := SELECT(
ImageLists.AllColumns,
ImageSchemaItems.AllColumns,
).
FROM(
ImageLists.
INNER_JOIN(ImageSchemaItems, ImageSchemaItems.ImageID.EQ(ImageLists.ImageID)),
).
WHERE(ImageLists.ListID.EQ(UUID(listID)))
listItems := make([]ImageWithSchema, 0)
err := getListItems.QueryContext(ctx, m.dbPool, &listItems)
return listItems, err
}
type IDValue struct {
ID string `json:"id"`
Value string `json:"value"`
}
func (m ListModel) SaveInto(ctx context.Context, listId uuid.UUID, imageId uuid.UUID, schemaValues []IDValue) error { func (m ListModel) SaveInto(ctx context.Context, listId uuid.UUID, imageId uuid.UUID, schemaValues []IDValue) error {
imageSchemaItems := make([]model.ImageSchemaItems, len(schemaValues)) imageSchemaItems := make([]model.ImageSchemaItems, len(schemaValues))
@ -175,6 +212,16 @@ func (m ListModel) SaveInto(ctx context.Context, listId uuid.UUID, imageId uuid.
return err return err
} }
func (m ListModel) SaveProcessing(ctx context.Context, userID uuid.UUID, title string, fields string) error {
insertListToProcess := ProcessingLists.
INSERT(ProcessingLists.UserID, ProcessingLists.Title, ProcessingLists.Fields).
VALUES(userID, title, fields)
_, err := insertListToProcess.ExecContext(ctx, m.dbPool)
return err
}
func NewListModel(db *sql.DB) ListModel { func NewListModel(db *sql.DB) ListModel {
return ListModel{dbPool: db} return ListModel{dbPool: db}
} }

View File

@ -52,6 +52,18 @@ CREATE TABLE haystack.lists (
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
); );
CREATE TABLE haystack.processing_lists (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES haystack.users (id),
title TEXT NOT NULL,
fields TEXT NOT NULL,
status haystack.progress NOT NULL DEFAULT 'not-started',
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
CREATE TABLE haystack.image_lists ( CREATE TABLE haystack.image_lists (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@ -104,6 +116,14 @@ PERFORM pg_notify('new_processing_image_status', NEW.id::text || NEW.status::tex
END END
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION notify_new_stacks()
RETURNS TRIGGER AS $$
BEGIN
PERFORM pg_notify('new_stack', NEW.id::text);
RETURN NEW;
END
$$ LANGUAGE plpgsql;
/* -----| Triggers |----- */ /* -----| Triggers |----- */
CREATE OR REPLACE TRIGGER on_new_image AFTER INSERT CREATE OR REPLACE TRIGGER on_new_image AFTER INSERT
@ -117,4 +137,9 @@ ON haystack.user_images_to_process
FOR EACH ROW FOR EACH ROW
EXECUTE PROCEDURE notify_new_processing_image_status(); EXECUTE PROCEDURE notify_new_processing_image_status();
CREATE OR REPLACE TRIGGER on_new_image AFTER INSERT
ON haystack.processing_lists
FOR EACH ROW
EXECUTE PROCEDURE notify_new_stacks();
/* -----| Test Data |----- */ /* -----| Test Data |----- */

View File

@ -3,6 +3,7 @@ package stacks
import ( import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"io"
"net/http" "net/http"
"os" "os"
"screenmark/screenmark/middleware" "screenmark/screenmark/middleware"
@ -10,7 +11,6 @@ import (
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/google/uuid"
) )
func writeJsonOrError[K any](logger *log.Logger, object K, w http.ResponseWriter) { func writeJsonOrError[K any](logger *log.Logger, object K, w http.ResponseWriter) {
@ -30,26 +30,36 @@ type StackHandler struct {
stackModel models.ListModel stackModel models.ListModel
} }
func (h *StackHandler) withUserID( func withValidatedPost[K any](
fn func(userID uuid.UUID, w http.ResponseWriter, r *http.Request), fn func(request K, w http.ResponseWriter, r *http.Request),
) func(w http.ResponseWriter, r *http.Request) { ) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() request := new(K)
userID, err := middleware.GetUserID(ctx) body, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
h.logger.Warn("could not get users in get all stacks", "err", err) w.WriteHeader(http.StatusBadRequest)
w.WriteHeader(http.StatusUnauthorized)
return return
} }
fn(userID, w, r) err = json.Unmarshal(body, request)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
fn(*request, w, r)
} }
} }
func (h *StackHandler) getAllStacks(userID uuid.UUID, w http.ResponseWriter, r *http.Request) { func (h *StackHandler) getAllStacks(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
userID, err := middleware.GetUserID(ctx, h.logger, w)
if err != nil {
return
}
lists, err := h.stackModel.List(ctx, userID) lists, err := h.stackModel.List(ctx, userID)
if err != nil { if err != nil {
h.logger.Warn("could not get stacks", "err", err) h.logger.Warn("could not get stacks", "err", err)
@ -60,26 +70,21 @@ func (h *StackHandler) getAllStacks(userID uuid.UUID, w http.ResponseWriter, r *
writeJsonOrError(h.logger, lists, w) writeJsonOrError(h.logger, lists, w)
} }
func (h *StackHandler) getStackItems(userID uuid.UUID, w http.ResponseWriter, r *http.Request) { func (h *StackHandler) getStackItems(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
_, err := middleware.GetUserID(ctx, h.logger, w)
listID := r.PathValue("listID") if err != nil {
if len(listID) == 0 {
h.logger.Warn("listID is not present in path")
w.WriteHeader(http.StatusBadRequest)
return return
} }
uuidListID, err := uuid.Parse(listID) listID, err := middleware.GetPathParamID(h.logger, "listID", w, r)
if err != nil { if err != nil {
h.logger.Warn("could not parse list id uuid", "err", err)
w.WriteHeader(http.StatusUnauthorized)
return return
} }
// TODO: must check for permission here. // TODO: must check for permission here.
lists, err := h.stackModel.ListItems(ctx, uuidListID) lists, err := h.stackModel.ListItems(ctx, listID)
if err != nil { if err != nil {
h.logger.Warn("could not get list items", "err", err) h.logger.Warn("could not get list items", "err", err)
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
@ -89,6 +94,38 @@ func (h *StackHandler) getStackItems(userID uuid.UUID, w http.ResponseWriter, r
writeJsonOrError(h.logger, lists, w) writeJsonOrError(h.logger, lists, w)
} }
type EditStack struct {
Hello string `json:"hello"`
}
func (h *StackHandler) editStack(req EditStack, w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
}
type CreateStackBody struct {
Title string `json:"title"`
// We want a regular string because AI will take care of creating these for us.
Fields string `json:"fields"`
}
func (h *StackHandler) createStack(body CreateStackBody, w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
userID, err := middleware.GetUserID(ctx, h.logger, w)
if err != nil {
return
}
err = h.stackModel.SaveProcessing(ctx, userID, body.Title, body.Fields)
if err != nil {
h.logger.Warn("could not save processing", "err", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *StackHandler) CreateRoutes(r chi.Router) { func (h *StackHandler) CreateRoutes(r chi.Router) {
h.logger.Info("Mounting stack router") h.logger.Info("Mounting stack router")
@ -96,8 +133,12 @@ func (h *StackHandler) CreateRoutes(r chi.Router) {
r.Use(middleware.ProtectedRoute) r.Use(middleware.ProtectedRoute)
r.Use(middleware.SetJson) r.Use(middleware.SetJson)
r.Get("/", h.withUserID(h.getAllStacks)) r.Get("/", h.getAllStacks)
r.Get("/{listID}", h.withUserID(h.getStackItems)) r.Get("/{listID}", h.getStackItems)
r.Post("/", withValidatedPost(h.createStack))
r.Patch("/{listID}", withValidatedPost(h.editStack))
}) })
} }