diff --git a/PROJECT b/PROJECT index 6f463e7c8..218deb688 100644 --- a/PROJECT +++ b/PROJECT @@ -151,4 +151,13 @@ resources: kind: RAG path: github.com/kubeagi/arcadia/api/evaluation/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: arcadia.kubeagi.k8s.com.cn + group: chain + kind: APIChain + path: github.com/kubeagi/arcadia/api/app-node/chain/v1alpha1 + version: v1alpha1 version: "3" diff --git a/api/app-node/chain/v1alpha1/apichain_types.go b/api/app-node/chain/v1alpha1/apichain_types.go new file mode 100644 index 000000000..f200748f2 --- /dev/null +++ b/api/app-node/chain/v1alpha1/apichain_types.go @@ -0,0 +1,80 @@ +/* +Copyright 2024 KubeAGI. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + node "github.com/kubeagi/arcadia/api/app-node" + "github.com/kubeagi/arcadia/api/base/v1alpha1" +) + +// APIChainSpec defines the desired state of APIChain +type APIChainSpec struct { + v1alpha1.CommonSpec `json:",inline"` + + CommonChainConfig `json:",inline"` + // APIDoc is the api doc for this chain, "api_docs" + APIDoc string `json:"apiDoc"` +} + +// APIChainStatus defines the observed state of APIChain +type APIChainStatus struct { + // ObservedGeneration is the last observed generation. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // ConditionedStatus is the current status + v1alpha1.ConditionedStatus `json:",inline"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// APIChain is a chain that makes API calls and summarizes the responses to answer a question. +type APIChain struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec APIChainSpec `json:"spec,omitempty"` + Status APIChainStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// APIChainList contains a list of APIChain +type APIChainList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []APIChain `json:"items"` +} + +func init() { + SchemeBuilder.Register(&APIChain{}, &APIChainList{}) +} + +var _ node.Node = (*APIChain)(nil) + +func (c *APIChain) SetRef() { + annotations := node.SetRefAnnotations(c.GetAnnotations(), []node.Ref{node.LLMRef.Len(1), node.PromptRef.Len(1)}, []node.Ref{node.OutputRef.Len(1)}) + if c.GetAnnotations() == nil { + c.SetAnnotations(annotations) + } + for k, v := range annotations { + c.Annotations[k] = v + } +} diff --git a/api/app-node/chain/v1alpha1/zz_generated.deepcopy.go b/api/app-node/chain/v1alpha1/zz_generated.deepcopy.go index 5b084cc0d..efd6edd5f 100644 --- a/api/app-node/chain/v1alpha1/zz_generated.deepcopy.go +++ b/api/app-node/chain/v1alpha1/zz_generated.deepcopy.go @@ -25,6 +25,98 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIChain) DeepCopyInto(out *APIChain) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIChain. +func (in *APIChain) DeepCopy() *APIChain { + if in == nil { + return nil + } + out := new(APIChain) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *APIChain) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIChainList) DeepCopyInto(out *APIChainList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]APIChain, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIChainList. +func (in *APIChainList) DeepCopy() *APIChainList { + if in == nil { + return nil + } + out := new(APIChainList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *APIChainList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIChainSpec) DeepCopyInto(out *APIChainSpec) { + *out = *in + out.CommonSpec = in.CommonSpec + in.CommonChainConfig.DeepCopyInto(&out.CommonChainConfig) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIChainSpec. +func (in *APIChainSpec) DeepCopy() *APIChainSpec { + if in == nil { + return nil + } + out := new(APIChainSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIChainStatus) DeepCopyInto(out *APIChainStatus) { + *out = *in + in.ConditionedStatus.DeepCopyInto(&out.ConditionedStatus) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIChainStatus. +func (in *APIChainStatus) DeepCopy() *APIChainStatus { + if in == nil { + return nil + } + out := new(APIChainStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CommonChainConfig) DeepCopyInto(out *CommonChainConfig) { *out = *in diff --git a/config/crd/bases/chain.arcadia.kubeagi.k8s.com.cn_apichains.yaml b/config/crd/bases/chain.arcadia.kubeagi.k8s.com.cn_apichains.yaml new file mode 100644 index 000000000..62acca9f8 --- /dev/null +++ b/config/crd/bases/chain.arcadia.kubeagi.k8s.com.cn_apichains.yaml @@ -0,0 +1,166 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null + name: apichains.chain.arcadia.kubeagi.k8s.com.cn +spec: + group: chain.arcadia.kubeagi.k8s.com.cn + names: + kind: APIChain + listKind: APIChainList + plural: apichains + singular: apichain + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: APIChain is a chain that makes API calls and summarizes the responses + to answer a question. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: APIChainSpec defines the desired state of APIChain + properties: + apiDoc: + description: APIDoc is the api doc for this chain, "api_docs" + type: string + creator: + description: Creator defines datasource creator (AUTO-FILLED by webhook) + type: string + description: + description: Description defines datasource description + type: string + displayName: + description: DisplayName defines datasource display name + type: string + maxLength: + default: 1024 + description: MaxLength is the maximum length of the generated text + in a llm call. + maximum: 4096 + minimum: 10 + type: integer + maxTokens: + description: MaxTokens is the maximum number of tokens to generate + to use in a llm call. + type: integer + memory: + description: for memory + properties: + conversionWindowSize: + default: 5 + description: ConversionWindowSize is the maximum number of conversation + rounds in memory.Can only use MaxTokenLimit or ConversionWindowSize. + maximum: 30 + minimum: 0 + type: integer + maxTokenLimit: + description: MaxTokenLimit is the maximum number of tokens to + keep in memory. Can only use MaxTokenLimit or ConversionWindowSize. + type: integer + type: object + minLength: + description: MinLength is the minimum length of the generated text + in a llm call. + type: integer + model: + description: Model is the model to use in an llm call.like `gpt-3.5-turbo` + or `chatglm_turbo` Usually this value is just empty + type: string + repetitionPenalty: + description: RepetitionPenalty is the repetition penalty for sampling + in a llm call. + type: number + seed: + description: Seed is a seed for deterministic sampling in a llm call. + type: integer + stopWords: + description: StopWords is a list of words to stop on to use in a llm + call. + items: + type: string + type: array + temperature: + default: 0.7 + description: Temperature is the temperature for sampling to use in + a llm call, between 0 and 1. + maximum: 1 + minimum: 0 + type: number + topK: + description: TopK is the number of tokens to consider for top-k sampling + in a llm call. + type: integer + topP: + description: TopP is the cumulative probability for top-p sampling + in a llm call. + type: number + required: + - apiDoc + type: object + status: + description: APIChainStatus defines the observed state of APIChain + properties: + conditions: + description: Conditions of the resource. + items: + description: A Condition that may apply to a resource. + properties: + lastSuccessfulTime: + description: LastSuccessfulTime is repository Last Successful + Update Time + format: date-time + type: string + lastTransitionTime: + description: LastTransitionTime is the last time this condition + transitioned from one status to another. + format: date-time + type: string + message: + description: A Message containing details about this condition's + last transition from one status to another, if any. + type: string + reason: + description: A Reason for this condition's last transition from + one status to another. + type: string + status: + description: Status of this condition; is it currently True, + False, or Unknown + type: string + type: + description: Type of this condition. At most one of each condition + type may apply to a resource at any point in time. + type: string + required: + - lastTransitionTime + - reason + - status + - type + type: object + type: array + observedGeneration: + description: ObservedGeneration is the last observed generation. + format: int64 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 14257ca7c..b932f0f6b 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -18,6 +18,7 @@ resources: - bases/prompt.arcadia.kubeagi.k8s.com.cn_prompts.yaml - bases/retriever.arcadia.kubeagi.k8s.com.cn_knowledgebaseretrievers.yaml - bases/evaluation.arcadia.kubeagi.k8s.com.cn_rags.yaml +- bases/chain.kubeagi.k8s.com.cn_apichains.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -35,6 +36,7 @@ patchesStrategicMerge: #- patches/webhook_in_models.yaml #- patches/webhook_in_applications.yaml #- patches/webhook_in_rags.yaml +#- patches/webhook_in_apichains.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -51,6 +53,7 @@ patchesStrategicMerge: #- patches/cainjection_in_vectorstores.yaml #- patches/cainjection_in_applications.yaml #- patches/cainjection_in_rags.yaml +#- patches/cainjection_in_apichains.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/crd/patches/cainjection_in_chain_apichains.yaml b/config/crd/patches/cainjection_in_chain_apichains.yaml new file mode 100644 index 000000000..c09ee2100 --- /dev/null +++ b/config/crd/patches/cainjection_in_chain_apichains.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: apichains.chain.kubeagi.k8s.com.cn diff --git a/config/crd/patches/webhook_in_chain_apichains.yaml b/config/crd/patches/webhook_in_chain_apichains.yaml new file mode 100644 index 000000000..4b6e86e71 --- /dev/null +++ b/config/crd/patches/webhook_in_chain_apichains.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: apichains.chain.kubeagi.k8s.com.cn +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/rbac/chain_apichain_editor_role.yaml b/config/rbac/chain_apichain_editor_role.yaml new file mode 100644 index 000000000..678992399 --- /dev/null +++ b/config/rbac/chain_apichain_editor_role.yaml @@ -0,0 +1,24 @@ +# permissions for end users to edit apichains. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: apichain-editor-role +rules: +- apiGroups: + - chain.kubeagi.k8s.com.cn + resources: + - apichains + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - chain.kubeagi.k8s.com.cn + resources: + - apichains/status + verbs: + - get diff --git a/config/rbac/chain_apichain_viewer_role.yaml b/config/rbac/chain_apichain_viewer_role.yaml new file mode 100644 index 000000000..d8cf28748 --- /dev/null +++ b/config/rbac/chain_apichain_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end users to view apichains. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: apichain-viewer-role +rules: +- apiGroups: + - chain.kubeagi.k8s.com.cn + resources: + - apichains + verbs: + - get + - list + - watch +- apiGroups: + - chain.kubeagi.k8s.com.cn + resources: + - apichains/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 3dc8bb6dc..17c6ce854 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -408,6 +408,32 @@ rules: - subjectaccessreviews verbs: - create +- apiGroups: + - chain.arcadia.kubeagi.k8s.com.cn + resources: + - apichains + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - chain.arcadia.kubeagi.k8s.com.cn + resources: + - apichains/finalizers + verbs: + - update +- apiGroups: + - chain.arcadia.kubeagi.k8s.com.cn + resources: + - apichains/status + verbs: + - get + - patch + - update - apiGroups: - chain.arcadia.kubeagi.k8s.com.cn resources: diff --git a/config/samples/app_apichain_movie.yaml b/config/samples/app_apichain_movie.yaml new file mode 100644 index 000000000..aa6c30e20 --- /dev/null +++ b/config/samples/app_apichain_movie.yaml @@ -0,0 +1,82 @@ +apiVersion: arcadia.kubeagi.k8s.com.cn/v1alpha1 +kind: Application +metadata: + name: movie-bot + namespace: arcadia +spec: + displayName: "搜索电影的Bot" + description: "" + prologue: "Hello, I am KubeAGI Bot🤖, Tell me something?" + nodes: + - name: Input + displayName: "用户输入" + description: "用户输入节点,必须" + ref: + kind: Input + name: Input + nextNodeName: ["prompt-node"] + - name: prompt-node + displayName: "prompt" + description: "设定prompt,template中可以使用{{xx}}来替换变量" + ref: + apiGroup: prompt.arcadia.kubeagi.k8s.com.cn + kind: Prompt + name: movie-bot + nextNodeName: ["chain-node"] + - name: llm-node + displayName: "zhipu大模型服务" + description: "设定大模型的访问信息" + ref: + apiGroup: arcadia.kubeagi.k8s.com.cn + kind: LLM + name: app-shared-llm-service + nextNodeName: ["chain-node"] + - name: chain-node + displayName: "apichain" + description: "chain是langchain的核心概念,apiChain用于从特定的api文档中构建api请求,并将结果传递给LLM来回答问题" + ref: + apiGroup: chain.arcadia.kubeagi.k8s.com.cn + kind: APIChain + name: movie-bot + nextNodeName: ["Output"] + - name: Output + displayName: "最终输出" + description: "最终输出节点,必须" + ref: + kind: Output + name: Output +--- +apiVersion: prompt.arcadia.kubeagi.k8s.com.cn/v1alpha1 +kind: Prompt +metadata: + name: movie-bot + namespace: arcadia + annotations: + arcadia.kubeagi.k8s.com.cn/input-rules: '[{"kind":"Input","length":1}]' + arcadia.kubeagi.k8s.com.cn/output-rules: '[{"length":1}]' +spec: + displayName: "设定对话的prompt" + description: "设定对话的prompt" + userMessage: | + 请回答我的问题:{{.question}} +--- +apiVersion: chain.arcadia.kubeagi.k8s.com.cn/v1alpha1 +kind: APIChain +metadata: + name: movie-bot + namespace: arcadia + annotations: + arcadia.kubeagi.k8s.com.cn/input-rules: '[{"kind":"LLM","group":"arcadia.kubeagi.k8s.com.cn","length":1},{"kind":"prompt","group":"prompt.arcadia.kubeagi.k8s.com.cn","length":1}]' + arcadia.kubeagi.k8s.com.cn/output-rules: '[{"kind":"Output","length":1}]' +spec: + displayName: "api chain" + description: "api chain" + memory: + conversionWindowSize: 2 + model: chatglm_pro # notice: default model chatglm_lite or chatglm_turbo gets poor results in most cases, openai's gpt-3.5-turbo is also good enough + apiDoc: | + 提供如下API接口: + https://api.wmdb.tv/api/v1/movie/search?q=英雄本色&limit=10&lang=Cn + 其中,q为搜索项,必填。如果是中文,请使用urlencode对这个字段进行编码,英文则不需要 + limit为搜索条数,可选,默认填10 + lang为搜索语言,可选,默认填Cn diff --git a/config/samples/chain_v1alpha1_apichain.yaml b/config/samples/chain_v1alpha1_apichain.yaml new file mode 100644 index 000000000..03d054724 --- /dev/null +++ b/config/samples/chain_v1alpha1_apichain.yaml @@ -0,0 +1,6 @@ +apiVersion: chain.kubeagi.k8s.com.cn/v1alpha1 +kind: APIChain +metadata: + name: apichain-sample +spec: + # TODO(user): Add fields here diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index e89e00d2f..a8b8e3a88 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -13,4 +13,5 @@ resources: - arcadia_v1alpha1_application.yaml - app_llmchain_englishteacher.yaml - evaluation.arcadia_v1alpha1_rag.yaml +- chain_v1alpha1_apichain.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/controllers/app-node/chain/apichain_controller.go b/controllers/app-node/chain/apichain_controller.go new file mode 100644 index 000000000..0283ebbfb --- /dev/null +++ b/controllers/app-node/chain/apichain_controller.go @@ -0,0 +1,149 @@ +/* +Copyright 2024 KubeAGI. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package chain + +import ( + "context" + "reflect" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + api "github.com/kubeagi/arcadia/api/app-node/chain/v1alpha1" + arcadiav1alpha1 "github.com/kubeagi/arcadia/api/base/v1alpha1" + appnode "github.com/kubeagi/arcadia/controllers/app-node" +) + +// APIChainReconciler reconciles an APIChain object +type APIChainReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=chain.arcadia.kubeagi.k8s.com.cn,resources=apichains,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=chain.arcadia.kubeagi.k8s.com.cn,resources=apichains/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=chain.arcadia.kubeagi.k8s.com.cn,resources=apichains/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.12.2/pkg/reconcile +func (r *APIChainReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := ctrl.LoggerFrom(ctx) + log.V(5).Info("Start APIChain Reconcile") + instance := &api.APIChain{} + if err := r.Get(ctx, req.NamespacedName, instance); err != nil { + // There's no need to requeue if the resource no longer exists. + // Otherwise, we'll be requeued implicitly because we return an error. + log.V(1).Info("Failed to get APIChain") + return ctrl.Result{}, client.IgnoreNotFound(err) + } + log = log.WithValues("Generation", instance.GetGeneration(), "ObservedGeneration", instance.Status.ObservedGeneration, "creator", instance.Spec.Creator) + log.V(5).Info("Get APIChain instance") + + // Add a finalizer.Then, we can define some operations which should + // occur before the APIChain to be deleted. + // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers + if newAdded := controllerutil.AddFinalizer(instance, arcadiav1alpha1.Finalizer); newAdded { + log.Info("Try to add Finalizer for APIChain") + if err := r.Update(ctx, instance); err != nil { + log.Error(err, "Failed to update APIChain to add finalizer, will try again later") + return ctrl.Result{}, err + } + log.Info("Adding Finalizer for APIChain done") + return ctrl.Result{}, nil + } + + // Check if the APIChain instance is marked to be deleted, which is + // indicated by the deletion timestamp being set. + if instance.GetDeletionTimestamp() != nil && controllerutil.ContainsFinalizer(instance, arcadiav1alpha1.Finalizer) { + log.Info("Performing Finalizer Operations for APIChain before delete CR") + // TODO perform the finalizer operations here, for example: remove vectorstore data? + log.Info("Removing Finalizer for APIChain after successfully performing the operations") + controllerutil.RemoveFinalizer(instance, arcadiav1alpha1.Finalizer) + if err := r.Update(ctx, instance); err != nil { + log.Error(err, "Failed to remove the finalizer for APIChain") + return ctrl.Result{}, err + } + log.Info("Remove APIChain done") + return ctrl.Result{}, nil + } + + instance, result, err := r.reconcile(ctx, log, instance) + + // Update status after reconciliation. + if updateStatusErr := r.patchStatus(ctx, instance); updateStatusErr != nil { + log.Error(updateStatusErr, "unable to update status after reconciliation") + return ctrl.Result{Requeue: true}, updateStatusErr + } + + return result, err +} + +func (r *APIChainReconciler) reconcile(ctx context.Context, log logr.Logger, instance *api.APIChain) (*api.APIChain, ctrl.Result, error) { + // Observe generation change + if instance.Status.ObservedGeneration != instance.Generation { + instance.Status.ObservedGeneration = instance.Generation + r.setCondition(instance, instance.Status.WaitingCompleteCondition()...) + if updateStatusErr := r.patchStatus(ctx, instance); updateStatusErr != nil { + log.Error(updateStatusErr, "unable to update status after generation update") + return instance, ctrl.Result{Requeue: true}, updateStatusErr + } + } + + if instance.Status.IsReady() { + return instance, ctrl.Result{}, nil + } + // Note: should change here + // TODO: we should do more checks later.For example: + // API status + // Prompt status + if err := appnode.CheckAndUpdateAnnotation(ctx, log, r.Client, instance); err != nil { + instance.Status.SetConditions(instance.Status.ErrorCondition(err.Error())...) + } else { + instance.Status.SetConditions(instance.Status.ReadyCondition()...) + } + return instance, ctrl.Result{}, nil +} + +func (r *APIChainReconciler) patchStatus(ctx context.Context, instance *api.APIChain) error { + latest := &api.APIChain{} + if err := r.Client.Get(ctx, client.ObjectKeyFromObject(instance), latest); err != nil { + return err + } + if reflect.DeepEqual(instance.Status, latest.Status) { + return nil + } + patch := client.MergeFrom(latest.DeepCopy()) + latest.Status = instance.Status + return r.Client.Status().Patch(ctx, latest, patch, client.FieldOwner("APIChain-controller")) +} + +// SetupWithManager sets up the controller with the Manager. +func (r *APIChainReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&api.APIChain{}). + Complete(r) +} + +func (r *APIChainReconciler) setCondition(instance *api.APIChain, condition ...arcadiav1alpha1.Condition) *api.APIChain { + instance.Status.SetConditions(condition...) + return instance +} diff --git a/deploy/charts/arcadia/Chart.yaml b/deploy/charts/arcadia/Chart.yaml index d04dbe5ba..f20742a51 100644 --- a/deploy/charts/arcadia/Chart.yaml +++ b/deploy/charts/arcadia/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: arcadia description: A Helm chart(KubeBB Component) for KubeAGI Arcadia type: application -version: 0.2.12 +version: 0.2.13 appVersion: "0.1.0" keywords: diff --git a/deploy/charts/arcadia/crds/chain.arcadia.kubeagi.k8s.com.cn_apichains.yaml b/deploy/charts/arcadia/crds/chain.arcadia.kubeagi.k8s.com.cn_apichains.yaml new file mode 100644 index 000000000..62acca9f8 --- /dev/null +++ b/deploy/charts/arcadia/crds/chain.arcadia.kubeagi.k8s.com.cn_apichains.yaml @@ -0,0 +1,166 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null + name: apichains.chain.arcadia.kubeagi.k8s.com.cn +spec: + group: chain.arcadia.kubeagi.k8s.com.cn + names: + kind: APIChain + listKind: APIChainList + plural: apichains + singular: apichain + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: APIChain is a chain that makes API calls and summarizes the responses + to answer a question. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: APIChainSpec defines the desired state of APIChain + properties: + apiDoc: + description: APIDoc is the api doc for this chain, "api_docs" + type: string + creator: + description: Creator defines datasource creator (AUTO-FILLED by webhook) + type: string + description: + description: Description defines datasource description + type: string + displayName: + description: DisplayName defines datasource display name + type: string + maxLength: + default: 1024 + description: MaxLength is the maximum length of the generated text + in a llm call. + maximum: 4096 + minimum: 10 + type: integer + maxTokens: + description: MaxTokens is the maximum number of tokens to generate + to use in a llm call. + type: integer + memory: + description: for memory + properties: + conversionWindowSize: + default: 5 + description: ConversionWindowSize is the maximum number of conversation + rounds in memory.Can only use MaxTokenLimit or ConversionWindowSize. + maximum: 30 + minimum: 0 + type: integer + maxTokenLimit: + description: MaxTokenLimit is the maximum number of tokens to + keep in memory. Can only use MaxTokenLimit or ConversionWindowSize. + type: integer + type: object + minLength: + description: MinLength is the minimum length of the generated text + in a llm call. + type: integer + model: + description: Model is the model to use in an llm call.like `gpt-3.5-turbo` + or `chatglm_turbo` Usually this value is just empty + type: string + repetitionPenalty: + description: RepetitionPenalty is the repetition penalty for sampling + in a llm call. + type: number + seed: + description: Seed is a seed for deterministic sampling in a llm call. + type: integer + stopWords: + description: StopWords is a list of words to stop on to use in a llm + call. + items: + type: string + type: array + temperature: + default: 0.7 + description: Temperature is the temperature for sampling to use in + a llm call, between 0 and 1. + maximum: 1 + minimum: 0 + type: number + topK: + description: TopK is the number of tokens to consider for top-k sampling + in a llm call. + type: integer + topP: + description: TopP is the cumulative probability for top-p sampling + in a llm call. + type: number + required: + - apiDoc + type: object + status: + description: APIChainStatus defines the observed state of APIChain + properties: + conditions: + description: Conditions of the resource. + items: + description: A Condition that may apply to a resource. + properties: + lastSuccessfulTime: + description: LastSuccessfulTime is repository Last Successful + Update Time + format: date-time + type: string + lastTransitionTime: + description: LastTransitionTime is the last time this condition + transitioned from one status to another. + format: date-time + type: string + message: + description: A Message containing details about this condition's + last transition from one status to another, if any. + type: string + reason: + description: A Reason for this condition's last transition from + one status to another. + type: string + status: + description: Status of this condition; is it currently True, + False, or Unknown + type: string + type: + description: Type of this condition. At most one of each condition + type may apply to a resource at any point in time. + type: string + required: + - lastTransitionTime + - reason + - status + - type + type: object + type: array + observedGeneration: + description: ObservedGeneration is the last observed generation. + format: int64 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/deploy/charts/arcadia/templates/rbac.yaml b/deploy/charts/arcadia/templates/rbac.yaml index d3df64aa3..8df7d3834 100644 --- a/deploy/charts/arcadia/templates/rbac.yaml +++ b/deploy/charts/arcadia/templates/rbac.yaml @@ -425,6 +425,32 @@ rules: - subjectaccessreviews verbs: - create +- apiGroups: + - chain.arcadia.kubeagi.k8s.com.cn + resources: + - apichains + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - chain.arcadia.kubeagi.k8s.com.cn + resources: + - apichains/finalizers + verbs: + - update +- apiGroups: + - chain.arcadia.kubeagi.k8s.com.cn + resources: + - apichains/status + verbs: + - get + - patch + - update - apiGroups: - chain.arcadia.kubeagi.k8s.com.cn resources: diff --git a/main.go b/main.go index 1c5478a9e..a09bcec03 100644 --- a/main.go +++ b/main.go @@ -278,6 +278,13 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "RAG") os.Exit(1) } + if err = (&chaincontrollers.APIChainReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "APIChain") + os.Exit(1) + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/pkg/appruntime/app_runtime.go b/pkg/appruntime/app_runtime.go index e5a2e14a2..2538b807a 100644 --- a/pkg/appruntime/app_runtime.go +++ b/pkg/appruntime/app_runtime.go @@ -231,6 +231,9 @@ func InitNode(ctx context.Context, appNamespace, name string, ref arcadiav1alpha case "retrievalqachain": logger.V(3).Info("initnode retrievalqachain") return chain.NewRetrievalQAChain(baseNode), nil + case "apichain": + logger.V(3).Info("initnode llmchain") + return chain.NewAPIChain(baseNode), nil default: return nil, err } diff --git a/pkg/appruntime/chain/apichain.go b/pkg/appruntime/chain/apichain.go new file mode 100644 index 000000000..f0297a393 --- /dev/null +++ b/pkg/appruntime/chain/apichain.go @@ -0,0 +1,118 @@ +/* +Copyright 2024 KubeAGI. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package chain + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/tmc/langchaingo/chains" + "github.com/tmc/langchaingo/llms" + "github.com/tmc/langchaingo/prompts" + langchaingoschema "github.com/tmc/langchaingo/schema" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "k8s.io/klog/v2" + + "github.com/kubeagi/arcadia/api/app-node/chain/v1alpha1" + "github.com/kubeagi/arcadia/pkg/appruntime/base" +) + +type APIChain struct { + chains.APIChain + base.BaseNode +} + +func NewAPIChain(baseNode base.BaseNode) *APIChain { + return &APIChain{ + chains.APIChain{}, + baseNode, + } +} + +func (l *APIChain) Run(ctx context.Context, cli dynamic.Interface, args map[string]any) (map[string]any, error) { + v1, ok := args["llm"] + if !ok { + return args, errors.New("no llm") + } + llm, ok := v1.(llms.LLM) + if !ok { + return args, errors.New("llm not llms.LanguageModel") + } + v2, ok := args["prompt"] + if !ok { + return args, errors.New("no prompt") + } + prompt, ok := v2.(prompts.FormatPrompter) + if !ok { + return args, errors.New("prompt not prompts.FormatPrompter") + } + p, err := prompt.FormatPrompt(args) + if err != nil { + return args, fmt.Errorf("can't format prompt: %w", err) + } + args["input"] = p.String() + v3, ok := args["_history"] + if !ok { + return args, errors.New("no history") + } + history, ok := v3.(langchaingoschema.ChatMessageHistory) + if !ok { + 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) + + chain := chains.NewAPIChain(llm, http.DefaultClient) + chain.RequestChain.Memory = getMemory(llm, instance.Spec.Memory, history, "", "") + chain.AnswerChain.Memory = getMemory(llm, instance.Spec.Memory, history, "input", "") + l.APIChain = chain + apiDoc := instance.Spec.APIDoc + if apiDoc == "" { + return args, errors.New("no apidoc in apichain") + } + args["api_docs"] = apiDoc + var out string + if needStream, ok := args["_need_stream"].(bool); ok && needStream { + options = append(options, chains.WithStreamingFunc(stream(args))) + out, err = chains.Predict(ctx, l.APIChain, args, options...) + } else { + if len(options) > 0 { + out, err = chains.Predict(ctx, l.APIChain, args, options...) + } else { + out, err = chains.Predict(ctx, l.APIChain, args) + } + } + klog.FromContext(ctx).V(5).Info("use apichain, blocking out:" + out) + if err == nil { + args["_answer"] = out + return args, nil + } + return args, fmt.Errorf("apichain run error: %w", err) +} diff --git a/pkg/appruntime/chain/common.go b/pkg/appruntime/chain/common.go index c78e6b596..6ebe26aa6 100644 --- a/pkg/appruntime/chain/common.go +++ b/pkg/appruntime/chain/common.go @@ -83,12 +83,18 @@ func getChainOptions(config v1alpha1.CommonChainConfig) []chains.ChainCallOption return options } -func getMemory(llm llms.LLM, config v1alpha1.Memory, history langchaingoschema.ChatMessageHistory) langchaingoschema.Memory { +func getMemory(llm llms.LLM, config v1alpha1.Memory, history langchaingoschema.ChatMessageHistory, inputKey, outputKey string) langchaingoschema.Memory { + if inputKey == "" { + inputKey = "question" + } + if outputKey == "" { + outputKey = "text" + } if config.MaxTokenLimit > 0 { - return memory.NewConversationTokenBuffer(llm, config.MaxTokenLimit, memory.WithInputKey("question"), memory.WithOutputKey("text"), memory.WithChatHistory(history)) + return memory.NewConversationTokenBuffer(llm, config.MaxTokenLimit, memory.WithInputKey(inputKey), memory.WithOutputKey(outputKey), memory.WithChatHistory(history)) } if config.ConversionWindowSize > 0 { - return memory.NewConversationWindowBuffer(config.ConversionWindowSize, memory.WithInputKey("question"), memory.WithOutputKey("text"), memory.WithChatHistory(history)) + return memory.NewConversationWindowBuffer(config.ConversionWindowSize, memory.WithInputKey(inputKey), memory.WithOutputKey(outputKey), memory.WithChatHistory(history)) } return memory.NewSimple() } diff --git a/pkg/appruntime/chain/llmchain.go b/pkg/appruntime/chain/llmchain.go index 3bc2d91cc..7b35260c0 100644 --- a/pkg/appruntime/chain/llmchain.go +++ b/pkg/appruntime/chain/llmchain.go @@ -89,7 +89,7 @@ func (l *LLMChain) Run(ctx context.Context, cli dynamic.Interface, args map[stri chain := chains.NewLLMChain(llm, prompt) if history != nil { - chain.Memory = getMemory(llm, instance.Spec.Memory, history) + chain.Memory = getMemory(llm, instance.Spec.Memory, history, "", "") } l.LLMChain = *chain var out string diff --git a/pkg/appruntime/chain/retrievalqachain.go b/pkg/appruntime/chain/retrievalqachain.go index d846f1954..e00c9c052 100644 --- a/pkg/appruntime/chain/retrievalqachain.go +++ b/pkg/appruntime/chain/retrievalqachain.go @@ -104,7 +104,7 @@ func (l *RetrievalQAChain) Run(ctx context.Context, cli dynamic.Interface, args } else { baseChain = chains.NewStuffDocuments(llmChain) } - chain := chains.NewConversationalRetrievalQA(baseChain, chains.LoadCondenseQuestionGenerator(llm), retriever, getMemory(llm, instance.Spec.Memory, history)) + chain := chains.NewConversationalRetrievalQA(baseChain, chains.LoadCondenseQuestionGenerator(llm), retriever, getMemory(llm, instance.Spec.Memory, history, "", "")) l.ConversationalRetrievalQA = chain args["query"] = args["question"] var out string diff --git a/pkg/appruntime/llm/llm.go b/pkg/appruntime/llm/llm.go index c6ce4f158..760866be6 100644 --- a/pkg/appruntime/llm/llm.go +++ b/pkg/appruntime/llm/llm.go @@ -25,6 +25,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" + "k8s.io/klog/v2" "github.com/kubeagi/arcadia/api/base/v1alpha1" "github.com/kubeagi/arcadia/pkg/appruntime/base" @@ -62,7 +63,10 @@ func (z *LLM) Init(ctx context.Context, cli dynamic.Interface, args map[string]a return nil } -func (z *LLM) Run(_ context.Context, _ dynamic.Interface, args map[string]any) (map[string]any, error) { +func (z *LLM) Run(ctx context.Context, _ dynamic.Interface, args map[string]any) (map[string]any, error) { args["llm"] = z + logger := klog.FromContext(ctx) + ns := base.GetAppNamespace(ctx) + logger.Info("use llm", "name", z.Ref.Name, "namespace", z.Ref.GetNamespace(ns)) return args, nil } diff --git a/pkg/appruntime/prompt/prompt.go b/pkg/appruntime/prompt/prompt.go index 11a9b9c3a..8626b7210 100644 --- a/pkg/appruntime/prompt/prompt.go +++ b/pkg/appruntime/prompt/prompt.go @@ -53,10 +53,14 @@ func (p *Prompt) Run(ctx context.Context, cli dynamic.Interface, args map[string if err != nil { return args, fmt.Errorf("can't convert the prompt in cluster: %w", err) } - template := prompts.NewChatPromptTemplate([]prompts.MessageFormatter{ - prompts.NewSystemMessagePromptTemplate(instance.Spec.SystemMessage, []string{}), - prompts.NewHumanMessagePromptTemplate(instance.Spec.UserMessage, []string{"question"}), - }) + ps := make([]prompts.MessageFormatter, 0) + if instance.Spec.SystemMessage != "" { + ps = append(ps, prompts.NewSystemMessagePromptTemplate(instance.Spec.SystemMessage, []string{})) + } + if instance.Spec.UserMessage != "" { + ps = append(ps, prompts.NewHumanMessagePromptTemplate(instance.Spec.UserMessage, []string{"question"})) + } + template := prompts.NewChatPromptTemplate(ps) // todo format p.ChatPromptTemplate = template args["prompt"] = p diff --git a/tests/example-test.sh b/tests/example-test.sh index b6b10c271..b0391b670 100755 --- a/tests/example-test.sh +++ b/tests/example-test.sh @@ -397,6 +397,18 @@ curl -XPOST http://127.0.0.1:8081/chat/messages --data "$data" # exit 1 #fi +# note: The test is temporarily banned, because of the results returned by zhipuai are unstable. +#info "8.5 apichain test" +#kubectl apply -f config/samples/app_apichain_movie.yaml +#waitCRDStatusReady "Application" "arcadia" "movie-bot" +#sleep 3 +#getRespInAppChat "weather-bot" "arcadia" "Furious 7 的主演有谁?" "" "false" +#echo $resp +#if [[ $resp != *"温度"* ]]; then +# echo "Because conversationWindowSize is enabled to be 2, llm should record history, but resp:"$resp "dont contains Jim" +# exit 1 +#fi + info "9. show apiserver logs for debug" kubectl logs --tail=100 -n arcadia -l app=arcadia-apiserver >/tmp/apiserver.log cat /tmp/apiserver.log