diff --git a/apiserver/docs/docs.go b/apiserver/docs/docs.go index 0b51a4da9..5ce25c5ea 100644 --- a/apiserver/docs/docs.go +++ b/apiserver/docs/docs.go @@ -840,6 +840,61 @@ const docTemplate = `{ } } }, + "/prompt-starter": { + "post": { + "description": "get app's prompt starters", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "application" + ], + "summary": "get app's prompt starters", + "parameters": [ + { + "type": "integer", + "description": "how many prompts you need should \u003e 0 and \u003c 10", + "name": "limit", + "in": "query" + }, + { + "description": "query params", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/chat.APPMetadata" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/chat.ErrorResp" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/chat.ErrorResp" + } + } + } + } + }, "/versioneddataset/files/csv": { "get": { "description": "Read a file line by line", @@ -991,6 +1046,11 @@ const docTemplate = `{ "type": "string", "example": "2023-12-21T10:21:06.389359092+08:00" }, + "latency": { + "description": "Latency(ms) is how much time the server cost to process a certain request.", + "type": "integer", + "example": 1000 + }, "message": { "description": "Message is what AI say", "type": "string", @@ -1396,6 +1456,10 @@ const docTemplate = `{ "type": "string", "example": "4f3546dd-5404-4bf8-a3bc-4fa3f9a7ba24" }, + "latency": { + "type": "integer", + "example": 1000 + }, "query": { "type": "string", "example": "旷工最小计算单位为多少天?" diff --git a/apiserver/docs/swagger.json b/apiserver/docs/swagger.json index 95aea7a9c..ceb4cc0f0 100644 --- a/apiserver/docs/swagger.json +++ b/apiserver/docs/swagger.json @@ -829,6 +829,61 @@ } } }, + "/prompt-starter": { + "post": { + "description": "get app's prompt starters", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "application" + ], + "summary": "get app's prompt starters", + "parameters": [ + { + "type": "integer", + "description": "how many prompts you need should \u003e 0 and \u003c 10", + "name": "limit", + "in": "query" + }, + { + "description": "query params", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/chat.APPMetadata" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/chat.ErrorResp" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/chat.ErrorResp" + } + } + } + } + }, "/versioneddataset/files/csv": { "get": { "description": "Read a file line by line", @@ -980,6 +1035,11 @@ "type": "string", "example": "2023-12-21T10:21:06.389359092+08:00" }, + "latency": { + "description": "Latency(ms) is how much time the server cost to process a certain request.", + "type": "integer", + "example": 1000 + }, "message": { "description": "Message is what AI say", "type": "string", @@ -1385,6 +1445,10 @@ "type": "string", "example": "4f3546dd-5404-4bf8-a3bc-4fa3f9a7ba24" }, + "latency": { + "type": "integer", + "example": 1000 + }, "query": { "type": "string", "example": "旷工最小计算单位为多少天?" diff --git a/apiserver/docs/swagger.yaml b/apiserver/docs/swagger.yaml index f37af1127..c1db29411 100644 --- a/apiserver/docs/swagger.yaml +++ b/apiserver/docs/swagger.yaml @@ -54,6 +54,11 @@ definitions: description: CreatedAt is the time when the message is created example: "2023-12-21T10:21:06.389359092+08:00" type: string + latency: + description: Latency(ms) is how much time the server cost to process a certain + request. + example: 1000 + type: integer message: description: Message is what AI say example: 旷工最小计算单位为0.5天。 @@ -337,6 +342,9 @@ definitions: id: example: 4f3546dd-5404-4bf8-a3bc-4fa3f9a7ba24 type: string + latency: + example: 1000 + type: integer query: example: 旷工最小计算单位为多少天? type: string @@ -898,6 +906,42 @@ paths: summary: Statistics file information tags: - MinioAPI + /prompt-starter: + post: + consumes: + - application/json + description: get app's prompt starters + parameters: + - description: how many prompts you need should > 0 and < 10 + in: query + name: limit + type: integer + - description: query params + in: body + name: request + required: true + schema: + $ref: '#/definitions/chat.APPMetadata' + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + type: string + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/chat.ErrorResp' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/chat.ErrorResp' + summary: get app's prompt starters + tags: + - application /versioneddataset/files/csv: get: consumes: diff --git a/apiserver/pkg/chat/chat_server.go b/apiserver/pkg/chat/chat_server.go index be819acb8..fc50e4ea7 100644 --- a/apiserver/pkg/chat/chat_server.go +++ b/apiserver/pkg/chat/chat_server.go @@ -17,13 +17,20 @@ limitations under the License. package chat import ( + "bytes" "context" "errors" "fmt" + "io" + "path/filepath" + "strings" "sync" "time" + "github.com/tmc/langchaingo/chains" + langchainllms "github.com/tmc/langchaingo/llms" "github.com/tmc/langchaingo/memory" + "github.com/tmc/langchaingo/prompts" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -36,9 +43,13 @@ import ( "github.com/kubeagi/arcadia/apiserver/pkg/client" "github.com/kubeagi/arcadia/pkg/appruntime" "github.com/kubeagi/arcadia/pkg/appruntime/base" + appruntimechain "github.com/kubeagi/arcadia/pkg/appruntime/chain" + "github.com/kubeagi/arcadia/pkg/appruntime/knowledgebase" + "github.com/kubeagi/arcadia/pkg/appruntime/llm" "github.com/kubeagi/arcadia/pkg/appruntime/retriever" pkgconfig "github.com/kubeagi/arcadia/pkg/config" "github.com/kubeagi/arcadia/pkg/datasource" + pkgdocumentloaders "github.com/kubeagi/arcadia/pkg/documentloaders" ) type ChatServer struct { @@ -52,6 +63,7 @@ func NewChatServer(cli dynamic.Interface) *ChatServer { cli: cli, } } + func (cs *ChatServer) Storage() storage.Storage { if cs.storage == nil { cs.once.Do(func() { @@ -91,23 +103,9 @@ func (cs *ChatServer) Storage() storage.Storage { } func (cs *ChatServer) AppRun(ctx context.Context, req ChatReqBody, respStream chan string, messageID string) (*ChatRespBody, error) { - token := auth.ForOIDCToken(ctx) - c, err := client.GetClient(token) - if err != nil { - return nil, fmt.Errorf("failed to get a dynamic client: %w", err) - } - obj, err := c.Resource(schema.GroupVersionResource{Group: v1alpha1.GroupVersion.Group, Version: v1alpha1.GroupVersion.Version, Resource: "applications"}). - Namespace(req.AppNamespace).Get(ctx, req.APPName, metav1.GetOptions{}) - if err != nil { - return nil, fmt.Errorf("failed to get application: %w", err) - } - app := &v1alpha1.Application{} - err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), app) + app, c, err := cs.getApp(ctx, req.APPName, req.AppNamespace) if err != nil { - return nil, fmt.Errorf("failed to convert application: %w", err) - } - if !app.Status.IsReady() { - return nil, errors.New("application is not ready") + return nil, err } var conversation *storage.Conversation history := memory.NewChatMessageHistory() @@ -134,8 +132,7 @@ func (cs *ChatServer) AppRun(ctx context.Context, req ChatReqBody, respStream ch ID: req.ConversationID, AppName: req.APPName, AppNamespace: req.AppNamespace, - StartedAt: time.Now(), - UpdatedAt: time.Now(), + StartedAt: req.StartTime, Messages: make([]storage.Message, 0), User: currentUser, Debug: req.Debug, @@ -157,9 +154,10 @@ func (cs *ChatServer) AppRun(ctx context.Context, req ChatReqBody, respStream ch return nil, err } - conversation.UpdatedAt = time.Now() + conversation.UpdatedAt = req.StartTime conversation.Messages[len(conversation.Messages)-1].Answer = out.Answer conversation.Messages[len(conversation.Messages)-1].References = out.References + conversation.Messages[len(conversation.Messages)-1].Latency = time.Since(req.StartTime).Milliseconds() if err := cs.Storage().UpdateConversation(conversation); err != nil { return nil, err } @@ -206,4 +204,184 @@ func (cs *ChatServer) GetMessageReferences(ctx context.Context, req MessageReqBo return nil, errors.New("conversation or message is not found") } +// ListPromptStarters PromptStarter are examples for users to help them get up and running with the application quickly. We use same name with chatgpt +func (cs *ChatServer) ListPromptStarters(ctx context.Context, req APPMetadata, limit int) (promptStarters []string, err error) { + app, c, err := cs.getApp(ctx, req.APPName, req.AppNamespace) + if err != nil { + return nil, err + } + var kb *v1alpha1.KnowledgeBase + var chainOptions []chains.ChainCallOption + var model langchainllms.LLM + for _, n := range app.Spec.Nodes { + baseNode := base.NewBaseNode(app.Namespace, n.Name, *n.Ref) + switch baseNode.Group() { + case "chain": + switch baseNode.Kind() { + case "llmchain": + ch := appruntimechain.NewLLMChain(baseNode) + if err := ch.Init(ctx, c, nil); err != nil { + klog.Infof("init llmchain err:%s, will use empty chain config", err) + } + chainOptions = appruntimechain.GetChainOptions(ch.Instance.Spec.CommonChainConfig) + case "retrievalqachain": + ch := appruntimechain.NewRetrievalQAChain(baseNode) + if err := ch.Init(ctx, c, nil); err != nil { + klog.Infof("init retrievalqachain err:%s, will use empty chain config", err) + } + chainOptions = appruntimechain.GetChainOptions(ch.Instance.Spec.CommonChainConfig) + case "apichain": + ch := appruntimechain.NewAPIChain(baseNode) + if err := ch.Init(ctx, c, nil); err != nil { + klog.Infof("init apichain err:%s, will use empty chain config", err) + } + chainOptions = appruntimechain.GetChainOptions(ch.Instance.Spec.CommonChainConfig) + default: + klog.Infoln("can't find chain config in app, use empty chain config") + } + case "": + switch baseNode.Kind() { + case "llm": + l := llm.NewLLM(baseNode) + if err := l.Init(ctx, c, nil); err != nil { + klog.Infof("init llm err:%s, abort", err) + return nil, err + } + model = l.LLM + case "knowledgebase": + k := knowledgebase.NewKnowledgebase(baseNode) + if err := k.Init(ctx, c, nil); err != nil { + klog.Infof("init knowledgebase err:%s, abort", err) + return nil, err + } + kb = k.Instance + } + } + } + promptStarters = make([]string, 0, limit) + remains := limit + if kb != nil { + system, err := pkgconfig.GetSystemDatasource(ctx, nil, c) + if err != nil { + return nil, err + } + endpoint := system.Spec.Endpoint.DeepCopy() + if endpoint != nil && endpoint.AuthSecret != nil { + endpoint.AuthSecret.WithNameSpace(system.Namespace) + } + ds, err := datasource.NewLocal(ctx, nil, c, endpoint) + if err != nil { + return nil, err + } + Outer: + for _, detail := range kb.Status.FileGroupDetail { + if detail.Source == nil || detail.Source.Name == "" { + continue + } + obj, err := c.Resource(schema.GroupVersionResource{Group: v1alpha1.GroupVersion.Group, Version: v1alpha1.GroupVersion.Version, Resource: "versioneddatasets"}). + Namespace(detail.Source.GetNamespace(kb.Namespace)).Get(ctx, detail.Source.Name, metav1.GetOptions{}) + if err != nil { + klog.Infof("failed to get versionedDataset: %s, try next one", err) + continue + } + versionedDataset := &v1alpha1.VersionedDataset{} + err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), versionedDataset) + if err != nil { + klog.Infof("failed to convert versionedDataset: %s, try next one", err) + continue + } + if !versionedDataset.Status.IsReady() { + klog.Infof("versionedDataset is not ready, try next one") + continue + } + info := &v1alpha1.OSS{Bucket: versionedDataset.Namespace} + for _, fileDetails := range detail.FileDetails { + info.Object = filepath.Join("dataset", versionedDataset.Name, versionedDataset.Spec.Version, fileDetails.Path) + file, err := ds.ReadFile(ctx, info) + if err != nil { + klog.Infof("failed to read file: %s, try next one", err) + continue + } + defer file.Close() + data, err := io.ReadAll(file) + if err != nil { + klog.Infof("failed to read file: %s, try next one", err) + continue + } + dataReader := bytes.NewReader(data) + doc, err := pkgdocumentloaders.NewQACSV(dataReader, "").Load(ctx) + if err != nil { + klog.Infof("failed to load doc file: %s, try next one", err) + continue + } + for i := 0; i < remains && i < len(doc); i++ { + promptStarters = append(promptStarters, doc[i].PageContent) + } + remains = limit - len(promptStarters) + if remains == 0 { + break Outer + } + } + } + } else { + klog.V(3).Infoln("app has no knowlegebase, let llm generate some question") + if model != nil { + p := prompts.NewPromptTemplate(PromptForGeneratePromptStarters, []string{"limit", "displayName", "description"}) + var c *chains.LLMChain + if len(chainOptions) > 0 { + c = chains.NewLLMChain(model, p, chainOptions...) + } else { + c = chains.NewLLMChain(model, p) + } + result, err := chains.Predict(ctx, c, + map[string]any{ + "limit": limit, + "displayName": app.Spec.DisplayName, + "description": app.Spec.Description, + }, + ) + if err != nil { + return nil, err + } + res := strings.Split(result, "\n") + for _, r := range res { + promptStarters = append(promptStarters, strings.TrimSpace(r)) + } + } + } + return promptStarters, nil +} + +const PromptForGeneratePromptStarters = `You are the friendly and curious questioner, please ask {{.limit}} questions based on the name and description of this app below. + +Requires language consistent with the name and description of the application, no restating of my words, questions only, one question per line, no subheadings. + +The name of the application is: {{.displayName}} + +The description of the application is: {{.description}} + +The question you asked is:` + +func (cs *ChatServer) getApp(ctx context.Context, appName, appNamespace string) (*v1alpha1.Application, dynamic.Interface, error) { + token := auth.ForOIDCToken(ctx) + c, err := client.GetClient(token) + if err != nil { + return nil, nil, fmt.Errorf("failed to get a dynamic client: %w", err) + } + obj, err := c.Resource(schema.GroupVersionResource{Group: v1alpha1.GroupVersion.Group, Version: v1alpha1.GroupVersion.Version, Resource: "applications"}). + Namespace(appNamespace).Get(ctx, appName, metav1.GetOptions{}) + if err != nil { + return nil, c, fmt.Errorf("failed to get application: %w", err) + } + app := &v1alpha1.Application{} + err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), app) + if err != nil { + return nil, c, fmt.Errorf("failed to convert application: %w", err) + } + if !app.Status.IsReady() { + return nil, c, errors.New("application is not ready") + } + return app, c, nil +} + // todo Reuse the flow without having to rebuild req same, not finish, Flow doesn't start with/contain nodes that depend on incomingInput.question diff --git a/apiserver/pkg/chat/rest_type.go b/apiserver/pkg/chat/rest_type.go index 8c1fa202d..9b1e57e04 100644 --- a/apiserver/pkg/chat/rest_type.go +++ b/apiserver/pkg/chat/rest_type.go @@ -62,8 +62,9 @@ type ChatReqBody struct { // * Streaming - means the response will use Server-Sent Events ResponseMode ResponseMode `json:"response_mode" binding:"required" example:"blocking"` ConversationReqBody `json:",inline"` - Debug bool `json:"-"` - NewChat bool `json:"-"` + Debug bool `json:"-"` + NewChat bool `json:"-"` + StartTime time.Time `json:"-"` } type ChatRespBody struct { @@ -75,6 +76,8 @@ type ChatRespBody struct { CreatedAt time.Time `json:"created_at" example:"2023-12-21T10:21:06.389359092+08:00"` // References is the list of references References []retriever.Reference `json:"references,omitempty"` + // Latency(ms) is how much time the server cost to process a certain request. + Latency int64 `json:"latency,omitempty" example:"1000"` } type ErrorResp struct { diff --git a/apiserver/pkg/chat/storage/storage.go b/apiserver/pkg/chat/storage/storage.go index c86955a1e..8be6f6985 100644 --- a/apiserver/pkg/chat/storage/storage.go +++ b/apiserver/pkg/chat/storage/storage.go @@ -49,6 +49,7 @@ type Message struct { Answer string `gorm:"column:answer;type:string;comment:ai response" json:"answer" example:"旷工最小计算单位为0.5天。"` References References `gorm:"column:references;type:json;comment:references" json:"references,omitempty"` ConversationID string `gorm:"column:conversation_id;type:uuid;comment:conversation id" json:"-"` + Latency int64 `gorm:"column:latency;type:int;comment:request latency, in ms" json:"latency" example:"1000"` } type References []retriever.Reference diff --git a/apiserver/service/chat.go b/apiserver/service/chat.go index c1d26698c..cfe5f5c2f 100644 --- a/apiserver/service/chat.go +++ b/apiserver/service/chat.go @@ -21,6 +21,7 @@ import ( "fmt" "io" "net/http" + "strconv" "strings" "time" @@ -40,6 +41,8 @@ import ( const ( // Time interval to check if the chat stream should be closed if no more message arrives WaitTimeoutForChatStreaming = 5 + // default prompt starter + PromptLimit = 4 ) type ChatService struct { @@ -66,7 +69,7 @@ func NewChatService(cli dynamic.Interface) (*ChatService, error) { // @Router / [post] func (cs *ChatService) ChatHandler() gin.HandlerFunc { return func(c *gin.Context) { - req := chat.ChatReqBody{} + req := chat.ChatReqBody{StartTime: time.Now()} if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, chat.ErrorResp{Err: err.Error()}) return @@ -102,6 +105,7 @@ func (cs *ChatService) ChatHandler() gin.HandlerFunc { ConversationID: req.ConversationID, Message: err.Error(), CreatedAt: time.Now(), + Latency: time.Since(req.StartTime).Milliseconds(), }) // c.JSON(http.StatusInternalServerError, chat.ErrorResp{Err: err.Error()}) klog.FromContext(c.Request.Context()).Error(err, "error resp") @@ -148,6 +152,7 @@ func (cs *ChatService) ChatHandler() gin.HandlerFunc { ConversationID: req.ConversationID, Message: msg, CreatedAt: time.Now(), + Latency: time.Since(req.StartTime).Milliseconds(), }) hasData = true buf.WriteString(msg) @@ -304,6 +309,50 @@ func (cs *ChatService) ReferenceHandler() gin.HandlerFunc { } } +// @Summary get app's prompt starters +// @Schemes +// @Description get app's prompt starters +// @Tags application +// @Accept json +// @Produce json +// @Param limit query int false "how many prompts you need should > 0 and < 10" +// @Param request body chat.APPMetadata true "query params" +// @Success 200 {object} []string +// @Failure 400 {object} chat.ErrorResp +// @Failure 500 {object} chat.ErrorResp +// @Router /prompt-starter [post] +func (cs *ChatService) PromptStartersHandler() gin.HandlerFunc { + return func(c *gin.Context) { + req := chat.APPMetadata{} + if err := c.ShouldBindJSON(&req); err != nil { + klog.FromContext(c.Request.Context()).Error(err, "PromptStartersHandler: error binding json") + c.JSON(http.StatusBadRequest, chat.ErrorResp{Err: err.Error()}) + return + } + limit := c.Query("limit") + limitVal := PromptLimit + if limit != "" { + var err error + limitVal, err = strconv.Atoi(limit) + if err != nil { + c.JSON(http.StatusBadRequest, chat.ErrorResp{Err: err.Error()}) + return + } + if limitVal > 10 || limitVal < 1 { + limitVal = PromptLimit + } + } + resp, err := cs.server.ListPromptStarters(c.Request.Context(), req, limitVal) + if err != nil { + klog.FromContext(c.Request.Context()).Error(err, "error get Prompt Starters") + c.JSON(http.StatusInternalServerError, chat.ErrorResp{Err: err.Error()}) + return + } + klog.FromContext(c.Request.Context()).V(3).Info("get Prompt Starters done", "req", req) + c.JSON(http.StatusOK, resp) + } +} + func registerChat(g *gin.RouterGroup, conf config.ServerConfig) { c, err := client.GetClient(nil) if err != nil { @@ -322,4 +371,6 @@ func registerChat(g *gin.RouterGroup, conf config.ServerConfig) { g.POST("/messages", auth.AuthInterceptor(conf.EnableOIDC, oidc.Verifier, "get", "applications"), requestid.RequestIDInterceptor(), chatService.HistoryHandler()) // messages history g.POST("/messages/:messageID/references", auth.AuthInterceptor(conf.EnableOIDC, oidc.Verifier, "get", "applications"), requestid.RequestIDInterceptor(), chatService.ReferenceHandler()) // messages reference + + g.POST("/prompt-starter", auth.AuthInterceptor(conf.EnableOIDC, oidc.Verifier, "get", "applications"), requestid.RequestIDInterceptor(), chatService.PromptStartersHandler()) } diff --git a/controllers/base/knowledgebase_controller.go b/controllers/base/knowledgebase_controller.go index bf869a2d7..1d2c300b4 100644 --- a/controllers/base/knowledgebase_controller.go +++ b/controllers/base/knowledgebase_controller.go @@ -300,7 +300,7 @@ func (r *KnowledgeBaseReconciler) reconcileFileGroup(ctx context.Context, log lo if endpoint != nil && endpoint.AuthSecret != nil { endpoint.AuthSecret.WithNameSpace(system.Namespace) } - ds, err := datasource.NewLocal(ctx, r.Client, endpoint) + ds, err := datasource.NewLocal(ctx, r.Client, nil, endpoint) if err != nil { return err } diff --git a/controllers/base/model_controller.go b/controllers/base/model_controller.go index 28ddbd482..f1b0ffea5 100644 --- a/controllers/base/model_controller.go +++ b/controllers/base/model_controller.go @@ -190,7 +190,7 @@ func (r *ModelReconciler) CheckModel(ctx context.Context, logger logr.Logger, in if endpoint != nil && endpoint.AuthSecret != nil { endpoint.AuthSecret.WithNameSpace(system.Namespace) } - ds, err = datasource.NewLocal(ctx, r.Client, endpoint) + ds, err = datasource.NewLocal(ctx, r.Client, nil, endpoint) if err != nil { return r.UpdateStatus(ctx, instance, err) } @@ -225,7 +225,7 @@ func (r *ModelReconciler) RemoveModel(ctx context.Context, logger logr.Logger, i if endpoint != nil && endpoint.AuthSecret != nil { endpoint.AuthSecret.WithNameSpace(system.Namespace) } - ds, err = datasource.NewLocal(ctx, r.Client, endpoint) + ds, err = datasource.NewLocal(ctx, r.Client, nil, endpoint) if err != nil { return r.UpdateStatus(ctx, instance, err) } diff --git a/pkg/appruntime/chain/apichain.go b/pkg/appruntime/chain/apichain.go index f0297a393..fe7a3ff08 100644 --- a/pkg/appruntime/chain/apichain.go +++ b/pkg/appruntime/chain/apichain.go @@ -36,15 +36,32 @@ import ( type APIChain struct { chains.APIChain base.BaseNode + Instance *v1alpha1.APIChain } func NewAPIChain(baseNode base.BaseNode) *APIChain { return &APIChain{ - chains.APIChain{}, - baseNode, + APIChain: chains.APIChain{}, + BaseNode: baseNode, } } +func (l *APIChain) Init(ctx context.Context, cli dynamic.Interface, _ map[string]any) error { + ns := base.GetAppNamespace(ctx) + instance := &v1alpha1.APIChain{} + obj, err := cli.Resource(schema.GroupVersionResource{Group: v1alpha1.GroupVersion.Group, Version: v1alpha1.GroupVersion.Version, Resource: "apichains"}). + Namespace(l.Ref.GetNamespace(ns)).Get(ctx, l.Ref.Name, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("can't find the chain in cluster: %w", err) + } + err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), instance) + if err != nil { + return fmt.Errorf("can't convert obj to APIChain: %w", err) + } + l.Instance = instance + return nil +} + func (l *APIChain) Run(ctx context.Context, cli dynamic.Interface, args map[string]any) (map[string]any, error) { v1, ok := args["llm"] if !ok { @@ -76,18 +93,8 @@ func (l *APIChain) Run(ctx context.Context, cli dynamic.Interface, args map[stri return args, errors.New("history not memory.ChatMessageHistory") } - ns := base.GetAppNamespace(ctx) - instance := &v1alpha1.APIChain{} - obj, err := cli.Resource(schema.GroupVersionResource{Group: v1alpha1.GroupVersion.Group, Version: v1alpha1.GroupVersion.Version, Resource: "apichains"}). - Namespace(l.Ref.GetNamespace(ns)).Get(ctx, l.Ref.Name, metav1.GetOptions{}) - if err != nil { - return args, fmt.Errorf("can't find the chain in cluster: %w", err) - } - err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), instance) - if err != nil { - return args, fmt.Errorf("can't convert obj to LLMChain: %w", err) - } - options := getChainOptions(instance.Spec.CommonChainConfig) + instance := l.Instance + options := GetChainOptions(instance.Spec.CommonChainConfig) chain := chains.NewAPIChain(llm, http.DefaultClient) chain.RequestChain.Memory = getMemory(llm, instance.Spec.Memory, history, "", "") diff --git a/pkg/appruntime/chain/common.go b/pkg/appruntime/chain/common.go index e4b356b18..c8b6a30b2 100644 --- a/pkg/appruntime/chain/common.go +++ b/pkg/appruntime/chain/common.go @@ -53,7 +53,7 @@ func stream(res map[string]any) func(ctx context.Context, chunk []byte) error { } } -func getChainOptions(config v1alpha1.CommonChainConfig) []chains.ChainCallOption { +func GetChainOptions(config v1alpha1.CommonChainConfig) []chains.ChainCallOption { options := make([]chains.ChainCallOption, 0) if config.MaxTokens > 0 { options = append(options, chains.WithMaxTokens(config.MaxTokens)) diff --git a/pkg/appruntime/chain/llmchain.go b/pkg/appruntime/chain/llmchain.go index d4bbe0936..88e1bd5e7 100644 --- a/pkg/appruntime/chain/llmchain.go +++ b/pkg/appruntime/chain/llmchain.go @@ -38,16 +38,32 @@ import ( type LLMChain struct { chains.LLMChain base.BaseNode + Instance *v1alpha1.LLMChain } func NewLLMChain(baseNode base.BaseNode) *LLMChain { return &LLMChain{ - chains.LLMChain{}, - baseNode, + LLMChain: chains.LLMChain{}, + BaseNode: baseNode, } } +func (l *LLMChain) Init(ctx context.Context, cli dynamic.Interface, _ map[string]any) error { + ns := base.GetAppNamespace(ctx) + instance := &v1alpha1.LLMChain{} + obj, err := cli.Resource(schema.GroupVersionResource{Group: v1alpha1.GroupVersion.Group, Version: v1alpha1.GroupVersion.Version, Resource: "llmchains"}). + Namespace(l.Ref.GetNamespace(ns)).Get(ctx, l.Ref.Name, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("can't find the chain in cluster: %w", err) + } + err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), instance) + if err != nil { + return fmt.Errorf("can't convert obj to LLMChain: %w", err) + } + l.Instance = instance + return nil +} -func (l *LLMChain) Run(ctx context.Context, cli dynamic.Interface, args map[string]any) (map[string]any, error) { +func (l *LLMChain) Run(ctx context.Context, cli dynamic.Interface, args map[string]any) (outArgs map[string]any, err error) { v1, ok := args["llm"] if !ok { return args, errors.New("no llm") @@ -73,19 +89,8 @@ func (l *LLMChain) Run(ctx context.Context, cli dynamic.Interface, args map[stri return args, errors.New("history not memory.ChatMessageHistory") } } - - ns := base.GetAppNamespace(ctx) - instance := &v1alpha1.LLMChain{} - obj, err := cli.Resource(schema.GroupVersionResource{Group: v1alpha1.GroupVersion.Group, Version: v1alpha1.GroupVersion.Version, Resource: "llmchains"}). - Namespace(l.Ref.GetNamespace(ns)).Get(ctx, l.Ref.Name, metav1.GetOptions{}) - if err != nil { - return args, fmt.Errorf("can't find the chain in cluster: %w", err) - } - err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), instance) - if err != nil { - return args, fmt.Errorf("can't convert obj to LLMChain: %w", err) - } - options := getChainOptions(instance.Spec.CommonChainConfig) + instance := l.Instance + options := GetChainOptions(instance.Spec.CommonChainConfig) // Add the answer to the context if it's not empty if args["_answer"] != nil { klog.Infoln("get answer from upstream:", args["_answer"]) diff --git a/pkg/appruntime/chain/retrievalqachain.go b/pkg/appruntime/chain/retrievalqachain.go index d2f8880d9..bfffca220 100644 --- a/pkg/appruntime/chain/retrievalqachain.go +++ b/pkg/appruntime/chain/retrievalqachain.go @@ -39,16 +39,33 @@ import ( type RetrievalQAChain struct { chains.ConversationalRetrievalQA base.BaseNode + Instance *v1alpha1.RetrievalQAChain } func NewRetrievalQAChain(baseNode base.BaseNode) *RetrievalQAChain { return &RetrievalQAChain{ - chains.ConversationalRetrievalQA{}, - baseNode, + ConversationalRetrievalQA: chains.ConversationalRetrievalQA{}, + BaseNode: baseNode, } } -func (l *RetrievalQAChain) Run(ctx context.Context, cli dynamic.Interface, args map[string]any) (map[string]any, error) { +func (l *RetrievalQAChain) Init(ctx context.Context, cli dynamic.Interface, _ map[string]any) error { + ns := base.GetAppNamespace(ctx) + instance := &v1alpha1.RetrievalQAChain{} + obj, err := cli.Resource(schema.GroupVersionResource{Group: v1alpha1.GroupVersion.Group, Version: v1alpha1.GroupVersion.Version, Resource: "retrievalqachains"}). + Namespace(l.Ref.GetNamespace(ns)).Get(ctx, l.Ref.Name, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("can't find the chain in cluster: %w", err) + } + err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), instance) + if err != nil { + return fmt.Errorf("can't convert obj to RetrievalQAChain: %w", err) + } + l.Instance = instance + return nil +} + +func (l *RetrievalQAChain) Run(ctx context.Context, cli dynamic.Interface, args map[string]any) (outArgs map[string]any, err error) { v1, ok := args["llm"] if !ok { return args, errors.New("no llm") @@ -82,18 +99,8 @@ func (l *RetrievalQAChain) Run(ctx context.Context, cli dynamic.Interface, args return args, errors.New("prompt not prompts.FormatPrompter") } - ns := base.GetAppNamespace(ctx) - instance := &v1alpha1.RetrievalQAChain{} - obj, err := cli.Resource(schema.GroupVersionResource{Group: v1alpha1.GroupVersion.Group, Version: v1alpha1.GroupVersion.Version, Resource: "retrievalqachains"}). - Namespace(l.Ref.GetNamespace(ns)).Get(ctx, l.Ref.Name, metav1.GetOptions{}) - if err != nil { - return args, fmt.Errorf("can't find the chain in cluster: %w", err) - } - err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), instance) - if err != nil { - return args, fmt.Errorf("can't convert obj to RetrievalQAChain: %w", err) - } - options := getChainOptions(instance.Spec.CommonChainConfig) + instance := l.Instance + options := GetChainOptions(instance.Spec.CommonChainConfig) args = runTools(ctx, args, instance.Spec.Tools) llmChain := chains.NewLLMChain(llm, prompt) diff --git a/pkg/appruntime/knowledgebase/knowledgebase.go b/pkg/appruntime/knowledgebase/knowledgebase.go index 38d8290d0..bc84cfb17 100644 --- a/pkg/appruntime/knowledgebase/knowledgebase.go +++ b/pkg/appruntime/knowledgebase/knowledgebase.go @@ -18,22 +18,44 @@ package knowledgebase import ( "context" + "fmt" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" + "github.com/kubeagi/arcadia/api/base/v1alpha1" "github.com/kubeagi/arcadia/pkg/appruntime/base" ) type Knowledgebase struct { base.BaseNode + Instance *v1alpha1.KnowledgeBase } func NewKnowledgebase(baseNode base.BaseNode) *Knowledgebase { return &Knowledgebase{ - baseNode, + BaseNode: baseNode, } } +func (k *Knowledgebase) Init(ctx context.Context, cli dynamic.Interface, _ map[string]any) error { + ns := base.GetAppNamespace(ctx) + instance := &v1alpha1.KnowledgeBase{} + obj, err := cli.Resource(schema.GroupVersionResource{Group: v1alpha1.GroupVersion.Group, Version: v1alpha1.GroupVersion.Version, Resource: "knowledgebases"}). + Namespace(k.Ref.GetNamespace(ns)).Get(ctx, k.Ref.Name, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("can't find the knowledgebase in cluster: %w", err) + } + err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), instance) + if err != nil { + return fmt.Errorf("can't convert the knowledgebase in cluster: %w", err) + } + k.Instance = instance + return nil +} + func (k *Knowledgebase) Run(ctx context.Context, cli dynamic.Interface, args map[string]any) (map[string]any, error) { return args, nil } diff --git a/pkg/datasource/datasource.go b/pkg/datasource/datasource.go index 525e975a5..46b71e025 100644 --- a/pkg/datasource/datasource.go +++ b/pkg/datasource/datasource.go @@ -22,6 +22,7 @@ import ( "io" "github.com/minio/minio-go/v7" + "k8s.io/client-go/dynamic" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/kubeagi/arcadia/api/base/v1alpha1" @@ -172,8 +173,8 @@ type Local struct { oss *OSS } -func NewLocal(ctx context.Context, c client.Client, endpoint *v1alpha1.Endpoint) (*Local, error) { - oss, err := NewOSS(ctx, c, nil, endpoint) +func NewLocal(ctx context.Context, c client.Client, dc dynamic.Interface, endpoint *v1alpha1.Endpoint) (*Local, error) { + oss, err := NewOSS(ctx, c, dc, endpoint) if err != nil { return nil, err } diff --git a/tests/example-test.sh b/tests/example-test.sh index 09e32eea4..8377e4696 100755 --- a/tests/example-test.sh +++ b/tests/example-test.sh @@ -422,6 +422,13 @@ if [[ $resp == *"$delete_conversation_id"* ]]; then echo "delete conversation failed" exit 1 fi +info "8.4.5 get app prompt starters" +resp=$(curl -s -XPOST http://127.0.0.1:8081/chat/prompt-starter --data '{"app_name": "base-chat-with-bot", "app_namespace": "arcadia"}') +echo $resp | jq . +if [[ $resp == *"err"* ]]; then + echo "failed" + exit 1 +fi # There is uncertainty in the AI replies, most of the time, it will pass the test, a small percentage of the time, the AI will call names in each reply, causing the test to fail, therefore, temporarily disable the following tests #getRespInAppChat "base-chat-with-bot" "arcadia" "What is your model?" ${resp_conversation_id} "false" @@ -446,12 +453,12 @@ if [[ $GITHUB_ACTIONS != "true" ]]; then info "8.6 bingsearch test" kubectl apply -f config/samples/app_llmchain_chat_with_bot_bing.yaml waitCRDStatusReady "Application" "arcadia" "base-chat-with-bot-bing" - sleep 3 - getRespInAppChat "base-chat-with-bot-bing" "arcadia" "介绍一下微软的产品" "" "false" - if [ -z "$references" ] || [ "$references" = "null" ]; then - echo $resp - exit 1 - fi + sleep 3 + getRespInAppChat "base-chat-with-bot-bing" "arcadia" "介绍一下微软的产品" "" "false" + if [ -z "$references" ] || [ "$references" = "null" ]; then + echo $resp + exit 1 + fi fi info "9. show apiserver logs for debug"