Skip to content
This repository has been archived by the owner on Jan 9, 2025. It is now read-only.

Commit

Permalink
feat(instillapp): implement read and write chat message task (#348)
Browse files Browse the repository at this point in the history
Because

- we want to build ai assistant with app backend

This commit

- build instill app component
  • Loading branch information
chuang8511 authored Sep 19, 2024
1 parent e46d228 commit 2c8a6a0
Show file tree
Hide file tree
Showing 12 changed files with 3,292 additions and 5,267 deletions.
142 changes: 142 additions & 0 deletions application/instillapp/v0/README.mdx
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 |










246 changes: 246 additions & 0 deletions application/instillapp/v0/app_operation.go
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)

}
Loading

0 comments on commit 2c8a6a0

Please sign in to comment.