diff --git a/apps/jan-api-gateway/application/app/domain/conversation/conversation.go b/apps/jan-api-gateway/application/app/domain/conversation/conversation.go index 7e121e80..5b642344 100644 --- a/apps/jan-api-gateway/application/app/domain/conversation/conversation.go +++ b/apps/jan-api-gateway/application/app/domain/conversation/conversation.go @@ -168,21 +168,23 @@ type IncompleteDetails struct { } type Conversation struct { - ID uint `json:"-"` - PublicID string `json:"id"` // OpenAI-compatible string ID like "conv_abc123" - Title *string `json:"title,omitempty"` - UserID uint `json:"-"` - Status ConversationStatus `json:"status"` - Items []Item `json:"items,omitempty"` - Metadata map[string]string `json:"metadata,omitempty"` - IsPrivate bool `json:"is_private"` - CreatedAt time.Time `json:"created_at"` // Unix timestamp for OpenAI compatibility - UpdatedAt time.Time `json:"updated_at"` // Unix timestamp for OpenAI compatibility + ID uint `json:"-"` + PublicID string `json:"id"` // OpenAI-compatible string ID like "conv_abc123" + Title *string `json:"title,omitempty"` + UserID uint `json:"-"` + WorkspacePublicID *string `json:"workspace_id,omitempty"` + Status ConversationStatus `json:"status"` + Items []Item `json:"items,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` + IsPrivate bool `json:"is_private"` + CreatedAt time.Time `json:"created_at"` // Unix timestamp for OpenAI compatibility + UpdatedAt time.Time `json:"updated_at"` // Unix timestamp for OpenAI compatibility } type ConversationFilter struct { - PublicID *string - UserID *uint + PublicID *string + UserID *uint + WorkspacePublicID *string } type ItemFilter struct { @@ -200,6 +202,7 @@ type ConversationRepository interface { FindByPublicID(ctx context.Context, publicID string) (*Conversation, error) Update(ctx context.Context, conversation *Conversation) error Delete(ctx context.Context, id uint) error + DeleteByWorkspacePublicID(ctx context.Context, workspacePublicID string) error AddItem(ctx context.Context, conversationID uint, item *Item) error SearchItems(ctx context.Context, conversationID uint, query string) ([]*Item, error) BulkAddItems(ctx context.Context, conversationID uint, items []*Item) error diff --git a/apps/jan-api-gateway/application/app/domain/conversation/conversation_service.go b/apps/jan-api-gateway/application/app/domain/conversation/conversation_service.go index ca753901..206c5619 100644 --- a/apps/jan-api-gateway/application/app/domain/conversation/conversation_service.go +++ b/apps/jan-api-gateway/application/app/domain/conversation/conversation_service.go @@ -60,7 +60,7 @@ func (s *ConversationService) CountConversationsByFilter(ctx context.Context, fi return count, nil } -func (s *ConversationService) CreateConversation(ctx context.Context, userID uint, title *string, isPrivate bool, metadata map[string]string) (*Conversation, *common.Error) { +func (s *ConversationService) CreateConversation(ctx context.Context, userID uint, title *string, isPrivate bool, metadata map[string]string, workspacePublicID *string) (*Conversation, *common.Error) { if err := s.validator.ValidateConversationInput(title, metadata); err != nil { return nil, common.NewError(err, "c3d4e5f6-g7h8-9012-cdef-345678901234") } @@ -71,12 +71,13 @@ func (s *ConversationService) CreateConversation(ctx context.Context, userID uin } conversation := &Conversation{ - PublicID: publicID, - Title: title, - UserID: userID, - Status: ConversationStatusActive, - IsPrivate: isPrivate, - Metadata: metadata, + PublicID: publicID, + Title: title, + UserID: userID, + WorkspacePublicID: workspacePublicID, + Status: ConversationStatusActive, + IsPrivate: isPrivate, + Metadata: metadata, } if err := s.conversationRepo.Create(ctx, conversation); err != nil { @@ -136,6 +137,14 @@ func (s *ConversationService) UpdateConversation(ctx context.Context, entity *Co return entity, nil } +func (s *ConversationService) UpdateConversationWorkspace(ctx context.Context, conv *Conversation, workspacePublicID *string) (*Conversation, *common.Error) { + conv.WorkspacePublicID = workspacePublicID + if err := s.conversationRepo.Update(ctx, conv); err != nil { + return nil, common.NewError(err, "a3b4c5d6-e7f8-9012-abcd-ef3456789012") + } + return conv, nil +} + func (s *ConversationService) DeleteConversation(ctx context.Context, conv *Conversation) (bool, *common.Error) { if err := s.conversationRepo.Delete(ctx, conv.ID); err != nil { return false, common.NewError(err, "m3n4o5p6-q7r8-9012-mnop-345678901234") diff --git a/apps/jan-api-gateway/application/app/domain/response/response_service.go b/apps/jan-api-gateway/application/app/domain/response/response_service.go index 0f6e40f9..a8a7372c 100644 --- a/apps/jan-api-gateway/application/app/domain/response/response_service.go +++ b/apps/jan-api-gateway/application/app/domain/response/response_service.go @@ -554,7 +554,7 @@ func (s *ResponseService) HandleConversation(ctx context.Context, userID uint, r } // Create new conversation - conv, err := s.conversationService.CreateConversation(ctx, userID, nil, true, nil) + conv, err := s.conversationService.CreateConversation(ctx, userID, nil, true, nil, nil) if err != nil { return nil, err } diff --git a/apps/jan-api-gateway/application/app/domain/service_provider.go b/apps/jan-api-gateway/application/app/domain/service_provider.go index 4b45bccf..a330505b 100644 --- a/apps/jan-api-gateway/application/app/domain/service_provider.go +++ b/apps/jan-api-gateway/application/app/domain/service_provider.go @@ -12,6 +12,7 @@ import ( "menlo.ai/jan-api-gateway/app/domain/project" "menlo.ai/jan-api-gateway/app/domain/response" "menlo.ai/jan-api-gateway/app/domain/user" + "menlo.ai/jan-api-gateway/app/domain/workspace" ) var ServiceProvider = wire.NewSet( @@ -22,6 +23,7 @@ var ServiceProvider = wire.NewSet( apikey.NewService, user.NewService, conversation.NewService, + workspace.NewWorkspaceService, response.NewResponseService, response.NewResponseModelService, response.NewStreamModelService, diff --git a/apps/jan-api-gateway/application/app/domain/workspace/workspace.go b/apps/jan-api-gateway/application/app/domain/workspace/workspace.go new file mode 100644 index 00000000..24a07e90 --- /dev/null +++ b/apps/jan-api-gateway/application/app/domain/workspace/workspace.go @@ -0,0 +1,48 @@ +package workspace + +import ( + "context" + "strings" + "time" + + "menlo.ai/jan-api-gateway/app/domain/common" + "menlo.ai/jan-api-gateway/app/domain/query" +) + +type Workspace struct { + ID uint + PublicID string + UserID uint + Name string + Instruction *string + CreatedAt time.Time + UpdatedAt time.Time +} + +func (w *Workspace) Normalize() error { + trimmedName := strings.TrimSpace(w.Name) + if trimmedName == "" { + return common.NewErrorWithMessage("workspace name is required", "3a5dcb2f-9f1c-4f4b-8893-4a62f72f7a00") + } + if len([]rune(trimmedName)) > 50 { + return common.NewErrorWithMessage("workspace name is too long", "94a6a12b-d4f0-4594-8125-95de7f9ce3d6") + } + return nil +} + +type WorkspaceFilter struct { + UserID *uint + PublicID *string + PublicIDs *[]string + IDs *[]uint +} + +type WorkspaceRepository interface { + Create(ctx context.Context, workspace *Workspace) error + Update(ctx context.Context, workspace *Workspace) error + Delete(ctx context.Context, id uint) error + FindByID(ctx context.Context, id uint) (*Workspace, error) + FindByPublicID(ctx context.Context, publicID string) (*Workspace, error) + FindByFilter(ctx context.Context, filter WorkspaceFilter, pagination *query.Pagination) ([]*Workspace, error) + Count(ctx context.Context, filter WorkspaceFilter) (int64, error) +} diff --git a/apps/jan-api-gateway/application/app/domain/workspace/workspace_service.go b/apps/jan-api-gateway/application/app/domain/workspace/workspace_service.go new file mode 100644 index 00000000..441b0dea --- /dev/null +++ b/apps/jan-api-gateway/application/app/domain/workspace/workspace_service.go @@ -0,0 +1,187 @@ +package workspace + +import ( + "context" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + + "menlo.ai/jan-api-gateway/app/domain/auth" + "menlo.ai/jan-api-gateway/app/domain/common" + "menlo.ai/jan-api-gateway/app/domain/conversation" + "menlo.ai/jan-api-gateway/app/domain/query" + "menlo.ai/jan-api-gateway/app/interfaces/http/responses" + "menlo.ai/jan-api-gateway/app/utils/idgen" +) + +type WorkspaceContextKey string + +const ( + WorkspaceContextKeyPublicID WorkspaceContextKey = "workspace_public_id" + WorkspaceContextEntity WorkspaceContextKey = "WorkspaceContextEntity" +) + +type WorkspaceService struct { + repo WorkspaceRepository + conversationRepo conversation.ConversationRepository +} + +func NewWorkspaceService(repo WorkspaceRepository, conversationRepo conversation.ConversationRepository) *WorkspaceService { + return &WorkspaceService{ + repo: repo, + conversationRepo: conversationRepo, + } +} + +func (s *WorkspaceService) FindWorkspacesByFilter(ctx context.Context, filter WorkspaceFilter, pagination *query.Pagination) ([]*Workspace, *common.Error) { + workspaces, err := s.repo.FindByFilter(ctx, filter, pagination) + if err != nil { + return nil, common.NewError(err, "13df5d74-32c4-4b87-9066-6f9c546f4ad2") + } + return workspaces, nil +} + +func (s *WorkspaceService) CreateWorkspace(ctx context.Context, workspace *Workspace) (*Workspace, *common.Error) { + + publicID, err := idgen.GenerateSecureID("ws", 24) + if err != nil { + return nil, common.NewError(err, "6d4af582-0c23-4f91-b45e-253956218b64") + } + + workspace.PublicID = publicID + + if err := workspace.Normalize(); err != nil { + return nil, common.NewError(err, "26f0e93a-ff64-443f-8221-d18d36280336") + } + + if err := s.repo.Create(ctx, workspace); err != nil { + return nil, common.NewError(err, "7ef72c57-90f8-4d59-8d08-2b2edf61d8da") + } + + return workspace, nil +} + +func (s *WorkspaceService) GetWorkspaceByPublicIDAndUserID(ctx context.Context, publicID string, userID uint) (*Workspace, *common.Error) { + if publicID == "" { + return nil, common.NewErrorWithMessage("workspace id is required", "70d9041a-a3a5-4654-af30-2b530eb3e734") + } + + workspaces, err := s.repo.FindByFilter(ctx, WorkspaceFilter{ + PublicID: &publicID, + UserID: &userID, + }, nil) + if err != nil { + return nil, common.NewError(err, "ad9be074-4c1e-4d43-828d-fc9e7efc0c52") + } + if len(workspaces) == 0 { + return nil, common.NewErrorWithMessage("workspace not found", "c8bc424c-5b20-4cf9-8ca1-7d9ad1b098c8") + } + if len(workspaces) > 1 { + return nil, common.NewErrorWithMessage("multiple workspaces found", "0d0ff761-aa21-4d0b-91c3-acc0f3fa652f") + } + return workspaces[0], nil +} + +func (s *WorkspaceService) UpdateWorkspaceName(ctx context.Context, workspace *Workspace, name string) (*Workspace, *common.Error) { + workspace.Name = strings.TrimSpace(name) + if err := workspace.Normalize(); err != nil { + return nil, common.NewError(err, "447ec22a-09a6-45c7-bf87-f2fe2ca89631") + } + if err := s.repo.Update(ctx, workspace); err != nil { + return nil, common.NewError(err, "4e4c3a63-9e3c-420a-84f7-4415a7c21e61") + } + return workspace, nil +} + +func (s *WorkspaceService) UpdateWorkspaceInstruction(ctx context.Context, workspace *Workspace, instruction *string) (*Workspace, *common.Error) { + workspace.Instruction = sanitizeInstruction(instruction) + if err := s.repo.Update(ctx, workspace); err != nil { + return nil, common.NewError(err, "1c59f37a-56fa-4f64-9d8c-8a6c99b2e3ee") + } + return workspace, nil +} + +func (s *WorkspaceService) DeleteWorkspaceWithConversations(ctx context.Context, workspace *Workspace) *common.Error { + if workspace == nil { + return common.NewErrorWithMessage("workspace is required", "5d35c9b3-61f6-4c40-b6f8-31e0de1d7688") + } + if workspace.ID == 0 { + return common.NewErrorWithMessage("workspace id is required", "7e2f82a6-1c4f-4f67-9ef6-8790896eb99c") + } + if s.conversationRepo != nil { + if err := s.conversationRepo.DeleteByWorkspacePublicID(ctx, workspace.PublicID); err != nil { + return common.NewError(err, "2adf58f7-df2c-4f7f-bc11-2e9a2928c1f9") + } + } + if err := s.repo.Delete(ctx, workspace.ID); err != nil { + return common.NewError(err, "4cfb58ef-8016-4f24-8fcb-48d414d351d2") + } + return nil +} + +func (s *WorkspaceService) GetWorkspaceMiddleware() gin.HandlerFunc { + return func(reqCtx *gin.Context) { + ctx := reqCtx.Request.Context() + workspaceID := reqCtx.Param(string(WorkspaceContextKeyPublicID)) + if workspaceID == "" { + reqCtx.AbortWithStatusJSON(http.StatusBadRequest, responses.ErrorResponse{ + Code: "8dbbdf92-0ff6-4b70-99ee-0a6fe48eab8a", + Error: "missing workspace id", + }) + return + } + + user, ok := auth.GetUserFromContext(reqCtx) + if !ok { + reqCtx.AbortWithStatusJSON(http.StatusUnauthorized, responses.ErrorResponse{ + Code: "19d3e0aa-38db-42f4-9ed0-d4f02b8c7c2d", + Error: "user not found", + }) + return + } + + workspace, err := s.GetWorkspaceByPublicIDAndUserID(ctx, workspaceID, user.ID) + if err != nil { + status := http.StatusInternalServerError + if err.GetCode() == "c8bc424c-5b20-4cf9-8ca1-7d9ad1b098c8" { + status = http.StatusNotFound + } + reqCtx.AbortWithStatusJSON(status, responses.ErrorResponse{ + Code: err.GetCode(), + Error: err.Error(), + }) + return + } + + SetWorkspaceOnContext(reqCtx, workspace) + reqCtx.Next() + } +} + +func sanitizeInstruction(instruction *string) *string { + if instruction == nil { + return nil + } + trimmed := strings.TrimSpace(*instruction) + if trimmed == "" { + return nil + } + return &trimmed +} + +func SetWorkspaceOnContext(reqCtx *gin.Context, workspace *Workspace) { + reqCtx.Set(string(WorkspaceContextEntity), workspace) +} + +func GetWorkspaceFromContext(reqCtx *gin.Context) (*Workspace, bool) { + value, ok := reqCtx.Get(string(WorkspaceContextEntity)) + if !ok { + return nil, false + } + workspace, ok := value.(*Workspace) + if !ok { + return nil, false + } + return workspace, true +} diff --git a/apps/jan-api-gateway/application/app/infrastructure/database/dbschema/conversation.go b/apps/jan-api-gateway/application/app/infrastructure/database/dbschema/conversation.go index 40f145c3..a14a8364 100644 --- a/apps/jan-api-gateway/application/app/infrastructure/database/dbschema/conversation.go +++ b/apps/jan-api-gateway/application/app/infrastructure/database/dbschema/conversation.go @@ -16,14 +16,16 @@ func init() { type Conversation struct { BaseModel - PublicID string `gorm:"type:varchar(50);uniqueIndex;not null"` - Title string `gorm:"type:varchar(255)"` - UserID uint `gorm:"not null;index"` - Status string `gorm:"type:varchar(20);not null;default:'active';index"` - Metadata string `gorm:"type:text"` - IsPrivate bool `gorm:"not null;default:true;index"` - Items []Item `gorm:"foreignKey:ConversationID"` - User User `gorm:"foreignKey:UserID"` + PublicID string `gorm:"type:varchar(50);uniqueIndex;not null"` + Title string `gorm:"type:varchar(255)"` + UserID uint `gorm:"not null;index"` + WorkspacePublicID *string `gorm:"type:varchar(50);index"` + Status string `gorm:"type:varchar(20);not null;default:'active';index"` + Metadata string `gorm:"type:text"` + IsPrivate bool `gorm:"not null;default:true;index"` + Items []Item `gorm:"foreignKey:ConversationID;constraint:OnDelete:CASCADE;"` + User User `gorm:"foreignKey:UserID"` + Workspace *Workspace `gorm:"foreignKey:WorkspacePublicID;references:PublicID;constraint:OnDelete:CASCADE;"` } type Item struct { @@ -57,12 +59,13 @@ func NewSchemaConversation(c *conversation.Conversation) *Conversation { BaseModel: BaseModel{ ID: c.ID, }, - PublicID: c.PublicID, - Title: ptr.FromString(c.Title), - UserID: c.UserID, - Status: string(c.Status), - Metadata: metadataJSON, - IsPrivate: c.IsPrivate, + PublicID: c.PublicID, + Title: ptr.FromString(c.Title), + UserID: c.UserID, + WorkspacePublicID: c.WorkspacePublicID, + Status: string(c.Status), + Metadata: metadataJSON, + IsPrivate: c.IsPrivate, } } @@ -75,15 +78,16 @@ func (c *Conversation) EtoD() *conversation.Conversation { title := ptr.ToString(c.Title) return &conversation.Conversation{ - ID: c.ID, - PublicID: c.PublicID, - Title: title, - UserID: c.UserID, - Status: conversation.ConversationStatus(c.Status), - Metadata: metadata, - IsPrivate: c.IsPrivate, - CreatedAt: c.CreatedAt, - UpdatedAt: c.UpdatedAt, + ID: c.ID, + PublicID: c.PublicID, + Title: title, + UserID: c.UserID, + WorkspacePublicID: c.WorkspacePublicID, + Status: conversation.ConversationStatus(c.Status), + Metadata: metadata, + IsPrivate: c.IsPrivate, + CreatedAt: c.CreatedAt, + UpdatedAt: c.UpdatedAt, } } diff --git a/apps/jan-api-gateway/application/app/infrastructure/database/dbschema/workspace.go b/apps/jan-api-gateway/application/app/infrastructure/database/dbschema/workspace.go new file mode 100644 index 00000000..ca4c341e --- /dev/null +++ b/apps/jan-api-gateway/application/app/infrastructure/database/dbschema/workspace.go @@ -0,0 +1,42 @@ +package dbschema + +import ( + "menlo.ai/jan-api-gateway/app/domain/workspace" + "menlo.ai/jan-api-gateway/app/infrastructure/database" +) + +func init() { + database.RegisterSchemaForAutoMigrate(Workspace{}) +} + +type Workspace struct { + BaseModel + PublicID string `gorm:"type:varchar(50);uniqueIndex;not null"` + UserID uint `gorm:"not null;index"` + Name string `gorm:"type:varchar(255);not null"` + Instruction *string `gorm:"type:text"` + Conversations []Conversation `gorm:"foreignKey:WorkspacePublicID;references:PublicID;constraint:OnDelete:CASCADE;"` + User User `gorm:"foreignKey:UserID"` +} + +func NewSchemaWorkspace(w *workspace.Workspace) *Workspace { + return &Workspace{ + BaseModel: BaseModel{ID: w.ID}, + PublicID: w.PublicID, + UserID: w.UserID, + Name: w.Name, + Instruction: w.Instruction, + } +} + +func (w *Workspace) EtoD() *workspace.Workspace { + return &workspace.Workspace{ + ID: w.ID, + PublicID: w.PublicID, + UserID: w.UserID, + Name: w.Name, + Instruction: w.Instruction, + CreatedAt: w.CreatedAt, + UpdatedAt: w.UpdatedAt, + } +} diff --git a/apps/jan-api-gateway/application/app/infrastructure/database/gormgen/conversations.gen.go b/apps/jan-api-gateway/application/app/infrastructure/database/gormgen/conversations.gen.go index fdf8fb89..3fb68eaf 100644 --- a/apps/jan-api-gateway/application/app/infrastructure/database/gormgen/conversations.gen.go +++ b/apps/jan-api-gateway/application/app/infrastructure/database/gormgen/conversations.gen.go @@ -35,6 +35,7 @@ func newConversation(db *gorm.DB, opts ...gen.DOOption) conversation { _conversation.PublicID = field.NewString(tableName, "public_id") _conversation.Title = field.NewString(tableName, "title") _conversation.UserID = field.NewUint(tableName, "user_id") + _conversation.WorkspacePublicID = field.NewString(tableName, "workspace_public_id") _conversation.Status = field.NewString(tableName, "status") _conversation.Metadata = field.NewString(tableName, "metadata") _conversation.IsPrivate = field.NewBool(tableName, "is_private") @@ -53,6 +54,15 @@ func newConversation(db *gorm.DB, opts ...gen.DOOption) conversation { field.RelationField } } + Workspace struct { + field.RelationField + User struct { + field.RelationField + } + Conversations struct { + field.RelationField + } + } Items struct { field.RelationField } @@ -79,6 +89,27 @@ func newConversation(db *gorm.DB, opts ...gen.DOOption) conversation { RelationField: field.NewRelation("Items.Conversation.User.Projects", "dbschema.ProjectMember"), }, }, + Workspace: struct { + field.RelationField + User struct { + field.RelationField + } + Conversations struct { + field.RelationField + } + }{ + RelationField: field.NewRelation("Items.Conversation.Workspace", "dbschema.Workspace"), + User: struct { + field.RelationField + }{ + RelationField: field.NewRelation("Items.Conversation.Workspace.User", "dbschema.User"), + }, + Conversations: struct { + field.RelationField + }{ + RelationField: field.NewRelation("Items.Conversation.Workspace.Conversations", "dbschema.Conversation"), + }, + }, Items: struct { field.RelationField }{ @@ -122,6 +153,12 @@ func newConversation(db *gorm.DB, opts ...gen.DOOption) conversation { RelationField: field.NewRelation("User", "dbschema.User"), } + _conversation.Workspace = conversationBelongsToWorkspace{ + db: db.Session(&gorm.Session{}), + + RelationField: field.NewRelation("Workspace", "dbschema.Workspace"), + } + _conversation.fillFieldMap() return _conversation @@ -130,21 +167,24 @@ func newConversation(db *gorm.DB, opts ...gen.DOOption) conversation { type conversation struct { conversationDo - ALL field.Asterisk - ID field.Uint - CreatedAt field.Time - UpdatedAt field.Time - DeletedAt field.Field - PublicID field.String - Title field.String - UserID field.Uint - Status field.String - Metadata field.String - IsPrivate field.Bool - Items conversationHasManyItems + ALL field.Asterisk + ID field.Uint + CreatedAt field.Time + UpdatedAt field.Time + DeletedAt field.Field + PublicID field.String + Title field.String + UserID field.Uint + WorkspacePublicID field.String + Status field.String + Metadata field.String + IsPrivate field.Bool + Items conversationHasManyItems User conversationBelongsToUser + Workspace conversationBelongsToWorkspace + fieldMap map[string]field.Expr } @@ -167,6 +207,7 @@ func (c *conversation) updateTableName(table string) *conversation { c.PublicID = field.NewString(table, "public_id") c.Title = field.NewString(table, "title") c.UserID = field.NewUint(table, "user_id") + c.WorkspacePublicID = field.NewString(table, "workspace_public_id") c.Status = field.NewString(table, "status") c.Metadata = field.NewString(table, "metadata") c.IsPrivate = field.NewBool(table, "is_private") @@ -186,7 +227,7 @@ func (c *conversation) GetFieldByName(fieldName string) (field.OrderExpr, bool) } func (c *conversation) fillFieldMap() { - c.fieldMap = make(map[string]field.Expr, 12) + c.fieldMap = make(map[string]field.Expr, 14) c.fieldMap["id"] = c.ID c.fieldMap["created_at"] = c.CreatedAt c.fieldMap["updated_at"] = c.UpdatedAt @@ -194,6 +235,7 @@ func (c *conversation) fillFieldMap() { c.fieldMap["public_id"] = c.PublicID c.fieldMap["title"] = c.Title c.fieldMap["user_id"] = c.UserID + c.fieldMap["workspace_public_id"] = c.WorkspacePublicID c.fieldMap["status"] = c.Status c.fieldMap["metadata"] = c.Metadata c.fieldMap["is_private"] = c.IsPrivate @@ -206,6 +248,8 @@ func (c conversation) clone(db *gorm.DB) conversation { c.Items.db.Statement.ConnPool = db.Statement.ConnPool c.User.db = db.Session(&gorm.Session{Initialized: true}) c.User.db.Statement.ConnPool = db.Statement.ConnPool + c.Workspace.db = db.Session(&gorm.Session{Initialized: true}) + c.Workspace.db.Statement.ConnPool = db.Statement.ConnPool return c } @@ -213,6 +257,7 @@ func (c conversation) replaceDB(db *gorm.DB) conversation { c.conversationDo.ReplaceDB(db) c.Items.db = db.Session(&gorm.Session{}) c.User.db = db.Session(&gorm.Session{}) + c.Workspace.db = db.Session(&gorm.Session{}) return c } @@ -232,6 +277,15 @@ type conversationHasManyItems struct { field.RelationField } } + Workspace struct { + field.RelationField + User struct { + field.RelationField + } + Conversations struct { + field.RelationField + } + } Items struct { field.RelationField } @@ -406,6 +460,87 @@ func (a conversationBelongsToUserTx) Unscoped() *conversationBelongsToUserTx { return &a } +type conversationBelongsToWorkspace struct { + db *gorm.DB + + field.RelationField +} + +func (a conversationBelongsToWorkspace) Where(conds ...field.Expr) *conversationBelongsToWorkspace { + if len(conds) == 0 { + return &a + } + + exprs := make([]clause.Expression, 0, len(conds)) + for _, cond := range conds { + exprs = append(exprs, cond.BeCond().(clause.Expression)) + } + a.db = a.db.Clauses(clause.Where{Exprs: exprs}) + return &a +} + +func (a conversationBelongsToWorkspace) WithContext(ctx context.Context) *conversationBelongsToWorkspace { + a.db = a.db.WithContext(ctx) + return &a +} + +func (a conversationBelongsToWorkspace) Session(session *gorm.Session) *conversationBelongsToWorkspace { + a.db = a.db.Session(session) + return &a +} + +func (a conversationBelongsToWorkspace) Model(m *dbschema.Conversation) *conversationBelongsToWorkspaceTx { + return &conversationBelongsToWorkspaceTx{a.db.Model(m).Association(a.Name())} +} + +func (a conversationBelongsToWorkspace) Unscoped() *conversationBelongsToWorkspace { + a.db = a.db.Unscoped() + return &a +} + +type conversationBelongsToWorkspaceTx struct{ tx *gorm.Association } + +func (a conversationBelongsToWorkspaceTx) Find() (result *dbschema.Workspace, err error) { + return result, a.tx.Find(&result) +} + +func (a conversationBelongsToWorkspaceTx) Append(values ...*dbschema.Workspace) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Append(targetValues...) +} + +func (a conversationBelongsToWorkspaceTx) Replace(values ...*dbschema.Workspace) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Replace(targetValues...) +} + +func (a conversationBelongsToWorkspaceTx) Delete(values ...*dbschema.Workspace) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Delete(targetValues...) +} + +func (a conversationBelongsToWorkspaceTx) Clear() error { + return a.tx.Clear() +} + +func (a conversationBelongsToWorkspaceTx) Count() int64 { + return a.tx.Count() +} + +func (a conversationBelongsToWorkspaceTx) Unscoped() *conversationBelongsToWorkspaceTx { + a.tx = a.tx.Unscoped() + return &a +} + type conversationDo struct{ gen.DO } type IConversationDo interface { diff --git a/apps/jan-api-gateway/application/app/infrastructure/database/gormgen/gen.go b/apps/jan-api-gateway/application/app/infrastructure/database/gormgen/gen.go index 7924d13c..5c692a6a 100644 --- a/apps/jan-api-gateway/application/app/infrastructure/database/gormgen/gen.go +++ b/apps/jan-api-gateway/application/app/infrastructure/database/gormgen/gen.go @@ -27,6 +27,7 @@ var ( ProjectMember *projectMember Response *response User *user + Workspace *workspace ) func SetDefault(db *gorm.DB, opts ...gen.DOOption) { @@ -41,6 +42,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) { ProjectMember = &Q.ProjectMember Response = &Q.Response User = &Q.User + Workspace = &Q.Workspace } func Use(db *gorm.DB, opts ...gen.DOOption) *Query { @@ -56,6 +58,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query { ProjectMember: newProjectMember(db, opts...), Response: newResponse(db, opts...), User: newUser(db, opts...), + Workspace: newWorkspace(db, opts...), } } @@ -72,6 +75,7 @@ type Query struct { ProjectMember projectMember Response response User user + Workspace workspace } func (q *Query) Available() bool { return q.db != nil } @@ -89,6 +93,7 @@ func (q *Query) clone(db *gorm.DB) *Query { ProjectMember: q.ProjectMember.clone(db), Response: q.Response.clone(db), User: q.User.clone(db), + Workspace: q.Workspace.clone(db), } } @@ -113,6 +118,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query { ProjectMember: q.ProjectMember.replaceDB(db), Response: q.Response.replaceDB(db), User: q.User.replaceDB(db), + Workspace: q.Workspace.replaceDB(db), } } @@ -127,6 +133,7 @@ type queryCtx struct { ProjectMember IProjectMemberDo Response IResponseDo User IUserDo + Workspace IWorkspaceDo } func (q *Query) WithContext(ctx context.Context) *queryCtx { @@ -141,6 +148,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx { ProjectMember: q.ProjectMember.WithContext(ctx), Response: q.Response.WithContext(ctx), User: q.User.WithContext(ctx), + Workspace: q.Workspace.WithContext(ctx), } } diff --git a/apps/jan-api-gateway/application/app/infrastructure/database/gormgen/items.gen.go b/apps/jan-api-gateway/application/app/infrastructure/database/gormgen/items.gen.go index 0f495980..ea83fab9 100644 --- a/apps/jan-api-gateway/application/app/infrastructure/database/gormgen/items.gen.go +++ b/apps/jan-api-gateway/application/app/infrastructure/database/gormgen/items.gen.go @@ -67,6 +67,27 @@ func newItem(db *gorm.DB, opts ...gen.DOOption) item { RelationField: field.NewRelation("Conversation.User.Projects", "dbschema.ProjectMember"), }, }, + Workspace: struct { + field.RelationField + User struct { + field.RelationField + } + Conversations struct { + field.RelationField + } + }{ + RelationField: field.NewRelation("Conversation.Workspace", "dbschema.Workspace"), + User: struct { + field.RelationField + }{ + RelationField: field.NewRelation("Conversation.Workspace.User", "dbschema.User"), + }, + Conversations: struct { + field.RelationField + }{ + RelationField: field.NewRelation("Conversation.Workspace.Conversations", "dbschema.Conversation"), + }, + }, Items: struct { field.RelationField Conversation struct { @@ -249,6 +270,15 @@ type itemBelongsToConversation struct { field.RelationField } } + Workspace struct { + field.RelationField + User struct { + field.RelationField + } + Conversations struct { + field.RelationField + } + } Items struct { field.RelationField Conversation struct { diff --git a/apps/jan-api-gateway/application/app/infrastructure/database/gormgen/responses.gen.go b/apps/jan-api-gateway/application/app/infrastructure/database/gormgen/responses.gen.go index 2f2d8f40..7c7fd91a 100644 --- a/apps/jan-api-gateway/application/app/infrastructure/database/gormgen/responses.gen.go +++ b/apps/jan-api-gateway/application/app/infrastructure/database/gormgen/responses.gen.go @@ -79,6 +79,15 @@ func newResponse(db *gorm.DB, opts ...gen.DOOption) response { field.RelationField } } + Workspace struct { + field.RelationField + User struct { + field.RelationField + } + Conversations struct { + field.RelationField + } + } Items struct { field.RelationField } @@ -105,6 +114,27 @@ func newResponse(db *gorm.DB, opts ...gen.DOOption) response { RelationField: field.NewRelation("Items.Conversation.User.Projects", "dbschema.ProjectMember"), }, }, + Workspace: struct { + field.RelationField + User struct { + field.RelationField + } + Conversations struct { + field.RelationField + } + }{ + RelationField: field.NewRelation("Items.Conversation.Workspace", "dbschema.Workspace"), + User: struct { + field.RelationField + }{ + RelationField: field.NewRelation("Items.Conversation.Workspace.User", "dbschema.User"), + }, + Conversations: struct { + field.RelationField + }{ + RelationField: field.NewRelation("Items.Conversation.Workspace.Conversations", "dbschema.Conversation"), + }, + }, Items: struct { field.RelationField }{ @@ -347,6 +377,15 @@ type responseHasManyItems struct { field.RelationField } } + Workspace struct { + field.RelationField + User struct { + field.RelationField + } + Conversations struct { + field.RelationField + } + } Items struct { field.RelationField } diff --git a/apps/jan-api-gateway/application/app/infrastructure/database/gormgen/workspaces.gen.go b/apps/jan-api-gateway/application/app/infrastructure/database/gormgen/workspaces.gen.go new file mode 100644 index 00000000..38f0f348 --- /dev/null +++ b/apps/jan-api-gateway/application/app/infrastructure/database/gormgen/workspaces.gen.go @@ -0,0 +1,727 @@ +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. + +package gormgen + +import ( + "context" + "database/sql" + + "gorm.io/gorm" + "gorm.io/gorm/clause" + "gorm.io/gorm/schema" + + "gorm.io/gen" + "gorm.io/gen/field" + + "gorm.io/plugin/dbresolver" + + "menlo.ai/jan-api-gateway/app/infrastructure/database/dbschema" +) + +func newWorkspace(db *gorm.DB, opts ...gen.DOOption) workspace { + _workspace := workspace{} + + _workspace.workspaceDo.UseDB(db, opts...) + _workspace.workspaceDo.UseModel(&dbschema.Workspace{}) + + tableName := _workspace.workspaceDo.TableName() + _workspace.ALL = field.NewAsterisk(tableName) + _workspace.ID = field.NewUint(tableName, "id") + _workspace.CreatedAt = field.NewTime(tableName, "created_at") + _workspace.UpdatedAt = field.NewTime(tableName, "updated_at") + _workspace.DeletedAt = field.NewField(tableName, "deleted_at") + _workspace.PublicID = field.NewString(tableName, "public_id") + _workspace.UserID = field.NewUint(tableName, "user_id") + _workspace.Name = field.NewString(tableName, "name") + _workspace.Instruction = field.NewString(tableName, "instruction") + _workspace.Conversations = workspaceHasManyConversations{ + db: db.Session(&gorm.Session{}), + + RelationField: field.NewRelation("Conversations", "dbschema.Conversation"), + User: struct { + field.RelationField + Organizations struct { + field.RelationField + } + Projects struct { + field.RelationField + } + }{ + RelationField: field.NewRelation("Conversations.User", "dbschema.User"), + Organizations: struct { + field.RelationField + }{ + RelationField: field.NewRelation("Conversations.User.Organizations", "dbschema.OrganizationMember"), + }, + Projects: struct { + field.RelationField + }{ + RelationField: field.NewRelation("Conversations.User.Projects", "dbschema.ProjectMember"), + }, + }, + Workspace: struct { + field.RelationField + User struct { + field.RelationField + } + Conversations struct { + field.RelationField + } + }{ + RelationField: field.NewRelation("Conversations.Workspace", "dbschema.Workspace"), + User: struct { + field.RelationField + }{ + RelationField: field.NewRelation("Conversations.Workspace.User", "dbschema.User"), + }, + Conversations: struct { + field.RelationField + }{ + RelationField: field.NewRelation("Conversations.Workspace.Conversations", "dbschema.Conversation"), + }, + }, + Items: struct { + field.RelationField + Conversation struct { + field.RelationField + } + Response struct { + field.RelationField + UserEntity struct { + field.RelationField + } + Conversation struct { + field.RelationField + } + Items struct { + field.RelationField + } + } + }{ + RelationField: field.NewRelation("Conversations.Items", "dbschema.Item"), + Conversation: struct { + field.RelationField + }{ + RelationField: field.NewRelation("Conversations.Items.Conversation", "dbschema.Conversation"), + }, + Response: struct { + field.RelationField + UserEntity struct { + field.RelationField + } + Conversation struct { + field.RelationField + } + Items struct { + field.RelationField + } + }{ + RelationField: field.NewRelation("Conversations.Items.Response", "dbschema.Response"), + UserEntity: struct { + field.RelationField + }{ + RelationField: field.NewRelation("Conversations.Items.Response.UserEntity", "dbschema.User"), + }, + Conversation: struct { + field.RelationField + }{ + RelationField: field.NewRelation("Conversations.Items.Response.Conversation", "dbschema.Conversation"), + }, + Items: struct { + field.RelationField + }{ + RelationField: field.NewRelation("Conversations.Items.Response.Items", "dbschema.Item"), + }, + }, + }, + } + + _workspace.User = workspaceBelongsToUser{ + db: db.Session(&gorm.Session{}), + + RelationField: field.NewRelation("User", "dbschema.User"), + } + + _workspace.fillFieldMap() + + return _workspace +} + +type workspace struct { + workspaceDo + + ALL field.Asterisk + ID field.Uint + CreatedAt field.Time + UpdatedAt field.Time + DeletedAt field.Field + PublicID field.String + UserID field.Uint + Name field.String + Instruction field.String + Conversations workspaceHasManyConversations + + User workspaceBelongsToUser + + fieldMap map[string]field.Expr +} + +func (w workspace) Table(newTableName string) *workspace { + w.workspaceDo.UseTable(newTableName) + return w.updateTableName(newTableName) +} + +func (w workspace) As(alias string) *workspace { + w.workspaceDo.DO = *(w.workspaceDo.As(alias).(*gen.DO)) + return w.updateTableName(alias) +} + +func (w *workspace) updateTableName(table string) *workspace { + w.ALL = field.NewAsterisk(table) + w.ID = field.NewUint(table, "id") + w.CreatedAt = field.NewTime(table, "created_at") + w.UpdatedAt = field.NewTime(table, "updated_at") + w.DeletedAt = field.NewField(table, "deleted_at") + w.PublicID = field.NewString(table, "public_id") + w.UserID = field.NewUint(table, "user_id") + w.Name = field.NewString(table, "name") + w.Instruction = field.NewString(table, "instruction") + + w.fillFieldMap() + + return w +} + +func (w *workspace) GetFieldByName(fieldName string) (field.OrderExpr, bool) { + _f, ok := w.fieldMap[fieldName] + if !ok || _f == nil { + return nil, false + } + _oe, ok := _f.(field.OrderExpr) + return _oe, ok +} + +func (w *workspace) fillFieldMap() { + w.fieldMap = make(map[string]field.Expr, 10) + w.fieldMap["id"] = w.ID + w.fieldMap["created_at"] = w.CreatedAt + w.fieldMap["updated_at"] = w.UpdatedAt + w.fieldMap["deleted_at"] = w.DeletedAt + w.fieldMap["public_id"] = w.PublicID + w.fieldMap["user_id"] = w.UserID + w.fieldMap["name"] = w.Name + w.fieldMap["instruction"] = w.Instruction + +} + +func (w workspace) clone(db *gorm.DB) workspace { + w.workspaceDo.ReplaceConnPool(db.Statement.ConnPool) + w.Conversations.db = db.Session(&gorm.Session{Initialized: true}) + w.Conversations.db.Statement.ConnPool = db.Statement.ConnPool + w.User.db = db.Session(&gorm.Session{Initialized: true}) + w.User.db.Statement.ConnPool = db.Statement.ConnPool + return w +} + +func (w workspace) replaceDB(db *gorm.DB) workspace { + w.workspaceDo.ReplaceDB(db) + w.Conversations.db = db.Session(&gorm.Session{}) + w.User.db = db.Session(&gorm.Session{}) + return w +} + +type workspaceHasManyConversations struct { + db *gorm.DB + + field.RelationField + + User struct { + field.RelationField + Organizations struct { + field.RelationField + } + Projects struct { + field.RelationField + } + } + Workspace struct { + field.RelationField + User struct { + field.RelationField + } + Conversations struct { + field.RelationField + } + } + Items struct { + field.RelationField + Conversation struct { + field.RelationField + } + Response struct { + field.RelationField + UserEntity struct { + field.RelationField + } + Conversation struct { + field.RelationField + } + Items struct { + field.RelationField + } + } + } +} + +func (a workspaceHasManyConversations) Where(conds ...field.Expr) *workspaceHasManyConversations { + if len(conds) == 0 { + return &a + } + + exprs := make([]clause.Expression, 0, len(conds)) + for _, cond := range conds { + exprs = append(exprs, cond.BeCond().(clause.Expression)) + } + a.db = a.db.Clauses(clause.Where{Exprs: exprs}) + return &a +} + +func (a workspaceHasManyConversations) WithContext(ctx context.Context) *workspaceHasManyConversations { + a.db = a.db.WithContext(ctx) + return &a +} + +func (a workspaceHasManyConversations) Session(session *gorm.Session) *workspaceHasManyConversations { + a.db = a.db.Session(session) + return &a +} + +func (a workspaceHasManyConversations) Model(m *dbschema.Workspace) *workspaceHasManyConversationsTx { + return &workspaceHasManyConversationsTx{a.db.Model(m).Association(a.Name())} +} + +func (a workspaceHasManyConversations) Unscoped() *workspaceHasManyConversations { + a.db = a.db.Unscoped() + return &a +} + +type workspaceHasManyConversationsTx struct{ tx *gorm.Association } + +func (a workspaceHasManyConversationsTx) Find() (result []*dbschema.Conversation, err error) { + return result, a.tx.Find(&result) +} + +func (a workspaceHasManyConversationsTx) Append(values ...*dbschema.Conversation) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Append(targetValues...) +} + +func (a workspaceHasManyConversationsTx) Replace(values ...*dbschema.Conversation) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Replace(targetValues...) +} + +func (a workspaceHasManyConversationsTx) Delete(values ...*dbschema.Conversation) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Delete(targetValues...) +} + +func (a workspaceHasManyConversationsTx) Clear() error { + return a.tx.Clear() +} + +func (a workspaceHasManyConversationsTx) Count() int64 { + return a.tx.Count() +} + +func (a workspaceHasManyConversationsTx) Unscoped() *workspaceHasManyConversationsTx { + a.tx = a.tx.Unscoped() + return &a +} + +type workspaceBelongsToUser struct { + db *gorm.DB + + field.RelationField +} + +func (a workspaceBelongsToUser) Where(conds ...field.Expr) *workspaceBelongsToUser { + if len(conds) == 0 { + return &a + } + + exprs := make([]clause.Expression, 0, len(conds)) + for _, cond := range conds { + exprs = append(exprs, cond.BeCond().(clause.Expression)) + } + a.db = a.db.Clauses(clause.Where{Exprs: exprs}) + return &a +} + +func (a workspaceBelongsToUser) WithContext(ctx context.Context) *workspaceBelongsToUser { + a.db = a.db.WithContext(ctx) + return &a +} + +func (a workspaceBelongsToUser) Session(session *gorm.Session) *workspaceBelongsToUser { + a.db = a.db.Session(session) + return &a +} + +func (a workspaceBelongsToUser) Model(m *dbschema.Workspace) *workspaceBelongsToUserTx { + return &workspaceBelongsToUserTx{a.db.Model(m).Association(a.Name())} +} + +func (a workspaceBelongsToUser) Unscoped() *workspaceBelongsToUser { + a.db = a.db.Unscoped() + return &a +} + +type workspaceBelongsToUserTx struct{ tx *gorm.Association } + +func (a workspaceBelongsToUserTx) Find() (result *dbschema.User, err error) { + return result, a.tx.Find(&result) +} + +func (a workspaceBelongsToUserTx) Append(values ...*dbschema.User) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Append(targetValues...) +} + +func (a workspaceBelongsToUserTx) Replace(values ...*dbschema.User) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Replace(targetValues...) +} + +func (a workspaceBelongsToUserTx) Delete(values ...*dbschema.User) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Delete(targetValues...) +} + +func (a workspaceBelongsToUserTx) Clear() error { + return a.tx.Clear() +} + +func (a workspaceBelongsToUserTx) Count() int64 { + return a.tx.Count() +} + +func (a workspaceBelongsToUserTx) Unscoped() *workspaceBelongsToUserTx { + a.tx = a.tx.Unscoped() + return &a +} + +type workspaceDo struct{ gen.DO } + +type IWorkspaceDo interface { + gen.SubQuery + Debug() IWorkspaceDo + WithContext(ctx context.Context) IWorkspaceDo + WithResult(fc func(tx gen.Dao)) gen.ResultInfo + ReplaceDB(db *gorm.DB) + ReadDB() IWorkspaceDo + WriteDB() IWorkspaceDo + As(alias string) gen.Dao + Session(config *gorm.Session) IWorkspaceDo + Columns(cols ...field.Expr) gen.Columns + Clauses(conds ...clause.Expression) IWorkspaceDo + Not(conds ...gen.Condition) IWorkspaceDo + Or(conds ...gen.Condition) IWorkspaceDo + Select(conds ...field.Expr) IWorkspaceDo + Where(conds ...gen.Condition) IWorkspaceDo + Order(conds ...field.Expr) IWorkspaceDo + Distinct(cols ...field.Expr) IWorkspaceDo + Omit(cols ...field.Expr) IWorkspaceDo + Join(table schema.Tabler, on ...field.Expr) IWorkspaceDo + LeftJoin(table schema.Tabler, on ...field.Expr) IWorkspaceDo + RightJoin(table schema.Tabler, on ...field.Expr) IWorkspaceDo + Group(cols ...field.Expr) IWorkspaceDo + Having(conds ...gen.Condition) IWorkspaceDo + Limit(limit int) IWorkspaceDo + Offset(offset int) IWorkspaceDo + Count() (count int64, err error) + Scopes(funcs ...func(gen.Dao) gen.Dao) IWorkspaceDo + Unscoped() IWorkspaceDo + Create(values ...*dbschema.Workspace) error + CreateInBatches(values []*dbschema.Workspace, batchSize int) error + Save(values ...*dbschema.Workspace) error + First() (*dbschema.Workspace, error) + Take() (*dbschema.Workspace, error) + Last() (*dbschema.Workspace, error) + Find() ([]*dbschema.Workspace, error) + FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*dbschema.Workspace, err error) + FindInBatches(result *[]*dbschema.Workspace, batchSize int, fc func(tx gen.Dao, batch int) error) error + Pluck(column field.Expr, dest interface{}) error + Delete(...*dbschema.Workspace) (info gen.ResultInfo, err error) + Update(column field.Expr, value interface{}) (info gen.ResultInfo, err error) + UpdateSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error) + Updates(value interface{}) (info gen.ResultInfo, err error) + UpdateColumn(column field.Expr, value interface{}) (info gen.ResultInfo, err error) + UpdateColumnSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error) + UpdateColumns(value interface{}) (info gen.ResultInfo, err error) + UpdateFrom(q gen.SubQuery) gen.Dao + Attrs(attrs ...field.AssignExpr) IWorkspaceDo + Assign(attrs ...field.AssignExpr) IWorkspaceDo + Joins(fields ...field.RelationField) IWorkspaceDo + Preload(fields ...field.RelationField) IWorkspaceDo + FirstOrInit() (*dbschema.Workspace, error) + FirstOrCreate() (*dbschema.Workspace, error) + FindByPage(offset int, limit int) (result []*dbschema.Workspace, count int64, err error) + ScanByPage(result interface{}, offset int, limit int) (count int64, err error) + Rows() (*sql.Rows, error) + Row() *sql.Row + Scan(result interface{}) (err error) + Returning(value interface{}, columns ...string) IWorkspaceDo + UnderlyingDB() *gorm.DB + schema.Tabler +} + +func (w workspaceDo) Debug() IWorkspaceDo { + return w.withDO(w.DO.Debug()) +} + +func (w workspaceDo) WithContext(ctx context.Context) IWorkspaceDo { + return w.withDO(w.DO.WithContext(ctx)) +} + +func (w workspaceDo) ReadDB() IWorkspaceDo { + return w.Clauses(dbresolver.Read) +} + +func (w workspaceDo) WriteDB() IWorkspaceDo { + return w.Clauses(dbresolver.Write) +} + +func (w workspaceDo) Session(config *gorm.Session) IWorkspaceDo { + return w.withDO(w.DO.Session(config)) +} + +func (w workspaceDo) Clauses(conds ...clause.Expression) IWorkspaceDo { + return w.withDO(w.DO.Clauses(conds...)) +} + +func (w workspaceDo) Returning(value interface{}, columns ...string) IWorkspaceDo { + return w.withDO(w.DO.Returning(value, columns...)) +} + +func (w workspaceDo) Not(conds ...gen.Condition) IWorkspaceDo { + return w.withDO(w.DO.Not(conds...)) +} + +func (w workspaceDo) Or(conds ...gen.Condition) IWorkspaceDo { + return w.withDO(w.DO.Or(conds...)) +} + +func (w workspaceDo) Select(conds ...field.Expr) IWorkspaceDo { + return w.withDO(w.DO.Select(conds...)) +} + +func (w workspaceDo) Where(conds ...gen.Condition) IWorkspaceDo { + return w.withDO(w.DO.Where(conds...)) +} + +func (w workspaceDo) Order(conds ...field.Expr) IWorkspaceDo { + return w.withDO(w.DO.Order(conds...)) +} + +func (w workspaceDo) Distinct(cols ...field.Expr) IWorkspaceDo { + return w.withDO(w.DO.Distinct(cols...)) +} + +func (w workspaceDo) Omit(cols ...field.Expr) IWorkspaceDo { + return w.withDO(w.DO.Omit(cols...)) +} + +func (w workspaceDo) Join(table schema.Tabler, on ...field.Expr) IWorkspaceDo { + return w.withDO(w.DO.Join(table, on...)) +} + +func (w workspaceDo) LeftJoin(table schema.Tabler, on ...field.Expr) IWorkspaceDo { + return w.withDO(w.DO.LeftJoin(table, on...)) +} + +func (w workspaceDo) RightJoin(table schema.Tabler, on ...field.Expr) IWorkspaceDo { + return w.withDO(w.DO.RightJoin(table, on...)) +} + +func (w workspaceDo) Group(cols ...field.Expr) IWorkspaceDo { + return w.withDO(w.DO.Group(cols...)) +} + +func (w workspaceDo) Having(conds ...gen.Condition) IWorkspaceDo { + return w.withDO(w.DO.Having(conds...)) +} + +func (w workspaceDo) Limit(limit int) IWorkspaceDo { + return w.withDO(w.DO.Limit(limit)) +} + +func (w workspaceDo) Offset(offset int) IWorkspaceDo { + return w.withDO(w.DO.Offset(offset)) +} + +func (w workspaceDo) Scopes(funcs ...func(gen.Dao) gen.Dao) IWorkspaceDo { + return w.withDO(w.DO.Scopes(funcs...)) +} + +func (w workspaceDo) Unscoped() IWorkspaceDo { + return w.withDO(w.DO.Unscoped()) +} + +func (w workspaceDo) Create(values ...*dbschema.Workspace) error { + if len(values) == 0 { + return nil + } + return w.DO.Create(values) +} + +func (w workspaceDo) CreateInBatches(values []*dbschema.Workspace, batchSize int) error { + return w.DO.CreateInBatches(values, batchSize) +} + +// Save : !!! underlying implementation is different with GORM +// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values) +func (w workspaceDo) Save(values ...*dbschema.Workspace) error { + if len(values) == 0 { + return nil + } + return w.DO.Save(values) +} + +func (w workspaceDo) First() (*dbschema.Workspace, error) { + if result, err := w.DO.First(); err != nil { + return nil, err + } else { + return result.(*dbschema.Workspace), nil + } +} + +func (w workspaceDo) Take() (*dbschema.Workspace, error) { + if result, err := w.DO.Take(); err != nil { + return nil, err + } else { + return result.(*dbschema.Workspace), nil + } +} + +func (w workspaceDo) Last() (*dbschema.Workspace, error) { + if result, err := w.DO.Last(); err != nil { + return nil, err + } else { + return result.(*dbschema.Workspace), nil + } +} + +func (w workspaceDo) Find() ([]*dbschema.Workspace, error) { + result, err := w.DO.Find() + return result.([]*dbschema.Workspace), err +} + +func (w workspaceDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*dbschema.Workspace, err error) { + buf := make([]*dbschema.Workspace, 0, batchSize) + err = w.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error { + defer func() { results = append(results, buf...) }() + return fc(tx, batch) + }) + return results, err +} + +func (w workspaceDo) FindInBatches(result *[]*dbschema.Workspace, batchSize int, fc func(tx gen.Dao, batch int) error) error { + return w.DO.FindInBatches(result, batchSize, fc) +} + +func (w workspaceDo) Attrs(attrs ...field.AssignExpr) IWorkspaceDo { + return w.withDO(w.DO.Attrs(attrs...)) +} + +func (w workspaceDo) Assign(attrs ...field.AssignExpr) IWorkspaceDo { + return w.withDO(w.DO.Assign(attrs...)) +} + +func (w workspaceDo) Joins(fields ...field.RelationField) IWorkspaceDo { + for _, _f := range fields { + w = *w.withDO(w.DO.Joins(_f)) + } + return &w +} + +func (w workspaceDo) Preload(fields ...field.RelationField) IWorkspaceDo { + for _, _f := range fields { + w = *w.withDO(w.DO.Preload(_f)) + } + return &w +} + +func (w workspaceDo) FirstOrInit() (*dbschema.Workspace, error) { + if result, err := w.DO.FirstOrInit(); err != nil { + return nil, err + } else { + return result.(*dbschema.Workspace), nil + } +} + +func (w workspaceDo) FirstOrCreate() (*dbschema.Workspace, error) { + if result, err := w.DO.FirstOrCreate(); err != nil { + return nil, err + } else { + return result.(*dbschema.Workspace), nil + } +} + +func (w workspaceDo) FindByPage(offset int, limit int) (result []*dbschema.Workspace, count int64, err error) { + result, err = w.Offset(offset).Limit(limit).Find() + if err != nil { + return + } + + if size := len(result); 0 < limit && 0 < size && size < limit { + count = int64(size + offset) + return + } + + count, err = w.Offset(-1).Limit(-1).Count() + return +} + +func (w workspaceDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) { + count, err = w.Count() + if err != nil { + return + } + + err = w.Offset(offset).Limit(limit).Scan(result) + return +} + +func (w workspaceDo) Scan(result interface{}) (err error) { + return w.DO.Scan(result) +} + +func (w workspaceDo) Delete(models ...*dbschema.Workspace) (result gen.ResultInfo, err error) { + return w.DO.Delete(models) +} + +func (w *workspaceDo) withDO(do gen.Dao) *workspaceDo { + w.DO = *do.(*gen.DO) + return w +} diff --git a/apps/jan-api-gateway/application/app/infrastructure/database/repository/conversationrepo/conversation_repository.go b/apps/jan-api-gateway/application/app/infrastructure/database/repository/conversationrepo/conversation_repository.go index 612f1f0d..84a05eab 100644 --- a/apps/jan-api-gateway/application/app/infrastructure/database/repository/conversationrepo/conversation_repository.go +++ b/apps/jan-api-gateway/application/app/infrastructure/database/repository/conversationrepo/conversation_repository.go @@ -30,6 +30,8 @@ func (r *ConversationGormRepository) Create(ctx context.Context, conversation *d return err } conversation.ID = model.ID + conversation.CreatedAt = model.CreatedAt + conversation.UpdatedAt = model.UpdatedAt return nil } @@ -58,7 +60,11 @@ func (r *ConversationGormRepository) Update(ctx context.Context, conversation *d model.ID = conversation.ID query := r.db.GetQuery(ctx) - _, err := query.Conversation.WithContext(ctx).Where(query.Conversation.ID.Eq(conversation.ID)).Updates(model) + + // select to update workspace nil as removing + err := query.Conversation.WithContext(ctx). + Where(query.Conversation.ID.Eq(conversation.ID)). + Save(model) return err } @@ -68,6 +74,12 @@ func (r *ConversationGormRepository) Delete(ctx context.Context, id uint) error return err } +func (r *ConversationGormRepository) DeleteByWorkspacePublicID(ctx context.Context, workspacePublicID string) error { + query := r.db.GetQuery(ctx) + _, err := query.Conversation.WithContext(ctx).Where(query.Conversation.WorkspacePublicID.Eq(workspacePublicID)).Delete() + return err +} + func (r *ConversationGormRepository) AddItem(ctx context.Context, conversationID uint, item *domain.Item) error { model := dbschema.NewSchemaItem(item) model.ConversationID = conversationID @@ -170,6 +182,13 @@ func (repo *ConversationGormRepository) applyFilter( if filter.UserID != nil { sql = sql.Where(query.Conversation.UserID.Eq(*filter.UserID)) } + if filter.WorkspacePublicID != nil { + if strings.EqualFold(*filter.WorkspacePublicID, "none") { + sql = sql.Where(query.Conversation.WorkspacePublicID.IsNull()) + } else { + sql = sql.Where(query.Conversation.WorkspacePublicID.Eq(*filter.WorkspacePublicID)) + } + } return sql } diff --git a/apps/jan-api-gateway/application/app/infrastructure/database/repository/repository_provider.go b/apps/jan-api-gateway/application/app/infrastructure/database/repository/repository_provider.go index 8de51130..b07e8733 100644 --- a/apps/jan-api-gateway/application/app/infrastructure/database/repository/repository_provider.go +++ b/apps/jan-api-gateway/application/app/infrastructure/database/repository/repository_provider.go @@ -11,6 +11,7 @@ import ( "menlo.ai/jan-api-gateway/app/infrastructure/database/repository/responserepo" "menlo.ai/jan-api-gateway/app/infrastructure/database/repository/transaction" "menlo.ai/jan-api-gateway/app/infrastructure/database/repository/userrepo" + "menlo.ai/jan-api-gateway/app/infrastructure/database/repository/workspacerepo" ) var RepositoryProvider = wire.NewSet( @@ -22,5 +23,6 @@ var RepositoryProvider = wire.NewSet( conversationrepo.NewConversationGormRepository, itemrepo.NewItemGormRepository, responserepo.NewResponseGormRepository, + workspacerepo.NewWorkspaceGormRepository, transaction.NewDatabase, ) diff --git a/apps/jan-api-gateway/application/app/infrastructure/database/repository/workspacerepo/workspace_repository.go b/apps/jan-api-gateway/application/app/infrastructure/database/repository/workspacerepo/workspace_repository.go new file mode 100644 index 00000000..0d608550 --- /dev/null +++ b/apps/jan-api-gateway/application/app/infrastructure/database/repository/workspacerepo/workspace_repository.go @@ -0,0 +1,129 @@ +package workspacerepo + +import ( + "context" + + "menlo.ai/jan-api-gateway/app/domain/query" + domain "menlo.ai/jan-api-gateway/app/domain/workspace" + "menlo.ai/jan-api-gateway/app/infrastructure/database/dbschema" + "menlo.ai/jan-api-gateway/app/infrastructure/database/gormgen" + "menlo.ai/jan-api-gateway/app/infrastructure/database/repository/transaction" + "menlo.ai/jan-api-gateway/app/utils/functional" +) + +type WorkspaceGormRepository struct { + db *transaction.Database +} + +var _ domain.WorkspaceRepository = (*WorkspaceGormRepository)(nil) + +func NewWorkspaceGormRepository(db *transaction.Database) domain.WorkspaceRepository { + return &WorkspaceGormRepository{db: db} +} + +func (repo *WorkspaceGormRepository) applyFilter(query *gormgen.Query, sql gormgen.IWorkspaceDo, filter domain.WorkspaceFilter) gormgen.IWorkspaceDo { + if filter.UserID != nil { + sql = sql.Where(query.Workspace.UserID.Eq(*filter.UserID)) + } + if filter.PublicID != nil { + sql = sql.Where(query.Workspace.PublicID.Eq(*filter.PublicID)) + } + if filter.PublicIDs != nil && len(*filter.PublicIDs) > 0 { + sql = sql.Where(query.Workspace.PublicID.In((*filter.PublicIDs)...)) + } + if filter.IDs != nil && len(*filter.IDs) > 0 { + sql = sql.Where(query.Workspace.ID.In((*filter.IDs)...)) + } + return sql +} + +func (repo *WorkspaceGormRepository) Create(ctx context.Context, workspace *domain.Workspace) error { + model := dbschema.NewSchemaWorkspace(workspace) + query := repo.db.GetQuery(ctx) + if err := query.Workspace.WithContext(ctx).Create(model); err != nil { + return err + } + workspace.ID = model.ID + workspace.CreatedAt = model.CreatedAt + workspace.UpdatedAt = model.UpdatedAt + return nil +} + +func (repo *WorkspaceGormRepository) Update(ctx context.Context, workspace *domain.Workspace) error { + model := dbschema.NewSchemaWorkspace(workspace) + query := repo.db.GetQuery(ctx) + if err := query.Workspace.WithContext(ctx).Save(model); err != nil { + return err + } + workspace.UpdatedAt = model.UpdatedAt + return nil +} + +func (repo *WorkspaceGormRepository) Delete(ctx context.Context, id uint) error { + query := repo.db.GetQuery(ctx) + _, err := query.Workspace.WithContext(ctx).Where(query.Workspace.ID.Eq(id)).Delete() + return err +} + +func (repo *WorkspaceGormRepository) FindByID(ctx context.Context, id uint) (*domain.Workspace, error) { + query := repo.db.GetQuery(ctx) + model, err := query.Workspace.WithContext(ctx).Where(query.Workspace.ID.Eq(id)).First() + if err != nil { + return nil, err + } + return model.EtoD(), nil +} + +func (repo *WorkspaceGormRepository) FindByPublicID(ctx context.Context, publicID string) (*domain.Workspace, error) { + query := repo.db.GetQuery(ctx) + model, err := query.Workspace.WithContext(ctx).Where(query.Workspace.PublicID.Eq(publicID)).First() + if err != nil { + return nil, err + } + return model.EtoD(), nil +} + +func (repo *WorkspaceGormRepository) FindByFilter(ctx context.Context, filter domain.WorkspaceFilter, pagination *query.Pagination) ([]*domain.Workspace, error) { + query := repo.db.GetQuery(ctx) + sql := query.Workspace.WithContext(ctx) + sql = repo.applyFilter(query, sql, filter) + + if pagination != nil { + if pagination.Limit != nil && *pagination.Limit > 0 { + sql = sql.Limit(*pagination.Limit) + } + if pagination.Offset != nil { + sql = sql.Offset(*pagination.Offset) + } + if pagination.After != nil { + if pagination.Order == "desc" { + sql = sql.Where(query.Workspace.ID.Lt(*pagination.After)) + } else { + sql = sql.Where(query.Workspace.ID.Gt(*pagination.After)) + } + } + if pagination.Order == "desc" { + sql = sql.Order(query.Workspace.ID.Desc()) + } else { + sql = sql.Order(query.Workspace.ID.Asc()) + } + } else { + sql = sql.Order(query.Workspace.ID.Asc()) + } + + rows, err := sql.Find() + if err != nil { + return nil, err + } + + return functional.Map(rows, func(item *dbschema.Workspace) *domain.Workspace { + return item.EtoD() + }), nil +} + +func (repo *WorkspaceGormRepository) Count(ctx context.Context, filter domain.WorkspaceFilter) (int64, error) { + query := repo.db.GetQuery(ctx) + sql := query.Workspace.WithContext(ctx) + sql = repo.applyFilter(query, sql, filter) + return sql.Count() +} diff --git a/apps/jan-api-gateway/application/app/interfaces/http/requests/response_requests.go b/apps/jan-api-gateway/application/app/interfaces/http/requests/response_requests.go index 2aaf6df1..8e8723b0 100644 --- a/apps/jan-api-gateway/application/app/interfaces/http/requests/response_requests.go +++ b/apps/jan-api-gateway/application/app/interfaces/http/requests/response_requests.go @@ -69,6 +69,9 @@ type CreateResponseRequest struct { // The conversation ID to append items to. If not set or set to ClientCreatedRootConversationID, a new conversation will be created. Conversation *string `json:"conversation,omitempty"` + // The workspace ID to associate the response with for shared instructions. + Workspace *string `json:"workspace,omitempty"` + // The ID of the previous response to continue from. If set, the conversation will be loaded from the previous response. PreviousResponseID *string `json:"previous_response_id,omitempty"` diff --git a/apps/jan-api-gateway/application/app/interfaces/http/routes/routes_provider.go b/apps/jan-api-gateway/application/app/interfaces/http/routes/routes_provider.go index 9feffdad..70414648 100644 --- a/apps/jan-api-gateway/application/app/interfaces/http/routes/routes_provider.go +++ b/apps/jan-api-gateway/application/app/interfaces/http/routes/routes_provider.go @@ -31,6 +31,7 @@ var RouteProvider = wire.NewSet( conv_chat.NewConvMCPAPI, conv_chat.NewCompletionNonStreamHandler, conv_chat.NewCompletionStreamHandler, + conv_chat.NewWorkspaceRoute, mcp.NewMCPAPI, v1.NewModelAPI, responses.NewResponseRoute, diff --git a/apps/jan-api-gateway/application/app/interfaces/http/routes/v1/conv/conv_completion_route.go b/apps/jan-api-gateway/application/app/interfaces/http/routes/v1/conv/conv_completion_route.go index f04ff8df..3813190c 100644 --- a/apps/jan-api-gateway/application/app/interfaces/http/routes/v1/conv/conv_completion_route.go +++ b/apps/jan-api-gateway/application/app/interfaces/http/routes/v1/conv/conv_completion_route.go @@ -321,7 +321,7 @@ func (api *ConvCompletionAPI) createNewConversation(reqCtx *gin.Context, message title := api.generateTitleFromMessages(messages) conv, convErr := api.conversationService.CreateConversation(ctx, user.ID, &title, true, map[string]string{ "model": "jan-v1-4b", // Default model - }) + }, nil) if convErr != nil { // If creation fails, return nil return nil, false diff --git a/apps/jan-api-gateway/application/app/interfaces/http/routes/v1/conv/conv_workspace_route.go b/apps/jan-api-gateway/application/app/interfaces/http/routes/v1/conv/conv_workspace_route.go new file mode 100644 index 00000000..03ca0b3d --- /dev/null +++ b/apps/jan-api-gateway/application/app/interfaces/http/routes/v1/conv/conv_workspace_route.go @@ -0,0 +1,370 @@ +package conv + +import ( + "fmt" + "net/http" + "strings" + "time" + + "github.com/gin-gonic/gin" + + "menlo.ai/jan-api-gateway/app/domain/auth" + "menlo.ai/jan-api-gateway/app/domain/workspace" + "menlo.ai/jan-api-gateway/app/interfaces/http/responses" + "menlo.ai/jan-api-gateway/app/utils/ptr" +) + +type WorkspaceRoute struct { + authService *auth.AuthService + workspaceService *workspace.WorkspaceService +} + +type CreateWorkspaceRequest struct { + Name string `json:"name" binding:"required"` + Instruction *string `json:"instruction"` +} + +type PatchWorkspaceRequest struct { + Name string `json:"name" binding:"required"` +} + +type UpdateWorkspaceInstructionRequest struct { + Instruction *string `json:"instruction"` +} + +func (req CreateWorkspaceRequest) ConvertToWorkspace(userID uint) *workspace.Workspace { + var instruction *string + if req.Instruction != nil { + if trimmed := strings.TrimSpace(*req.Instruction); trimmed != "" { + instruction = &trimmed + } + } + + return &workspace.Workspace{ + UserID: userID, + Name: strings.TrimSpace(req.Name), + Instruction: instruction, + } +} + +type WorkspaceResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Instruction *string `json:"instruction,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type WorkspaceDeletedResponse struct { + ID string `json:"id"` + Deleted bool `json:"deleted"` +} + +func NewWorkspaceRoute(authService *auth.AuthService, workspaceService *workspace.WorkspaceService) *WorkspaceRoute { + return &WorkspaceRoute{ + authService: authService, + workspaceService: workspaceService, + } +} + +func (route *WorkspaceRoute) RegisterRouter(router gin.IRouter) { + convRouter := router.Group("/conv", + route.authService.AppUserAuthMiddleware(), + route.authService.RegisteredUserMiddleware(), + ) + + workspacesRouter := convRouter.Group("/workspaces") + workspacesRouter.POST("", route.CreateWorkspace) + workspacesRouter.GET("", route.ListWorkspaces) + + workspaceMiddleware := route.workspaceService.GetWorkspaceMiddleware() + workspacesRouter.PATCH( + fmt.Sprintf("/:%s", workspace.WorkspaceContextKeyPublicID), + workspaceMiddleware, + route.UpdateWorkspaceName, + ) + workspacesRouter.PATCH( + fmt.Sprintf("/:%s/instruction", workspace.WorkspaceContextKeyPublicID), + workspaceMiddleware, + route.UpdateWorkspaceInstruction, + ) + workspacesRouter.DELETE( + fmt.Sprintf("/:%s", workspace.WorkspaceContextKeyPublicID), + workspaceMiddleware, + route.DeleteWorkspace, + ) +} + +// CreateWorkspace godoc +// @Summary Create Workspace +// @Description Creates a new workspace for the authenticated user. +// @Tags conv Workspaces API +// @Security BearerAuth +// @Accept json +// @Produce json +// @Param request body CreateWorkspaceRequest true "Workspace creation payload" +// @Success 201 {object} WorkspaceCreateResponse +// @Failure 400 {object} responses.ErrorResponse +// @Failure 401 {object} responses.ErrorResponse +// @Failure 500 {object} responses.ErrorResponse +// @Router /v1/conv/workspaces [post] +func (route *WorkspaceRoute) CreateWorkspace(reqCtx *gin.Context) { + var request CreateWorkspaceRequest + if err := reqCtx.ShouldBindJSON(&request); err != nil { + reqCtx.AbortWithStatusJSON(http.StatusBadRequest, responses.ErrorResponse{ + Code: "e663a5c4-bfc8-452f-bbe9-6d3f65d3d7dc", + Error: "invalid request payload", + }) + return + } + + user, ok := auth.GetUserFromContext(reqCtx) + if !ok { + reqCtx.AbortWithStatusJSON(http.StatusUnauthorized, responses.ErrorResponse{ + Code: "f1b9907a-1560-4c7b-8eb4-874fa6ab54b2", + Error: "user not found", + }) + return + } + + ctx := reqCtx.Request.Context() + workspace := request.ConvertToWorkspace(user.ID) + workspaceEntity, err := route.workspaceService.CreateWorkspace(ctx, workspace) + if err != nil { + status := http.StatusInternalServerError + if err.GetCode() == "3a5dcb2f-9f1c-4f4b-8893-4a62f72f7a00" || err.GetCode() == "94a6a12b-d4f0-4594-8125-95de7f9ce3d6" { + status = http.StatusBadRequest + } + reqCtx.AbortWithStatusJSON(status, responses.ErrorResponse{ + Code: err.GetCode(), + Error: err.Error(), + }) + return + } + + reqCtx.JSON(http.StatusCreated, toWorkspaceResponse(workspaceEntity)) +} + +// ListWorkspaces godoc +// @Summary List Workspaces +// @Description Lists all workspaces for the authenticated user. +// @Tags conv Workspaces API +// @Security BearerAuth +// @Produce json +// @Success 200 {object} WorkspaceListResponse +// @Failure 401 {object} responses.ErrorResponse +// @Failure 500 {object} responses.ErrorResponse +// @Router /v1/conv/workspaces [get] +func (route *WorkspaceRoute) ListWorkspaces(reqCtx *gin.Context) { + user, ok := auth.GetUserFromContext(reqCtx) + if !ok { + reqCtx.AbortWithStatusJSON(http.StatusUnauthorized, responses.ErrorResponse{ + Code: "b7a4fb56-451f-4b44-8d85-d5f222528b0d", + Error: "user not found", + }) + return + } + + ctx := reqCtx.Request.Context() + workspaces, err := route.workspaceService.FindWorkspacesByFilter(ctx, workspace.WorkspaceFilter{ + UserID: &user.ID, + }, nil) + if err != nil { + reqCtx.AbortWithStatusJSON(http.StatusInternalServerError, responses.ErrorResponse{ + Code: err.GetCode(), + Error: err.Error(), + }) + return + } + + responsesList := make([]WorkspaceResponse, len(workspaces)) + for i, entity := range workspaces { + responsesList[i] = toWorkspaceResponse(entity) + } + + var firstID *string + var lastID *string + if len(workspaces) > 0 { + firstID = ptr.ToString(workspaces[0].PublicID) + lastID = ptr.ToString(workspaces[len(workspaces)-1].PublicID) + } + + reqCtx.JSON(http.StatusOK, responses.ListResponse[WorkspaceResponse]{ + Status: responses.ResponseCodeOk, + Total: int64(len(workspaces)), + Results: responsesList, + FirstID: firstID, + LastID: lastID, + HasMore: false, + }) +} + +// UpdateWorkspaceName godoc +// @Summary Update Workspace Name +// @Description Updates the name of a workspace. +// @Tags conv Workspaces API +// @Security BearerAuth +// @Accept json +// @Produce json +// @Param workspace_id path string true "Workspace ID" +// @Param request body PatchWorkspaceRequest true "Patch model request" +// @Success 200 {object} WorkspaceCreateResponse +// @Failure 400 {object} responses.ErrorResponse +// @Failure 401 {object} responses.ErrorResponse +// @Failure 404 {object} responses.ErrorResponse +// @Failure 500 {object} responses.ErrorResponse +// @Router /v1/conv/workspaces/{workspace_id} [patch] +func (route *WorkspaceRoute) UpdateWorkspaceName(reqCtx *gin.Context) { + var request PatchWorkspaceRequest + if err := reqCtx.ShouldBindJSON(&request); err != nil { + reqCtx.AbortWithStatusJSON(http.StatusBadRequest, responses.ErrorResponse{ + Code: "6dc4fb8f-8af9-4b66-a67a-c2efdec59f5e", + Error: "invalid request payload", + }) + return + } + + workspaceEntity, ok := workspace.GetWorkspaceFromContext(reqCtx) + if !ok { + reqCtx.AbortWithStatusJSON(http.StatusNotFound, responses.ErrorResponse{ + Code: "c8bc424c-5b20-4cf9-8ca1-7d9ad1b098c8", + Error: "workspace not found", + }) + return + } + + ctx := reqCtx.Request.Context() + updated, err := route.workspaceService.UpdateWorkspaceName(ctx, workspaceEntity, request.Name) + if err != nil { + status := http.StatusInternalServerError + if err.GetCode() == "71cf6385-8ca9-4f25-9ad5-2f3ec0e0f765" || err.GetCode() == "d36f9e9f-db49-4d06-81db-75adf127cd7c" { + status = http.StatusBadRequest + } + reqCtx.AbortWithStatusJSON(status, responses.ErrorResponse{ + Code: err.GetCode(), + Error: err.Error(), + }) + return + } + + reqCtx.JSON(http.StatusOK, toWorkspaceResponse(updated)) +} + +// UpdateWorkspaceInstruction godoc +// @Summary Update Workspace Instruction +// @Description Updates the shared instruction for a workspace. +// @Tags conv Workspaces API +// @Security BearerAuth +// @Accept json +// @Produce json +// @Param workspace_id path string true "Workspace ID" +// @Param request body UpdateWorkspaceInstructionRequest true "Workspace instruction update payload" +// @Success 200 {object} WorkspaceCreateResponse +// @Failure 400 {object} responses.ErrorResponse +// @Failure 401 {object} responses.ErrorResponse +// @Failure 404 {object} responses.ErrorResponse +// @Failure 500 {object} responses.ErrorResponse +// @Router /v1/conv/workspaces/{workspace_id}/instruction [patch] +func (route *WorkspaceRoute) UpdateWorkspaceInstruction(reqCtx *gin.Context) { + var request UpdateWorkspaceInstructionRequest + if err := reqCtx.ShouldBindJSON(&request); err != nil { + reqCtx.AbortWithStatusJSON(http.StatusBadRequest, responses.ErrorResponse{ + Code: "c26be0cb-0c74-486f-bc72-7bb67ad839b1", + Error: "invalid request payload", + }) + return + } + + workspaceEntity, ok := workspace.GetWorkspaceFromContext(reqCtx) + if !ok { + reqCtx.AbortWithStatusJSON(http.StatusNotFound, responses.ErrorResponse{ + Code: "c8bc424c-5b20-4cf9-8ca1-7d9ad1b098c8", + Error: "workspace not found", + }) + return + } + + ctx := reqCtx.Request.Context() + updated, err := route.workspaceService.UpdateWorkspaceInstruction(ctx, workspaceEntity, request.Instruction) + if err != nil { + reqCtx.AbortWithStatusJSON(http.StatusInternalServerError, responses.ErrorResponse{ + Code: err.GetCode(), + Error: err.Error(), + }) + return + } + + reqCtx.JSON(http.StatusOK, toWorkspaceResponse(updated)) +} + +// DeleteWorkspace godoc +// @Summary Delete Workspace +// @Description Deletes a workspace and cascades to its conversations. +// @Tags conv Workspaces API +// @Security BearerAuth +// @Param workspace_id path string true "Workspace ID" +// @Success 200 {object} WorkspaceDeleteResponse +// @Failure 401 {object} responses.ErrorResponse +// @Failure 404 {object} responses.ErrorResponse +// @Failure 500 {object} responses.ErrorResponse +// @Router /v1/conv/workspaces/{workspace_id} [delete] +func (route *WorkspaceRoute) DeleteWorkspace(reqCtx *gin.Context) { + workspaceEntity, ok := workspace.GetWorkspaceFromContext(reqCtx) + if !ok { + reqCtx.AbortWithStatusJSON(http.StatusNotFound, responses.ErrorResponse{ + Code: "c8bc424c-5b20-4cf9-8ca1-7d9ad1b098c8", + Error: "workspace not found", + }) + return + } + + ctx := reqCtx.Request.Context() + if err := route.workspaceService.DeleteWorkspaceWithConversations(ctx, workspaceEntity); err != nil { + reqCtx.AbortWithStatusJSON(http.StatusInternalServerError, responses.ErrorResponse{ + Code: err.GetCode(), + Error: err.Error(), + }) + return + } + + result := WorkspaceDeletedResponse{ + ID: workspaceEntity.PublicID, + Deleted: true, + } + + reqCtx.JSON(http.StatusOK, result) +} + +func toWorkspaceResponse(entity *workspace.Workspace) WorkspaceResponse { + var instruction *string + if entity.Instruction != nil { + instruction = ptr.ToString(*entity.Instruction) + } + + return WorkspaceResponse{ + ID: entity.PublicID, + Name: entity.Name, + Instruction: instruction, + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + } +} + +type WorkspaceCreateResponse struct { + Status string `json:"status"` + Result WorkspaceResponse `json:"result"` +} + +type WorkspaceListResponse struct { + Status string `json:"status"` + Total int64 `json:"total"` + Results []WorkspaceResponse `json:"results"` + FirstID *string `json:"first_id"` + LastID *string `json:"last_id"` + HasMore bool `json:"has_more"` +} + +type WorkspaceDeleteResponse struct { + Status string `json:"status"` + Result WorkspaceDeletedResponse `json:"result"` +} diff --git a/apps/jan-api-gateway/application/app/interfaces/http/routes/v1/conversations/conversations_route.go b/apps/jan-api-gateway/application/app/interfaces/http/routes/v1/conversations/conversations_route.go index 24e378e4..ed5567d7 100644 --- a/apps/jan-api-gateway/application/app/interfaces/http/routes/v1/conversations/conversations_route.go +++ b/apps/jan-api-gateway/application/app/interfaces/http/routes/v1/conversations/conversations_route.go @@ -3,11 +3,13 @@ package conversations import ( "fmt" "net/http" + "strings" "github.com/gin-gonic/gin" "menlo.ai/jan-api-gateway/app/domain/auth" "menlo.ai/jan-api-gateway/app/domain/conversation" "menlo.ai/jan-api-gateway/app/domain/query" + "menlo.ai/jan-api-gateway/app/domain/workspace" "menlo.ai/jan-api-gateway/app/interfaces/http/responses" "menlo.ai/jan-api-gateway/app/interfaces/http/responses/openai" @@ -19,13 +21,15 @@ import ( type ConversationAPI struct { conversationService *conversation.ConversationService authService *auth.AuthService + workspaceService *workspace.WorkspaceService } // Request structs type CreateConversationRequest struct { - Title string `json:"title"` - Metadata map[string]string `json:"metadata,omitempty"` - Items []ConversationItemRequest `json:"items,omitempty"` + Title string `json:"title"` + Metadata map[string]string `json:"metadata,omitempty"` + Items []ConversationItemRequest `json:"items,omitempty"` + WorkspaceID string `json:"workspace_id,omitempty"` } type UpdateConversationRequest struct { @@ -33,6 +37,10 @@ type UpdateConversationRequest struct { Metadata *map[string]string `json:"metadata"` } +type UpdateConversationWorkspaceRequest struct { + WorkspaceID *string `json:"workspace_id,omitempty"` +} + type ConversationItemRequest struct { Type string `json:"type" binding:"required"` Role conversation.ItemRole `json:"role,omitempty"` @@ -50,11 +58,12 @@ type CreateItemsRequest struct { // Response structs type ExtendedConversationResponse struct { - ID string `json:"id"` - Title string `json:"title"` - Object string `json:"object"` - CreatedAt int64 `json:"created_at"` - Metadata map[string]string `json:"metadata"` + ID string `json:"id"` + Title string `json:"title"` + Object string `json:"object"` + WorkspacePublicID string `json:"workspace_id,omitempty"` + CreatedAt int64 `json:"created_at"` + Metadata map[string]string `json:"metadata"` } type DeletedConversationResponse struct { @@ -119,10 +128,12 @@ type AnnotationResponse struct { // NewConversationAPI creates a new conversation API instance func NewConversationAPI( conversationService *conversation.ConversationService, - authService *auth.AuthService) *ConversationAPI { + authService *auth.AuthService, + workspaceService *workspace.WorkspaceService) *ConversationAPI { return &ConversationAPI{ conversationService, authService, + workspaceService, } } @@ -137,6 +148,11 @@ func (api *ConversationAPI) RegisterRouter(router *gin.RouterGroup) { conversationsRouter.GET("", api.ListConversationsHandler) conversationMiddleWare := api.conversationService.GetConversationMiddleWare() + conversationsRouter.PATCH( + fmt.Sprintf("/:%s/workspace", conversation.ConversationContextKeyPublicID), + conversationMiddleWare, + api.UpdateConversationWorkspaceHandler, + ) conversationsRouter.GET(fmt.Sprintf("/:%s", conversation.ConversationContextKeyPublicID), conversationMiddleWare, api.GetConversationHandler) conversationsRouter.PATCH(fmt.Sprintf("/:%s", conversation.ConversationContextKeyPublicID), conversationMiddleWare, api.UpdateConversationHandler) conversationsRouter.DELETE(fmt.Sprintf("/:%s", conversation.ConversationContextKeyPublicID), conversationMiddleWare, api.DeleteConversationHandler) @@ -173,7 +189,8 @@ func (api *ConversationAPI) RegisterRouter(router *gin.RouterGroup) { // @Param limit query int false "The maximum number of items to return" default(20) // @Param after query string false "A cursor for use in pagination. The ID of the last object from the previous page" // @Param order query string false "Order of items (asc/desc)" -// @Success 200 {object} openai.ListResponse[ExtendedConversationResponse] "Successfully retrieved the list of conversations" +// @Param workspace_id query string false "Filter conversations by workspace public ID" +// @Success 200 {object} object "Successfully retrieved the list of conversations" // @Failure 400 {object} responses.ErrorResponse "Bad Request - Invalid pagination parameters" // @Failure 401 {object} responses.ErrorResponse "Unauthorized - invalid or missing API key" // @Failure 500 {object} responses.ErrorResponse "Internal Server Error" @@ -183,11 +200,17 @@ func (api *ConversationAPI) ListConversationsHandler(reqCtx *gin.Context) { user, _ := auth.GetUserFromContext(reqCtx) userID := user.ID + workspaceIDParam := strings.TrimSpace(reqCtx.Query("workspace_id")) + pagination, err := query.GetCursorPaginationFromQuery(reqCtx, func(lastID string) (*uint, error) { - convs, convErr := api.conversationService.FindConversationsByFilter(ctx, conversation.ConversationFilter{ + filter := conversation.ConversationFilter{ UserID: &userID, PublicID: &lastID, - }, nil) + } + if workspaceIDParam != "" { + filter.WorkspacePublicID = &workspaceIDParam + } + convs, convErr := api.conversationService.FindConversationsByFilter(ctx, filter, nil) if convErr != nil { return nil, convErr } @@ -207,6 +230,9 @@ func (api *ConversationAPI) ListConversationsHandler(reqCtx *gin.Context) { filter := conversation.ConversationFilter{ UserID: &userID, } + if workspaceIDParam != "" { + filter.WorkspacePublicID = &workspaceIDParam + } conversations, convErr := api.conversationService.FindConversationsByFilter(ctx, filter, pagination) if convErr != nil { reqCtx.AbortWithStatusJSON(http.StatusInternalServerError, responses.ErrorResponse{ @@ -317,8 +343,20 @@ func (api *ConversationAPI) CreateConversationHandler(reqCtx *gin.Context) { return } + // validate workspace ID belongs to user and fetch the workspace public ID + workspace, err := api.workspaceService.GetWorkspaceByPublicIDAndUserID(ctx, request.WorkspaceID, userId) + if err != nil { + reqCtx.AbortWithStatusJSON(http.StatusBadRequest, responses.ErrorResponse{ + Code: "019952d0-1dc9-746e-82ff-dd42b1e7930f", + ErrorInstance: err.GetError(), + }) + return + } + + workspacePublicID := workspace.PublicID + // Create conversation - conv, err := api.conversationService.CreateConversation(ctx, userId, &request.Title, true, request.Metadata) + conv, err := api.conversationService.CreateConversation(ctx, userId, &request.Title, true, request.Metadata, &workspacePublicID) if err != nil { reqCtx.AbortWithStatusJSON(http.StatusBadRequest, responses.ErrorResponse{ Code: "019952d0-3e32-76ba-a97f-711223df2c84", @@ -450,6 +488,74 @@ func (api *ConversationAPI) DeleteConversationHandler(reqCtx *gin.Context) { reqCtx.JSON(http.StatusOK, response) } +// @Summary Update conversation workspace +// @Description Moves a conversation to another workspace or removes it from a workspace +// @Tags Conversations API +// @Security BearerAuth +// @Produce json +// @Param conversation_id path string true "Conversation ID" +// @Param request body UpdateConversationWorkspaceRequest true "Workspace assignment payload" +// @Success 200 {object} ExtendedConversationResponse "Updated conversation" +// @Failure 400 {object} responses.ErrorResponse "Invalid request payload" +// @Failure 401 {object} responses.ErrorResponse "Unauthorized" +// @Failure 403 {object} responses.ErrorResponse "Access denied" +// @Failure 404 {object} responses.ErrorResponse "Workspace or conversation not found" +// @Failure 500 {object} responses.ErrorResponse "Internal server error" +// @Router /v1/conversations/{conversation_id}/workspace [patch] +func (api *ConversationAPI) UpdateConversationWorkspaceHandler(reqCtx *gin.Context) { + ctx := reqCtx.Request.Context() + conv, ok := conversation.GetConversationFromContext(reqCtx) + if !ok { + reqCtx.AbortWithStatusJSON(http.StatusBadRequest, responses.ErrorResponse{ + Code: "a4fb6e9b-00c8-423c-9836-a83080e34d28", + Error: "conversation not found", + }) + return + } + + var request UpdateConversationWorkspaceRequest + if err := reqCtx.ShouldBindJSON(&request); err != nil { + reqCtx.AbortWithStatusJSON(http.StatusBadRequest, responses.ErrorResponse{ + Code: "7733200c-9a0b-43e1-8807-6b5fa2799f77", + Error: "Invalid request payload", + }) + return + } + + var workspacePublicID *string + if request.WorkspaceID != nil && strings.TrimSpace(*request.WorkspaceID) != "" { + trimmedWorkspaceID := strings.TrimSpace(*request.WorkspaceID) + workspaceEntity, err := api.workspaceService.GetWorkspaceByPublicIDAndUserID(ctx, trimmedWorkspaceID, conv.UserID) + if err != nil { + status := http.StatusInternalServerError + if err.GetCode() == "c8bc424c-5b20-4cf9-8ca1-7d9ad1b098c8" { + status = http.StatusNotFound + } + reqCtx.AbortWithStatusJSON(status, responses.ErrorResponse{ + Code: err.GetCode(), + Error: err.Error(), + }) + return + } + publicID := workspaceEntity.PublicID + workspacePublicID = &publicID + } else { + workspacePublicID = nil + } + + updatedConv, err := api.conversationService.UpdateConversationWorkspace(ctx, conv, workspacePublicID) + if err != nil { + reqCtx.AbortWithStatusJSON(http.StatusInternalServerError, responses.ErrorResponse{ + Code: "4c46036b-1f5b-4251-80c9-3d20292a0194", + Error: err.Error(), + }) + return + } + + response := domainToExtendedConversationResponse(updatedConv) + reqCtx.JSON(http.StatusOK, response) +} + // @Summary Create items in a conversation // @Description Adds multiple items to a conversation with OpenAI-compatible format // @Tags Conversations API @@ -725,11 +831,12 @@ func domainToExtendedConversationResponse(entity *conversation.Conversation) *Ex metadata = make(map[string]string) } return &ExtendedConversationResponse{ - ID: entity.PublicID, - Object: "conversation", - Title: ptr.FromString(entity.Title), - CreatedAt: entity.CreatedAt.Unix(), - Metadata: metadata, + ID: entity.PublicID, + Object: "conversation", + Title: ptr.FromString(entity.Title), + WorkspacePublicID: ptr.FromString(entity.WorkspacePublicID), + CreatedAt: entity.CreatedAt.Unix(), + Metadata: metadata, } } diff --git a/apps/jan-api-gateway/application/app/interfaces/http/routes/v1/v1_route.go b/apps/jan-api-gateway/application/app/interfaces/http/routes/v1/v1_route.go index c7b6b6b0..5f2bdb4a 100644 --- a/apps/jan-api-gateway/application/app/interfaces/http/routes/v1/v1_route.go +++ b/apps/jan-api-gateway/application/app/interfaces/http/routes/v1/v1_route.go @@ -15,20 +15,22 @@ import ( ) type V1Route struct { - organizationRoute *organization.OrganizationRoute - chatRoute *chat.ChatRoute - convChatRoute *conv.ConvChatRoute - conversationAPI *conversations.ConversationAPI - modelAPI *ModelAPI - mcpAPI *mcp.MCPAPI - authRoute *auth.AuthRoute - responsesRoute *responses.ResponseRoute + organizationRoute *organization.OrganizationRoute + chatRoute *chat.ChatRoute + convChatRoute *conv.ConvChatRoute + convWorkspaceRoute *conv.WorkspaceRoute + conversationAPI *conversations.ConversationAPI + modelAPI *ModelAPI + mcpAPI *mcp.MCPAPI + authRoute *auth.AuthRoute + responsesRoute *responses.ResponseRoute } func NewV1Route( organizationRoute *organization.OrganizationRoute, chatRoute *chat.ChatRoute, convChatRoute *conv.ConvChatRoute, + convWorkspaceRoute *conv.WorkspaceRoute, conversationAPI *conversations.ConversationAPI, modelAPI *ModelAPI, mcpAPI *mcp.MCPAPI, @@ -39,6 +41,7 @@ func NewV1Route( organizationRoute, chatRoute, convChatRoute, + convWorkspaceRoute, conversationAPI, modelAPI, mcpAPI, @@ -52,6 +55,7 @@ func (v1Route *V1Route) RegisterRouter(router gin.IRouter) { v1Router.GET("/version", GetVersion) v1Route.chatRoute.RegisterRouter(v1Router) v1Route.convChatRoute.RegisterRouter(v1Router) + v1Route.convWorkspaceRoute.RegisterRouter(v1Router) v1Route.conversationAPI.RegisterRouter(v1Router) v1Route.modelAPI.RegisterRouter(v1Router) v1Route.mcpAPI.RegisterRouter(v1Router) diff --git a/apps/jan-api-gateway/application/cmd/server/wire_gen.go b/apps/jan-api-gateway/application/cmd/server/wire_gen.go index bcfec80e..c43eb021 100644 --- a/apps/jan-api-gateway/application/cmd/server/wire_gen.go +++ b/apps/jan-api-gateway/application/cmd/server/wire_gen.go @@ -20,6 +20,7 @@ import ( "menlo.ai/jan-api-gateway/app/domain/project" "menlo.ai/jan-api-gateway/app/domain/response" "menlo.ai/jan-api-gateway/app/domain/user" + "menlo.ai/jan-api-gateway/app/domain/workspace" "menlo.ai/jan-api-gateway/app/infrastructure/cache" "menlo.ai/jan-api-gateway/app/infrastructure/database" "menlo.ai/jan-api-gateway/app/infrastructure/database/repository/apikeyrepo" @@ -31,6 +32,7 @@ import ( "menlo.ai/jan-api-gateway/app/infrastructure/database/repository/responserepo" "menlo.ai/jan-api-gateway/app/infrastructure/database/repository/transaction" "menlo.ai/jan-api-gateway/app/infrastructure/database/repository/userrepo" + "menlo.ai/jan-api-gateway/app/infrastructure/database/repository/workspacerepo" "menlo.ai/jan-api-gateway/app/infrastructure/inference" "menlo.ai/jan-api-gateway/app/interfaces/http" "menlo.ai/jan-api-gateway/app/interfaces/http/routes/v1" @@ -95,7 +97,10 @@ func CreateApplication() (*Application, error) { serperMCP := mcpimpl.NewSerperMCP(serperService) convMCPAPI := conv.NewConvMCPAPI(authService, serperMCP) convChatRoute := conv.NewConvChatRoute(authService, convCompletionAPI, convMCPAPI) - conversationAPI := conversations.NewConversationAPI(conversationService, authService) + workspaceRepository := workspacerepo.NewWorkspaceGormRepository(transactionDatabase) + workspaceService := workspace.NewWorkspaceService(workspaceRepository, conversationRepository) + workspaceRoute := conv.NewWorkspaceRoute(authService, workspaceService) + conversationAPI := conversations.NewConversationAPI(conversationService, authService, workspaceService) modelAPI := v1.NewModelAPI(inferenceModelRegistry) mcpapi := mcp.NewMCPAPI(serperMCP, authService) googleAuthAPI := google.NewGoogleAuthAPI(userService, authService) @@ -106,7 +111,7 @@ func CreateApplication() (*Application, error) { streamModelService := response.NewStreamModelService(responseModelService) nonStreamModelService := response.NewNonStreamModelService(responseModelService) responseRoute := responses.NewResponseRoute(responseModelService, authService, responseService, streamModelService, nonStreamModelService) - v1Route := v1.NewV1Route(organizationRoute, chatRoute, convChatRoute, conversationAPI, modelAPI, mcpapi, authRoute, responseRoute) + v1Route := v1.NewV1Route(organizationRoute, chatRoute, convChatRoute, workspaceRoute, conversationAPI, modelAPI, mcpapi, authRoute, responseRoute) httpServer := http.NewHttpServer(v1Route) cronService := cron.NewService(janInferenceClient, inferenceModelRegistry) application := &Application{ diff --git a/apps/jan-api-gateway/application/docs/docs.go b/apps/jan-api-gateway/application/docs/docs.go index 39eb2e89..db48731d 100644 --- a/apps/jan-api-gateway/application/docs/docs.go +++ b/apps/jan-api-gateway/application/docs/docs.go @@ -413,6 +413,285 @@ const docTemplate = `{ } } }, + "/v1/conv/workspaces": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Lists all workspaces for the authenticated user.", + "produces": [ + "application/json" + ], + "tags": [ + "conv Workspaces API" + ], + "summary": "List Workspaces", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_conv.WorkspaceListResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Creates a new workspace for the authenticated user.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "conv Workspaces API" + ], + "summary": "Create Workspace", + "parameters": [ + { + "description": "Workspace creation payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_conv.CreateWorkspaceRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_conv.WorkspaceCreateResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + } + } + } + }, + "/v1/conv/workspaces/{workspace_id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Deletes a workspace and cascades to its conversations.", + "tags": [ + "conv Workspaces API" + ], + "summary": "Delete Workspace", + "parameters": [ + { + "type": "string", + "description": "Workspace ID", + "name": "workspace_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_conv.WorkspaceDeleteResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + } + } + }, + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Updates the name of a workspace.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "conv Workspaces API" + ], + "summary": "Update Workspace Name", + "parameters": [ + { + "type": "string", + "description": "Workspace ID", + "name": "workspace_id", + "in": "path", + "required": true + }, + { + "description": "Patch model request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_conv.PatchWorkspaceRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_conv.WorkspaceCreateResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + } + } + } + }, + "/v1/conv/workspaces/{workspace_id}/instruction": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Updates the shared instruction for a workspace.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "conv Workspaces API" + ], + "summary": "Update Workspace Instruction", + "parameters": [ + { + "type": "string", + "description": "Workspace ID", + "name": "workspace_id", + "in": "path", + "required": true + }, + { + "description": "Workspace instruction update payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_conv.UpdateWorkspaceInstructionRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_conv.WorkspaceCreateResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + } + } + } + }, "/v1/conversations": { "get": { "security": [ @@ -444,13 +723,19 @@ const docTemplate = `{ "description": "Order of items (asc/desc)", "name": "order", "in": "query" + }, + { + "type": "string", + "description": "Filter conversations by workspace public ID", + "name": "workspace_id", + "in": "query" } ], "responses": { "200": { "description": "Successfully retrieved the list of conversations", "schema": { - "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses_openai.ListResponse-app_interfaces_http_routes_v1_conversations_ExtendedConversationResponse" + "type": "object" } }, "400": { @@ -1007,6 +1292,79 @@ const docTemplate = `{ } } }, + "/v1/conversations/{conversation_id}/workspace": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Moves a conversation to another workspace or removes it from a workspace", + "produces": [ + "application/json" + ], + "tags": [ + "Conversations API" + ], + "summary": "Update conversation workspace", + "parameters": [ + { + "type": "string", + "description": "Conversation ID", + "name": "conversation_id", + "in": "path", + "required": true + }, + { + "description": "Workspace assignment payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_conversations.UpdateConversationWorkspaceRequest" + } + } + ], + "responses": { + "200": { + "description": "Updated conversation", + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_conversations.ExtendedConversationResponse" + } + }, + "400": { + "description": "Invalid request payload", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "403": { + "description": "Access denied", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "404": { + "description": "Workspace or conversation not found", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + } + } + } + }, "/v1/mcp": { "post": { "security": [ @@ -2382,6 +2740,20 @@ const docTemplate = `{ } } }, + "app_interfaces_http_routes_v1_conv.CreateWorkspaceRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "instruction": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "app_interfaces_http_routes_v1_conv.ExtendedChatCompletionRequest": { "type": "object", "properties": { @@ -2608,6 +2980,17 @@ const docTemplate = `{ } } }, + "app_interfaces_http_routes_v1_conv.PatchWorkspaceRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + } + }, "app_interfaces_http_routes_v1_conv.ResponseMetadata": { "type": "object", "properties": { @@ -2634,6 +3017,93 @@ const docTemplate = `{ } } }, + "app_interfaces_http_routes_v1_conv.UpdateWorkspaceInstructionRequest": { + "type": "object", + "properties": { + "instruction": { + "type": "string" + } + } + }, + "app_interfaces_http_routes_v1_conv.WorkspaceCreateResponse": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_conv.WorkspaceResponse" + }, + "status": { + "type": "string" + } + } + }, + "app_interfaces_http_routes_v1_conv.WorkspaceDeleteResponse": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_conv.WorkspaceDeletedResponse" + }, + "status": { + "type": "string" + } + } + }, + "app_interfaces_http_routes_v1_conv.WorkspaceDeletedResponse": { + "type": "object", + "properties": { + "deleted": { + "type": "boolean" + }, + "id": { + "type": "string" + } + } + }, + "app_interfaces_http_routes_v1_conv.WorkspaceListResponse": { + "type": "object", + "properties": { + "first_id": { + "type": "string" + }, + "has_more": { + "type": "boolean" + }, + "last_id": { + "type": "string" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_conv.WorkspaceResponse" + } + }, + "status": { + "type": "string" + }, + "total": { + "type": "integer" + } + } + }, + "app_interfaces_http_routes_v1_conv.WorkspaceResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "instruction": { + "type": "string" + }, + "name": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "app_interfaces_http_routes_v1_conversations.AnnotationResponse": { "type": "object", "properties": { @@ -2770,6 +3240,9 @@ const docTemplate = `{ }, "title": { "type": "string" + }, + "workspace_id": { + "type": "string" } } }, @@ -2821,6 +3294,9 @@ const docTemplate = `{ }, "title": { "type": "string" + }, + "workspace_id": { + "type": "string" } } }, @@ -2891,6 +3367,14 @@ const docTemplate = `{ } } }, + "app_interfaces_http_routes_v1_conversations.UpdateConversationWorkspaceRequest": { + "type": "object", + "properties": { + "workspace_id": { + "type": "string" + } + } + }, "app_interfaces_http_routes_v1_organization.AdminAPIKeyDeletedResponse": { "type": "object", "properties": { @@ -3328,6 +3812,10 @@ const docTemplate = `{ "user": { "description": "The user to use for this response.", "type": "string" + }, + "workspace": { + "description": "The workspace ID to associate the response with for shared instructions.", + "type": "string" } } }, @@ -4078,32 +4566,6 @@ const docTemplate = `{ } } }, - "menlo_ai_jan-api-gateway_app_interfaces_http_responses_openai.ListResponse-app_interfaces_http_routes_v1_conversations_ExtendedConversationResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/app_interfaces_http_routes_v1_conversations.ExtendedConversationResponse" - } - }, - "first_id": { - "type": "string" - }, - "has_more": { - "type": "boolean" - }, - "last_id": { - "type": "string" - }, - "object": { - "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses_openai.ObjectTypeList" - }, - "total": { - "type": "integer" - } - } - }, "menlo_ai_jan-api-gateway_app_interfaces_http_responses_openai.ListResponse-app_interfaces_http_routes_v1_organization_invites_InviteResponse": { "type": "object", "properties": { diff --git a/apps/jan-api-gateway/application/docs/swagger.json b/apps/jan-api-gateway/application/docs/swagger.json index 37284e55..6ef334a3 100644 --- a/apps/jan-api-gateway/application/docs/swagger.json +++ b/apps/jan-api-gateway/application/docs/swagger.json @@ -406,6 +406,285 @@ } } }, + "/v1/conv/workspaces": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Lists all workspaces for the authenticated user.", + "produces": [ + "application/json" + ], + "tags": [ + "conv Workspaces API" + ], + "summary": "List Workspaces", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_conv.WorkspaceListResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Creates a new workspace for the authenticated user.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "conv Workspaces API" + ], + "summary": "Create Workspace", + "parameters": [ + { + "description": "Workspace creation payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_conv.CreateWorkspaceRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_conv.WorkspaceCreateResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + } + } + } + }, + "/v1/conv/workspaces/{workspace_id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Deletes a workspace and cascades to its conversations.", + "tags": [ + "conv Workspaces API" + ], + "summary": "Delete Workspace", + "parameters": [ + { + "type": "string", + "description": "Workspace ID", + "name": "workspace_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_conv.WorkspaceDeleteResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + } + } + }, + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Updates the name of a workspace.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "conv Workspaces API" + ], + "summary": "Update Workspace Name", + "parameters": [ + { + "type": "string", + "description": "Workspace ID", + "name": "workspace_id", + "in": "path", + "required": true + }, + { + "description": "Patch model request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_conv.PatchWorkspaceRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_conv.WorkspaceCreateResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + } + } + } + }, + "/v1/conv/workspaces/{workspace_id}/instruction": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Updates the shared instruction for a workspace.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "conv Workspaces API" + ], + "summary": "Update Workspace Instruction", + "parameters": [ + { + "type": "string", + "description": "Workspace ID", + "name": "workspace_id", + "in": "path", + "required": true + }, + { + "description": "Workspace instruction update payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_conv.UpdateWorkspaceInstructionRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_conv.WorkspaceCreateResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + } + } + } + }, "/v1/conversations": { "get": { "security": [ @@ -437,13 +716,19 @@ "description": "Order of items (asc/desc)", "name": "order", "in": "query" + }, + { + "type": "string", + "description": "Filter conversations by workspace public ID", + "name": "workspace_id", + "in": "query" } ], "responses": { "200": { "description": "Successfully retrieved the list of conversations", "schema": { - "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses_openai.ListResponse-app_interfaces_http_routes_v1_conversations_ExtendedConversationResponse" + "type": "object" } }, "400": { @@ -1000,6 +1285,79 @@ } } }, + "/v1/conversations/{conversation_id}/workspace": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Moves a conversation to another workspace or removes it from a workspace", + "produces": [ + "application/json" + ], + "tags": [ + "Conversations API" + ], + "summary": "Update conversation workspace", + "parameters": [ + { + "type": "string", + "description": "Conversation ID", + "name": "conversation_id", + "in": "path", + "required": true + }, + { + "description": "Workspace assignment payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_conversations.UpdateConversationWorkspaceRequest" + } + } + ], + "responses": { + "200": { + "description": "Updated conversation", + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_conversations.ExtendedConversationResponse" + } + }, + "400": { + "description": "Invalid request payload", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "403": { + "description": "Access denied", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "404": { + "description": "Workspace or conversation not found", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + } + } + } + }, "/v1/mcp": { "post": { "security": [ @@ -2375,6 +2733,20 @@ } } }, + "app_interfaces_http_routes_v1_conv.CreateWorkspaceRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "instruction": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "app_interfaces_http_routes_v1_conv.ExtendedChatCompletionRequest": { "type": "object", "properties": { @@ -2601,6 +2973,17 @@ } } }, + "app_interfaces_http_routes_v1_conv.PatchWorkspaceRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + } + }, "app_interfaces_http_routes_v1_conv.ResponseMetadata": { "type": "object", "properties": { @@ -2627,6 +3010,93 @@ } } }, + "app_interfaces_http_routes_v1_conv.UpdateWorkspaceInstructionRequest": { + "type": "object", + "properties": { + "instruction": { + "type": "string" + } + } + }, + "app_interfaces_http_routes_v1_conv.WorkspaceCreateResponse": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_conv.WorkspaceResponse" + }, + "status": { + "type": "string" + } + } + }, + "app_interfaces_http_routes_v1_conv.WorkspaceDeleteResponse": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_conv.WorkspaceDeletedResponse" + }, + "status": { + "type": "string" + } + } + }, + "app_interfaces_http_routes_v1_conv.WorkspaceDeletedResponse": { + "type": "object", + "properties": { + "deleted": { + "type": "boolean" + }, + "id": { + "type": "string" + } + } + }, + "app_interfaces_http_routes_v1_conv.WorkspaceListResponse": { + "type": "object", + "properties": { + "first_id": { + "type": "string" + }, + "has_more": { + "type": "boolean" + }, + "last_id": { + "type": "string" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_conv.WorkspaceResponse" + } + }, + "status": { + "type": "string" + }, + "total": { + "type": "integer" + } + } + }, + "app_interfaces_http_routes_v1_conv.WorkspaceResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "instruction": { + "type": "string" + }, + "name": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "app_interfaces_http_routes_v1_conversations.AnnotationResponse": { "type": "object", "properties": { @@ -2763,6 +3233,9 @@ }, "title": { "type": "string" + }, + "workspace_id": { + "type": "string" } } }, @@ -2814,6 +3287,9 @@ }, "title": { "type": "string" + }, + "workspace_id": { + "type": "string" } } }, @@ -2884,6 +3360,14 @@ } } }, + "app_interfaces_http_routes_v1_conversations.UpdateConversationWorkspaceRequest": { + "type": "object", + "properties": { + "workspace_id": { + "type": "string" + } + } + }, "app_interfaces_http_routes_v1_organization.AdminAPIKeyDeletedResponse": { "type": "object", "properties": { @@ -3321,6 +3805,10 @@ "user": { "description": "The user to use for this response.", "type": "string" + }, + "workspace": { + "description": "The workspace ID to associate the response with for shared instructions.", + "type": "string" } } }, @@ -4071,32 +4559,6 @@ } } }, - "menlo_ai_jan-api-gateway_app_interfaces_http_responses_openai.ListResponse-app_interfaces_http_routes_v1_conversations_ExtendedConversationResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/app_interfaces_http_routes_v1_conversations.ExtendedConversationResponse" - } - }, - "first_id": { - "type": "string" - }, - "has_more": { - "type": "boolean" - }, - "last_id": { - "type": "string" - }, - "object": { - "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses_openai.ObjectTypeList" - }, - "total": { - "type": "integer" - } - } - }, "menlo_ai_jan-api-gateway_app_interfaces_http_responses_openai.ListResponse-app_interfaces_http_routes_v1_organization_invites_InviteResponse": { "type": "object", "properties": { diff --git a/apps/jan-api-gateway/application/docs/swagger.yaml b/apps/jan-api-gateway/application/docs/swagger.yaml index eadf5799..24bc491f 100644 --- a/apps/jan-api-gateway/application/docs/swagger.yaml +++ b/apps/jan-api-gateway/application/docs/swagger.yaml @@ -65,6 +65,15 @@ definitions: url: type: string type: object + app_interfaces_http_routes_v1_conv.CreateWorkspaceRequest: + properties: + instruction: + type: string + name: + type: string + required: + - name + type: object app_interfaces_http_routes_v1_conv.ExtendedChatCompletionRequest: properties: chat_template_kwargs: @@ -248,6 +257,13 @@ definitions: object: type: string type: object + app_interfaces_http_routes_v1_conv.PatchWorkspaceRequest: + properties: + name: + type: string + required: + - name + type: object app_interfaces_http_routes_v1_conv.ResponseMetadata: properties: ask_item_id: @@ -265,6 +281,62 @@ definitions: store_reasoning: type: boolean type: object + app_interfaces_http_routes_v1_conv.UpdateWorkspaceInstructionRequest: + properties: + instruction: + type: string + type: object + app_interfaces_http_routes_v1_conv.WorkspaceCreateResponse: + properties: + result: + $ref: '#/definitions/app_interfaces_http_routes_v1_conv.WorkspaceResponse' + status: + type: string + type: object + app_interfaces_http_routes_v1_conv.WorkspaceDeleteResponse: + properties: + result: + $ref: '#/definitions/app_interfaces_http_routes_v1_conv.WorkspaceDeletedResponse' + status: + type: string + type: object + app_interfaces_http_routes_v1_conv.WorkspaceDeletedResponse: + properties: + deleted: + type: boolean + id: + type: string + type: object + app_interfaces_http_routes_v1_conv.WorkspaceListResponse: + properties: + first_id: + type: string + has_more: + type: boolean + last_id: + type: string + results: + items: + $ref: '#/definitions/app_interfaces_http_routes_v1_conv.WorkspaceResponse' + type: array + status: + type: string + total: + type: integer + type: object + app_interfaces_http_routes_v1_conv.WorkspaceResponse: + properties: + created_at: + type: string + id: + type: string + instruction: + type: string + name: + type: string + updated_at: + type: string + type: object app_interfaces_http_routes_v1_conversations.AnnotationResponse: properties: end_index: @@ -355,6 +427,8 @@ definitions: type: object title: type: string + workspace_id: + type: string type: object app_interfaces_http_routes_v1_conversations.CreateItemsRequest: properties: @@ -388,6 +462,8 @@ definitions: type: string title: type: string + workspace_id: + type: string type: object app_interfaces_http_routes_v1_conversations.FileContentResponse: properties: @@ -432,6 +508,11 @@ definitions: title: type: string type: object + app_interfaces_http_routes_v1_conversations.UpdateConversationWorkspaceRequest: + properties: + workspace_id: + type: string + type: object app_interfaces_http_routes_v1_organization.AdminAPIKeyDeletedResponse: properties: deleted: @@ -734,6 +815,9 @@ definitions: user: description: The user to use for this response. type: string + workspace: + description: The workspace ID to associate the response with for shared instructions. + type: string required: - input - model @@ -1236,23 +1320,6 @@ definitions: total: type: integer type: object - ? menlo_ai_jan-api-gateway_app_interfaces_http_responses_openai.ListResponse-app_interfaces_http_routes_v1_conversations_ExtendedConversationResponse - : properties: - data: - items: - $ref: '#/definitions/app_interfaces_http_routes_v1_conversations.ExtendedConversationResponse' - type: array - first_id: - type: string - has_more: - type: boolean - last_id: - type: string - object: - $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses_openai.ObjectTypeList' - total: - type: integer - type: object ? menlo_ai_jan-api-gateway_app_interfaces_http_responses_openai.ListResponse-app_interfaces_http_routes_v1_organization_invites_InviteResponse : properties: data: @@ -2097,6 +2164,184 @@ paths: summary: List available models for conversation-aware chat tags: - Conversation-aware Chat API + /v1/conv/workspaces: + get: + description: Lists all workspaces for the authenticated user. + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/app_interfaces_http_routes_v1_conv.WorkspaceListResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + security: + - BearerAuth: [] + summary: List Workspaces + tags: + - conv Workspaces API + post: + consumes: + - application/json + description: Creates a new workspace for the authenticated user. + parameters: + - description: Workspace creation payload + in: body + name: request + required: true + schema: + $ref: '#/definitions/app_interfaces_http_routes_v1_conv.CreateWorkspaceRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/app_interfaces_http_routes_v1_conv.WorkspaceCreateResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + security: + - BearerAuth: [] + summary: Create Workspace + tags: + - conv Workspaces API + /v1/conv/workspaces/{workspace_id}: + delete: + description: Deletes a workspace and cascades to its conversations. + parameters: + - description: Workspace ID + in: path + name: workspace_id + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/app_interfaces_http_routes_v1_conv.WorkspaceDeleteResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + security: + - BearerAuth: [] + summary: Delete Workspace + tags: + - conv Workspaces API + patch: + consumes: + - application/json + description: Updates the name of a workspace. + parameters: + - description: Workspace ID + in: path + name: workspace_id + required: true + type: string + - description: Patch model request + in: body + name: request + required: true + schema: + $ref: '#/definitions/app_interfaces_http_routes_v1_conv.PatchWorkspaceRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/app_interfaces_http_routes_v1_conv.WorkspaceCreateResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + security: + - BearerAuth: [] + summary: Update Workspace Name + tags: + - conv Workspaces API + /v1/conv/workspaces/{workspace_id}/instruction: + patch: + consumes: + - application/json + description: Updates the shared instruction for a workspace. + parameters: + - description: Workspace ID + in: path + name: workspace_id + required: true + type: string + - description: Workspace instruction update payload + in: body + name: request + required: true + schema: + $ref: '#/definitions/app_interfaces_http_routes_v1_conv.UpdateWorkspaceInstructionRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/app_interfaces_http_routes_v1_conv.WorkspaceCreateResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + security: + - BearerAuth: [] + summary: Update Workspace Instruction + tags: + - conv Workspaces API /v1/conversations: get: description: Retrieves a paginated list of conversations for the authenticated @@ -2116,11 +2361,15 @@ paths: in: query name: order type: string + - description: Filter conversations by workspace public ID + in: query + name: workspace_id + type: string responses: "200": description: Successfully retrieved the list of conversations schema: - $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses_openai.ListResponse-app_interfaces_http_routes_v1_conversations_ExtendedConversationResponse' + type: object "400": description: Bad Request - Invalid pagination parameters schema: @@ -2486,6 +2735,54 @@ paths: summary: Get an item from a conversation tags: - Conversations API + /v1/conversations/{conversation_id}/workspace: + patch: + description: Moves a conversation to another workspace or removes it from a + workspace + parameters: + - description: Conversation ID + in: path + name: conversation_id + required: true + type: string + - description: Workspace assignment payload + in: body + name: request + required: true + schema: + $ref: '#/definitions/app_interfaces_http_routes_v1_conversations.UpdateConversationWorkspaceRequest' + produces: + - application/json + responses: + "200": + description: Updated conversation + schema: + $ref: '#/definitions/app_interfaces_http_routes_v1_conversations.ExtendedConversationResponse' + "400": + description: Invalid request payload + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + "403": + description: Access denied + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + "404": + description: Workspace or conversation not found + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + security: + - BearerAuth: [] + summary: Update conversation workspace + tags: + - Conversations API /v1/mcp: post: consumes: