3 Commits

Author SHA1 Message Date
ece8536b72 feat: creating list processor 2025-10-05 15:02:12 +01:00
64abf79f9c feat: stack model processor 2025-10-05 14:54:24 +01:00
0d41a65435 feat: deleting column from frontend 2025-10-05 14:53:04 +01:00
10 changed files with 242 additions and 59 deletions

View File

@ -21,6 +21,8 @@ and add a good description for each one.
You can add fields if you think they make a lot of sense. 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. You can remove fields if they are not correct, but be sure before you do this.
You must respond in json format
` `
const listJsonSchema = ` const listJsonSchema = `
@ -76,10 +78,10 @@ type createNewListArguments struct {
type CreateListAgent struct { type CreateListAgent struct {
client client.AgentClient client client.AgentClient
listModel models.StackModel stackModel models.StackModel
} }
func (agent *CreateListAgent) CreateList(log *log.Logger, userID uuid.UUID, userReq string) error { func (agent *CreateListAgent) CreateList(log *log.Logger, userID uuid.UUID, stackID uuid.UUID, title string, userReq string) error {
request := client.AgentRequestBody{ request := client.AgentRequestBody{
Model: "policy/images", Model: "policy/images",
Temperature: 0.3, Temperature: 0.3,
@ -93,7 +95,10 @@ func (agent *CreateListAgent) CreateList(log *log.Logger, userID uuid.UUID, user
} }
request.Chat.AddSystem(agent.client.Options.SystemPrompt) request.Chat.AddSystem(agent.client.Options.SystemPrompt)
request.Chat.AddUser(userReq)
req := fmt.Sprintf("List title: %s | Users list description: %s", title, userReq)
request.Chat.AddUser(req)
resp, err := agent.client.Request(&request) resp, err := agent.client.Request(&request)
if err != nil { if err != nil {
@ -104,6 +109,8 @@ func (agent *CreateListAgent) CreateList(log *log.Logger, userID uuid.UUID, user
structuredOutput := resp.Choices[0].Message.Content structuredOutput := resp.Choices[0].Message.Content
log.Info("", "res", structuredOutput)
var createListArgs createNewListArguments var createListArgs createNewListArguments
err = json.Unmarshal([]byte(structuredOutput), &createListArgs) err = json.Unmarshal([]byte(structuredOutput), &createListArgs)
if err != nil { if err != nil {
@ -113,6 +120,8 @@ func (agent *CreateListAgent) CreateList(log *log.Logger, userID uuid.UUID, user
schemaItems := make([]model.SchemaItems, 0) schemaItems := make([]model.SchemaItems, 0)
for _, field := range createListArgs.Fields { for _, field := range createListArgs.Fields {
schemaItems = append(schemaItems, model.SchemaItems{ schemaItems = append(schemaItems, model.SchemaItems{
StackID: stackID,
Item: field.Name, Item: field.Name,
Description: field.Description, Description: field.Description,
@ -120,12 +129,7 @@ func (agent *CreateListAgent) CreateList(log *log.Logger, userID uuid.UUID, user
}) })
} }
_, err = agent.listModel.Save(ctx, userID, createListArgs.Title, createListArgs.Description, model.Progress_Complete) err = agent.stackModel.SaveItems(ctx, schemaItems)
if err != nil {
return fmt.Errorf("creating list agent, saving list: %w", err)
}
err = agent.listModel.SaveItems(ctx, schemaItems)
if err != nil { if err != nil {
return fmt.Errorf("creating list agent, saving items: %w", err) return fmt.Errorf("creating list agent, saving items: %w", err)
} }

View File

@ -176,7 +176,7 @@ type addToListArguments struct {
Schema []models.IDValue Schema []models.IDValue
} }
func NewListAgent(log *log.Logger, stackModel models.StackModel, limitsMethods limits.LimitsManagerMethods) client.AgentClient { func NewStackAgent(log *log.Logger, stackModel models.StackModel, limitsMethods limits.LimitsManagerMethods) client.AgentClient {
agentClient := client.CreateAgentClient(client.CreateAgentClientOptions{ agentClient := client.CreateAgentClient(client.CreateAgentClientOptions{
SystemPrompt: listPrompt, SystemPrompt: listPrompt,
JsonTools: listTools, JsonTools: listTools,

View File

@ -142,6 +142,20 @@ func (m StackModel) SaveSchemaItems(ctx context.Context, imageID uuid.UUID, item
return err return err
} }
// ========================================
// UPDATE methods
// ========================================
func (m StackModel) UpdateProcess(ctx context.Context, stackID uuid.UUID, process model.Progress) error {
updateStackProgressStmt := Stacks.UPDATE(Stacks.Status).
SET(process).
WHERE(Stacks.ID.EQ(UUID(stackID)))
_, err := updateStackProgressStmt.ExecContext(ctx, m.dbPool)
return err
}
// ======================================== // ========================================
// DELETE methods // DELETE methods
// ======================================== // ========================================

View File

@ -9,7 +9,7 @@ import (
const ( const (
IMAGE_TYPE = "image" IMAGE_TYPE = "image"
LIST_TYPE = "list" STACK_TYPE = "stack"
) )
type ImageNotification struct { type ImageNotification struct {
@ -21,18 +21,18 @@ type ImageNotification struct {
Status string Status string
} }
type ListNotification struct { type StackNotification struct {
Type string Type string
ListID uuid.UUID StackID uuid.UUID
Name string Name string
Status string Status string
} }
type Notification struct { type Notification struct {
image *ImageNotification image *ImageNotification
list *ListNotification stack *StackNotification
} }
func GetImageNotification(image ImageNotification) Notification { func GetImageNotification(image ImageNotification) Notification {
@ -41,9 +41,9 @@ func GetImageNotification(image ImageNotification) Notification {
} }
} }
func GetListNotification(list ListNotification) Notification { func GetStackNotification(list StackNotification) Notification {
return Notification{ return Notification{
list: &list, stack: &list,
} }
} }
@ -52,8 +52,8 @@ func (n Notification) MarshalJSON() ([]byte, error) {
return json.Marshal(n.image) return json.Marshal(n.image)
} }
if n.list != nil { if n.stack != nil {
return json.Marshal(n.list) return json.Marshal(n.stack)
} }
return nil, fmt.Errorf("no image or list present") return nil, fmt.Errorf("no image or list present")

View File

@ -23,11 +23,8 @@ type ImageProcessor struct {
descriptionAgent agents.DescriptionAgent descriptionAgent agents.DescriptionAgent
stackAgent client.AgentClient stackAgent client.AgentClient
// TODO: add the notifier here
Processor *Processor[model.Image] Processor *Processor[model.Image]
notifier *notifications.Notifier[notifications.Notification]
notifier *notifications.Notifier[notifications.Notification]
} }
func (p *ImageProcessor) setImageToProcess(ctx context.Context, image model.Image) { func (p *ImageProcessor) setImageToProcess(ctx context.Context, image model.Image) {
@ -128,7 +125,7 @@ func NewImageProcessor(
} }
descriptionAgent := agents.NewDescriptionAgent(logger, imageModel) descriptionAgent := agents.NewDescriptionAgent(logger, imageModel)
stackAgent := agents.NewListAgent(logger, listModel, limitsManager) stackAgent := agents.NewStackAgent(logger, listModel, limitsManager)
imageProcessor := ImageProcessor{ imageProcessor := ImageProcessor{
imageModel: imageModel, imageModel: imageModel,

127
backend/processor/stack.go Normal file
View File

@ -0,0 +1,127 @@
package processor
import (
"context"
"fmt"
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/agents"
"screenmark/screenmark/models"
"screenmark/screenmark/notifications"
"sync"
"github.com/charmbracelet/log"
)
const STACK_PROCESS_AT_A_TIME = 10
// TODO:
// This processor contains a lot of shared stuff.
// If we ever want to do more generic stuff with "in-progress" and stuff
// we can extract that into a common thing
//
// However, this will require a pretty big DB shuffle.
type StackProcessor struct {
stackModel models.StackModel
logger *log.Logger
stackAgent agents.CreateListAgent
Processor *Processor[model.Stacks]
notifier *notifications.Notifier[notifications.Notification]
}
func (p *StackProcessor) setStackToProcess(ctx context.Context, stack model.Stacks) {
err := p.stackModel.UpdateProcess(ctx, stack.ID, model.Progress_InProgress)
if err != nil {
// TODO: what can we actually do here for the errors?
// We can't stop the work for the others
p.logger.Error("failed to update stack", "err", err)
// TODO: we can use context here to actually pass some information through
return
}
}
func (p *StackProcessor) extractInfo(ctx context.Context, stack model.Stacks) {
err := p.stackAgent.CreateList(p.logger, stack.UserID, stack.ID, stack.Name, stack.Description)
if err != nil {
// Again, wtf do we do?
// Although i think the agent actually returns an error when it's finished
p.logger.Error("failed to process image", "err", err)
return
}
}
func (p *StackProcessor) processImage(stack model.Stacks) {
p.logger.Info("Processing image", "ID", stack.ID)
ctx := context.Background()
p.setStackToProcess(ctx, stack)
var wg sync.WaitGroup
// Future proofing!
wg.Add(1)
stackNotification := notifications.GetStackNotification(notifications.StackNotification{
Type: notifications.STACK_TYPE,
Status: string(model.Progress_InProgress),
StackID: stack.ID,
Name: stack.Name,
})
err := p.notifier.SendAndCreate(stack.UserID.String(), stackNotification)
if err != nil {
p.logger.Error("sending in progress notification", "err", err)
return
}
go func() {
p.extractInfo(ctx, stack)
wg.Done()
}()
wg.Wait()
// TODO: there is some repeated code here. The ergonomicts of the notifications,
// isn't the best.
stackNotification = notifications.GetStackNotification(notifications.StackNotification{
Type: notifications.STACK_TYPE,
Status: string(model.Progress_Complete),
StackID: stack.ID,
Name: stack.Name,
})
err = p.notifier.SendAndCreate(stack.UserID.String(), stackNotification)
if err != nil {
p.logger.Error("sending done notification", "err", err)
return
}
}
func NewStackProcessor(
logger *log.Logger,
stackModel models.StackModel,
notifier *notifications.Notifier[notifications.Notification],
) (StackProcessor, error) {
if notifier == nil {
return StackProcessor{}, fmt.Errorf("notifier is nil")
}
stackAgent := agents.NewCreateListAgent(logger, stackModel)
imageProcessor := StackProcessor{
logger: logger,
stackModel: stackModel,
stackAgent: stackAgent,
notifier: notifier,
}
imageProcessor.Processor = NewProcessor(int(IMAGE_PROCESS_AT_A_TIME), imageProcessor.processImage)
return imageProcessor, nil
}

View File

@ -41,9 +41,16 @@ func setupRouter(db *sql.DB, jwtManager *ourmiddleware.JwtManager) (chi.Router,
return nil, fmt.Errorf("processor: %w", err) return nil, fmt.Errorf("processor: %w", err)
} }
go imageProcessor.Processor.Work() stackProcessorLog := createLogger("Stack Processor", os.Stdout)
stackProcessor, err := processor.NewStackProcessor(stackProcessorLog, stackModel, &notifier)
if err != nil {
return nil, fmt.Errorf("processor: %w", err)
}
stackHandler := stacks.CreateStackHandler(db, limitsManager, jwtManager) go imageProcessor.Processor.Work()
go stackProcessor.Processor.Work()
stackHandler := stacks.CreateStackHandler(db, limitsManager, jwtManager, stackProcessor.Processor)
authHandler := auth.CreateAuthHandler(db, jwtManager) authHandler := auth.CreateAuthHandler(db, jwtManager)
imageHandler := images.CreateImageHandler(db, limitsManager, jwtManager, imageProcessor.Processor) imageHandler := images.CreateImageHandler(db, limitsManager, jwtManager, imageProcessor.Processor)

View File

@ -8,6 +8,7 @@ import (
"screenmark/screenmark/limits" "screenmark/screenmark/limits"
"screenmark/screenmark/middleware" "screenmark/screenmark/middleware"
"screenmark/screenmark/models" "screenmark/screenmark/models"
"screenmark/screenmark/processor"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
@ -23,6 +24,8 @@ type StackHandler struct {
limitsManager limits.LimitsManagerMethods limitsManager limits.LimitsManagerMethods
jwtManager *middleware.JwtManager jwtManager *middleware.JwtManager
processor *processor.Processor[model.Stacks]
} }
func (h *StackHandler) getAllStacks(w http.ResponseWriter, r *http.Request) { func (h *StackHandler) getAllStacks(w http.ResponseWriter, r *http.Request) {
@ -147,10 +150,10 @@ func (h *StackHandler) deleteImageFromStack(w http.ResponseWriter, r *http.Reque
func (h *StackHandler) deleteImageStackSchemaItem(w http.ResponseWriter, r *http.Request) { func (h *StackHandler) deleteImageStackSchemaItem(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
stringImageID := chi.URLParam(r, "stackID") stringStackID := chi.URLParam(r, "stackID")
stringSchemaItemID := chi.URLParam(r, "schemaItemID") stringSchemaItemID := chi.URLParam(r, "schemaItemID")
imageID, err := uuid.Parse(stringImageID) stackID, err := uuid.Parse(stringStackID)
if err != nil { if err != nil {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
return return
@ -169,8 +172,9 @@ func (h *StackHandler) deleteImageStackSchemaItem(w http.ResponseWriter, r *http
return return
} }
stack, err := h.stackModel.Get(ctx, schemaItemID) stack, err := h.stackModel.Get(ctx, stackID)
if err != nil { if err != nil {
h.logger.Error("could not get stack model", "err", err)
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
return return
} }
@ -185,7 +189,7 @@ func (h *StackHandler) deleteImageStackSchemaItem(w http.ResponseWriter, r *http
// manipulations. So we could create a middleware. // manipulations. So we could create a middleware.
// If you repeat this 3 times, then organise it :) // If you repeat this 3 times, then organise it :)
err = h.stackModel.DeleteSchemaItem(ctx, schemaItemID, imageID) err = h.stackModel.DeleteSchemaItem(ctx, stackID, schemaItemID)
if err != nil { if err != nil {
h.logger.Warn("failed to delete image from list", "error", err) h.logger.Warn("failed to delete image from list", "error", err)
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
@ -208,14 +212,16 @@ func (h *StackHandler) createStack(body CreateStackBody, w http.ResponseWriter,
} }
// TODO: Add the stack processor here // TODO: Add the stack processor here
_, err = h.stackModel.Save(ctx, userID, body.Title, body.Description, model.Progress_NotStarted) stack, err := h.stackModel.Save(ctx, userID, body.Title, body.Description, model.Progress_NotStarted)
if err != nil { if err != nil {
h.logger.Warn("could not save stack", "err", err) h.logger.Warn("could not save stack", "err", err)
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
return return
} }
w.WriteHeader(http.StatusOK) h.processor.Add(stack)
middleware.WriteJsonOrError(h.logger, stack, w)
} }
func (h *StackHandler) CreateRoutes(r chi.Router) { func (h *StackHandler) CreateRoutes(r chi.Router) {
@ -236,7 +242,12 @@ func (h *StackHandler) CreateRoutes(r chi.Router) {
}) })
} }
func CreateStackHandler(db *sql.DB, limitsManager limits.LimitsManagerMethods, jwtManager *middleware.JwtManager) StackHandler { func CreateStackHandler(
db *sql.DB,
limitsManager limits.LimitsManagerMethods,
jwtManager *middleware.JwtManager,
processor *processor.Processor[model.Stacks],
) StackHandler {
stackModel := models.NewStackModel(db) stackModel := models.NewStackModel(db)
imageModel := models.NewImageModel(db) imageModel := models.NewImageModel(db)
logger := log.New(os.Stdout).WithPrefix("Stacks") logger := log.New(os.Stdout).WithPrefix("Stacks")
@ -247,5 +258,6 @@ func CreateStackHandler(db *sql.DB, limitsManager limits.LimitsManagerMethods, j
stackModel: stackModel, stackModel: stackModel,
limitsManager: limitsManager, limitsManager: limitsManager,
jwtManager: jwtManager, jwtManager: jwtManager,
processor: processor,
} }
} }

View File

@ -7,7 +7,13 @@ import {
createResource, createResource,
useContext, useContext,
} from "solid-js"; } from "solid-js";
import { deleteImage, deleteImageFromStack, getUserImages, JustTheImageWhatAreTheseNames } from "../network"; import {
deleteImage,
deleteImageFromStack,
deleteStackItem,
getUserImages,
JustTheImageWhatAreTheseNames,
} from "../network";
export type SearchImageStore = { export type SearchImageStore = {
imagesByDate: Accessor< imagesByDate: Accessor<
@ -21,40 +27,41 @@ export type SearchImageStore = {
onRefetchImages: () => void; onRefetchImages: () => void;
onDeleteImage: (imageID: string) => void; onDeleteImage: (imageID: string) => void;
onDeleteImageFromStack: (stackID: string, imageID: string) => void; onDeleteImageFromStack: (stackID: string, imageID: string) => void;
onDeleteStackItem: (stackID: string, schemaItemID: string) => void;
}; };
const SearchImageContext = createContext<SearchImageStore>(); const SearchImageContext = createContext<SearchImageStore>();
export const SearchImageContextProvider: Component<ParentProps> = (props) => { export const SearchImageContextProvider: Component<ParentProps> = (props) => {
const [data, { refetch }] = createResource(getUserImages); const [data, { refetch }] = createResource(getUserImages);
const sortedImages = createMemo<ReturnType<SearchImageStore["imagesByDate"]>>( const sortedImages = createMemo<
() => { ReturnType<SearchImageStore["imagesByDate"]>
const d = data(); >(() => {
if (d == null) { const d = data();
return []; if (d == null) {
return [];
}
// Sorted by day. But we could potentially add more in the future.
const buckets: Record<string, JustTheImageWhatAreTheseNames> = {};
for (const image of d.UserImages) {
if (image.CreatedAt == null) {
continue;
} }
// Sorted by day. But we could potentially add more in the future. const date = new Date(image.CreatedAt).toDateString();
const buckets: Record<string, JustTheImageWhatAreTheseNames> = {}; if (!(date in buckets)) {
buckets[date] = [];
for (const image of d.UserImages) {
if (image.CreatedAt == null) {
continue;
}
const date = new Date(image.CreatedAt).toDateString();
if (!(date in buckets)) {
buckets[date] = [];
}
buckets[date].push(image);
} }
return Object.entries(buckets) buckets[date].push(image);
.map(([date, images]) => ({ date: new Date(date), images })) }
.sort((a, b) => b.date.getTime() - a.date.getTime());
}, return Object.entries(buckets)
); .map(([date, images]) => ({ date: new Date(date), images }))
.sort((a, b) => b.date.getTime() - a.date.getTime());
});
return ( return (
<SearchImageContext.Provider <SearchImageContext.Provider
@ -68,7 +75,10 @@ export const SearchImageContextProvider: Component<ParentProps> = (props) => {
}, },
onDeleteImageFromStack: (stackID: string, imageID: string) => { onDeleteImageFromStack: (stackID: string, imageID: string) => {
deleteImageFromStack(stackID, imageID).then(refetch); deleteImageFromStack(stackID, imageID).then(refetch);
} },
onDeleteStackItem: (stackID: string, schemaItemID: string) => {
deleteStackItem(stackID, schemaItemID).then(refetch);
},
}} }}
> >
{props.children} {props.children}

View File

@ -121,6 +121,18 @@ export const deleteImageFromStack = async (listID: string, imageID: string): Pro
await fetch(request); await fetch(request);
} }
export const deleteStackItem = async (
stackID: string,
schemaItemID: string,
): Promise<void> => {
const request = await getBaseAuthorizedRequest({
path: `stacks/${stackID}/${schemaItemID}`,
method: "DELETE",
});
await fetch(request);
}
export const deleteList = async (listID: string): Promise<void> => { export const deleteList = async (listID: string): Promise<void> => {
const request = await getBaseAuthorizedRequest({ const request = await getBaseAuthorizedRequest({
path: `stacks/${listID}`, path: `stacks/${listID}`,