Skip to content

Conversation

locnguyen1986
Copy link
Contributor

@locnguyen1986 locnguyen1986 commented Aug 28, 2025

Description:

This PR implements conversation-like functionality based on the OpenAI Conversations API to allow users to create and manage conversations with context retention. The implementation follows OpenAI's API specification exactly and uses API key authentication for enhanced security.

OpenAI Platform Reference: https://platform.openai.com/docs/api-reference/conversations

Available Endpoints:

V1 API (OpenAI-Compatible):

  • [POST] /v1/conversations - Create conversation with initial items
  • [GET] /v1/conversations/{conversation_id} - Get conversation details
  • [POST] /v1/conversations/{conversation_id} - Update conversation metadata (OpenAI uses POST, not PATCH)
  • [DELETE] /v1/conversations/{conversation_id} - Delete conversation
  • [POST] /v1/conversations/{conversation_id}/items - Create multiple items
  • [GET] /v1/conversations/{conversation_id}/items - List conversation items
  • [GET] /v1/conversations/{conversation_id}/items/{item_id} - Retrieve a specific item
  • [DELETE] /v1/conversations/{conversation_id}/items/{item_id} - Delete an item (returns updated conversation)

Jan API Endpoints (Identical to V1):

  • [POST] /jan/v1/conversations - Create conversation with initial items
  • [GET] /jan/v1/conversations/{conversation_id} - Get conversation details
  • [POST] /jan/v1/conversations/{conversation_id} - Update conversation metadata
  • [DELETE] /jan/v1/conversations/{conversation_id} - Delete conversation
  • [POST] /jan/v1/conversations/{conversation_id}/items - Create multiple items
  • [GET] /jan/v1/conversations/{conversation_id}/items - List conversation items
  • [GET] /jan/v1/conversations/{conversation_id}/items/{item_id} - Retrieve a specific item
  • [DELETE] /jan/v1/conversations/{conversation_id}/items/{item_id} - Delete an item (returns updated conversation)

Change-Specific Checklist

Domain Changes

This PR introduces changes to the domain models or business logic.

List of Changed Domains:

  • conversation - New domain for conversation and item management
  • user - Enhanced with conversation-related functionality

Database Migration

This PR includes a database migration.

Table Changes:

  • conversations - New table for storing conversation metadata with proper indexing
  • items - New table for storing conversation items/messages with foreign key relationships

Migration Strategy:

  • Reset database (new tables added)

Endpoint Changes

This PR adds, modifies, or removes API endpoints.

Swagger/API Docs have been updated for all changed endpoints.

Breaking Change: No - This PR introduces new endpoints only

Key Features Implemented:

OpenAI API Compliance: 100% adherence to OpenAI Conversations API specification with exact request/response structures
Response Structure Compliance: All responses match OpenAI schemas
Custom Error Types: Robust error handling with errors.Is() and specific error types for different failure scenarios
Dual API Support: Identical functionality available on both /v1/ (OpenAI-compatible) and /jan/v1/ (Jan-specific) routes
Context Retention: Conversations maintain full context history with proper item relationships
Private Mode: Conversations are private by default with proper access control validation
Structured Content: Support for complex content types with proper JSON marshaling/unmarshaling

Comment on lines 148 to 153
func stringToStringPtr(s string) *string {
if s == "" {
return nil
}
return &s
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use ptr.ToString(...) instead.

// @Accept json
// @Produce json
// @Param conversation_id path string true "Conversation ID"
// @Success 200 {object} conversation.Conversation "Conversation details"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return a Response object instead of a domain object.

conversationsRouter.PATCH("/:conversation_id", api.UpdateConversation)
conversationsRouter.DELETE("/:conversation_id", api.DeleteConversation)
conversationsRouter.POST("/:conversation_id/items", api.AddItem)
conversationsRouter.GET("/:conversation_id/items/search", api.SearchItems)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there no such endpoint in the OpenAI platform API documentation?

// @Failure 500 {object} responses.ErrorResponse "Internal server error"
// @Router /v1/conversations [post]
func (api *ConversationAPI) CreateConversation(ctx *gin.Context) {
ownerID, err := api.validateAPIKey(ctx)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have concerns about this. Should we separate users into client users and API users?
Currently, they share the same user ID. Assuming the client has a "list conversations" function, the current implementation would cause conversations created using an API key to be retrieved in the client's "list conversations," which is not our intended design. We should fix this in the next iteration.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may keep same user, The client user may register the API and still work with their history. And they should aware that. I think we should keep same user base now.

return conversation, nil
}

func (s *ConversationService) GetConversation(ctx context.Context, publicID string, userID uint) (*Conversation, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current name, GetConversation, is misleading because the function not only retrieves a conversation by its public ID but also performs these additional actions:

  • Checks access permissions based on userID.
  • Loads associated items.
  • Populates the Items field on the returned Conversation object.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its name is changed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code is refactored with naming function and reused if needed

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetConversation still takes publicID and userID. Also, Go doesn't support function overloading. Consider renaming it to something like GetConversationByPublicIDAndUserID.

return conversation, nil
}

func (s *ConversationService) UpdateConversation(ctx context.Context, publicID string, userID uint, title *string, metadata map[string]string) (*Conversation, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming Considerations

  • Why UpdateConversation Could Be Misleading: It suggests a simple update operation, but the function also handles retrieval and access control, which are significant responsibilities.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its name is changed

Comment on lines 693 to 695
HasMore: false, // For now, we don't implement pagination
FirstID: "",
LastID: "",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider following the pattern of /projects.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added it later

if len(data) > 0 {
  response.FirstID = data[0].ID
  response.LastID = data[len(data)-1].ID
}

but now we don't have paging in these API so it should be fine.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we plan to add these back later, please leave a // TODO: comment.

Comment on lines 33 to 55
func GenerateConversationID() (string, error) {
return GenerateSecureID("conv", 16)
}

// GenerateItemID generates an item/message ID with format "msg_..."
func GenerateItemID() (string, error) {
return GenerateSecureID("msg", 16)
}

// GenerateAPIKeyID generates an API key ID with format "sk_..."
func GenerateAPIKeyID() (string, error) {
return GenerateSecureID("sk", 24)
}

// GenerateOrganizationID generates an organization ID with format "org_..."
func GenerateOrganizationID() (string, error) {
return GenerateSecureID("org", 16)
}

// GenerateProjectID generates a project ID with format "proj_..."
func GenerateProjectID() (string, error) {
return GenerateSecureID("proj", 16)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move to the domain or service level.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right, It should belong to domain services, let me create the shared service for that.

Comment on lines 85 to 107
func ValidateConversationID(id string) bool {
return ValidateIDFormat(id, "conv")
}

// ValidateItemID validates an item/message ID format
func ValidateItemID(id string) bool {
return ValidateIDFormat(id, "msg")
}

// ValidateAPIKeyID validates an API key ID format
func ValidateAPIKeyID(id string) bool {
return ValidateIDFormat(id, "sk")
}

// ValidateOrganizationID validates an organization ID format
func ValidateOrganizationID(id string) bool {
return ValidateIDFormat(id, "org")
}

// ValidateProjectID validates a project ID format
func ValidateProjectID(id string) bool {
return ValidateIDFormat(id, "proj")
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move to the domain or service level.

return conversation, nil
}

func (s *ConversationService) GetConversation(ctx context.Context, publicID string, userID uint) (*Conversation, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetConversation still takes publicID and userID. Also, Go doesn't support function overloading. Consider renaming it to something like GetConversationByPublicIDAndUserID.

@@ -0,0 +1,839 @@
package conversation
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is a good idea; the introduction of this handler leads to high coupling between /v1/jan/conversation and /v1/conversation.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But we can discuss this later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yah, I introduce handler now as we try to keep the same feature for both Jan and OpenAI, too much duplicated code, if we plan to merge these APIs endpoint or custom handling, we may do it in different way. Now almost of them are same. And I think they will be the same of the future too.

})
return
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validate length of items here: Initial items to include in the conversation context. You may add up to 20 items at a time.

Comment on lines 95 to 121
func (v *ConversationValidator) ValidatePublicID(publicID string) error {
if publicID == "" {
return fmt.Errorf("public ID cannot be empty")
}

if len(publicID) < 5 || len(publicID) > 50 {
return fmt.Errorf("public ID must be between 5 and 50 characters")
}

// Use centralized ID validation logic
if strings.HasPrefix(publicID, "conv_") {
if !idutils.ValidateConversationID(publicID) {
return fmt.Errorf("invalid conversation ID format")
}
} else if strings.HasPrefix(publicID, "msg_") {
if !idutils.ValidateItemID(publicID) {
return fmt.Errorf("invalid item ID format")
}
} else {
// Fallback to regex pattern for unknown prefixes
if !v.publicIDPattern.MatchString(publicID) {
return fmt.Errorf("public ID contains invalid characters")
}
}

return nil
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The public ID of a domain object should have a single, consistent format.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we support conversation, message as public id now. It not the conversation only.

}

// Convert all items at once for batch processing
itemsToCreate := make([]struct {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the Item domain object.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above, It's for batch processing only. However, I move it to global struct so it's easier for maintain

}

conversationID := ctx.Param("conversation_id")

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Query parameters?
after
include
limit
order

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we plan to add these back later, please leave a // TODO: comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, ignore all the business like paging or handling, I have another PRs with introduce the business layer which gather the conversation ( core business ) and will introduce the Conversation API for Jan ( will replicate with Jan Desktop API now ), We will define it later. This PR focus on structure and simple CRUDs.

@@ -0,0 +1,186 @@
package conversations
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's disregard the /jan/v1/conversation interfaces for now. It's exposed to our client. We don't need to adhere to OpenAI's standard. Let's focus on OpenAI's.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have to keep that, it will be used by Jan Web as migration. Now we have to maintain both of them to make everything works first.

Comment on lines 693 to 695
HasMore: false, // For now, we don't implement pagination
FirstID: "",
LastID: "",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we plan to add these back later, please leave a // TODO: comment.

@@ -0,0 +1,136 @@
package id
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This service violates SPR.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments from chatgpt:

Creating a single service like IDService to handle ID generation for all domain entities is generally not a good idea due to several key design principles it violates: separation of concerns, maintainability, and domain-driven design (DDD).

Separation of Concerns

A single IDService becomes a god object, which is an anti-pattern where one object knows too much or does too much. This violates the principle of separation of concerns by coupling the low-level technical detail of ID generation with the high-level business logic of different domains. The IDService knows about conversations, organizations, users, and API keys, which are distinct business concepts. In a well-designed system, the Organization domain should be responsible for its own identity, not a shared, centralized service. This also creates a single point of failure: if a change to the ID generation logic for one domain entity (e.g., UserID) breaks something, it could potentially affect all other domains using the same service.

Dependency Hell:

All parts of the application that need an ID must depend on this single service. This tight coupling makes it hard to refactor or replace a part of the system without affecting every other part.

Comment on lines 62 to 64
func (s *IDService) GenerateProjectID() (string, error) {
return s.GenerateSecureID("proj", 16)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The specification belongs to the project domain.

// The service has a dependency on the repository interface.
repo ProjectRepository
repo ProjectRepository
idService *id.IDService
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dependency Hell:

All parts of the application that need an ID must depend on this single service. This tight coupling makes it hard to refactor or replace a part of the system without affecting every other part.

@locnguyen1986 locnguyen1986 merged commit 3c3a3f2 into main Sep 1, 2025
1 check passed
@locnguyen1986 locnguyen1986 deleted the feat/71/add-threads-functionality branch September 1, 2025 07:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants