diff --git a/.github/workflows/_build-image-to-registry.yml b/.github/workflows/_build-image-to-registry.yml index 905f3a9..40778c5 100644 --- a/.github/workflows/_build-image-to-registry.yml +++ b/.github/workflows/_build-image-to-registry.yml @@ -39,5 +39,5 @@ jobs: - name: Build Image and Push Image run: | sudo apt install ansible -y - ansible-playbook build-image-to-registry.yml -e "container_registry=${OPEA_IMAGE_REPO}opea" -e "container_tag=${{ inputs.tag }}" + ansible-playbook buildpush-genaistudio-images.yml -e "container_registry=${OPEA_IMAGE_REPO}opea" -e "container_tag=${{ inputs.tag }}" working-directory: ${{ github.workspace }}/setup-scripts/build-image-to-registry/ \ No newline at end of file diff --git a/.github/workflows/_e2e-test.yml b/.github/workflows/_e2e-test.yml index fcdf36b..f19bdff 100644 --- a/.github/workflows/_e2e-test.yml +++ b/.github/workflows/_e2e-test.yml @@ -36,16 +36,9 @@ jobs: ref: ${{ env.CHECKOUT_REF }} fetch-depth: 0 - - name: Update Manifest - run: | - find . -type f -name 'studio-manifest.yaml' -exec sed -i 's/value: opea/value: ${REGISTRY}/g' {} \; - working-directory: ${{ github.workspace }}/setup-scripts/setup-genai-studio/manifests/ - - name: Deploy GenAI Studio run: | - sudo apt install ansible -y - sed -i 's/value: "${TAG}"/value: latest/' manifests/studio-manifest.yaml ansible-playbook genai-studio.yml -e "container_registry=${OPEA_IMAGE_REPO}opea" -e "container_tag=${{ inputs.tag }}" -e "mysql_host=mysql.mysql.svc.cluster.local" sleep 5 kubectl wait --for=condition=ready pod --all --namespace=studio --timeout=300s --field-selector=status.phase!=Succeeded diff --git a/README.md b/README.md index 9227277..8d40974 100644 --- a/README.md +++ b/README.md @@ -214,7 +214,7 @@ The downloaded zip file includes the necessary configurations for deploying the 3. Access the application by opening your web browser and go to: ```bash - http://:8080 + http://:8090 ``` diff --git a/app-backend/megaservice.py b/app-backend/megaservice.py index 6064cad..db1d215 100644 --- a/app-backend/megaservice.py +++ b/app-backend/megaservice.py @@ -170,7 +170,7 @@ def align_inputs(self, inputs, *args, **kwargs): elif self.services[node_id].service_type == ServiceType.LLM: # convert TGI/vLLM to unified OpenAI /v1/chat/completions format next_inputs = {} - next_inputs["model"] = inputs.get("model") or "Intel/neural-chat-7b-v3-3" + next_inputs["model"] = inputs.get("model") or "NA" if inputs.get("inputs"): next_inputs["messages"] = [{"role": "user", "content": inputs["inputs"]}] elif inputs.get("query") and inputs.get("documents"): @@ -401,7 +401,7 @@ def start(self): if __name__ == "__main__": print('pre initialize appService') - app = AppService(host="0.0.0.0", port=8888) + app = AppService(host="0.0.0.0", port=8899) print('after initialize appService') app.add_remote_service() print('after add_remote_service') diff --git a/app-backend/opea_telemetry.py b/app-backend/opea_telemetry.py index b885c62..0cee9c7 100644 --- a/app-backend/opea_telemetry.py +++ b/app-backend/opea_telemetry.py @@ -18,6 +18,7 @@ logger = CustomLogger("OpeaComponent") +# studio update def get_k8s_namespace(): try: with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r") as f: @@ -41,6 +42,7 @@ def detach_ignore_err(self, token: object) -> None: # bypass the ValueError that ContextVar context was created in a different Context from StreamingResponse ContextVarsRuntimeContext.detach = detach_ignore_err +# studio update namespace_name = get_k8s_namespace() resource = Resource.create({ SERVICE_NAME: "opea", @@ -67,7 +69,7 @@ def opea_telemetry(func): @wraps(func) async def wrapper(*args, **kwargs): - with tracer.start_as_current_span(func.__name__) if ENABLE_OPEA_TELEMETRY else contextlib.nullcontext(): + with tracer.start_as_current_span(func.__qualname__) if ENABLE_OPEA_TELEMETRY else contextlib.nullcontext(): res = await func(*args, **kwargs) return res @@ -75,7 +77,7 @@ async def wrapper(*args, **kwargs): @wraps(func) def wrapper(*args, **kwargs): - with tracer.start_as_current_span(func.__name__) if ENABLE_OPEA_TELEMETRY else contextlib.nullcontext(): + with tracer.start_as_current_span(func.__qualname__) if ENABLE_OPEA_TELEMETRY else contextlib.nullcontext(): res = func(*args, **kwargs) return res diff --git a/app-backend/orchestrator.py b/app-backend/orchestrator.py index 41b0b6e..f4b949b 100644 --- a/app-backend/orchestrator.py +++ b/app-backend/orchestrator.py @@ -27,6 +27,7 @@ LOGFLAG = os.getenv("LOGFLAG", False) ENABLE_OPEA_TELEMETRY = bool(os.environ.get("TELEMETRY_ENDPOINT")) + class OrchestratorMetrics: def __init__(self) -> None: # locking for latency metric creation / method change @@ -134,7 +135,7 @@ async def schedule(self, initial_inputs: Dict | BaseModel, llm_parameters: LLMPa if LOGFLAG: logger.info(initial_inputs) - timeout = aiohttp.ClientTimeout(total=1000) + timeout = aiohttp.ClientTimeout(total=2000) async with aiohttp.ClientSession(trust_env=True, timeout=timeout) as session: pending = { asyncio.create_task( @@ -241,8 +242,7 @@ async def execute( **kwargs, ): # send the cur_node request/reply - endpoint = self.services[cur_node].endpoint_path - access_token = self.services[cur_node].api_key_value + llm_parameters_dict = llm_parameters.dict() is_llm_vlm = self.services[cur_node].service_type in (ServiceType.LLM, ServiceType.LVM) @@ -253,7 +253,11 @@ async def execute( inputs[field] = value # pre-process inputs = self.align_inputs(inputs, cur_node, runtime_graph, llm_parameters_dict, **kwargs) - + access_token = self.services[cur_node].api_key_value + if access_token: + endpoint = self.services[cur_node].endpoint_path(inputs["model"]) + else: + endpoint = self.services[cur_node].endpoint_path(None) if is_llm_vlm and llm_parameters.stream: # Still leave to sync requests.post for StreamingResponse if LOGFLAG: @@ -270,7 +274,7 @@ async def execute( headers={"Content-type": "application/json", "Authorization": f"Bearer {access_token}"}, proxies={"http": None}, stream=True, - timeout=1000, + timeout=2000, ) else: response = requests.post( @@ -281,7 +285,7 @@ async def execute( }, proxies={"http": None}, stream=True, - timeout=1000, + timeout=2000, ) downstream = runtime_graph.downstream(cur_node) @@ -289,7 +293,7 @@ async def execute( assert len(downstream) == 1, "Not supported multiple stream downstreams yet!" cur_node = downstream[0] hitted_ends = [".", "?", "!", "。", ",", "!"] - downstream_endpoint = self.services[downstream[0]].endpoint_path + downstream_endpoint = self.services[downstream[0]].endpoint_path() def generate(): token_start = req_start @@ -313,6 +317,7 @@ def generate(): "Authorization": f"Bearer {access_token}", }, proxies={"http": None}, + timeout=2000, ) else: res = requests.post( @@ -322,6 +327,7 @@ def generate(): "Content-type": "application/json", }, proxies={"http": None}, + timeout=2000, ) res_json = res.json() if "text" in res_json: @@ -367,7 +373,6 @@ def generate(): span.set_attribute("llm.input", str(input_data)) span.set_attribute("llm.output", await response.text()) - if response.content_type == "audio/wav": audio_data = await response.read() data = self.align_outputs(audio_data, cur_node, inputs, runtime_graph, llm_parameters_dict, **kwargs) @@ -419,4 +424,4 @@ def token_generator(self, sentence: str, token_start: float, is_first: bool, is_ yield prefix + repr(token.replace("\\n", "\n").encode("utf-8")) + suffix is_first = False if is_last: - yield "data: [DONE]\n\n" + yield "data: [DONE]\n\n" \ No newline at end of file diff --git a/app-frontend/react/.env b/app-frontend/react/.env index 82f44a0..ad8c238 100644 --- a/app-frontend/react/.env +++ b/app-frontend/react/.env @@ -1,2 +1,2 @@ -VITE_CHAT_SERVICE_URL=http://backend_address:8888/v1/chatqna +VITE_CHAT_SERVICE_URL=http://backend_address:8899/v1/chatqna VITE_DATA_PREP_SERVICE_URL=http://backend_address:6007/v1/dataprep \ No newline at end of file diff --git a/app-frontend/react/src/components/Conversation/Conversation.tsx b/app-frontend/react/src/components/Conversation/Conversation.tsx index 716cb30..3f423c6 100644 --- a/app-frontend/react/src/components/Conversation/Conversation.tsx +++ b/app-frontend/react/src/components/Conversation/Conversation.tsx @@ -74,7 +74,7 @@ const Conversation = ({ title, enabledUiFeatures }: ConversationProps) => { messages, maxTokens: tokenLimit, temperature: temperature, - model: "Intel/neural-chat-7b-v3-3", + model: "", // setIsInThinkMode }); setPrompt(""); diff --git a/setup-scripts/build-image-to-registry/buildpush-genaicomps-images.yml b/setup-scripts/build-image-to-registry/buildpush-genaicomps-images.yml new file mode 100755 index 0000000..97a2309 --- /dev/null +++ b/setup-scripts/build-image-to-registry/buildpush-genaicomps-images.yml @@ -0,0 +1,45 @@ +--- +- name: Clone or update GenAIComps repo and build/push images + hosts: localhost + vars_files: + - vars.yml + tasks: + - name: Check if /tmp/GenAIComps exists + stat: + path: /tmp/GenAIComps + register: genaicomp_dir + + - name: Clone GenAIComps repo if not present + git: + repo: https://github.com/opea-project/GenAIComps.git + dest: /tmp/GenAIComps + clone: yes + update: no + when: not genaicomp_dir.stat.exists + + - name: Pull latest changes in GenAIComps repo + git: + repo: https://github.com/opea-project/GenAIComps.git + dest: /tmp/GenAIComps + update: yes + when: genaicomp_dir.stat.exists + + - name: Build and push GenAIComps images + vars: + genaicomp_images: + - { name: 'embedding', dockerfile: 'comps/embeddings/src/Dockerfile' } + - { name: 'reranking', dockerfile: 'comps/rerankings/src/Dockerfile' } + - { name: 'retriever', dockerfile: 'comps/retrievers/src/Dockerfile' } + - { name: 'llm-textgen', dockerfile: 'comps/llms/src/text-generation/Dockerfile' } + - { name: 'dataprep', dockerfile: 'comps/dataprep/src/Dockerfile' } + - { name: 'agent', dockerfile: 'comps/agent/src/Dockerfile' } + block: + - name: Build image + command: docker build -t {{ container_registry }}/{{ item.name }}:{{ container_tag }} -f {{ item.dockerfile }} . + args: + chdir: /tmp/GenAIComps + loop: "{{ genaicomp_images }}" + + - name: Push image + command: docker push {{ container_registry }}/{{ item.name }}:{{ container_tag }} + loop: "{{ genaicomp_images }}" \ No newline at end of file diff --git a/setup-scripts/build-image-to-registry/build-image-to-registry.yml b/setup-scripts/build-image-to-registry/buildpush-genaistudio-images.yml similarity index 74% rename from setup-scripts/build-image-to-registry/build-image-to-registry.yml rename to setup-scripts/build-image-to-registry/buildpush-genaistudio-images.yml index a33a816..e4f916f 100755 --- a/setup-scripts/build-image-to-registry/build-image-to-registry.yml +++ b/setup-scripts/build-image-to-registry/buildpush-genaistudio-images.yml @@ -5,7 +5,7 @@ - vars.yml tasks: - name: Build Docker image - command: docker build -t "{{ item.image_name }}:latest" . + command: docker build -t "{{ container_registry }}/{{ item.image_name }}:{{ container_tag }}" . args: chdir: "{{ item.directory }}" loop: @@ -15,14 +15,6 @@ - { directory: '../../app-backend/', image_name: 'app-backend' } register: build_results - - name: Tag Docker image - command: docker tag "{{ item.image_name }}:latest" "{{ container_registry }}/{{ item.image_name }}:{{ container_tag }}" - loop: - - { image_name: 'studio-frontend' } - - { image_name: 'studio-backend' } - - { image_name: 'app-frontend' } - - { image_name: 'app-backend' } - - name: Push Docker image command: docker push "{{ container_registry }}/{{ item.image_name }}:{{ container_tag }}" loop: diff --git a/setup-scripts/build-image-to-registry/readme.md b/setup-scripts/build-image-to-registry/readme.md index 96d1b03..c2a1dfb 100644 --- a/setup-scripts/build-image-to-registry/readme.md +++ b/setup-scripts/build-image-to-registry/readme.md @@ -14,5 +14,11 @@ The ansible scripts used here are building, tag and push to the specified contai Run below commands: ```sh sudo apt install ansible -y -ansible-playbook build-image-to-registry.yml +ansible-playbook buildpush-genaistudio-images.yml ``` + +If would like to build GenAIComps images to use +```sh +sudo apt install ansible -y +ansible-playbook buildpush-genaicomps-images.yml +``` \ No newline at end of file diff --git a/setup-scripts/setup-genai-studio/manifests/studio-manifest.yaml b/setup-scripts/setup-genai-studio/manifests/studio-manifest.yaml index 80a73ef..d3e19b0 100644 --- a/setup-scripts/setup-genai-studio/manifests/studio-manifest.yaml +++ b/setup-scripts/setup-genai-studio/manifests/studio-manifest.yaml @@ -276,15 +276,18 @@ rules: - apiGroups: [""] resources: ["pods"] verbs: ["list", "get"] +- apiGroups: [""] + resources: ["pods/log"] + verbs: ["get"] +- apiGroups: [""] + resources: ["events"] + verbs: ["list", "watch"] - apiGroups: [""] resources: ["nodes"] verbs: ["list", "get"] - apiGroups: [""] resources: ["persistentvolumeclaims"] verbs: ["get", "create", "list", "watch"] -- apiGroups: [""] - resources: ["persistentvolumeclaims"] - verbs: ["get", "create", "list", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding @@ -328,7 +331,7 @@ spec: - name: REGISTRY value: ${REGISTRY} - name: TAG - value: "${TAG}" + value: ${TAG} - name: SBX_HTTP_PROXY value: ${HTTP_PROXY} - name: SBX_NO_PROXY @@ -353,6 +356,18 @@ spec: periodSeconds: 30 timeoutSeconds: 10 failureThreshold: 3 + volumeMounts: + - name: ssh-key-volume + mountPath: /root/.ssh + readOnly: true + volumes: + - name: ssh-key-volume + secret: + secretName: ssh-keys + items: + - key: studio-id_rsa + path: id_rsa + mode: 0400 serviceAccountName: studio-backend-sa --- apiVersion: v1 @@ -407,7 +422,7 @@ spec: - name: DATABASE_NAME value: studio - name: DATABASE_SSL - value: "true" + value: "false" ports: - name: studio-frontend containerPort: 8080 @@ -416,9 +431,19 @@ spec: volumeMounts: - mountPath: /tmp name: tmp + - name: ssh-key-volume + mountPath: /root/.ssh + readOnly: true volumes: - name: tmp emptyDir: {} + - name: ssh-key-volume + secret: + secretName: ssh-keys + items: + - key: studio-id_rsa.pub + path: id_rsa.pub + mode: 0644 --- apiVersion: apps/v1 kind: Deployment diff --git a/setup-scripts/setup-genai-studio/playbooks/create-ssh-secrets.yml b/setup-scripts/setup-genai-studio/playbooks/create-ssh-secrets.yml new file mode 100644 index 0000000..4bf2c8a --- /dev/null +++ b/setup-scripts/setup-genai-studio/playbooks/create-ssh-secrets.yml @@ -0,0 +1,46 @@ +- name: Create ssh keys in k8 secrets using shell and kubectl commands + hosts: localhost + + tasks: + + - name: Check if Kubernetes secret exists + command: kubectl -n studio get secret ssh-keys + register: kubectl_secret_check + failed_when: kubectl_secret_check.rc not in [0, 1] + changed_when: False + + - name: Run Ubuntu pod + command: | + kubectl -n studio run ubuntu-ssh-keygen --image=ubuntu --restart=Never -- bash -c "sleep 120" + register: run_pod + failed_when: "'created' not in run_pod.stdout and 'already exists' not in run_pod.stderr" + when: "'NotFound' in kubectl_secret_check.stderr" + + - name: Wait for Ubuntu pod to be ready + command: kubectl wait --for=condition=Ready pod/ubuntu-ssh-keygen -n studio --timeout=60s + when: "'NotFound' in kubectl_secret_check.stderr" + + - name: Generate SSH key inside pod + shell: | + kubectl exec -n studio ubuntu-ssh-keygen -- bash -c "apt-get update && apt-get install -y openssh-client" + kubectl exec -n studio ubuntu-ssh-keygen -- bash -c "ssh-keygen -t rsa -b 2048 -f /tmp/id_rsa -N '' -C ''" + when: "'NotFound' in kubectl_secret_check.stderr" + + - name: Copy ssh keys from pod to local + shell: | + kubectl exec -n studio ubuntu-ssh-keygen -- cat /tmp/id_rsa > ./studio-id_rsa && \ + kubectl exec -n studio ubuntu-ssh-keygen -- cat /tmp/id_rsa.pub > ./studio-id_rsa.pub + when: "'NotFound' in kubectl_secret_check.stderr" + + - name: Create Kubernetes secret from the keys + command: | + kubectl -n studio create secret generic ssh-keys \ + --from-file=studio-id_rsa=./studio-id_rsa \ + --from-file=studio-id_rsa.pub=./studio-id_rsa.pub + when: "'NotFound' in kubectl_secret_check.stderr" + + - name: Cleanup Ubuntu pod and keys + shell: | + kubectl delete pod ubuntu-ssh-keygen -n studio + rm -f ./studio-id_rsa ./studio-id_rsa.pub + when: "'NotFound' in kubectl_secret_check.stderr" diff --git a/setup-scripts/setup-genai-studio/playbooks/deploy-studio.yml b/setup-scripts/setup-genai-studio/playbooks/deploy-studio.yml index b6ba011..1d2ecef 100644 --- a/setup-scripts/setup-genai-studio/playbooks/deploy-studio.yml +++ b/setup-scripts/setup-genai-studio/playbooks/deploy-studio.yml @@ -62,7 +62,7 @@ MYSQL_HOST: "{{ mysql_host }}" - name: Wait for all pods to be ready in studio namespace - shell: kubectl wait --for=condition=ready pod --all --namespace=studio --timeout=420s + shell: kubectl wait --for=condition=ready pod --all --namespace=studio --timeout=600s register: pod_ready_check failed_when: pod_ready_check.rc != 0 changed_when: false \ No newline at end of file diff --git a/setup-scripts/setup-genai-studio/studio-config.yaml b/setup-scripts/setup-genai-studio/studio-config.yaml index 4dca50b..85b540f 100644 --- a/setup-scripts/setup-genai-studio/studio-config.yaml +++ b/setup-scripts/setup-genai-studio/studio-config.yaml @@ -10,7 +10,7 @@ data: KEYCLOAK_DNS: "keycloak.studio.svc.cluster.local:8443" GRAFANA_DNS: "kube-prometheus-stack-grafana.monitoring.svc.cluster.local" STUDIO_FRONTEND_DNS: "studio-frontend.studio.svc.cluster.local:3000" - APP_FRONTEND_DNS: "app-frontend.$namespace.svc.cluster.local:5175" - APP_BACKEND_DNS: "app-backend.$namespace.svc.cluster.local:8888" + APP_FRONTEND_DNS: "app-frontend.$namespace.svc.cluster.local:5275" + APP_BACKEND_DNS: "app-backend.$namespace.svc.cluster.local:8899" PREPARE_DOC_REDIS_PREP_DNS: "prepare-doc-redis-prep-0.$namespace.svc.cluster.local:6007" STUDIO_BACKEND_DNS: "studio-backend.studio.svc.cluster.local:5000" \ No newline at end of file diff --git a/setup-scripts/setup-genai-studio/vars.yml b/setup-scripts/setup-genai-studio/vars.yml index 131d094..d53d819 100644 --- a/setup-scripts/setup-genai-studio/vars.yml +++ b/setup-scripts/setup-genai-studio/vars.yml @@ -2,4 +2,4 @@ container_registry: 'opea' container_tag: 'latest' http_proxy: '' no_proxy: '' -mysql_host: 'Your_External_Host_IP' \ No newline at end of file +mysql_host: 'mysql.mysql.svc.cluster.local' \ No newline at end of file diff --git a/setup-scripts/setup-onpremise-kubernetes/registry/registry-cmds.py b/setup-scripts/setup-onpremise-kubernetes/registry/registry-cmds.py index caf731b..c67373d 100644 --- a/setup-scripts/setup-onpremise-kubernetes/registry/registry-cmds.py +++ b/setup-scripts/setup-onpremise-kubernetes/registry/registry-cmds.py @@ -15,7 +15,7 @@ def get_manifest(image_name, image_tag): return response.json() def list_images_all(): - """List all images in the registry.""" + """List all images in the registry along with their digests.""" response = requests.get(f"{REGISTRY_URL}/v2/_catalog") response.raise_for_status() repositories = response.json().get('repositories', []) @@ -25,7 +25,13 @@ def list_images_all(): tags = tag_response.json().get('tags', []) if tags: for tag in tags: - print(f"{repo}:{tag}") + manifest_response = requests.head( + f"{REGISTRY_URL}/v2/{repo}/manifests/{tag}", + headers={'Accept': 'application/vnd.docker.distribution.manifest.v2+json'} + ) + manifest_response.raise_for_status() + digest = manifest_response.headers.get('Docker-Content-Digest', '') + print(f"{repo}:{tag} (digest: {digest})") else: print(f"{repo}:") diff --git a/studio-backend/Dockerfile b/studio-backend/Dockerfile index bb55723..b06463e 100644 --- a/studio-backend/Dockerfile +++ b/studio-backend/Dockerfile @@ -9,7 +9,7 @@ COPY app /usr/src/app # Upgrade libsqlite3 to a safe version RUN apt-get update -y && apt-get install -y --no-install-recommends --fix-missing \ - libsqlite3-0=3.40.1-2+deb12u1 && \ + libsqlite3-0=3.40.1-2+deb12u1 openssh-client && \ rm -rf /var/lib/apt/lists/* # Upgrade setuptools to a safe version and install any needed packages specified in requirements.txt diff --git a/studio-backend/app/main.py b/studio-backend/app/main.py index 57963b9..ed54961 100644 --- a/studio-backend/app/main.py +++ b/studio-backend/app/main.py @@ -1,6 +1,6 @@ -from kubernetes import config from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +from kubernetes import config # Load the kubeconfig file try: @@ -26,9 +26,11 @@ def read_health(): allow_headers=["*"], ) -from .routers import user_router, download_router, sandbox_router, llmtraces_router +from .routers import user_router, download_router, sandbox_router, llmtraces_router, debuglog_router, clickdeploy_router app.include_router(user_router.router, prefix="/studio-backend") app.include_router(download_router.router, prefix="/studio-backend") app.include_router(sandbox_router.router, prefix="/studio-backend") app.include_router(llmtraces_router.router, prefix="/studio-backend") +app.include_router(debuglog_router.router, prefix="/studio-backend") +app.include_router(clickdeploy_router.router, prefix="/studio-backend") \ No newline at end of file diff --git a/studio-backend/app/models/pipeline_model.py b/studio-backend/app/models/pipeline_model.py index 4b98a5c..97875c6 100644 --- a/studio-backend/app/models/pipeline_model.py +++ b/studio-backend/app/models/pipeline_model.py @@ -76,8 +76,14 @@ class PipelineFlow(BaseModel): sandboxStatus: Optional[str] = None sandboxAppUrl: Optional[str] = None sandboxGrafanaUrl: Optional[str] = None + sandboxDebugLogsUrl: Optional[str] = None createdDate: datetime updatedDate: datetime +class DeployPipelineFlow(BaseModel): + remoteHost: str + remoteUser: str + pipelineFlow: PipelineFlow + class WorkflowId(BaseModel): id: str \ No newline at end of file diff --git a/studio-backend/app/requirements.txt b/studio-backend/app/requirements.txt index 252593d..bb1ba66 100644 --- a/studio-backend/app/requirements.txt +++ b/studio-backend/app/requirements.txt @@ -5,4 +5,5 @@ requests==2.32.3 pydantic==1.10.18 starlette==0.41.2 websockets==10.3 -clickhouse-driver==0.2.9 \ No newline at end of file +clickhouse-driver==0.2.9 +paramiko==3.5.1 \ No newline at end of file diff --git a/studio-backend/app/routers/clickdeploy_router.py b/studio-backend/app/routers/clickdeploy_router.py new file mode 100644 index 0000000..efa13c3 --- /dev/null +++ b/studio-backend/app/routers/clickdeploy_router.py @@ -0,0 +1,156 @@ +from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect +from fastapi.concurrency import run_in_threadpool +import paramiko +import time +import json + +from app.models.pipeline_model import DeployPipelineFlow +from app.services.clickdeploy_service import deploy_pipeline + +router = APIRouter() + +@router.post("/click-deploy") +async def deploy(request: DeployPipelineFlow): + print("[DEBUG] Entered /click-deploy endpoint") + remote_host = request.remoteHost + remote_user = request.remoteUser + pipeline_flow = request.pipelineFlow + try: + print("[DEBUG] Calling deploy_pipeline...") + response = deploy_pipeline(remote_host, remote_user, pipeline_flow.dict()) + print("[DEBUG] deploy_pipeline returned") + except Exception as e: + print(f"[ERROR] Exception in /click-deploy: {e}") + raise HTTPException(status_code=500, detail=str(e)) + return response + +@router.websocket("/ws/clickdeploy-status") +async def check_clickdeploy_status(websocket: WebSocket): + print('checking clickdeploy status') + await websocket.accept() + try: + data = await websocket.receive_json() + print("Received data: ", data) + remote_host = data["hostname"] + remote_user = data["username"] + compose_dir = data["compose_dir"] + + def check_status(): + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(remote_host, username=remote_user) + + # Get the number of services defined in compose.yaml + _, stdout_num, _ = ssh.exec_command(f"cd {compose_dir} && docker compose config --services | wc -l") + num_services_output = stdout_num.read().decode().strip().splitlines() + num_services_lines = [line for line in num_services_output if not line.startswith('WARN') and line.strip()] + num_services_str = num_services_lines[-1] if num_services_lines else '0' + + # Run docker compose ps to get service status + _, stdout_ps, _ = ssh.exec_command(f"cd {compose_dir} && docker compose ps --all --format json") + out = stdout_ps.read().decode() + json_lines = [line for line in out.strip().splitlines() if line.strip() and not line.strip().startswith('WARN')] + out_filtered = '\n'.join(json_lines) + + # Read nohup.out for progress logs (always fetch latest 10 lines) + _, stdout_nohup, _ = ssh.exec_command(f"cd {compose_dir} && tail -n 10 nohup.out") + nohup_out_lines = stdout_nohup.read().decode().splitlines() + + ssh.close() + + try: + # If output contains multiple JSON objects, parse all and aggregate + json_lines = [line for line in out_filtered.strip().splitlines() if line.strip()] + all_services = [] + for line in json_lines: + try: + ps_data = json.loads(line) + if isinstance(ps_data, dict): + if 'services' in ps_data or 'containers' in ps_data: + services = ps_data.get('services') or ps_data.get('containers') or [] + if isinstance(services, dict): + services = list(services.values()) + elif not isinstance(services, list): + services = [] + all_services.extend(services) + else: + all_services.append(ps_data) + else: + all_services.append(ps_data) + except Exception as e: + return {"error": f"Failed to parse docker compose ps output: {line}\n{str(e)}"} + + if len(all_services) != int(num_services_str): + # If error in nohup.out, return as error + if any("error" in line.lower() or "fail" in line.lower() for line in nohup_out_lines): + return {"error": nohup_out_lines} + else: + print(f"[DEBUG] Docker pulling images..") + return { + "all_healthy": False, + "none_restarting": True, + "services_running": 0, + "services_exited": 0, + "services_defined": int(num_services_str), + "ps": all_services, + "error": None, + "nohup_out": nohup_out_lines + } + + services_exited = sum(1 for s in all_services if isinstance(s, dict) and s.get("State", "") == "exited") + services_running = sum(1 for s in all_services if isinstance(s, dict) and s.get("State", "") == "running") + print(f"[DEBUG] Number of services deployed: {services_running + services_exited}/{num_services_str}") + all_healthy = all((not isinstance(s, dict)) or (s.get("Health", "") in ("", "healthy")) for s in all_services) + none_restarting = all(isinstance(s, dict) and s.get("State", "") != "restarting" for s in all_services) + + print(f"[DEBUG] all_healthy: {all_healthy}") + print(f"[DEBUG] none_restarting: {none_restarting}") + + return { + "all_healthy": all_healthy, + "none_restarting": none_restarting, + "services_running": services_running, + "services_exited": services_exited, + "services_defined": int(num_services_str), + "ps": all_services, + "error": None, + "nohup_out": nohup_out_lines + } + except Exception as e: + return {"error": str(e)} + + while True: + + result = await run_in_threadpool(check_status) + + if result["error"]: + await websocket.send_json({"status": "Error", "error": result["error"]}) + break + + if (int(result["services_running"]) + int(result["services_exited"])) == result["services_defined"]: + if result["all_healthy"] and result["services_running"] == result["services_defined"]: + # Wait 5 seconds and recheck none_restarting + time.sleep(5) + recheck_result = await run_in_threadpool(check_status) + if recheck_result["none_restarting"]: + await websocket.send_json({"status": "Done", "success": f"All {result['services_running']} services are running and healthy. Open http://localhost:8090 in your machine's browser to access the application."}) + else: + restarting_services = [ + s.get("Name", "unknown") for s in recheck_result["ps"] + if isinstance(s, dict) and s.get("State", "") == "restarting" + ] + await websocket.send_json({"status": "Error", "error": f"Services stuck in restarting status: [{', '.join(restarting_services)}]"}) + else: + exited_services = [ + s.get("Name", "unknown") for s in result["ps"] + if isinstance(s, dict) and s.get("State", "") == "exited" + ] + await websocket.send_json({"status": "Error", "error": f"Services in exited state: [{', '.join(exited_services)}]"}) + break + # Send nohup_out in progress status + await websocket.send_json({"status": "In Progress", "ps": result["ps"], "nohup_out": result.get("nohup_out", [])}) + time.sleep(2) + except WebSocketDisconnect: + print("Client disconnected") + finally: + await websocket.close() \ No newline at end of file diff --git a/studio-backend/app/routers/debuglog_router.py b/studio-backend/app/routers/debuglog_router.py new file mode 100644 index 0000000..4b4654b --- /dev/null +++ b/studio-backend/app/routers/debuglog_router.py @@ -0,0 +1,68 @@ +from fastapi import APIRouter, HTTPException +from kubernetes import client + +router = APIRouter() + +@router.get("/podlogs/{namespace}", summary="Fetch all pods in a namespace") +async def get_all_pods_in_namespace(namespace: str): + core_v1_api = client.CoreV1Api() + pods = core_v1_api.list_namespaced_pod(namespace=namespace) + + if not pods.items: + return {"namespace": namespace, "pods": []} + + pod_list = [] + for pod in pods.items: + pod_name = pod.metadata.name + + # Initialize log_entries and event_entries + log_entries = [] + event_entries = [] + + # Fetch logs related to the pod + try: + pod_logs = core_v1_api.read_namespaced_pod_log(name=pod_name, namespace=namespace, tail_lines=200) + for line in pod_logs.splitlines(): + log_entries.append(line) + except Exception as e: + print(f"Error fetching logs: {str(e)}") + + # Fetch events related to the pod + try: + pod_events = core_v1_api.list_namespaced_event(namespace=namespace) + event_entries = [ + f"[{event.type}] {event.reason}: {event.message} (at {event.last_timestamp})" + for event in pod_events.items + if event.involved_object.name == pod_name + ] + except Exception as e: + print(f"Error fetching events: {str(e)}") + + # Determine the Ready and Status of the pod + ready_status = "Unknown" + pod_status = pod.status.phase + if pod.metadata.deletion_timestamp: + pod_status = "Terminating" + elif pod.status.init_container_statuses: + ready_count = sum(1 for status in pod.status.init_container_statuses if status.ready) + total_count = len(pod.status.init_container_statuses) + if ready_count < total_count: + pod_status = f"Init:{ready_count}/{total_count}" + ready_status = f"{ready_count}/{total_count}" + elif pod.status.container_statuses: + ready_count = sum(1 for status in pod.status.container_statuses if status.ready) + total_count = len(pod.status.container_statuses) + ready_status = f"{ready_count}/{total_count}" + + pod_list.append({ + "name": pod.metadata.name, + "namespace": pod.metadata.namespace, + "ready": ready_status, + "status": pod_status, + "labels": pod.metadata.labels, + "annotations": pod.metadata.annotations, + "logs": log_entries, + "events": event_entries, + }) + + return {"namespace": namespace, "pods": pod_list} \ No newline at end of file diff --git a/studio-backend/app/routers/llmtraces_router.py b/studio-backend/app/routers/llmtraces_router.py index 7b6565b..348b4cc 100644 --- a/studio-backend/app/routers/llmtraces_router.py +++ b/studio-backend/app/routers/llmtraces_router.py @@ -17,8 +17,9 @@ async def list_trace_ids(namespace: str): SELECT DISTINCT tts.TraceId, tts.Start, tts.End FROM otel.otel_traces_trace_id_ts AS tts INNER JOIN otel.otel_traces AS ot ON tts.TraceId = ot.TraceId - WHERE ot.ResourceAttributes['k8s.namespace.name'] = '%(namespace)s' + WHERE ot.ResourceAttributes['k8s.namespace.name'] = %(namespace)s """ + print(f"Query: {query}") result = client.execute(query, {'namespace': namespace}) if not result: diff --git a/studio-backend/app/services/clickdeploy_service.py b/studio-backend/app/services/clickdeploy_service.py new file mode 100644 index 0000000..ea665bb --- /dev/null +++ b/studio-backend/app/services/clickdeploy_service.py @@ -0,0 +1,137 @@ +import json +import os +import shutil +import logging +import paramiko +import tempfile +import json +import zipfile +import datetime +import time + +from app.services.exporter_service import convert_proj_info_to_compose +from app.services.workflow_info_service import WorkflowInfo +from app.utils.exporter_utils import process_opea_services + +def deploy_pipeline(hostname, username, pipeline_flow): + print("[INFO] Starting deployment to remote server...") + remote_zip_path = f"/home/{username}/docker-compose.zip" + temp_dir = None + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + remote_compose_dir = f"docker-compose-{timestamp}" + try: + print("[INFO] Creating ZIP locally...") + zip_path, temp_dir = create_zip_locally(pipeline_flow, hostname) + print(f"[INFO] ZIP created at {zip_path}") + + print("[INFO] Connecting to remote server via SSH...") + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(hostname, username=username) + print("[INFO] SSH connection established.") + + print("[INFO] Opening SFTP session...") + sftp = ssh.open_sftp() + print("[INFO] SFTP session opened.") + sftp.put(zip_path, remote_zip_path) + print(f"[INFO] ZIP uploaded to {remote_zip_path}") + sftp.close() + print("[INFO] SFTP session closed.") + + commands = [ + f"mkdir {remote_compose_dir}", + f"unzip -o {remote_zip_path} -d {remote_compose_dir}", + f"rm -f {remote_zip_path}", + f"cd {remote_compose_dir} && nohup docker compose up -d & sleep 0.1" + ] + for cmd in commands: + print(f"[INFO] Executing remote command: {cmd}") + _, stdout, stderr = ssh.exec_command(cmd, get_pty=True) + exit_status = stdout.channel.recv_exit_status() + stderr_str = stderr.read().decode().strip() + print(f"[INFO] Command exit status: {exit_status}") + if stderr_str: + print(f"[ERROR] Stderr: {stderr_str}") + + ssh.close() + print("[INFO] SSH connection closed.") + + return { + "status": "success", + "message": "docker compose up -d has been started.", + "compose_dir": remote_compose_dir + } + except Exception as e: + print(f"[ERROR] An error: {e}") + return {"error": str(e)} + finally: + if temp_dir: + clean_up_temp_dir(temp_dir) + +def create_zip_locally(request, hostname): + temp_dir = tempfile.mkdtemp() + env_file_path = os.path.join(temp_dir, ".env") + compose_file_path = os.path.join(temp_dir, "compose.yaml") + workflow_info_file_path = os.path.join(temp_dir, "workflow-info.json") + zip_path = os.path.join(temp_dir, "docker-compose.zip") + + # Only keep large objects in memory as long as needed + workflow_info_raw = WorkflowInfo(request) + workflow_info_json = workflow_info_raw.export_to_json() + workflow_info = json.loads(workflow_info_json) + services_info = process_opea_services(workflow_info) + ports_info = services_info["services"]["app"]["ports_info"] + additional_files_info = services_info.get("additional_files", []) + + try: + with open(env_file_path, 'w') as f: + f.write(f"public_host_ip={hostname}\n") + for key, value in ports_info.items(): + f.write(f"{key}={value}\n") + + compose_content = convert_proj_info_to_compose(workflow_info) + with open(compose_file_path, 'w') as f: + f.write(compose_content) + + with open(workflow_info_file_path, 'w') as f: + f.write(json.dumps(workflow_info, indent=4)) + + # Free up memory from large objects as soon as possible + del workflow_info_raw, workflow_info_json, workflow_info, services_info, ports_info + + # Use ZIP_DEFLATED for better disk usage (optional, can help with large files) + with zipfile.ZipFile(zip_path, 'w', compression=zipfile.ZIP_DEFLATED) as zipf: + zipf.write(env_file_path, arcname=".env") + zipf.write(compose_file_path, arcname="compose.yaml") + zipf.write(workflow_info_file_path, arcname="workflow-info.json") + + for file_info in additional_files_info: + source_path = file_info["source"] + target_path = file_info["target"] + if os.path.isdir(source_path): + # Walk directory and add files one by one to avoid memory spikes + for root, _, files in os.walk(source_path): + for file in files: + full_file_path = os.path.join(root, file) + relative_path = os.path.relpath(full_file_path, source_path) + arcname = os.path.join(target_path, relative_path) + zipf.write(full_file_path, arcname=arcname) + else: + # Add file directly by path (no memory spike) + zipf.write(source_path, arcname=target_path) + + # Optionally, delete large file paths from temp_dir after zipping + del additional_files_info + + return zip_path, temp_dir + + except Exception as e: + print(f"An error occurred while creating the ZIP: {e}") + clean_up_temp_dir(temp_dir) + raise RuntimeError(f"Failed to generate ZIP: {e}") + +def clean_up_temp_dir(dir_path: str): + try: + shutil.rmtree(dir_path) + except Exception as e: + logging.exception(f"An error occurred while deleting the temp directory {dir_path}.") \ No newline at end of file diff --git a/studio-backend/app/services/namespace_service.py b/studio-backend/app/services/namespace_service.py index 1091bd6..8651721 100644 --- a/studio-backend/app/services/namespace_service.py +++ b/studio-backend/app/services/namespace_service.py @@ -130,9 +130,10 @@ def check_ns_status(namespace_id, status_type, core_v1_api, apps_v1_api): sandbox_app_url = f"/?ns={namespace_name}" sandbox_grafana_url = f"/grafana/d/{namespace_id}" - sandbox_tracer_url = f"tracer/{namespace_name}" + sandbox_tracer_url = f"/tracer/{namespace_name}" + sandbox_debuglogs_url = f"/debuglogs/{namespace_name}" - return {f"status": "Ready", "sandbox_app_url": sandbox_app_url, "sandbox_grafana_url": sandbox_grafana_url, "sandbox_tracer_url": sandbox_tracer_url} + return {f"status": "Ready", "sandbox_app_url": sandbox_app_url, "sandbox_grafana_url": sandbox_grafana_url, "sandbox_tracer_url": sandbox_tracer_url, "sandbox_debuglogs_url": sandbox_debuglogs_url} elif status_type == "Stopping": diff --git a/studio-backend/app/templates/app/app.compose.yaml b/studio-backend/app/templates/app/app.compose.yaml index fb12458..1d4927d 100644 --- a/studio-backend/app/templates/app/app.compose.yaml +++ b/studio-backend/app/templates/app/app.compose.yaml @@ -7,7 +7,7 @@ app-backend: depends_on: __BACKEND_ENDPOINTS_LIST_PLACEHOLDER__ ports: - - 8888:8888 + - 8899:8899 environment: - no_proxy=${no_proxy} - https_proxy=${https_proxy} @@ -21,7 +21,7 @@ app-frontend: depends_on: - app-backend ports: - - 5175:80 + - 5275:80 environment: - no_proxy=${no_proxy} - https_proxy=${https_proxy} @@ -37,17 +37,17 @@ app-nginx: - app-frontend - app-backend ports: - - 8080:80 + - 8090:80 environment: - no_proxy=${no_proxy} - https_proxy=${https_proxy} - http_proxy=${http_proxy} - FRONTEND_SERVICE_IP=${public_host_ip} - - FRONTEND_SERVICE_PORT=5175 + - FRONTEND_SERVICE_PORT=5275 - BACKEND_SERVICE_NAME=app-backend - BACKEND_SERVICE_IP=${public_host_ip} - - BACKEND_SERVICE_PORT=8888 + - BACKEND_SERVICE_PORT=8899 - DATAPREP_SERVICE_IP=${public_host_ip} - - DATAPREP_SERVICE_PORT=${prepare_doc_redis_prep_0_port} + - DATAPREP_SERVICE_PORT=${prepare_doc_redis_prep_0_port:-1234} ipc: host restart: always \ No newline at end of file diff --git a/studio-backend/app/templates/app/app.manifest.aws.ecr.yaml b/studio-backend/app/templates/app/app.manifest.aws.ecr.yaml index b2c9437..971aeda 100644 --- a/studio-backend/app/templates/app/app.manifest.aws.ecr.yaml +++ b/studio-backend/app/templates/app/app.manifest.aws.ecr.yaml @@ -23,8 +23,8 @@ metadata: spec: type: ClusterIP ports: - - port: 8888 - targetPort: 8888 + - port: 8899 + targetPort: 8899 protocol: TCP name: app-backend selector: @@ -72,7 +72,7 @@ spec: subPath: workflow-info.json ports: - name: app-backend - containerPort: 8888 + containerPort: 8899 protocol: TCP resources: null imagePullSecrets: @@ -91,7 +91,7 @@ metadata: spec: type: ClusterIP ports: - - port: 5175 + - port: 5275 targetPort: 80 protocol: TCP name: app-frontend @@ -163,7 +163,7 @@ data: } location / { - proxy_pass http://app-frontend:5175; + proxy_pass http://app-frontend:5275; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -171,7 +171,7 @@ data: } location /v1/app-backend { - proxy_pass http://app-backend:8888; + proxy_pass http://app-backend:8899; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/studio-backend/app/templates/app/app.manifest.yaml b/studio-backend/app/templates/app/app.manifest.yaml index e77288e..f2b0389 100644 --- a/studio-backend/app/templates/app/app.manifest.yaml +++ b/studio-backend/app/templates/app/app.manifest.yaml @@ -22,8 +22,8 @@ metadata: spec: type: ClusterIP ports: - - port: 8888 - targetPort: 8888 + - port: 8899 + targetPort: 8899 protocol: TCP name: app-backend selector: @@ -51,6 +51,8 @@ spec: env: - name: USE_NODE_ID_AS_IP value: 'true' + - name: LOGFLAG + value: 'True' __TELEMETRY_ENDPOINT__ securityContext: allowPrivilegeEscalation: false @@ -75,7 +77,7 @@ spec: subPath: .env ports: - name: app-backend - containerPort: 8888 + containerPort: 8899 protocol: TCP resources: null volumes: @@ -95,7 +97,7 @@ metadata: spec: type: ClusterIP ports: - - port: 5175 + - port: 5275 targetPort: 80 protocol: TCP name: app-frontend @@ -165,7 +167,7 @@ data: } location / { - proxy_pass http://app-frontend:5175; + proxy_pass http://app-frontend:5275; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -173,7 +175,7 @@ data: } location /v1/app-backend { - proxy_pass http://app-backend:8888; + proxy_pass http://app-backend:8899; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/studio-backend/app/templates/microsvc-composes/data-prep.yaml b/studio-backend/app/templates/microsvc-composes/data-prep.yaml index 0be2069..df3eef5 100644 --- a/studio-backend/app/templates/microsvc-composes/data-prep.yaml +++ b/studio-backend/app/templates/microsvc-composes/data-prep.yaml @@ -17,4 +17,5 @@ INDEX_NAME: "rag-redis" TEI_EMBEDDING_ENDPOINT: "http://${public_host_ip}:{{tei_port}}" DATAPREP_COMPONENT_NAME: "OPEA_DATAPREP_REDIS" - HUGGINGFACEHUB_API_TOKEN: "{{huggingFaceToken}}" \ No newline at end of file + HUGGINGFACEHUB_API_TOKEN: "{{huggingFaceToken}}" + LOGFLAG: "True" \ No newline at end of file diff --git a/studio-backend/app/templates/microsvc-composes/embedding-usvc.yaml b/studio-backend/app/templates/microsvc-composes/embedding-usvc.yaml index 92a793f..90fa1c9 100644 --- a/studio-backend/app/templates/microsvc-composes/embedding-usvc.yaml +++ b/studio-backend/app/templates/microsvc-composes/embedding-usvc.yaml @@ -14,4 +14,5 @@ TEI_EMBEDDING_ENDPOINT: "http://${public_host_ip}:{{tei_port}}" TRANSFORMERS_CACHE: "/tmp/transformers_cache" HF_HOME: "/tmp/.cache/huggingface" + LOGFLAG: "True" restart: unless-stopped \ No newline at end of file diff --git a/studio-backend/app/templates/microsvc-composes/llm-uservice.yaml b/studio-backend/app/templates/microsvc-composes/llm-uservice.yaml index adfced5..93dd258 100644 --- a/studio-backend/app/templates/microsvc-composes/llm-uservice.yaml +++ b/studio-backend/app/templates/microsvc-composes/llm-uservice.yaml @@ -15,4 +15,6 @@ HUGGINGFACEHUB_API_TOKEN: "{{huggingFaceToken}}" HF_HUB_DISABLE_PROGRESS_BARS: 1 HF_HUB_ENABLE_HF_TRANSFER: 0 + LLM_MODEL_ID: "{{modelName}}" + LOGFLAG: "True" restart: unless-stopped \ No newline at end of file diff --git a/studio-backend/app/templates/microsvc-composes/reranking-usvc.yaml b/studio-backend/app/templates/microsvc-composes/reranking-usvc.yaml index fa0c021..6dc8701 100644 --- a/studio-backend/app/templates/microsvc-composes/reranking-usvc.yaml +++ b/studio-backend/app/templates/microsvc-composes/reranking-usvc.yaml @@ -15,4 +15,5 @@ HUGGINGFACEHUB_API_TOKEN: "{{huggingFaceToken}}" HF_HUB_DISABLE_PROGRESS_BARS: 1 HF_HUB_ENABLE_HF_TRANSFER: 0 + LOGFLAG: "True" restart: unless-stopped \ No newline at end of file diff --git a/studio-backend/app/templates/microsvc-composes/retriever-usvc.yaml b/studio-backend/app/templates/microsvc-composes/retriever-usvc.yaml index d336c37..a62811b 100644 --- a/studio-backend/app/templates/microsvc-composes/retriever-usvc.yaml +++ b/studio-backend/app/templates/microsvc-composes/retriever-usvc.yaml @@ -18,6 +18,6 @@ INDEX_NAME: "rag-redis" TEI_EMBEDDING_ENDPOINT: "http://${public_host_ip}:{{tei_port}}" HUGGINGFACEHUB_API_TOKEN: "{{huggingFaceToken}}" - LOGFLAG: ${LOGFLAG} + LOGFLAG: "True" RETRIEVER_COMPONENT_NAME: "OPEA_RETRIEVER_REDIS" restart: unless-stopped \ No newline at end of file diff --git a/studio-backend/app/templates/microsvc-composes/tei.yaml b/studio-backend/app/templates/microsvc-composes/tei.yaml index 9e76679..7db55ec 100644 --- a/studio-backend/app/templates/microsvc-composes/tei.yaml +++ b/studio-backend/app/templates/microsvc-composes/tei.yaml @@ -1,5 +1,5 @@ "{{endpoint}}": - image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.5 + image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.7 container_name: "{{endpoint}}" ports: - "{{port_key}}:80" diff --git a/studio-backend/app/templates/microsvc-manifests/data-prep.yaml b/studio-backend/app/templates/microsvc-manifests/data-prep.yaml index 430adb6..c377c3d 100644 --- a/studio-backend/app/templates/microsvc-manifests/data-prep.yaml +++ b/studio-backend/app/templates/microsvc-manifests/data-prep.yaml @@ -20,7 +20,7 @@ data: http_proxy: "" https_proxy: "" no_proxy: "" - LOGFLAG: "" + LOGFLAG: "True" --- # Source: data-prep/templates/service.yaml # Copyright (C) 2024 Intel Corporation diff --git a/studio-backend/app/templates/microsvc-manifests/embedding-usvc.yaml b/studio-backend/app/templates/microsvc-manifests/embedding-usvc.yaml index 5e75d23..997d13c 100644 --- a/studio-backend/app/templates/microsvc-manifests/embedding-usvc.yaml +++ b/studio-backend/app/templates/microsvc-manifests/embedding-usvc.yaml @@ -15,7 +15,7 @@ data: no_proxy: "" TRANSFORMERS_CACHE: "/tmp/transformers_cache" HF_HOME: "/tmp/.cache/huggingface" - LOGFLAG: "" + LOGFLAG: "True" --- # Source: embedding-usvc/templates/service.yaml # Copyright (C) 2024 Intel Corporation diff --git a/studio-backend/app/templates/microsvc-manifests/llm-uservice.yaml b/studio-backend/app/templates/microsvc-manifests/llm-uservice.yaml index 89556b0..238d104 100644 --- a/studio-backend/app/templates/microsvc-manifests/llm-uservice.yaml +++ b/studio-backend/app/templates/microsvc-manifests/llm-uservice.yaml @@ -15,7 +15,8 @@ data: http_proxy: "${HTTP_PROXY}" https_proxy: "${HTTP_PROXY}" no_proxy: "${NO_PROXY}" - LOGFLAG: "" + LOGFLAG: "True" + LLM_MODEL_ID: "{modelName}" --- # Source: llm-uservice/templates/service.yaml # Copyright (C) 2024 Intel Corporation diff --git a/studio-backend/app/templates/microsvc-manifests/reranking-usvc.yaml b/studio-backend/app/templates/microsvc-manifests/reranking-usvc.yaml index 0f37640..07cec32 100644 --- a/studio-backend/app/templates/microsvc-manifests/reranking-usvc.yaml +++ b/studio-backend/app/templates/microsvc-manifests/reranking-usvc.yaml @@ -13,7 +13,7 @@ data: http_proxy: "" https_proxy: "" no_proxy: "" - LOGFLAG: "" + LOGFLAG: "True" --- # Source: reranking-usvc/templates/service.yaml # Copyright (C) 2024 Intel Corporation diff --git a/studio-backend/app/templates/microsvc-manifests/retriever-usvc.yaml b/studio-backend/app/templates/microsvc-manifests/retriever-usvc.yaml index 575a3bd..0d5275a 100644 --- a/studio-backend/app/templates/microsvc-manifests/retriever-usvc.yaml +++ b/studio-backend/app/templates/microsvc-manifests/retriever-usvc.yaml @@ -19,7 +19,7 @@ data: no_proxy: "" HF_HOME: "/tmp/.cache/huggingface" HUGGINGFACEHUB_API_TOKEN: "{huggingFaceToken}" - LOGFLAG: "" + LOGFLAG: "True" RETRIEVER_COMPONENT_NAME: "OPEA_RETRIEVER_REDIS" --- # Source: retriever-usvc/templates/service.yaml diff --git a/studio-backend/app/templates/microsvc-manifests/tei.yaml b/studio-backend/app/templates/microsvc-manifests/tei.yaml index 94ce8ca..5badf72 100644 --- a/studio-backend/app/templates/microsvc-manifests/tei.yaml +++ b/studio-backend/app/templates/microsvc-manifests/tei.yaml @@ -67,7 +67,7 @@ spec: name: config-{endpoint} securityContext: {} - image: "ghcr.io/huggingface/text-embeddings-inference:cpu-1.5" + image: "ghcr.io/huggingface/text-embeddings-inference:cpu-1.7" imagePullPolicy: IfNotPresent args: - "--auto-truncate" diff --git a/studio-backend/app/templates/readmes/compose-readme.MD b/studio-backend/app/templates/readmes/compose-readme.MD index c07cb02..8ce6753 100644 --- a/studio-backend/app/templates/readmes/compose-readme.MD +++ b/studio-backend/app/templates/readmes/compose-readme.MD @@ -29,5 +29,5 @@ Follow these steps to docker compose the application: 4. Open application in browser with ```bash - http://:8080 + http://:8090 ``` \ No newline at end of file diff --git a/studio-backend/app/utils/exporter_utils.py b/studio-backend/app/utils/exporter_utils.py index b7df67d..256ef70 100644 --- a/studio-backend/app/utils/exporter_utils.py +++ b/studio-backend/app/utils/exporter_utils.py @@ -246,7 +246,7 @@ def process_opea_services(proj_info_json): updated_nodes[node_name][f"{prefix}_port"] = updated_nodes[connected_agent]['port'] updated_nodes[node_name].pop('connected_agent', None) if 'rag_agent' in node_name: - updated_nodes[node_name]['megasvc_endpoint_port'] = "8888/v1/app-backend" + updated_nodes[node_name]['megasvc_endpoint_port'] = "8899/v1/app-backend" updated_nodes[node_name]['rag_name'] = node_name.replace('opea_service@', '') if "llmEngine" in node_info and node_info["llmEngine"] == "openai": updated_nodes[node_name]['llm_endpoint'] = "NA" diff --git a/studio-backend/tests/exporter-groundtruth/gt_app-compose.yaml b/studio-backend/tests/exporter-groundtruth/gt_app-compose.yaml index 51c8bd8..9437ad6 100644 --- a/studio-backend/tests/exporter-groundtruth/gt_app-compose.yaml +++ b/studio-backend/tests/exporter-groundtruth/gt_app-compose.yaml @@ -9,7 +9,7 @@ services: - 6379:6379 - 8001:8001 tei-0: - image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.5 + image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.7 container_name: tei-0 ports: - 2081:80 @@ -38,7 +38,7 @@ services: HF_HUB_ENABLE_HF_TRANSFER: 0 command: --model-id Intel/neural-chat-7b-v3-3 --cuda-graphs 0 tei-1: - image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.5 + image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.7 container_name: tei-1 ports: - 2082:80 @@ -51,7 +51,7 @@ services: https_proxy: ${https_proxy} command: --model-id BAAI/bge-reranker-base --auto-truncate tei-2: - image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.5 + image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.7 container_name: tei-2 ports: - 2083:80 @@ -163,7 +163,7 @@ services: - reranking-tei-0 - retriever-redis-0 ports: - - 8888:8888 + - 8899:8899 environment: - no_proxy=${no_proxy} - https_proxy=${https_proxy} @@ -177,7 +177,7 @@ services: depends_on: - app-backend ports: - - 5175:80 + - 5275:80 environment: - no_proxy=${no_proxy} - https_proxy=${https_proxy} @@ -197,16 +197,16 @@ services: - app-frontend - app-backend ports: - - 8080:80 + - 8090:80 environment: - no_proxy=${no_proxy} - https_proxy=${https_proxy} - http_proxy=${http_proxy} - FRONTEND_SERVICE_IP=${public_host_ip} - - FRONTEND_SERVICE_PORT=5175 + - FRONTEND_SERVICE_PORT=5275 - BACKEND_SERVICE_NAME=app-backend - BACKEND_SERVICE_IP=${public_host_ip} - - BACKEND_SERVICE_PORT=8888 + - BACKEND_SERVICE_PORT=8899 - DATAPREP_SERVICE_IP=${public_host_ip} - DATAPREP_SERVICE_PORT=6007 ipc: host diff --git a/studio-backend/tests/exporter-groundtruth/gt_app-manifest-with-nginx.yaml b/studio-backend/tests/exporter-groundtruth/gt_app-manifest-with-nginx.yaml index 828007d..b6a1550 100644 --- a/studio-backend/tests/exporter-groundtruth/gt_app-manifest-with-nginx.yaml +++ b/studio-backend/tests/exporter-groundtruth/gt_app-manifest-with-nginx.yaml @@ -139,7 +139,7 @@ spec: - configMapRef: name: config-tei-0 securityContext: {} - image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.5 + image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.7 imagePullPolicy: IfNotPresent args: - --auto-truncate @@ -352,7 +352,7 @@ spec: - configMapRef: name: config-tei-1 securityContext: {} - image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.5 + image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.7 imagePullPolicy: IfNotPresent args: - --auto-truncate @@ -460,7 +460,7 @@ spec: - configMapRef: name: config-tei-2 securityContext: {} - image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.5 + image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.7 imagePullPolicy: IfNotPresent args: - --auto-truncate @@ -755,7 +755,7 @@ data: tools: /home/user/tools/worker_agent_tools.yaml require_human_feedback: 'false' llm_endpoint_url: http://tgi-0:9004 - RETRIEVAL_TOOL_URL: http://app-backend:8888/v1/app-backend/rag_agent_0 + RETRIEVAL_TOOL_URL: http://app-backend:8899/v1/app-backend/rag_agent_0 PORT: '9095' OPENAI_API_KEY: NA --- @@ -1576,8 +1576,8 @@ metadata: spec: type: ClusterIP ports: - - port: 8888 - targetPort: 8888 + - port: 8899 + targetPort: 8899 protocol: TCP name: app-backend selector: @@ -1630,7 +1630,7 @@ spec: subPath: .env ports: - name: app-backend - containerPort: 8888 + containerPort: 8899 protocol: TCP resources: null volumes: @@ -1652,7 +1652,7 @@ metadata: spec: type: ClusterIP ports: - - port: 5175 + - port: 5275 targetPort: 80 protocol: TCP name: app-frontend @@ -1733,7 +1733,7 @@ data: } location / { - proxy_pass http://app-frontend:5175; + proxy_pass http://app-frontend:5275; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -1741,7 +1741,7 @@ data: } location /v1/app-backend { - proxy_pass http://app-backend:8888; + proxy_pass http://app-backend:8899; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/studio-backend/tests/exporter-groundtruth/gt_app-manifest.yaml b/studio-backend/tests/exporter-groundtruth/gt_app-manifest.yaml index 24edd31..3a0db4b 100644 --- a/studio-backend/tests/exporter-groundtruth/gt_app-manifest.yaml +++ b/studio-backend/tests/exporter-groundtruth/gt_app-manifest.yaml @@ -139,7 +139,7 @@ spec: - configMapRef: name: config-tei-0 securityContext: {} - image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.5 + image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.7 imagePullPolicy: IfNotPresent args: - --auto-truncate @@ -352,7 +352,7 @@ spec: - configMapRef: name: config-tei-1 securityContext: {} - image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.5 + image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.7 imagePullPolicy: IfNotPresent args: - --auto-truncate @@ -460,7 +460,7 @@ spec: - configMapRef: name: config-tei-2 securityContext: {} - image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.5 + image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.7 imagePullPolicy: IfNotPresent args: - --auto-truncate @@ -755,7 +755,7 @@ data: tools: /home/user/tools/worker_agent_tools.yaml require_human_feedback: 'false' llm_endpoint_url: http://tgi-0:9004 - RETRIEVAL_TOOL_URL: http://app-backend:8888/v1/app-backend/rag_agent_0 + RETRIEVAL_TOOL_URL: http://app-backend:8899/v1/app-backend/rag_agent_0 PORT: '9095' OPENAI_API_KEY: NA --- @@ -1576,8 +1576,8 @@ metadata: spec: type: ClusterIP ports: - - port: 8888 - targetPort: 8888 + - port: 8899 + targetPort: 8899 protocol: TCP name: app-backend selector: @@ -1630,7 +1630,7 @@ spec: subPath: .env ports: - name: app-backend - containerPort: 8888 + containerPort: 8899 protocol: TCP resources: null volumes: @@ -1652,7 +1652,7 @@ metadata: spec: type: ClusterIP ports: - - port: 5175 + - port: 5275 targetPort: 80 protocol: TCP name: app-frontend diff --git a/studio-backend/tests/test_click-deploy.py b/studio-backend/tests/test_click-deploy.py new file mode 100644 index 0000000..bdb273f --- /dev/null +++ b/studio-backend/tests/test_click-deploy.py @@ -0,0 +1,44 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import os +import yaml +from pydantic import ValidationError +import pytest +import json + +from app.models.pipeline_model import PipelineFlow +from app.services.clickdeploy_service import deploy_pipeline + +@pytest.fixture +def setup_and_teardown(): + # Prepare the JSON payload with the YAML content + test_dir = os.path.dirname(os.path.abspath(__file__)) + flowise_pipeline_file = os.path.join(test_dir, "flowise-pipeline-inputs", "agentqna-tgi.json") + + with open(flowise_pipeline_file, "r") as file: + payload = json.load(file) + + remote_host = "xxx" + remote_user = "xxx" + + yield remote_host, remote_user, payload + + # No specific setup teardown needed for this test +def test_click_deploy(setup_and_teardown): + + remote_host, remote_user, payload = setup_and_teardown + + try: + PipelineFlow(**payload) + except ValidationError as e: + print("Validation Error: ", e.json(indent=4)) + assert False, "Payload validation failed" + + try: + response = deploy_pipeline(remote_host, remote_user, payload) + print("Response from deploy_pipeline:\n" + json.dumps(response, indent=2, ensure_ascii=False)) + except Exception as e: + assert False, f"deploy_pipeline failed with exception: {e}" + + assert True, "deploy_pipeline executed successfully" \ No newline at end of file diff --git a/studio-backend/tests/test_clickhouse_query_traces.py b/studio-backend/tests/test_clickhouse_query_traces.py deleted file mode 100644 index edbf1e4..0000000 --- a/studio-backend/tests/test_clickhouse_query_traces.py +++ /dev/null @@ -1,19 +0,0 @@ -from fastapi.testclient import TestClient -import pytest -from unittest.mock import patch -from app.main import app - -@pytest.fixture -def setup_and_teardown(): - # Setup: Mock ClickHouse client - pass - - # Teardown: No specific teardown needed for this test - -def test_get_span_attributes_success(setup_and_teardown): - test_client = TestClient(app) - trace_id = "191c1fb1992400f8a28a4ed2cf4ea5ee" - - response = test_client.get(f"/span-attributes/{trace_id}") - - assert response.status_code == 200 \ No newline at end of file diff --git a/studio-backend/tests/test_debuglog.py b/studio-backend/tests/test_debuglog.py new file mode 100644 index 0000000..f6b6df2 --- /dev/null +++ b/studio-backend/tests/test_debuglog.py @@ -0,0 +1,34 @@ +import json + +import pytest +from app.main import app +from fastapi.testclient import TestClient +from kubernetes import client as k8s_client +from kubernetes import config +from kubernetes.client.rest import ApiException +from pydantic import ValidationError + + +@pytest.fixture +def setup_and_teardown(): + pass + + # No specific setup teardown needed for this test + +def test_get_pod_logs(setup_and_teardown): + test_client = TestClient(app) + namespace = "xxx" + pod_name = "xxx" + + response = test_client.get(f"/studio-backend/podlogs/{namespace}") + # response = test_client.get(f"/studio-backend/podlogs/{namespace}/{pod_name}") + + # Print the response status code and body + print("Status Code:", response.status_code) + try: + print("Response JSON:", json.dumps(response.json(), indent=4)) + except Exception: + print("Response Text:", response.text) + + print(response) + assert response.status_code == 200 \ No newline at end of file diff --git a/studio-frontend/packages/server/src/controllers/chatflows/index.ts b/studio-frontend/packages/server/src/controllers/chatflows/index.ts index 3a015b9..d8166b2 100644 --- a/studio-frontend/packages/server/src/controllers/chatflows/index.ts +++ b/studio-frontend/packages/server/src/controllers/chatflows/index.ts @@ -203,7 +203,8 @@ const deployChatflowSandbox = async (req: Request, res: Response, next: NextFunc sandboxStatus: deployResponse.status, sandboxAppUrl: deployResponse.sandbox_app_url, sandboxGrafanaUrl: deployResponse.sandbox_grafana_url, - sandboxTracerUrl: deployResponse.sandbox_tracer_url + sandboxTracerUrl: deployResponse.sandbox_tracer_url, + sandboxDebugLogsUrl: deployResponse.sandbox_debuglogs_url } const updateChatflowObj = new ChatFlow() Object.assign(updateChatflowObj, newData) @@ -237,6 +238,7 @@ const stopChatflowSandbox = async (req: Request, res: Response, next: NextFuncti sandboxAppUrl: '', sandboxGrafanaUrl: '', sandboxTracerUrl: '', + sandboxDebugLogsUrl: '', } const updateChatflowObj = new ChatFlow() Object.assign(updateChatflowObj, newData) @@ -273,6 +275,29 @@ const buildDeploymentPackage = async (req: Request, res: Response, next: NextFun } } +const getPublicKey = async (req: Request, res: Response, next: NextFunction) => { + const fs = await import('fs/promises'); + try { + const publicKey = await fs.readFile('/root/.ssh/id_rsa.pub', 'utf-8'); + res.json({ pubkey: publicKey }) + } catch (error) { + next(error) + } +} + +const oneClickDeployment = async (req: Request, res: Response, next: NextFunction) => { + console.log('Deploying one click') + try { + if (typeof req.params === 'undefined' || !req.params.id) { + throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Error: chatflowsRouter.oneClickDeployment - id not provided!`) + } + const deployResponse = await chatflowsService.oneClickDeploymentService(req.params.id, req.body) + res.status(200).json(deployResponse) + } catch (error) { + next(error) + } +} + export default { checkIfChatflowIsValidForStreaming, checkIfChatflowIsValidForUploads, @@ -289,5 +314,7 @@ export default { getSinglePublicChatbotConfig, deployChatflowSandbox, stopChatflowSandbox, - buildDeploymentPackage + buildDeploymentPackage, + getPublicKey, + oneClickDeployment } diff --git a/studio-frontend/packages/server/src/database/entities/ChatFlow.ts b/studio-frontend/packages/server/src/database/entities/ChatFlow.ts index 82c8c62..707a73f 100644 --- a/studio-frontend/packages/server/src/database/entities/ChatFlow.ts +++ b/studio-frontend/packages/server/src/database/entities/ChatFlow.ts @@ -55,6 +55,9 @@ export class ChatFlow implements IChatFlow { @Column({nullable: true, type: 'text'}) sandboxTracerUrl?: string + @Column({nullable: true, type: 'text'}) + sandboxDebugLogsUrl?: string + @Column({ type: 'timestamp' }) @CreateDateColumn() createdDate: Date diff --git a/studio-frontend/packages/server/src/database/migrations/mysql/1749612373191-AddSandboxDebugLogsUrlToChatFlow.ts b/studio-frontend/packages/server/src/database/migrations/mysql/1749612373191-AddSandboxDebugLogsUrlToChatFlow.ts new file mode 100644 index 0000000..8fcac2e --- /dev/null +++ b/studio-frontend/packages/server/src/database/migrations/mysql/1749612373191-AddSandboxDebugLogsUrlToChatFlow.ts @@ -0,0 +1,12 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddSandboxDebugLogsUrlToChatFlow1749612373191 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const columnExists = await queryRunner.hasColumn('chat_flow', 'sandboxDebugLogsUrl') + if (!columnExists) queryRunner.query(`ALTER TABLE \`chat_flow\` ADD COLUMN \`sandboxDebugLogsUrl\` varchar(255) DEFAULT NULL;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`chat_flow\` DROP COLUMN \`sandboxDebugLogsUrl\`;`) + } +} diff --git a/studio-frontend/packages/server/src/database/migrations/mysql/index.ts b/studio-frontend/packages/server/src/database/migrations/mysql/index.ts index 598146c..611afb6 100644 --- a/studio-frontend/packages/server/src/database/migrations/mysql/index.ts +++ b/studio-frontend/packages/server/src/database/migrations/mysql/index.ts @@ -28,6 +28,7 @@ import { AddCustomTemplate1725629836652 } from './1725629836652-AddCustomTemplat import { AddArtifactsToChatMessage1726156258465 } from './1726156258465-AddArtifactsToChatMessage' import { AddStudioFieldsToChatFlow1733282099772 } from './1733282099772-AddStudioFieldsToChatFlow' import { AddSandboxTracerUrlToChatFlow1743740099772 } from './1743740099772-AddSandboxTracerUrlToChatFlow' +import { AddSandboxDebugLogsUrlToChatFlow1749612373191 } from './1749612373191-AddSandboxDebugLogsUrlToChatFlow' export const mysqlMigrations = [ @@ -60,5 +61,6 @@ export const mysqlMigrations = [ AddCustomTemplate1725629836652, AddArtifactsToChatMessage1726156258465, AddStudioFieldsToChatFlow1733282099772, - AddSandboxTracerUrlToChatFlow1743740099772 + AddSandboxTracerUrlToChatFlow1743740099772, + AddSandboxDebugLogsUrlToChatFlow1749612373191 ] diff --git a/studio-frontend/packages/server/src/database/migrations/sqlite/1749612373191-AddSandboxDebugLogsUrlToChatFlow.ts b/studio-frontend/packages/server/src/database/migrations/sqlite/1749612373191-AddSandboxDebugLogsUrlToChatFlow.ts new file mode 100644 index 0000000..8fcac2e --- /dev/null +++ b/studio-frontend/packages/server/src/database/migrations/sqlite/1749612373191-AddSandboxDebugLogsUrlToChatFlow.ts @@ -0,0 +1,12 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddSandboxDebugLogsUrlToChatFlow1749612373191 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const columnExists = await queryRunner.hasColumn('chat_flow', 'sandboxDebugLogsUrl') + if (!columnExists) queryRunner.query(`ALTER TABLE \`chat_flow\` ADD COLUMN \`sandboxDebugLogsUrl\` varchar(255) DEFAULT NULL;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`chat_flow\` DROP COLUMN \`sandboxDebugLogsUrl\`;`) + } +} diff --git a/studio-frontend/packages/server/src/database/migrations/sqlite/index.ts b/studio-frontend/packages/server/src/database/migrations/sqlite/index.ts index 16dea31..09149fd 100644 --- a/studio-frontend/packages/server/src/database/migrations/sqlite/index.ts +++ b/studio-frontend/packages/server/src/database/migrations/sqlite/index.ts @@ -27,6 +27,7 @@ import { AddArtifactsToChatMessage1726156258465 } from './1726156258465-AddArtif import { AddCustomTemplate1725629836652 } from './1725629836652-AddCustomTemplate' import { AddStudioFieldsToChatFlow1733282099772 } from './1733282099772-AddStudioFieldsToChatFlow' import { AddSandboxTracerUrlToChatFlow1743740099772 } from './1743740099772-AddSandboxTracerUrlToChatFlow' +import { AddSandboxDebugLogsUrlToChatFlow1749612373191 } from './1749612373191-AddSandboxDebugLogsUrlToChatFlow' export const sqliteMigrations = [ Init1693835579790, @@ -57,5 +58,6 @@ export const sqliteMigrations = [ AddArtifactsToChatMessage1726156258465, AddCustomTemplate1725629836652, AddStudioFieldsToChatFlow1733282099772, - AddSandboxTracerUrlToChatFlow1743740099772 + AddSandboxTracerUrlToChatFlow1743740099772, + AddSandboxDebugLogsUrlToChatFlow1749612373191 ] diff --git a/studio-frontend/packages/server/src/routes/chatflows-sandbox/index.ts b/studio-frontend/packages/server/src/routes/chatflows-sandbox/index.ts index 4c3d2cb..1b22281 100644 --- a/studio-frontend/packages/server/src/routes/chatflows-sandbox/index.ts +++ b/studio-frontend/packages/server/src/routes/chatflows-sandbox/index.ts @@ -10,5 +10,6 @@ router.post(['/stop/','/stop/:id'], chatflowsController.stopChatflowSandbox) router.post(['/build-deployment-package/','/build-deployment-package/:id'], chatflowsController.buildDeploymentPackage) +router.post('/one-click-deployment/:id', chatflowsController.oneClickDeployment); export default router diff --git a/studio-frontend/packages/server/src/routes/chatflows/index.ts b/studio-frontend/packages/server/src/routes/chatflows/index.ts index 495548b..c0bbe13 100644 --- a/studio-frontend/packages/server/src/routes/chatflows/index.ts +++ b/studio-frontend/packages/server/src/routes/chatflows/index.ts @@ -8,6 +8,7 @@ router.post('/importsamples', chatflowsController.importSampleChatflowsbyUserId) router.post('/importchatflows', chatflowsController.importChatflows) // READ +router.get('/pubkey', chatflowsController.getPublicKey) router.get('/', chatflowsController.getAllChatflowsbyUserId) router.get(['/', '/:id'], chatflowsController.getChatflowById) router.get(['/apikey/', '/apikey/:apikey'], chatflowsController.getChatflowByApiKey) diff --git a/studio-frontend/packages/server/src/services/chatflows/index.ts b/studio-frontend/packages/server/src/services/chatflows/index.ts index 48cefb2..55bbb83 100644 --- a/studio-frontend/packages/server/src/services/chatflows/index.ts +++ b/studio-frontend/packages/server/src/services/chatflows/index.ts @@ -499,6 +499,34 @@ const buildDeploymentPackageService = async (chatflowId: string, deploymentConfi } } +const oneClickDeploymentService = async (chatflowId: string, deploymentConfig: Record) => { + console.log('oneClickDeploymentService', chatflowId, deploymentConfig) + try { + const chatflow = await generatePipelineJson(chatflowId) + const studioServerUrl = STUDIO_SERVER_URL + const endpoint = 'studio-backend/click-deploy' + console.log('chatflow', JSON.stringify(chatflow)) + console.log('studioServerUrl', studioServerUrl) + console.log('deploymentConfig', deploymentConfig) + const response = await axios.post(`${studioServerUrl}/${endpoint}`, { + remoteHost: deploymentConfig.hostname, + remoteUser: deploymentConfig.username, + pipelineFlow: chatflow + }, { + headers: { 'Content-Type': 'application/json' }, + timeout: 60 * 1000 + }) + return response.data + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`Error: ${error.stack}`) + } else { + console.error(`An error occurred: ${error}`) + } + throw error + } +} + const _checkAndUpdateDocumentStoreUsage = async (chatflow: ChatFlow) => { const parsedFlowData: IReactFlowObject = JSON.parse(chatflow.flowData) const nodes = parsedFlowData.nodes @@ -527,5 +555,6 @@ export default { deployChatflowSandboxService, stopChatflowSandboxService, buildDeploymentPackageService, - getSinglePublicChatbotConfig + getSinglePublicChatbotConfig, + oneClickDeploymentService } diff --git a/studio-frontend/packages/ui/src/api/chatflows.js b/studio-frontend/packages/ui/src/api/chatflows.js index bacd1af..be642a4 100644 --- a/studio-frontend/packages/ui/src/api/chatflows.js +++ b/studio-frontend/packages/ui/src/api/chatflows.js @@ -33,6 +33,10 @@ const buildDeploymentPackage = (id, body) => client.post(`chatflows-sandbox/buil const importSampleChatflowsbyUserId = (userid) => client.post(`/chatflows/importsamples?userid=${userid}&type=OPEA`) +const getPublicKey = () => client.get('/chatflows/pubkey') + +const clickDeployment = (id, body) => client.post(`chatflows-sandbox/one-click-deployment/${id}`, body) + export default { getAllChatflows, getAllAgentflows, @@ -49,5 +53,7 @@ export default { getAllowChatflowUploads, deploySandbox, stopSandbox, - buildDeploymentPackage + buildDeploymentPackage, + getPublicKey, + clickDeployment } diff --git a/studio-frontend/packages/ui/src/routes/MainRoutes.jsx b/studio-frontend/packages/ui/src/routes/MainRoutes.jsx index 1f31705..cd77b07 100644 --- a/studio-frontend/packages/ui/src/routes/MainRoutes.jsx +++ b/studio-frontend/packages/ui/src/routes/MainRoutes.jsx @@ -10,6 +10,9 @@ const Opeaflows = Loadable(lazy(() => import('@/views/opeaflows'))) // tracer routing const Tracer = Loadable(lazy(() => import('@/views/tracer'))) +// debuglogs routing +const Debuglogs = Loadable(lazy(() => import('@/views/debuglogs'))) + // chatflows routing const Chatflows = Loadable(lazy(() => import('@/views/chatflows'))) @@ -60,6 +63,10 @@ const MainRoutes = { path:'/tracer/:ns', element: }, + { + path:'/debuglogs/:ns', + element: + }, { path: '/chatflows', element: diff --git a/studio-frontend/packages/ui/src/ui-component/dialog/OneClickDeploymentDialog.jsx b/studio-frontend/packages/ui/src/ui-component/dialog/OneClickDeploymentDialog.jsx new file mode 100644 index 0000000..92b60f8 --- /dev/null +++ b/studio-frontend/packages/ui/src/ui-component/dialog/OneClickDeploymentDialog.jsx @@ -0,0 +1,206 @@ +import { createPortal } from 'react-dom'; +import { useDispatch } from 'react-redux'; +import { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + OutlinedInput, + Typography, + IconButton, + Tooltip, + CircularProgress +} from '@mui/material'; + +import { StyledButton } from '@/ui-component/button/StyledButton'; +import { IconCopy, IconCheck } from '@tabler/icons-react'; +import { + closeSnackbar as closeSnackbarAction, + enqueueSnackbar as enqueueSnackbarAction, + HIDE_CANVAS_DIALOG, + SHOW_CANVAS_DIALOG +} from '@/store/actions'; +import chatflowsApi from '@/api/chatflows'; + +const OneClickDeploymentDialog = ({ show, dialogProps, onCancel, onConfirm, deployStatus, setDeployStatus, deploymentConfig, setDeploymentConfig }) => { + const portalElement = document.getElementById('portal'); + const dispatch = useDispatch(); + const [pubkey, setPubkey] = useState(''); + const [copied, setCopied] = useState(false); + const [deploying, setDeploying] = useState(false); + const [ws, setWs] = useState(null); + + useEffect(() => { + if (show) { + dispatch({ type: SHOW_CANVAS_DIALOG }); + chatflowsApi.getPublicKey().then(response => { + if (response.error) { + dispatch(enqueueSnackbarAction({ + message: 'Error loading public key', + options: { variant: 'error' } + })); + } else { + setPubkey(response.data.pubkey || ''); + } + }); + } else { + dispatch({ type: HIDE_CANVAS_DIALOG }); + } + return () => dispatch({ type: HIDE_CANVAS_DIALOG }); + }, [show, dispatch]); + + const handleCopy = () => { + navigator.clipboard.writeText(pubkey); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + }; + + const handleOneClickDeploy = async () => { + setDeploying(true); + setDeployStatus(['Info', 'Connecting to machine...']); + try { + const result = await onConfirm(dialogProps.id, deploymentConfig); + if (result && result.error) { + setDeployStatus(['Error', result.error]); + setDeploying(false); + return; + } + const compose_dir = result?.compose_dir; + const wsUrl = `${window.location.origin.replace(/^http/, 'ws')}/studio-backend/ws/clickdeploy-status`; + const wsInstance = new window.WebSocket(wsUrl); + setWs(wsInstance); + wsInstance.onopen = () => { + wsInstance.send(JSON.stringify({ hostname: deploymentConfig.hostname, username: deploymentConfig.username, compose_dir: compose_dir })); + }; + wsInstance.onmessage = (event) => { + let data; + try { data = JSON.parse(event.data); } catch { return; } + console.log('WebSocket message:', data); + if (data.status === 'Done') { + setDeployStatus(['Success', ...(data.success || '').split(',').map(line => line.trim())]); + setDeploying(false); + } else if (data.status === 'Error') { + let lines = []; + if (Array.isArray(data.error)) { + lines = data.error; + } else if (typeof data.error === 'string') { + lines = data.error.split(',').map(line => line.trim()); + } else { + lines = ['Unknown error']; + } + setDeployStatus(['Error', ...lines]); + setDeploying(false); + } else if (data.status === 'In Progress') { + setDeployStatus(['Info', data.nohup_out]); + } + }; + wsInstance.onerror = () => { + setDeployStatus(['Error', 'WebSocket error']); + setDeploying(false); + }; + wsInstance.onclose = () => setWs(null); + } catch (err) { + setDeployStatus(['Error', 'Deployment failed']); + setDeploying(false); + } + }; + + const renderStatus = () => { + if (!deployStatus) return null; + const [statusType, ...lines] = deployStatus; + let color = statusType === 'Error' ? 'red' : statusType === 'Success' ? 'green' : 'primary.main'; + let displayLines = lines; + let effectiveStatusType = statusType; + if (statusType === 'Info') { + let flatLines = Array.isArray(lines[0]) ? lines[0] : lines; + // Check for error/fail in any line + if (flatLines.some(line => typeof line === 'string' && (/error|fail/i).test(line))) { + color = 'red'; + effectiveStatusType = 'Error'; + } + displayLines = flatLines; + } + return ( + + + {effectiveStatusType} + + {deploying && } + + {displayLines.map((line, idx) =>
{line}
)} +
+
+
+
+ ); + }; + + const component = show ? ( + + + {dialogProps.title || 'One Click Deployment'} + + + + + Public Key * + + + {copied ? : } + + + + {pubkey} + + + Hostname * + setDeploymentConfig({ ...deploymentConfig, hostname: e.target.value })} placeholder='Enter hostname' /> + + + Username * + setDeploymentConfig({ ...deploymentConfig, username: e.target.value })} placeholder='Enter username' /> + + {renderStatus()} + + + + + {deploying ? 'Deploying...' : (dialogProps.confirmButtonName || 'Deploy')} + + + + ) : null; + + return createPortal(component, portalElement); +}; + +OneClickDeploymentDialog.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onCancel: PropTypes.func, + onConfirm: PropTypes.func, + deployStatus: PropTypes.array, + setDeployStatus: PropTypes.func, + deploymentConfig: PropTypes.object, + setDeploymentConfig: PropTypes.func +}; + +export default OneClickDeploymentDialog; diff --git a/studio-frontend/packages/ui/src/ui-component/table/FlowListTable.jsx b/studio-frontend/packages/ui/src/ui-component/table/FlowListTable.jsx index a2eaaac..7b3dcc7 100644 --- a/studio-frontend/packages/ui/src/ui-component/table/FlowListTable.jsx +++ b/studio-frontend/packages/ui/src/ui-component/table/FlowListTable.jsx @@ -20,7 +20,9 @@ import { TableSortLabel, Tooltip, Typography, - useTheme + useTheme, + Menu, + MenuItem } from '@mui/material' import { tableCellClasses } from '@mui/material/TableCell' import FlowListMenu from '../button/FlowListMenu' @@ -31,13 +33,16 @@ import { Analytics, PlayCircleOutline, UnarchiveOutlined, - ViewTimelineOutlined + ViewTimelineOutlined, + InstallDesktopOutlined, + TroubleshootOutlined, + TerminalOutlined } from '@mui/icons-material' import BuildDeploymentPackageDialog from '../dialog/BuildDeploymentPackageDialog' +import OneClickDeploymentDialog from '../dialog/OneClickDeploymentDialog' import chatflowsApi from '@/api/chatflows' import config from '@/config' -import { update } from 'lodash' const StyledTableCell = styled(TableCell)(({ theme }) => ({ borderColor: theme.palette.grey[900] + 25, @@ -110,28 +115,28 @@ export const FlowListTable = ({ data, images, isLoading, filterFunction, updateF useEffect(() => { console.log("triggering websocket") - const studio_server_url = config.studio_server_url; - const statusCheckEndpoint = config.sandbox_status_endpoint; const openConnections = []; - const openWebSocketConnection = (id, status) => { - const ws = new WebSocket(`${studio_server_url}/${statusCheckEndpoint}`); + const openWebSocketConnection = (id, status, type = 'sandbox') => { + let wsEndpoint = config.sandbox_status_endpoint; + const ws = new WebSocket(`${config.studio_server_url}/${wsEndpoint}`); ws.onopen = () => { - const payload = JSON.stringify({ id: id, status: status }); + let payload; + payload = JSON.stringify({ id: id, status: status }); ws.send(payload); - console.log('Connected to WebSocket server', id); + console.log('Connected to WebSocket server', id, type); }; ws.onmessage = (event) => { const data = JSON.parse(event.data); - console.log('Deployment status:', data.status, id); - if (data.status === 'Ready' || data.status === 'Error' || data.status === 'Not Running') { + console.log('Deployment status:', data.status, id, type); + if (data.status === 'Done' || data.status === 'Error' || data.status === 'Not Running' || data.status === 'Ready') { ws.close(); openConnections.splice(openConnections.indexOf(ws), 1); - updateSandboxStatus(id, data.status, data.sandbox_app_url, data.sandbox_grafana_url, data.sandbox_tracer_url); - updateFlowToServerApi(id, { sandboxStatus: data.status, sandboxAppUrl: data.sandbox_app_url, sandboxGrafanaUrl: data.sandbox_grafana_url, sandboxTracerUrl: data.sandbox_tracer_url }); + updateSandboxStatus(id, data.status, data.sandbox_app_url, data.sandbox_grafana_url, data.sandbox_tracer_url, data.sandbox_debuglogs_url); + updateFlowToServerApi(id, { sandboxStatus: data.status, sandboxAppUrl: data.sandbox_app_url, sandboxGrafanaUrl: data.sandbox_grafana_url, sandboxTracerUrl: data.sandbox_tracer_url, sandboxDebugLogsUrl: data.sandbox_debuglogs_url }); } }; ws.onclose = () => { - console.log('Disconnected from WebSocket server', id); + console.log('Disconnected from WebSocket server', id, type); }; return ws; }; @@ -148,10 +153,19 @@ export const FlowListTable = ({ data, images, isLoading, filterFunction, updateF }; }, [sortedData]); - const updateSandboxStatus = (id, newStatus, sandboxAppUrl = null, sandboxGrafanaUrl = null, sandboxTracerUrl = null) => { + const updateSandboxStatus = (id, newStatus, sandboxAppUrl = null, sandboxGrafanaUrl = null, sandboxTracerUrl = null, sandboxDebugLogsUrl = null) => { setSortedData((prevData) => prevData.map((row) => - row.id === id ? { ...row, sandboxStatus: newStatus, sandboxAppUrl: sandboxAppUrl || row.sandboxAppUrl, sandboxGrafanaUrl: sandboxGrafanaUrl || row.sandboxGrafanaUrl, sandboxTracerUrl: sandboxTracerUrl || row.sandboxTracerUrl } : row + row.id === id + ? { + ...row, + sandboxStatus: newStatus, + sandboxAppUrl: sandboxAppUrl || row.sandboxAppUrl, + sandboxGrafanaUrl: sandboxGrafanaUrl || row.sandboxGrafanaUrl, + sandboxTracerUrl: sandboxTracerUrl || row.sandboxTracerUrl, + sandboxDebugLogsUrl: sandboxDebugLogsUrl || row.sandboxDebugLogsUrl + } + : row ) ); }; @@ -159,7 +173,14 @@ export const FlowListTable = ({ data, images, isLoading, filterFunction, updateF const handleRunSandbox = async (id) => { updateSandboxStatus(id, 'Sending Request'); const res = await chatflowsApi.deploySandbox(id) - updateSandboxStatus(id, res.data?.sandboxStatus || 'Error', res.data?.sandboxAppUrl, res.data?.sandboxGrafanaUrl, res.data?.sandboxTracerUrl) + updateSandboxStatus( + id, + res.data?.sandboxStatus || 'Error', + res.data?.sandboxAppUrl, + res.data?.sandboxGrafanaUrl, + res.data?.sandboxTracerUrl, + res.data?.sandboxDebugLogsUrl + ); } const handleStopSandbox = async (id) => { @@ -176,7 +197,6 @@ export const FlowListTable = ({ data, images, isLoading, filterFunction, updateF } } - const [buildDeploymentPackageDialogOpen, setBuildDeploymentPackageDialogOpen] = useState(false) const [buildDeploymentPackageDialogProps, setBuildDeploymentPackageDialogProps] = useState({}) @@ -209,7 +229,45 @@ export const FlowListTable = ({ data, images, isLoading, filterFunction, updateF setBuildDeploymentPackageDialogOpen(true) } + const [oneClickDeploymentDialogOpen, setOneClickDeploymentDialogOpen] = useState(false) + const [oneClickDeploymentDialogProps, setOneClickDeploymentDialogProps] = useState({}) + + const oneClickDeployment = async (id, deploymentConfig) => { + try { + // Only call the backend API and return the response (including compose_dir) + const response = await chatflowsApi.clickDeployment(id, deploymentConfig) + return response.data; // Pass compose_dir and other info to the dialog + } catch (error) { + // Optionally show error + return { error: error?.message || 'Deployment failed' }; + } + } + + const handleOneClickDeployment = (id) => { + // Reset dialog state if switching to a different row + if (oneClickDeploymentDialogProps.id !== id) { + setOneClickDeploymentDialogProps({}); + setOneClickDeploymentDialogOpen(false); + setTimeout(() => { + setOneClickDeploymentDialogProps({ id }); + setOneClickDeploymentDialogOpen(true); + }, 0); + } else { + setOneClickDeploymentDialogProps({ id }); + setOneClickDeploymentDialogOpen(true); + } + }; + + const [deployStatusById, setDeployStatusById] = useState({}); + const [deployConfigById, setDeployConfigById] = useState({}); + const setDeployStatusForId = (id, status) => { + setDeployStatusById((prev) => ({ ...prev, [id]: status })); + }; + + const setDeployConfigForId = (id, config) => { + setDeployConfigById((prev) => ({ ...prev, [id]: config })); + }; useEffect(() => { setSortedData(handleSortData()); @@ -238,6 +296,11 @@ export const FlowListTable = ({ data, images, isLoading, filterFunction, updateF console.log('Opening URL', url); window.open(url, '_blank'); } + + // Add state for observability menu + const [observabilityAnchorEl, setObservabilityAnchorEl] = useState(null); + const [observabilityRow, setObservabilityRow] = useState(null); + return ( <> @@ -254,7 +317,7 @@ export const FlowListTable = ({ data, images, isLoading, filterFunction, updateF Workflow Name - + - + - + - Launch Monitoring Dashboard + Observability - + {/* - Launch Tracer + Deployment Package Generation - - + */} + - Deployment Package Generation + 1 Click Deployment - + - + } onClick={() => { + window.open(`/debuglogs/sandbox-${row.id}`, '_blank'); handleRunSandbox(row.id); }} disabled={row.sandboxStatus === 'Stopping'} @@ -493,6 +557,7 @@ export const FlowListTable = ({ data, images, isLoading, filterFunction, updateF + {/* Consolidated Observability column */} - + + setObservabilityAnchorEl(null)} + > + { + handleOpenUrl(row.sandboxGrafanaUrl); + setObservabilityAnchorEl(null); + }} + disabled={row.sandboxStatus !== 'Ready'} + > + Monitoring Dashboard + + { + handleOpenUrl(row.sandboxTracerUrl); + setObservabilityAnchorEl(null); + }} + disabled={row.sandboxStatus !== 'Ready'} + > + LLM Call Traces + + { + handleOpenUrl(row.sandboxDebugLogsUrl); + setObservabilityAnchorEl(null); + }} + disabled={row.sandboxStatus !== 'Ready'} + > + Debug Logs + + - + {/* - + - - + */} + - + @@ -590,6 +690,17 @@ export const FlowListTable = ({ data, images, isLoading, filterFunction, updateF onCancel={() => setBuildDeploymentPackageDialogOpen(false)} onConfirm={downloadDeploymentPackage} /> + setOneClickDeploymentDialogOpen(false)} + onConfirm={oneClickDeployment} + deployStatus={deployStatusById[oneClickDeploymentDialogProps.id]} + setDeployStatus={(status) => setDeployStatusForId(oneClickDeploymentDialogProps.id, status)} + deploymentConfig={deployConfigById[oneClickDeploymentDialogProps.id] || { hostname: '', username: '' }} + setDeploymentConfig={(config) => setDeployConfigForId(oneClickDeploymentDialogProps.id, config)} + /> ) } diff --git a/studio-frontend/packages/ui/src/views/debuglogs/index.jsx b/studio-frontend/packages/ui/src/views/debuglogs/index.jsx new file mode 100644 index 0000000..8ac3172 --- /dev/null +++ b/studio-frontend/packages/ui/src/views/debuglogs/index.jsx @@ -0,0 +1,225 @@ +import { useState, useEffect, useRef } from "react"; +import { useParams } from "react-router-dom"; +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Button, + Box, + Typography, + Divider, + Fade, + Modal, + Backdrop +} from "@mui/material"; +import { styled } from "@mui/material/styles"; +import { tableCellClasses } from "@mui/material/TableCell"; +import chatflowsApi from '@/api/chatflows'; +import useApi from '@/hooks/useApi'; +import ViewHeader from '@/layout/MainLayout/ViewHeader' + +const StyledTableCell = styled(TableCell)(({ theme }) => ({ + borderColor: theme.palette.grey[900] + 25, + [`&.${tableCellClasses.head}`]: { + color: theme.palette.grey[900] + }, + [`&.${tableCellClasses.body}`]: { + fontSize: 14, + height: 64 + } +})); + +const StyledTableRow = styled(TableRow)(() => ({ + '&:last-child td, &:last-child th': { + border: 0 + } +})); + +export default function PodLogsView() { + const [autoRefresh, setAutoRefresh] = useState(true); + const [podsData, setPodsData] = useState({ namespace: '', pods: [], workflowName: '' }); + const [selectedPodLogs, setSelectedPodLogs] = useState(null); + const [selectedPodEvents, setSelectedPodEvents] = useState(null); + const [workflowName, setWorkflowName] = useState(''); + + const logsRef = useRef(null); + const eventsRef = useRef(null); + + const { ns } = useParams(); + console.log("ns: ", ns); + + const debuglog_endpoint = '/studio-backend/podlogs'; + const getAllOpeaflowsApi = useApi(chatflowsApi.getAllOpeaflows); + + const fetchPodsData = async (ns) => { + console.log("fetchPodsData", ns); + const url = `${debuglog_endpoint}/${ns}`; + try { + const response = await fetch(url, { headers: { "Content-Type": "application/json" } }); + const data = await response.json(); + console.log("Pods data:", data); + setPodsData(data); + } catch (error) { + console.error("Failed to fetch pods data:", error); + } + }; + + useEffect(() => { + if (ns) { + fetchPodsData(ns); + } + }, [ns]); + + useEffect(() => { + if (!autoRefresh || !ns) return; + const interval = setInterval(() => { + fetchPodsData(ns); + }, 5000); + return () => clearInterval(interval); + }, [autoRefresh, ns]); + + useEffect(() => { + if (selectedPodLogs) { + setTimeout(() => { + logsRef.current?.scrollTo(0, logsRef.current.scrollHeight); + }, 0); + } + }, [selectedPodLogs]); + + useEffect(() => { + if (selectedPodEvents) { + setTimeout(() => { + eventsRef.current?.scrollTo(0, eventsRef.current.scrollHeight); + }, 0); + } + }, [selectedPodEvents]); + + useEffect(() => { + getAllOpeaflowsApi.request(); + }, []); + + useEffect(() => { + if (getAllOpeaflowsApi.data && ns) { + // Namespace is usually sandbox- + const flows = getAllOpeaflowsApi.data; + const found = flows.find(flow => `sandbox-${flow.id}` === ns); + setWorkflowName(found ? found.name : ''); + } + }, [getAllOpeaflowsApi.data, ns]); + + const toggleAutoRefresh = () => { + setAutoRefresh(!autoRefresh); + }; + + const handleExpandLogs = (podName) => { + setSelectedPodLogs(podName); + }; + + const handleExpandEvents = (podName) => { + setSelectedPodEvents(podName); + }; + + const selectedLogPod = podsData.pods.find(p => p.name === selectedPodLogs); + const selectedEventPod = podsData.pods.find(p => p.name === selectedPodEvents); + + return ( + // + + + {workflowName && ( + + Workflow name: {workflowName} + + )} + {/* Namespace: {podsData.namespace} */} + + + Auto refresh: + + + + + + + + Pod Name + Pod Ready + Pod Status + Pod Events + Pod Logs + + + + {podsData.pods.map((pod) => ( + + {pod.name} + {pod.ready} + {pod.status} + + {pod.events.length > 0 ? ( + + ) : ( + No events + )} + + + {pod.logs && pod.logs.length > 0 && ( + + )} + + + ))} + +
+
+ + setSelectedPodLogs(null)} + closeAfterTransition + BackdropComponent={Backdrop} + BackdropProps={{ timeout: 500 }} + > + + + {selectedLogPod?.name} Logs + +
+                            {selectedLogPod?.logs?.join('\n') || 'No logs available'}
+                        
+ + + +
+
+
+ + setSelectedPodEvents(null)} + closeAfterTransition + BackdropComponent={Backdrop} + BackdropProps={{ timeout: 500 }} + > + + + {selectedEventPod?.name} Events + +
+                            {selectedEventPod?.events?.join('\n') || 'No events available'}
+                        
+ + + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/studio-frontend/packages/ui/src/views/tracer/index.jsx b/studio-frontend/packages/ui/src/views/tracer/index.jsx index 3e8734b..f6034f0 100644 --- a/studio-frontend/packages/ui/src/views/tracer/index.jsx +++ b/studio-frontend/packages/ui/src/views/tracer/index.jsx @@ -17,7 +17,9 @@ import { Fade } from "@mui/material"; import { tableCellClasses } from '@mui/material/TableCell' - +import ViewHeader from '@/layout/MainLayout/ViewHeader' +import chatflowsApi from '@/api/chatflows'; +import useApi from '@/hooks/useApi'; import config from '@/config' @@ -67,6 +69,7 @@ export default function LLMTraces() { const [selectedSpan, setSelectedSpan] = useState(null); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); + const [workflowName, setWorkflowName] = useState(''); const { ns } = useParams(); console.log("ns: ", ns); @@ -75,6 +78,8 @@ export default function LLMTraces() { const sandbox_tracer_list_endpoint = config.sandbox_tracer_list_endpoint; const sandbox_tracer_tree_endpoint = config.sandbox_tracer_tree_endpoint; + const getAllOpeaflowsApi = useApi(chatflowsApi.getAllOpeaflows); + // Fetch trace ids from the server const fetchTraces = async (ns) => { console.log("fetchTraces", ns); @@ -150,13 +155,33 @@ export default function LLMTraces() { } , [selectedTrace, studio_server_url, sandbox_tracer_tree_endpoint]); + useEffect(() => { + getAllOpeaflowsApi.request(); + }, []); + + useEffect(() => { + if (getAllOpeaflowsApi.data && ns) { + // Namespace is usually sandbox- + const flows = getAllOpeaflowsApi.data; + const found = flows.find(flow => `sandbox-${flow.id}` === ns); + setWorkflowName(found ? found.name : ''); + } + }, [getAllOpeaflowsApi.data, ns]); + return ( - + // + + + {workflowName && ( + + Workflow name: {workflowName} + + )} {traceList.length > 0 ? ( <> - Traces + Traces: diff --git a/studio-frontend/packages/ui/src/views/tracetree/index.jsx b/studio-frontend/packages/ui/src/views/tracetree/index.jsx deleted file mode 100644 index 87aed6c..0000000 --- a/studio-frontend/packages/ui/src/views/tracetree/index.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; - -// Recursive component to render each span and its children -const SpanNode = ({ span }) => { - return ( -
-
- {span.span_name} (ID: {span.span_id}) -
-
Timestamp: {span.timestamp}
-
Duration: {span.duration} ns
-
Status: {span.status_code}
-
Service: {span.service_name}
-
LLM Input: {span.llm_input ? span.llm_input : 'N/A'}
-
LLM Output: {span.llm_output ? span.llm_output : 'N/A'}
- {span.children && span.children.length > 0 && ( -
- {span.children.map((childSpan) => ( - - ))} -
- )} -
- ); -}; - -// Main component to render the trace tree -const TraceTree = ({ traceData }) => { - return ( -
-

Trace ID: {traceData.trace_id}

- {traceData.spans.map((span) => ( - - ))} -
- ); -}; - -export default TraceTree; \ No newline at end of file diff --git a/tests/playwright/README.md b/tests/playwright/README.md index b47172e..7402962 100644 --- a/tests/playwright/README.md +++ b/tests/playwright/README.md @@ -6,7 +6,7 @@ Install npm dependencies: # Install package.json npm packages npm install # Install playwright dependencies -npx playwright install-deps +npx playwright install ``` Find baseURL variable in playwright.config.js and update it to point to your frontend application: @@ -20,7 +20,8 @@ Run the testcases: # Run all testcase npx playwright test # Run specific testcase -npx playwright test studio-e2e/.spec.ts +npx playwright test .spec.ts +npx playwright test .spec.ts --debug ``` Open the test report: diff --git a/tests/playwright/playwright.config.js b/tests/playwright/playwright.config.js index 0de2ff0..405f33e 100644 --- a/tests/playwright/playwright.config.js +++ b/tests/playwright/playwright.config.js @@ -11,7 +11,7 @@ const { defineConfig, devices } = require('@playwright/test'); * @see https://playwright.dev/docs/test-configuration */ module.exports = defineConfig({ - testDir: './', + testDir: '.', fullyParallel: false, // Disable fully parallel tests forbidOnly: !!process.env.CI, retries: 0, diff --git a/tests/playwright/studio-e2e/001_test_sandbox_deployment.spec.ts b/tests/playwright/studio-e2e/001_test_sandbox_deployment.spec.ts index 49478a5..b2aa007 100644 --- a/tests/playwright/studio-e2e/001_test_sandbox_deployment.spec.ts +++ b/tests/playwright/studio-e2e/001_test_sandbox_deployment.spec.ts @@ -38,7 +38,7 @@ test('001_test_sandbox_deployment', async ({ browser, baseURL }) => { // console.log(`Attempt ${i + 1} failed: ${error}`); // } // } - await waitForStatusText(page, 'td.MuiTableCell-root div.MuiStack-root p.MuiTypography-root', 'Ready', 10, 60000); + await waitForStatusText(page, 'td.MuiTableCell-root div.MuiStack-root p.MuiTypography-root', 'Getting Ready', 10, 60000); await page.waitForTimeout(8000); // Open APP-UI @@ -48,28 +48,46 @@ test('001_test_sandbox_deployment', async ({ browser, baseURL }) => { await expect(page2.getByRole('heading', { name: 'OPEA Studio' })).toBeVisible(); await page.bringToFront(); - // Open Dashboard + // Open Dashboard - update the locator for V1.4 + await page.getByRole('cell', { name: 'Observability Options' }).getByRole('button').click(); const page3Promise = page.waitForEvent('popup'); - await page.getByLabel('Click to open Monitoring').getByRole('button').nth(0).click(); + await page.getByRole('menuitem', { name: 'Monitoring Dashboard' }).click(); const page3 = await page3Promise; await expect(page3.getByRole('link', { name: 'Grafana' })).toBeVisible({ timeout: 60000 }); await page.bringToFront(); - // Generate Deployment Package - await page.getByLabel('Generate Deployment Package').getByRole('button').nth(0).click(); - const downloadPromise = page.waitForEvent('download'); - await page.getByRole('button', { name: 'Export Package' }).click(); - const download = await downloadPromise; - const downloadDir = path.join(os.homedir(), 'Downloads'); - const downloadPath = path.resolve(downloadDir, 'deployment_package_downloaded.json'); - await download.saveAs(downloadPath); - expect(fs.existsSync(downloadPath)).toBe(true); + // Open Trace - new for V1.4 + await page.getByRole('cell', { name: 'Observability Options' }).getByRole('button').click(); + const page4Promise = page.waitForEvent('popup'); + await page.getByRole('menuitem', { name: 'LLM Call Traces' }).click(); + const page4 = await page4Promise; + await expect(page4.getByText('No traces found')).toHaveText(/No traces found/, { timeout: 60000 }); + await page.bringToFront(); + + // Open Debug Logs - new for V1.4 + await page.getByRole('cell', { name: 'Observability Options' }).getByRole('button').click(); + const page5Promise = page.waitForEvent('popup'); + await page.getByRole('menuitem', { name: 'Debug Logs' }).click(); + const page5 = await page5Promise; + await expect(page5.getByRole('heading', { name: 'Workflow name: test_001' })).toHaveText(/Workflow name: test_001/, { timeout: 60000 }); + await page.bringToFront(); + + // Generate Deployment Package - to be deleted + //await page.getByLabel('Generate Deployment Package').getByRole('button').nth(0).click(); + //const downloadPromise = page.waitForEvent('download'); + //await page.getByRole('button', { name: 'Export Package' }).click(); + //const download = await downloadPromise; + //const downloadDir = path.join(os.homedir(), 'Downloads'); + //const downloadPath = path.resolve(downloadDir, 'deployment_package_downloaded.json'); + //await download.saveAs(downloadPath); + //expect(fs.existsSync(downloadPath)).toBe(true); // Stop & Delete Sandbox - await page.locator('button:has([data-testid="StopCircleOutlinedIcon"])').first().click(); + await page.getByRole('button', { name: 'Stop Sandbox' }).click(); // await expect(page.locator('td.MuiTableCell-root div.MuiStack-root p.MuiTypography-root').first()).toHaveText('Not Running', { timeout: statusChangeTimeout }); await waitForStatusText(page, 'td.MuiTableCell-root div.MuiStack-root p.MuiTypography-root', 'Not Running', 5, 60000); - await page.locator('#demo-customized-button').first().click(); + await page.locator('#demo-customized-button').click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); await page.getByRole('button', { name: 'Delete' }).click(); - }); \ No newline at end of file + }); \ No newline at end of file