This repository has been archived by the owner on Jan 9, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(instillapp): implement read and write chat message task (#348)
Because - we want to build ai assistant with app backend This commit - build instill app component
- Loading branch information
1 parent
e46d228
commit 2c8a6a0
Showing
12 changed files
with
3,292 additions
and
5,267 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
--- | ||
title: "Instill App" | ||
lang: "en-US" | ||
draft: false | ||
description: "Learn about how to set up a VDP Instill App component https://github.com/instill-ai/instill-core" | ||
--- | ||
|
||
The Instill App component is an application component that allows users to manipulate Instill App related resources.. | ||
It can carry out the following tasks: | ||
|
||
- [Read Chat History](#read-chat-history) | ||
- [Write Chat Message](#write-chat-message) | ||
|
||
|
||
|
||
## Release Stage | ||
|
||
`Alpha` | ||
|
||
|
||
|
||
## Configuration | ||
|
||
The component configuration is defined and maintained [here](https://github.com/instill-ai/component/blob/main/application/instillapp/v0/config/definition.json). | ||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
## Supported Tasks | ||
|
||
### Read Chat History | ||
|
||
Retrieve the chat history from the conversation. | ||
|
||
|
||
| Input | ID | Type | Description | | ||
| :--- | :--- | :--- | :--- | | ||
| Task ID (required) | `task` | string | `TASK_READ_CHAT_HISTORY` | | ||
| Namespace (required) | `namespace` | string | Fill in your namespace, you can get namespace through the tab of switching namespace. | | ||
| App ID (required) | `app-id` | string | Fill in your app ID. | | ||
| Conversation ID (required) | `conversation-id` | string | Fill in your conversation ID. | | ||
| Role | `role` | string | The role of the user you want to specify to retrieve in the conversation. The default is all with the blank setting. Now, we support 'user' and 'assistant'. | | ||
| Message Type | `message-type` | string | The message type of the chat history you want to retrieve. The default is all with blank setting. Now, we only support 'MESSAGE_TYPE_TEXT'. | | ||
| Duration | `duration` | string | The duration between now and how long ago to retrieve the chat history from. i.e. 2h45m5s. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". The default is all with blank setting. | | ||
| Max Message Count | `max-message-count` | integer | The maximum number of messages to retrieve. The default is all with blank setting. | | ||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
| Output | ID | Type | Description | | ||
| :--- | :--- | :--- | :--- | | ||
| [Chat Messages](#read-chat-history-chat-messages) | `messages` | array[object] | List of chat messages | | ||
|
||
|
||
|
||
|
||
<details> | ||
<summary> Output Objects in Read Chat History</summary> | ||
|
||
|
||
|
||
<h4 id="read-chat-history-chat-messages">Chat Messages</h4> | ||
|
||
| Field | Field ID | Type | Note | | ||
| :--- | :--- | :--- | :--- | | ||
| [Content](#read-chat-history-content) | `content` | array | The message content. | | ||
| Name | `name` | string | An optional name for the participant. Provides the model information to differentiate between participants of the same role. | | ||
| Role | `role` | string | The message role, i.e. 'system', 'user' or 'assistant'. | | ||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
</details> | ||
|
||
|
||
|
||
|
||
|
||
|
||
### Write Chat Message | ||
|
||
Write the chat message into the conversation in catalog. | ||
|
||
|
||
| Input | ID | Type | Description | | ||
| :--- | :--- | :--- | :--- | | ||
| Task ID (required) | `task` | string | `TASK_WRITE_CHAT_MESSAGE` | | ||
| Namespace (required) | `namespace` | string | Fill in your namespace, you can get namespace through the tab of switching namespace. | | ||
| App ID (required) | `app-id` | string | Fill in your app ID. | | ||
| Conversation ID (required) | `conversation-id` | string | Fill in your conversation ID. | | ||
| [Message](#write-chat-message-message) (required) | `message` | object | A chat message to be written into the conversation. | | ||
|
||
|
||
|
||
<details> | ||
<summary> Input Objects in Write Chat Message</summary> | ||
|
||
|
||
|
||
<h4 id="write-chat-message-message">Message</h4> | ||
|
||
| Field | Field ID | Type | Note | | ||
| :--- | :--- | :--- | :--- | | ||
| Content | `content` | string | The contents of the message. | | ||
| Role | `role` | string | The role of the author of this message. Now, we support 'user' and 'assistant'. | | ||
|
||
|
||
|
||
</details> | ||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
| Output | ID | Type | Description | | ||
| :--- | :--- | :--- | :--- | | ||
| Message ID | `message-uid` | string | The unique identifier of the message. | | ||
| Create Time | `create-time` | string | The creation time of the message in ISO 8601 format | | ||
| Update Time | `update-time` | string | The update time of the message in ISO 8601 format | | ||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,246 @@ | ||
package instillapp | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"time" | ||
|
||
"github.com/instill-ai/component/base" | ||
appPB "github.com/instill-ai/protogen-go/app/app/v1alpha" | ||
"google.golang.org/grpc/metadata" | ||
"google.golang.org/protobuf/types/known/structpb" | ||
) | ||
|
||
type ReadChatHistoryInput struct { | ||
Namespace string `json:"namespace"` | ||
AppUID string `json:"app-id"` | ||
ConversationID string `json:"conversation-id"` | ||
Role string `json:"role"` | ||
MessageType string `json:"message-type"` | ||
Duration string `json:"duration"` | ||
MaxMessageCount int `json:"max-message-count"` | ||
} | ||
|
||
type ReadChatHistoryOutput struct { | ||
Messages []Message `json:"messages"` | ||
} | ||
|
||
type Message struct { | ||
Content []Content `json:"content"` | ||
Role string `json:"role"` | ||
Name string `json:"name,omitempty"` | ||
} | ||
|
||
type Content struct { | ||
Type string `json:"type"` | ||
Text string `json:"text,omitempty"` | ||
ImageURL string `json:"image-url,omitempty"` | ||
ImageBase64 string `json:"image-base64,omitempty"` | ||
} | ||
|
||
func (in *ReadChatHistoryInput) Validate() error { | ||
if in.Role != "" && in.Role != "user" && in.Role != "assistant" { | ||
return fmt.Errorf("role must be either 'user' or 'assistant'") | ||
} | ||
|
||
if in.MessageType != "" && in.MessageType != "MESSAGE_TYPE_TEXT" { | ||
return fmt.Errorf("message-type must be 'MESSAGE_TYPE_TEXT'") | ||
} | ||
|
||
if in.Duration != "" { | ||
_, err := time.ParseDuration(in.Duration) | ||
if err != nil { | ||
return fmt.Errorf("invalid duration: %w", err) | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func (out *ReadChatHistoryOutput) Filter(inputStruct ReadChatHistoryInput, messages []*appPB.Message) { | ||
for _, message := range messages { | ||
|
||
if inputStruct.Role != "" && inputStruct.Role != message.Role { | ||
continue | ||
} | ||
|
||
if inputStruct.MessageType != "" && inputStruct.MessageType != message.Type.String() { | ||
continue | ||
} | ||
|
||
if inputStruct.Duration != "" { | ||
duration, _ := time.ParseDuration(inputStruct.Duration) | ||
if time.Since(message.CreateTime.AsTime()) > duration { | ||
continue | ||
} | ||
} | ||
if inputStruct.MaxMessageCount > 0 && len(out.Messages) >= inputStruct.MaxMessageCount { | ||
break | ||
} | ||
|
||
content := []Content{ | ||
{ | ||
Type: message.Type.String(), | ||
Text: message.Content, | ||
}, | ||
} | ||
|
||
out.Messages = append(out.Messages, Message{ | ||
Content: content, | ||
Role: message.Role, | ||
}) | ||
} | ||
} | ||
|
||
func (e *execution) readChatHistory(input *structpb.Struct) (*structpb.Struct, error) { | ||
|
||
inputStruct := ReadChatHistoryInput{} | ||
|
||
err := base.ConvertFromStructpb(input, &inputStruct) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to convert input struct: %w", err) | ||
} | ||
|
||
err = inputStruct.Validate() | ||
if err != nil { | ||
return nil, fmt.Errorf("invalid input: %w", err) | ||
} | ||
|
||
appClient, connection := e.client, e.connection | ||
defer connection.Close() | ||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) | ||
defer cancel() | ||
|
||
ctx = metadata.NewOutgoingContext(ctx, getRequestMetadata(e.SystemVariables)) | ||
|
||
res, err := appClient.ListMessages(ctx, &appPB.ListMessagesRequest{ | ||
NamespaceId: inputStruct.Namespace, | ||
AppId: inputStruct.AppUID, | ||
ConversationId: inputStruct.ConversationID, | ||
IncludeSystemMessages: true, | ||
}) | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("failed to list messages: %w", err) | ||
} | ||
|
||
output := ReadChatHistoryOutput{ | ||
Messages: make([]Message, 0), | ||
} | ||
|
||
output.Filter(inputStruct, res.Messages) | ||
|
||
for res.NextPageToken != "" || (len(output.Messages) < inputStruct.MaxMessageCount && inputStruct.MaxMessageCount > 0) { | ||
res, err = appClient.ListMessages(ctx, &appPB.ListMessagesRequest{ | ||
NamespaceId: inputStruct.Namespace, | ||
AppId: inputStruct.AppUID, | ||
ConversationId: inputStruct.ConversationID, | ||
IncludeSystemMessages: true, | ||
PageToken: res.NextPageToken, | ||
}) | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("failed to list messages: %w", err) | ||
} | ||
|
||
output.Filter(inputStruct, res.Messages) | ||
} | ||
|
||
return base.ConvertToStructpb(output) | ||
|
||
} | ||
|
||
type WriteChatMessageInput struct { | ||
Namespace string `json:"namespace"` | ||
AppUID string `json:"app-id"` | ||
ConversationID string `json:"conversation-id"` | ||
Message WriteMessage `json:"message"` | ||
} | ||
|
||
type WriteMessage struct { | ||
Content string `json:"content"` | ||
Role string `json:"role,omitempty"` | ||
} | ||
|
||
type WriteChatMessageOutput struct { | ||
MessageUID string `json:"message-uid"` | ||
CreateTime string `json:"create-time"` | ||
UpdateTime string `json:"update-time"` | ||
} | ||
|
||
func (in *WriteChatMessageInput) Validate() error { | ||
role := in.Message.Role | ||
if role != "" && role != "user" && role != "assistant" { | ||
return fmt.Errorf("role must be either 'user' or 'assistant'") | ||
} | ||
return nil | ||
} | ||
|
||
func (e *execution) writeChatMessage(input *structpb.Struct) (*structpb.Struct, error) { | ||
inputStruct := WriteChatMessageInput{} | ||
|
||
err := base.ConvertFromStructpb(input, &inputStruct) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to convert input struct: %w", err) | ||
} | ||
|
||
err = inputStruct.Validate() | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("invalid input: %w", err) | ||
} | ||
|
||
appClient, connection := e.client, e.connection | ||
defer connection.Close() | ||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) | ||
defer cancel() | ||
|
||
ctx = metadata.NewOutgoingContext(ctx, getRequestMetadata(e.SystemVariables)) | ||
|
||
conversations, err := appClient.ListConversations(ctx, &appPB.ListConversationsRequest{ | ||
NamespaceId: inputStruct.Namespace, | ||
AppId: inputStruct.AppUID, | ||
ConversationId: inputStruct.ConversationID, | ||
}) | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("failed to list conversations: %w", err) | ||
} | ||
|
||
if len(conversations.Conversations) == 0 { | ||
_, err = appClient.CreateConversation(ctx, &appPB.CreateConversationRequest{ | ||
NamespaceId: inputStruct.Namespace, | ||
AppId: inputStruct.AppUID, | ||
ConversationId: inputStruct.ConversationID, | ||
}) | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("failed to create conversation: %w", err) | ||
} | ||
} | ||
|
||
res, err := appClient.CreateMessage(ctx, &appPB.CreateMessageRequest{ | ||
NamespaceId: inputStruct.Namespace, | ||
AppId: inputStruct.AppUID, | ||
ConversationId: inputStruct.ConversationID, | ||
Role: inputStruct.Message.Role, | ||
Type: appPB.Message_MessageType(appPB.Message_MessageType_value["MESSAGE_TYPE_TEXT"]), | ||
Content: inputStruct.Message.Content, | ||
}) | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("failed to create message: %w", err) | ||
} | ||
|
||
messageOutput := res.Message | ||
|
||
output := WriteChatMessageOutput{ | ||
MessageUID: messageOutput.Uid, | ||
CreateTime: messageOutput.CreateTime.AsTime().Format(time.RFC3339), | ||
UpdateTime: messageOutput.UpdateTime.AsTime().Format(time.RFC3339), | ||
} | ||
|
||
return base.ConvertToStructpb(output) | ||
|
||
} |
Oops, something went wrong.