From dff6694e10c51f9260fea06d7fc6f2ea245d8fb0 Mon Sep 17 00:00:00 2001 From: lkk12014402 Date: Sun, 6 Jul 2025 13:21:54 +0000 Subject: [PATCH 01/13] init deep research agent structure. --- DeepResearchAgent/Dockerfile | 17 ++ .../benchmark/accuracy/README.md | 1 + DeepResearchAgent/build.yaml | 25 ++ DeepResearchAgent/deep_researcher.yaml | 10 + .../docker_compose/intel/hpu/gaudi/README.md | 205 +++++++++++++++++ .../intel/hpu/gaudi/compose.yaml | 215 ++++++++++++++++++ .../docker_compose/intel/set_env.sh | 89 ++++++++ DeepResearchAgent/requirements.txt | 1 + DeepResearchAgent/research_agent.py | 38 ++++ DeepResearchAgent/utils.py | 88 +++++++ 10 files changed, 689 insertions(+) create mode 100644 DeepResearchAgent/Dockerfile create mode 100644 DeepResearchAgent/benchmark/accuracy/README.md create mode 100644 DeepResearchAgent/build.yaml create mode 100644 DeepResearchAgent/deep_researcher.yaml create mode 100644 DeepResearchAgent/docker_compose/intel/hpu/gaudi/README.md create mode 100644 DeepResearchAgent/docker_compose/intel/hpu/gaudi/compose.yaml create mode 100644 DeepResearchAgent/docker_compose/intel/set_env.sh create mode 100644 DeepResearchAgent/requirements.txt create mode 100644 DeepResearchAgent/research_agent.py create mode 100644 DeepResearchAgent/utils.py diff --git a/DeepResearchAgent/Dockerfile b/DeepResearchAgent/Dockerfile new file mode 100644 index 0000000000..09688ba6a4 --- /dev/null +++ b/DeepResearchAgent/Dockerfile @@ -0,0 +1,17 @@ +ARG IMAGE_REPO=opea +ARG BASE_TAG=latest +FROM $IMAGE_REPO/comps-base:$BASE_TAG + +COPY ./deep_researcher.yaml $HOME/deep_researcher.yaml +COPY ./utils.py $HOME/utils.py +COPY ./requirements.txt $HOME/requirements.txt +COPY ./research_agent.py $HOME/research_agent.py + +USER root +ARG uvpip='uv pip install --system --no-cache-dir' +RUN pip install --no-cache-dir --upgrade pip setuptools uv && \ + $uvpip -r requirements.txt + +USER user + +ENTRYPOINT ["python", "research_agent.py"] diff --git a/DeepResearchAgent/benchmark/accuracy/README.md b/DeepResearchAgent/benchmark/accuracy/README.md new file mode 100644 index 0000000000..e384971d41 --- /dev/null +++ b/DeepResearchAgent/benchmark/accuracy/README.md @@ -0,0 +1 @@ +## Deep Research Agent Benchmarks diff --git a/DeepResearchAgent/build.yaml b/DeepResearchAgent/build.yaml new file mode 100644 index 0000000000..7cc1ae7d21 --- /dev/null +++ b/DeepResearchAgent/build.yaml @@ -0,0 +1,25 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +services: + agent: + build: + context: GenAIComps + dockerfile: comps/agent/src/Dockerfile + args: + http_proxy: ${http_proxy} + https_proxy: ${https_proxy} + no_proxy: ${no_proxy} + image: ${REGISTRY:-opea}/agent:${TAG:-latest} + vllm-gaudi: + build: + context: vllm-fork + dockerfile: Dockerfile.hpu + extends: agent + image: ${REGISTRY:-opea}/vllm-gaudi:${TAG:-latest} + vllm-rocm: + build: + context: GenAIComps + dockerfile: comps/third_parties/vllm/src/Dockerfile.amd_gpu + extends: agent + image: ${REGISTRY:-opea}/vllm-rocm:${TAG:-latest} diff --git a/DeepResearchAgent/deep_researcher.yaml b/DeepResearchAgent/deep_researcher.yaml new file mode 100644 index 0000000000..fda32c8ea6 --- /dev/null +++ b/DeepResearchAgent/deep_researcher.yaml @@ -0,0 +1,10 @@ +agent: + type: langchain_deep_researcher + search_api: "tavily" + planner_provider: "openai" + planner_model: "meta-llama/Llama-3.3-70B-Instruct" + planner_endpoint: "http://localhost:8000/v1" + writer_provider: "openai" + writer_model: "meta-llama/Llama-3.3-70B-Instruct" + writer_endpoint: "http://localhost:8000/v1" + max_search_depth: 2 diff --git a/DeepResearchAgent/docker_compose/intel/hpu/gaudi/README.md b/DeepResearchAgent/docker_compose/intel/hpu/gaudi/README.md new file mode 100644 index 0000000000..79f0a9dec9 --- /dev/null +++ b/DeepResearchAgent/docker_compose/intel/hpu/gaudi/README.md @@ -0,0 +1,205 @@ +# Deploy Finance Agent on Intel® Gaudi® AI Accelerator with Docker Compose + +This README provides instructions for deploying the Finance Agent application using Docker Compose on systems equipped with Intel® Gaudi® AI Accelerators. + +## Table of Contents + +- [Overview](#overview) +- [Prerequisites](#prerequisites) +- [Start Deployment](#start-deployment) +- [Validate Services](#validate-services) +- [Accessing the User Interface (UI)](#accessing-the-user-interface-ui) + +## Overview + +This guide focuses on running the pre-configured Finance Agent service using Docker Compose on Intel® Gaudi® AI Accelerators. It leverages containers optimized for Gaudi for the LLM serving component, along with CPU-based containers for other microservices like embedding, retrieval, data preparation and the UI. + +## Prerequisites + +- Docker and Docker Compose installed. +- Intel® Gaudi® AI Accelerator(s) with the necessary drivers and software stack installed on the host system. (Refer to Intel Gaudi Documentation). +- Git installed (for cloning repository). +- Hugging Face Hub API Token (for downloading models). +- Access to the internet (or a private model cache). +- Finnhub API Key. Go to https://docs.financialdatasets.ai/ to get your free api key +- Financial Datgasets API Key. Go to https://docs.financialdatasets.ai/ to get your free api key + +Clone the GenAIExamples repository: + +```shell +mkdir /path/to/your/workspace/ +export WORKDIR=/path/to/your/workspace/ +cd $WORKDIR +git clone https://github.com/opea-project/GenAIExamples.git +cd GenAIExamples/FinanceAgent/docker_compose/intel/hpu/gaudi +``` + +## Start Deployment + +This uses the default vLLM-based deployment profile (vllm-gaudi-server). + +### Configure Environment + +Set required environment variables in your shell: + +```shell +# Path to your model cache +export HF_CACHE_DIR="./data" +# Some models from Hugging Face require approval beforehand. Ensure you have the necessary permissions to access them. +export HF_TOKEN="your_huggingface_token" +export FINNHUB_API_KEY="your-finnhub-api-key" +export FINANCIAL_DATASETS_API_KEY="your-financial-datgasets-api-key" + +# Optional: Configure HOST_IP if needed +# Replace with your host's external IP address (do not use localhost or 127.0.0.1). +# export HOST_IP=$(hostname -I | awk '{print $1}') + +# Optional: Configure proxy if needed +# export HTTP_PROXY="${http_proxy}" +# export HTTPS_PROXY="${https_proxy}" +# export NO_PROXY="${NO_PROXY},${HOST_IP}" + +source ../../set_env.sh +``` + +Note: The compose file might read additional variables from set_env.sh. Ensure all required variables like ports (LLM_SERVICE_PORT, TEI_EMBEDDER_PORT, etc.) are set if not using defaults from the compose file. For instance, edit the set_env.sh to change the LLM model: + +### Start Services + +#### Deploy with Docker Compose + +Below is the command to launch services + +- vllm-gaudi-server +- tei-embedding-serving +- redis-vector-db +- redis-kv-store +- dataprep-redis-server-finance +- finqa-agent-endpoint +- research-agent-endpoint +- docsum-vllm-gaudi +- supervisor-agent-endpoint +- agent-ui + +```shell +docker compose -f compose.yaml up -d +``` + +#### [Optional] Build docker images + +This is only needed if the Docker image is unavailable or the pull operation fails. + +```bash +cd $WORKDIR/GenAIExamples/FinanceAgent/docker_image_build +# get GenAIComps repo +git clone https://github.com/opea-project/GenAIComps.git +# build the images +docker compose -f build.yaml build --no-cache +``` + +If deploy on Gaudi, also need to build vllm image. + +```bash +cd $WORKDIR +git clone https://github.com/HabanaAI/vllm-fork.git +# get the latest release tag of vllm gaudi +cd vllm-fork +VLLM_VER=$(git describe --tags "$(git rev-list --tags --max-count=1)") +echo "Check out vLLM tag ${VLLM_VER}" +git checkout ${VLLM_VER} +docker build --no-cache -f Dockerfile.hpu -t opea/vllm-gaudi:latest --shm-size=128g . --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy +``` + +## Validate Services + +Wait several minutes for models to download and services to initialize (Gaudi initialization can take time). Check container logs (docker compose logs -f , especially vllm-gaudi-server). + +```bash +docker logs --tail 2000 -f vllm-gaudi-server +``` + +> Below is the expected output of the `vllm-gaudi-server` service. + +``` + INFO: Started server process [1] + INFO: Waiting for application startup. + INFO: Application startup complete. + INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit) + INFO: : - "GET /health HTTP/1.1" 200 OK + +``` + +### Validate Data Services + +Ingest data and retrieval from database + +```bash +python $WORKDIR/GenAIExamples/FinanceAgent/tests/test_redis_finance.py --port 6007 --test_option ingest +python $WORKDIR/GenAIExamples/FinanceAgent/tests/test_redis_finance.py --port 6007 --test_option get +``` + +### Validate Agents + +FinQA Agent: + +```bash +export agent_port="9095" +prompt="What is Gap's revenue in 2024?" +python3 $WORKDIR/GenAIExamples/FinanceAgent/tests/test.py --prompt "$prompt" --agent_role "worker" --ext_port $agent_port +``` + +Research Agent: + +```bash +export agent_port="9096" +prompt="generate NVDA financial research report" +python3 $WORKDIR/GenAIExamples/FinanceAgent/tests/test.py --prompt "$prompt" --agent_role "worker" --ext_port $agent_port --tool_choice "get_current_date" --tool_choice "get_share_performance" +``` + +Supervisor Agent single turns: + +```bash +export agent_port="9090" +python3 $WORKDIR/GenAIExamples/FinanceAgent/tests/test.py --agent_role "supervisor" --ext_port $agent_port --stream +``` + +Supervisor Agent multi turn: + +```bash +python3 $WORKDIR/GenAIExamples/FinanceAgent/tests/test.py --agent_role "supervisor" --ext_port $agent_port --multi-turn --stream +``` + +## Accessing the User Interface (UI) + +The UI microservice is launched in the previous step with the other microservices. +To see the UI, open a web browser to `http://${HOST_IP}:5175` to access the UI. Note the `HOST_IP` here is the host IP of the UI microservice. + +1. Create Admin Account with a random value + +2. Enter the endpoints in the `Connections` settings + + First, click on the user icon in the upper right corner to open `Settings`. Click on `Admin Settings`. Click on `Connections`. + + Then, enter the supervisor agent endpoint in the `OpenAI API` section: `http://${HOST_IP}:9090/v1`. Enter the API key as "empty". Add an arbitrary model id in `Model IDs`, for example, "opea_agent". The `HOST_IP` here should be the host ip of the agent microservice. + + Then, enter the dataprep endpoint in the `Icloud File API` section. You first need to enable `Icloud File API` by clicking on the button on the right to turn it into green and then enter the endpoint url, for example, `http://${HOST_IP}:6007/v1`. The `HOST_IP` here should be the host ip of the dataprep microservice. + + You should see screen like the screenshot below when the settings are done. + +![opea-agent-setting](../../../../assets/ui_connections_settings.png) + +3. Upload documents with UI + + Click on the `Workplace` icon in the top left corner. Click `Knowledge`. Click on the "+" sign to the right of `iCloud Knowledge`. You can paste an url in the left hand side of the pop-up window, or upload a local file by click on the cloud icon on the right hand side of the pop-up window. Then click on the `Upload Confirm` button. Wait till the processing is done and the pop-up window will be closed on its own when the data ingestion is done. See the screenshot below. + Then, enter the dataprep endpoint in the `iCloud File API` section. You first need to enable `iCloud File API` by clicking on the button on the right to turn it into green and then enter the endpoint url, for example, `http://${HOST_IP}:6007/v1`. The `HOST_IP` here should be the host ip of the dataprep microservice. + Note: the data ingestion may take a few minutes depending on the length of the document. Please wait patiently and do not close the pop-up window. + +![upload-doc-ui](../../../../assets/upload_doc_ui.png) + +4. Test agent with UI + + After the settings are done and documents are ingested, you can start to ask questions to the agent. Click on the `New Chat` icon in the top left corner, and type in your questions in the text box in the middle of the UI. + + The UI will stream the agent's response tokens. You need to expand the `Thinking` tab to see the agent's reasoning process. After the agent made tool calls, you would also see the tool output after the tool returns output to the agent. Note: it may take a while to get the tool output back if the tool execution takes time. + +![opea-agent-test](../../../../assets/opea-agent-test.png) diff --git a/DeepResearchAgent/docker_compose/intel/hpu/gaudi/compose.yaml b/DeepResearchAgent/docker_compose/intel/hpu/gaudi/compose.yaml new file mode 100644 index 0000000000..8c6d579c3c --- /dev/null +++ b/DeepResearchAgent/docker_compose/intel/hpu/gaudi/compose.yaml @@ -0,0 +1,215 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +x-common-environment: + &common-env + no_proxy: ${NO_PROXY} + http_proxy: ${HTTP_PROXY} + https_proxy: ${HTTPS_PROXY} + +x-common-agent-environment: + &common-agent-env + <<: *common-env + HF_TOKEN: ${HF_TOKEN} + llm_endpoint_url: ${LLM_ENDPOINT} + model: ${LLM_MODEL_ID} + REDIS_URL_VECTOR: ${REDIS_URL_VECTOR} + REDIS_URL_KV: ${REDIS_URL_KV} + TEI_EMBEDDING_ENDPOINT: ${TEI_EMBEDDING_ENDPOINT} + ip_address: ${HOST_IP} + strategy: react_llama + require_human_feedback: false + +services: + + vllm-service: + image: ${REGISTRY:-opea}/vllm-gaudi:${TAG:-latest} + container_name: vllm-gaudi-server + ports: + - "8086:8000" + volumes: + - ${HF_CACHE_DIR:-./data}:/data + environment: + <<: *common-env + HF_TOKEN: ${HF_TOKEN} + HF_HOME: ./data + HABANA_VISIBLE_DEVICES: all + OMPI_MCA_btl_vader_single_copy_mechanism: none + LLM_MODEL_ID: ${LLM_MODEL_ID} + VLLM_TORCH_PROFILER_DIR: "/mnt" + VLLM_SKIP_WARMUP: true + PT_HPU_ENABLE_LAZY_COLLECTIVES: true + healthcheck: + test: ["CMD-SHELL", "curl -f http://$HOST_IP:8086/health || exit 1"] + interval: 10s + timeout: 10s + retries: 100 + runtime: habana + cap_add: + - SYS_NICE + ipc: host + command: --model ${LLM_MODEL_ID} --tensor-parallel-size ${NUM_CARDS} --host 0.0.0.0 --port 8000 --max-seq-len-to-capture $MAX_LEN + + tei-embedding-serving: + image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.7 + container_name: tei-embedding-serving + entrypoint: /bin/sh -c "apt-get update && apt-get install -y curl && text-embeddings-router --json-output --model-id ${EMBEDDING_MODEL_ID} --auto-truncate" + ports: + - "${TEI_EMBEDDER_PORT:-10221}:80" + volumes: + - ${HF_CACHE_DIR:-./data}:/data + shm_size: 1g + environment: + <<: *common-env + HF_TOKEN: ${HF_TOKEN} + host_ip: ${HOST_IP} + healthcheck: + test: ["CMD", "curl", "-f", "http://${HOST_IP}:${TEI_EMBEDDER_PORT}/health"] + interval: 10s + timeout: 6s + retries: 48 + + redis-vector-db: + image: redis/redis-stack:7.2.0-v9 + container_name: redis-vector-db + ports: + - "${REDIS_PORT1:-6379}:6379" + - "${REDIS_PORT2:-8001}:8001" + environment: + <<: *common-env + healthcheck: + test: ["CMD", "redis-cli", "ping"] + timeout: 10s + retries: 3 + start_period: 10s + + redis-kv-store: + image: redis/redis-stack:7.2.0-v9 + container_name: redis-kv-store + ports: + - "${REDIS_PORT3:-6380}:6379" + - "${REDIS_PORT4:-8002}:8001" + environment: + <<: *common-env + healthcheck: + test: ["CMD", "redis-cli", "ping"] + timeout: 10s + retries: 3 + start_period: 10s + + dataprep-redis-finance: + image: ${REGISTRY:-opea}/dataprep:${TAG:-latest} + container_name: dataprep-redis-server-finance + depends_on: + redis-vector-db: + condition: service_healthy + redis-kv-store: + condition: service_healthy + tei-embedding-serving: + condition: service_healthy + ports: + - "${DATAPREP_PORT:-6007}:5000" + environment: + <<: *common-env + DATAPREP_COMPONENT_NAME: ${DATAPREP_COMPONENT_NAME} + REDIS_URL_VECTOR: ${REDIS_URL_VECTOR} + REDIS_URL_KV: ${REDIS_URL_KV} + TEI_EMBEDDING_ENDPOINT: ${TEI_EMBEDDING_ENDPOINT} + LLM_ENDPOINT: ${LLM_ENDPOINT} + LLM_MODEL: ${LLM_MODEL_ID} + HF_TOKEN: ${HF_TOKEN} + LOGFLAG: true + + worker-finqa-agent: + image: opea/agent:latest + container_name: finqa-agent-endpoint + volumes: + - ${TOOLSET_PATH}:/home/user/tools/ + - ${PROMPT_PATH}:/home/user/prompts/ + ipc: host + ports: + - "9095:9095" + environment: + <<: *common-agent-env + with_memory: false + recursion_limit: ${RECURSION_LIMIT_WORKER} + temperature: ${TEMPERATURE} + max_new_tokens: ${MAX_TOKENS} + stream: false + tools: /home/user/tools/finqa_agent_tools.yaml + custom_prompt: /home/user/prompts/finqa_prompt.py + port: 9095 + + worker-research-agent: + image: opea/agent:latest + container_name: research-agent-endpoint + volumes: + - ${TOOLSET_PATH}:/home/user/tools/ + - ${PROMPT_PATH}:/home/user/prompts/ + ipc: host + ports: + - "9096:9096" + environment: + <<: *common-agent-env + with_memory: false + recursion_limit: ${RECURSION_LIMIT_WORKER} + stream: false + tools: /home/user/tools/research_agent_tools.yaml + custom_prompt: /home/user/prompts/research_prompt.py + FINNHUB_API_KEY: ${FINNHUB_API_KEY} + FINANCIAL_DATASETS_API_KEY: ${FINANCIAL_DATASETS_API_KEY} + port: 9096 + + docsum-vllm-gaudi: + image: opea/llm-docsum:latest + container_name: docsum-vllm-gaudi + ports: + - ${DOCSUM_PORT:-9000}:9000 + ipc: host + environment: + <<: *common-env + LLM_ENDPOINT: ${LLM_ENDPOINT} + LLM_MODEL_ID: ${LLM_MODEL_ID} + HF_TOKEN: ${HF_TOKEN} + LOGFLAG: ${LOGFLAG:-False} + MAX_INPUT_TOKENS: ${MAX_INPUT_TOKENS} + MAX_TOTAL_TOKENS: ${MAX_TOTAL_TOKENS} + DocSum_COMPONENT_NAME: ${DOCSUM_COMPONENT_NAME:-OpeaDocSumvLLM} + restart: unless-stopped + + supervisor-react-agent: + image: opea/agent:latest + container_name: supervisor-agent-endpoint + volumes: + - ${TOOLSET_PATH}:/home/user/tools/ + - ${PROMPT_PATH}:/home/user/prompts/ + ipc: host + depends_on: + - worker-finqa-agent + - worker-research-agent + ports: + - "9090:9090" + environment: + <<: *common-agent-env + with_memory: "true" + recursion_limit: ${RECURSION_LIMIT_SUPERVISOR} + temperature: ${TEMPERATURE} + max_new_tokens: ${MAX_TOKENS} + stream: "true" + tools: /home/user/tools/supervisor_agent_tools.yaml + custom_prompt: /home/user/prompts/supervisor_prompt.py + WORKER_FINQA_AGENT_URL: ${WORKER_FINQA_AGENT_URL} + WORKER_RESEARCH_AGENT_URL: ${WORKER_RESEARCH_AGENT_URL} + DOCSUM_ENDPOINT: ${DOCSUM_ENDPOINT} + port: 9090 + + agent-ui: + image: opea/agent-ui:latest + container_name: agent-ui + environment: + <<: *common-env + host_ip: ${HOST_IP} + ports: + - "5175:8080" + ipc: host diff --git a/DeepResearchAgent/docker_compose/intel/set_env.sh b/DeepResearchAgent/docker_compose/intel/set_env.sh new file mode 100644 index 0000000000..c8a36fabb0 --- /dev/null +++ b/DeepResearchAgent/docker_compose/intel/set_env.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash + +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# Navigate to the parent directory and source the environment +pushd "../../" > /dev/null +source .set_env.sh +popd > /dev/null + +# Function to check if a variable is set +check_var() { + local var_name="$1" + local var_value="${!var_name}" + if [ -z "${var_value}" ]; then + echo "Error: ${var_name} is not set. Please set ${var_name}." + return 1 # Return an error code but do not exit the script + fi +} + +# Check critical variables +check_var "HF_TOKEN" +check_var "HOST_IP" + +# VLLM configuration +export VLLM_PORT="${VLLM_PORT:-8086}" +export VLLM_VOLUME="${VLLM_VOLUME:-/data2/huggingface}" +export VLLM_IMAGE="${VLLM_IMAGE:-opea/vllm-gaudi:latest}" +export LLM_MODEL_ID="${LLM_MODEL_ID:-meta-llama/Llama-3.3-70B-Instruct}" +export LLM_ENDPOINT="http://${HOST_IP}:${VLLM_PORT}" +export MAX_LEN="${MAX_LEN:-16384}" +export NUM_CARDS="${NUM_CARDS:-4}" +export HF_CACHE_DIR="${HF_CACHE_DIR:-"./data"}" + +# Data preparation and embedding configuration +export DATAPREP_PORT="${DATAPREP_PORT:-6007}" +export TEI_EMBEDDER_PORT="${TEI_EMBEDDER_PORT:-10221}" +export REDIS_URL_VECTOR="redis://${HOST_IP}:6379" +export REDIS_URL_KV="redis://${HOST_IP}:6380" +export DATAPREP_COMPONENT_NAME="${DATAPREP_COMPONENT_NAME:-OPEA_DATAPREP_REDIS_FINANCE}" +export EMBEDDING_MODEL_ID="${EMBEDDING_MODEL_ID:-BAAI/bge-base-en-v1.5}" +export TEI_EMBEDDING_ENDPOINT="http://${HOST_IP}:${TEI_EMBEDDER_PORT}" + +# Hugging Face API token +export HF_TOKEN="${HF_TOKEN}" + +# Recursion limits +export RECURSION_LIMIT_WORKER="${RECURSION_LIMIT_WORKER:-12}" +export RECURSION_LIMIT_SUPERVISOR="${RECURSION_LIMIT_SUPERVISOR:-10}" + +# LLM configuration +export TEMPERATURE="${TEMPERATURE:-0.5}" +export MAX_TOKENS="${MAX_TOKENS:-4096}" +export MAX_INPUT_TOKENS="${MAX_INPUT_TOKENS:-2048}" +export MAX_TOTAL_TOKENS="${MAX_TOTAL_TOKENS:-4096}" + +# Worker URLs +export WORKER_FINQA_AGENT_URL="http://${HOST_IP}:9095/v1/chat/completions" +export WORKER_RESEARCH_AGENT_URL="http://${HOST_IP}:9096/v1/chat/completions" + +# DocSum configuration +export DOCSUM_COMPONENT_NAME="${DOCSUM_COMPONENT_NAME:-"OpeaDocSumvLLM"}" +export DOCSUM_ENDPOINT="http://${HOST_IP}:9000/v1/docsum" + +# API keys +check_var "FINNHUB_API_KEY" +check_var "FINANCIAL_DATASETS_API_KEY" +export FINNHUB_API_KEY="${FINNHUB_API_KEY}" +export FINANCIAL_DATASETS_API_KEY="${FINANCIAL_DATASETS_API_KEY}" + + +# Toolset and prompt paths +if check_var "WORKDIR"; then + export TOOLSET_PATH=$WORKDIR/GenAIExamples/FinanceAgent/tools/ + export PROMPT_PATH=$WORKDIR/GenAIExamples/FinanceAgent/prompts/ + + echo "TOOLSET_PATH=${TOOLSET_PATH}" + echo "PROMPT_PATH=${PROMPT_PATH}" + + # Array of directories to check + REQUIRED_DIRS=("${TOOLSET_PATH}" "${PROMPT_PATH}") + + for dir in "${REQUIRED_DIRS[@]}"; do + if [ ! -d "${dir}" ]; then + echo "Error: Required directory does not exist: ${dir}" + exit 1 + fi + done +fi diff --git a/DeepResearchAgent/requirements.txt b/DeepResearchAgent/requirements.txt new file mode 100644 index 0000000000..4cfa16bd4e --- /dev/null +++ b/DeepResearchAgent/requirements.txt @@ -0,0 +1 @@ +open_deep_research # https://github.com/langchain-ai/open_deep_research diff --git a/DeepResearchAgent/research_agent.py b/DeepResearchAgent/research_agent.py new file mode 100644 index 0000000000..da8cb88e58 --- /dev/null +++ b/DeepResearchAgent/research_agent.py @@ -0,0 +1,38 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import argparse +import json +import os +import re + +from comps import register_microservice, opea_microservices +from comps.cores.telemetry.opea_telemetry import opea_telemetry +from typing import List, Union +from pydantic import BaseModel +from utils import * + + +agent = create_agent("./deep_researcher.yaml") + + +class SimpleRequest(BaseModel): + question: Union[str, List[str]] + +@register_microservice( + name="opea_service@deep_research_agent", + endpoint="/v1/deep_research_agent", + host="0.0.0.0", + port=8022, +) +@opea_telemetry +async def run(request: SimpleRequest): + + question = ( + f"Question: {request.question}" + ) + + return agent(goal=question) + +if __name__ == "__main__": + opea_microservices["opea_service@deep_research_agent"].start() diff --git a/DeepResearchAgent/utils.py b/DeepResearchAgent/utils.py new file mode 100644 index 0000000000..4da9164b64 --- /dev/null +++ b/DeepResearchAgent/utils.py @@ -0,0 +1,88 @@ +from typing import Any +import yaml + +def load_config(config_path: str): + with open(config_path, "r") as file: + return yaml.safe_load(file) + +def create_agent(config: str, return_instance: bool = False) -> Any: + + config_dict = load_config(config) + + agent_config = config_dict.get("agent") + agent_type = agent_config.pop("type") + + try: + import uuid + + from langgraph.checkpoint.memory import MemorySaver + from open_deep_research.graph import builder + except ImportError as e: + raise ImportError( + f"Failed to import required modules for langchain deep researcher: {e}. Make sure langgraph and open_deep_research are installed. Also make sure that the benchmark directory is in your path. Also, you might need to install the with-open-deep-research extra dependencies (see README.md)." + ) + + memory = MemorySaver() + graph = builder.compile(checkpointer=memory) + + REPORT_STRUCTURE = """Use this structure to create a report on the user-provided topic: + + 1. Introduction (no research needed) + - Brief overview of the topic area + + 2. Main Body Sections: + - Each section should focus on a sub-topic of the user-provided topic + + 3. Conclusion + - Aim for 1 structural element (either a list of table) that distills the main body sections + - Provide a concise summary of the report""" + + # Extract configuration parameters + search_api = agent_config.get("search_api", "tavily") + planner_provider = agent_config.get("planner_provider") + planner_model = agent_config.get("planner_model") + planner_endpoint = agent_config.get("planner_endpoint") + writer_provider = agent_config.get("writer_provider") + writer_model = agent_config.get("writer_model") + writer_endpoint = agent_config.get("writer_endpoint") + max_search_depth = agent_config.get("max_search_depth", 3) + + def langchain_wrapper(goal: str): + import asyncio + + thread = { + "configurable": { + "thread_id": str(uuid.uuid4()), + "search_api": search_api, + "planner_provider": planner_provider, + "planner_model": planner_model, + "planner_model_kwargs": {"base_url": planner_endpoint}, + "writer_provider": writer_provider, + "writer_model": writer_model, + "writer_model_kwargs": {"base_url": writer_endpoint}, + "max_search_depth": max_search_depth, + "report_structure": REPORT_STRUCTURE, + } + } + + # NOTE: add research prompt to the goal for robust benchmarking purposes + goal= goal + " You must perform in-depth research to answer the question." + + results = [] + + async def run_graph(): + async for event in graph.astream({"topic": goal}, thread, stream_mode="updates"): + results.append(event) + + from langgraph.types import Command + async for event in graph.astream(Command(resume=True), thread, stream_mode="updates"): + results.append(event) + + final_state = graph.get_state(thread) + report = final_state.values.get('final_report') + + return report + + return asyncio.run(run_graph()) + + return langchain_wrapper From a55ad3bd99bdc34ccbd5ff341f1870b166b2b141 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 6 Jul 2025 13:18:54 +0000 Subject: [PATCH 02/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- DeepResearchAgent/Dockerfile | 3 +++ DeepResearchAgent/deep_researcher.yaml | 3 +++ DeepResearchAgent/research_agent.py | 13 ++++++------- DeepResearchAgent/utils.py | 11 +++++++++-- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/DeepResearchAgent/Dockerfile b/DeepResearchAgent/Dockerfile index 09688ba6a4..d35d3ea7ea 100644 --- a/DeepResearchAgent/Dockerfile +++ b/DeepResearchAgent/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (C) 2025 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + ARG IMAGE_REPO=opea ARG BASE_TAG=latest FROM $IMAGE_REPO/comps-base:$BASE_TAG diff --git a/DeepResearchAgent/deep_researcher.yaml b/DeepResearchAgent/deep_researcher.yaml index fda32c8ea6..6973cd29a7 100644 --- a/DeepResearchAgent/deep_researcher.yaml +++ b/DeepResearchAgent/deep_researcher.yaml @@ -1,3 +1,6 @@ +# Copyright (C) 2025 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + agent: type: langchain_deep_researcher search_api: "tavily" diff --git a/DeepResearchAgent/research_agent.py b/DeepResearchAgent/research_agent.py index da8cb88e58..3eda52e06f 100644 --- a/DeepResearchAgent/research_agent.py +++ b/DeepResearchAgent/research_agent.py @@ -5,20 +5,20 @@ import json import os import re +from typing import List, Union -from comps import register_microservice, opea_microservices +from comps import opea_microservices, register_microservice from comps.cores.telemetry.opea_telemetry import opea_telemetry -from typing import List, Union from pydantic import BaseModel from utils import * - agent = create_agent("./deep_researcher.yaml") class SimpleRequest(BaseModel): question: Union[str, List[str]] + @register_microservice( name="opea_service@deep_research_agent", endpoint="/v1/deep_research_agent", @@ -28,11 +28,10 @@ class SimpleRequest(BaseModel): @opea_telemetry async def run(request: SimpleRequest): - question = ( - f"Question: {request.question}" - ) - + question = f"Question: {request.question}" + return agent(goal=question) + if __name__ == "__main__": opea_microservices["opea_service@deep_research_agent"].start() diff --git a/DeepResearchAgent/utils.py b/DeepResearchAgent/utils.py index 4da9164b64..fb099531ff 100644 --- a/DeepResearchAgent/utils.py +++ b/DeepResearchAgent/utils.py @@ -1,10 +1,16 @@ +# Copyright (C) 2025 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + from typing import Any + import yaml + def load_config(config_path: str): with open(config_path, "r") as file: return yaml.safe_load(file) + def create_agent(config: str, return_instance: bool = False) -> Any: config_dict = load_config(config) @@ -66,7 +72,7 @@ def langchain_wrapper(goal: str): } # NOTE: add research prompt to the goal for robust benchmarking purposes - goal= goal + " You must perform in-depth research to answer the question." + goal = goal + " You must perform in-depth research to answer the question." results = [] @@ -75,11 +81,12 @@ async def run_graph(): results.append(event) from langgraph.types import Command + async for event in graph.astream(Command(resume=True), thread, stream_mode="updates"): results.append(event) final_state = graph.get_state(thread) - report = final_state.values.get('final_report') + report = final_state.values.get("final_report") return report From 88e1b94b14f3790569726e21c8bfcdbe42642974 Mon Sep 17 00:00:00 2001 From: lkk12014402 Date: Mon, 7 Jul 2025 12:17:36 +0000 Subject: [PATCH 03/13] add deep research agent ut. --- DeepResearchAgent/README.md | 42 ++++ DeepResearchAgent/build.yaml | 25 --- DeepResearchAgent/deep_researcher.yaml | 2 - .../docker_compose/intel/hpu/gaudi/README.md | 205 ------------------ .../intel/hpu/gaudi/compose.yaml | 182 +--------------- .../docker_compose/intel/set_env.sh | 63 +----- .../docker_image_build/build.yaml | 13 ++ DeepResearchAgent/research_agent.py | 4 +- .../tests/test_compose_on_gaudi.sh | 117 ++++++++++ DeepResearchAgent/utils.py | 27 +-- 10 files changed, 206 insertions(+), 474 deletions(-) create mode 100644 DeepResearchAgent/README.md delete mode 100644 DeepResearchAgent/build.yaml delete mode 100644 DeepResearchAgent/docker_compose/intel/hpu/gaudi/README.md create mode 100644 DeepResearchAgent/docker_image_build/build.yaml create mode 100644 DeepResearchAgent/tests/test_compose_on_gaudi.sh diff --git a/DeepResearchAgent/README.md b/DeepResearchAgent/README.md new file mode 100644 index 0000000000..eb9a5d732f --- /dev/null +++ b/DeepResearchAgent/README.md @@ -0,0 +1,42 @@ + +## Deep Research Agent Application + +Deep Research Agents are a new class of autonomous AI systems designed to perform complex, multi-step research tasks that typically require human-level reasoning, planning, and synthesis. + + +## Setup Deployment Environment + +``` +# get your TAVILY_API_KEY from https://app.tavily.com/ +export TAVILY_API_KEY="" +# get your HuggingFace Access Token from https://huggingface.co/docs/transformers.js/en/guides/private#step-1-generating-a-user-access-token +export HF_TOKEN="" + +# set proxy if needed by your machine +export http_proxy="" +export https_proxy="" +export no_proxy="" + + +source ./set_env.sh +``` + +## Deploy the Services Using Docker Compose + +To deploy the Deep Research Agent services, execute the docker compose up command with the appropriate arguments. For a default deployment, execute: + +``` +docker compose -f docker_compose/intel/hpu/gaudi/compose.yaml up -d + +``` + +## Validate Microservice + +```shell +curl http://${host_ip}:8022/v1/deep_research_agent \ + -X POST \ + -d '{"question":"What is Deep Learning?"}' \ + -H 'Content-Type: application/json' +``` + +## Benchmarks diff --git a/DeepResearchAgent/build.yaml b/DeepResearchAgent/build.yaml deleted file mode 100644 index 7cc1ae7d21..0000000000 --- a/DeepResearchAgent/build.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -services: - agent: - build: - context: GenAIComps - dockerfile: comps/agent/src/Dockerfile - args: - http_proxy: ${http_proxy} - https_proxy: ${https_proxy} - no_proxy: ${no_proxy} - image: ${REGISTRY:-opea}/agent:${TAG:-latest} - vllm-gaudi: - build: - context: vllm-fork - dockerfile: Dockerfile.hpu - extends: agent - image: ${REGISTRY:-opea}/vllm-gaudi:${TAG:-latest} - vllm-rocm: - build: - context: GenAIComps - dockerfile: comps/third_parties/vllm/src/Dockerfile.amd_gpu - extends: agent - image: ${REGISTRY:-opea}/vllm-rocm:${TAG:-latest} diff --git a/DeepResearchAgent/deep_researcher.yaml b/DeepResearchAgent/deep_researcher.yaml index 6973cd29a7..eb5b9c1640 100644 --- a/DeepResearchAgent/deep_researcher.yaml +++ b/DeepResearchAgent/deep_researcher.yaml @@ -6,8 +6,6 @@ agent: search_api: "tavily" planner_provider: "openai" planner_model: "meta-llama/Llama-3.3-70B-Instruct" - planner_endpoint: "http://localhost:8000/v1" writer_provider: "openai" writer_model: "meta-llama/Llama-3.3-70B-Instruct" - writer_endpoint: "http://localhost:8000/v1" max_search_depth: 2 diff --git a/DeepResearchAgent/docker_compose/intel/hpu/gaudi/README.md b/DeepResearchAgent/docker_compose/intel/hpu/gaudi/README.md deleted file mode 100644 index 79f0a9dec9..0000000000 --- a/DeepResearchAgent/docker_compose/intel/hpu/gaudi/README.md +++ /dev/null @@ -1,205 +0,0 @@ -# Deploy Finance Agent on Intel® Gaudi® AI Accelerator with Docker Compose - -This README provides instructions for deploying the Finance Agent application using Docker Compose on systems equipped with Intel® Gaudi® AI Accelerators. - -## Table of Contents - -- [Overview](#overview) -- [Prerequisites](#prerequisites) -- [Start Deployment](#start-deployment) -- [Validate Services](#validate-services) -- [Accessing the User Interface (UI)](#accessing-the-user-interface-ui) - -## Overview - -This guide focuses on running the pre-configured Finance Agent service using Docker Compose on Intel® Gaudi® AI Accelerators. It leverages containers optimized for Gaudi for the LLM serving component, along with CPU-based containers for other microservices like embedding, retrieval, data preparation and the UI. - -## Prerequisites - -- Docker and Docker Compose installed. -- Intel® Gaudi® AI Accelerator(s) with the necessary drivers and software stack installed on the host system. (Refer to Intel Gaudi Documentation). -- Git installed (for cloning repository). -- Hugging Face Hub API Token (for downloading models). -- Access to the internet (or a private model cache). -- Finnhub API Key. Go to https://docs.financialdatasets.ai/ to get your free api key -- Financial Datgasets API Key. Go to https://docs.financialdatasets.ai/ to get your free api key - -Clone the GenAIExamples repository: - -```shell -mkdir /path/to/your/workspace/ -export WORKDIR=/path/to/your/workspace/ -cd $WORKDIR -git clone https://github.com/opea-project/GenAIExamples.git -cd GenAIExamples/FinanceAgent/docker_compose/intel/hpu/gaudi -``` - -## Start Deployment - -This uses the default vLLM-based deployment profile (vllm-gaudi-server). - -### Configure Environment - -Set required environment variables in your shell: - -```shell -# Path to your model cache -export HF_CACHE_DIR="./data" -# Some models from Hugging Face require approval beforehand. Ensure you have the necessary permissions to access them. -export HF_TOKEN="your_huggingface_token" -export FINNHUB_API_KEY="your-finnhub-api-key" -export FINANCIAL_DATASETS_API_KEY="your-financial-datgasets-api-key" - -# Optional: Configure HOST_IP if needed -# Replace with your host's external IP address (do not use localhost or 127.0.0.1). -# export HOST_IP=$(hostname -I | awk '{print $1}') - -# Optional: Configure proxy if needed -# export HTTP_PROXY="${http_proxy}" -# export HTTPS_PROXY="${https_proxy}" -# export NO_PROXY="${NO_PROXY},${HOST_IP}" - -source ../../set_env.sh -``` - -Note: The compose file might read additional variables from set_env.sh. Ensure all required variables like ports (LLM_SERVICE_PORT, TEI_EMBEDDER_PORT, etc.) are set if not using defaults from the compose file. For instance, edit the set_env.sh to change the LLM model: - -### Start Services - -#### Deploy with Docker Compose - -Below is the command to launch services - -- vllm-gaudi-server -- tei-embedding-serving -- redis-vector-db -- redis-kv-store -- dataprep-redis-server-finance -- finqa-agent-endpoint -- research-agent-endpoint -- docsum-vllm-gaudi -- supervisor-agent-endpoint -- agent-ui - -```shell -docker compose -f compose.yaml up -d -``` - -#### [Optional] Build docker images - -This is only needed if the Docker image is unavailable or the pull operation fails. - -```bash -cd $WORKDIR/GenAIExamples/FinanceAgent/docker_image_build -# get GenAIComps repo -git clone https://github.com/opea-project/GenAIComps.git -# build the images -docker compose -f build.yaml build --no-cache -``` - -If deploy on Gaudi, also need to build vllm image. - -```bash -cd $WORKDIR -git clone https://github.com/HabanaAI/vllm-fork.git -# get the latest release tag of vllm gaudi -cd vllm-fork -VLLM_VER=$(git describe --tags "$(git rev-list --tags --max-count=1)") -echo "Check out vLLM tag ${VLLM_VER}" -git checkout ${VLLM_VER} -docker build --no-cache -f Dockerfile.hpu -t opea/vllm-gaudi:latest --shm-size=128g . --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy -``` - -## Validate Services - -Wait several minutes for models to download and services to initialize (Gaudi initialization can take time). Check container logs (docker compose logs -f , especially vllm-gaudi-server). - -```bash -docker logs --tail 2000 -f vllm-gaudi-server -``` - -> Below is the expected output of the `vllm-gaudi-server` service. - -``` - INFO: Started server process [1] - INFO: Waiting for application startup. - INFO: Application startup complete. - INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit) - INFO: : - "GET /health HTTP/1.1" 200 OK - -``` - -### Validate Data Services - -Ingest data and retrieval from database - -```bash -python $WORKDIR/GenAIExamples/FinanceAgent/tests/test_redis_finance.py --port 6007 --test_option ingest -python $WORKDIR/GenAIExamples/FinanceAgent/tests/test_redis_finance.py --port 6007 --test_option get -``` - -### Validate Agents - -FinQA Agent: - -```bash -export agent_port="9095" -prompt="What is Gap's revenue in 2024?" -python3 $WORKDIR/GenAIExamples/FinanceAgent/tests/test.py --prompt "$prompt" --agent_role "worker" --ext_port $agent_port -``` - -Research Agent: - -```bash -export agent_port="9096" -prompt="generate NVDA financial research report" -python3 $WORKDIR/GenAIExamples/FinanceAgent/tests/test.py --prompt "$prompt" --agent_role "worker" --ext_port $agent_port --tool_choice "get_current_date" --tool_choice "get_share_performance" -``` - -Supervisor Agent single turns: - -```bash -export agent_port="9090" -python3 $WORKDIR/GenAIExamples/FinanceAgent/tests/test.py --agent_role "supervisor" --ext_port $agent_port --stream -``` - -Supervisor Agent multi turn: - -```bash -python3 $WORKDIR/GenAIExamples/FinanceAgent/tests/test.py --agent_role "supervisor" --ext_port $agent_port --multi-turn --stream -``` - -## Accessing the User Interface (UI) - -The UI microservice is launched in the previous step with the other microservices. -To see the UI, open a web browser to `http://${HOST_IP}:5175` to access the UI. Note the `HOST_IP` here is the host IP of the UI microservice. - -1. Create Admin Account with a random value - -2. Enter the endpoints in the `Connections` settings - - First, click on the user icon in the upper right corner to open `Settings`. Click on `Admin Settings`. Click on `Connections`. - - Then, enter the supervisor agent endpoint in the `OpenAI API` section: `http://${HOST_IP}:9090/v1`. Enter the API key as "empty". Add an arbitrary model id in `Model IDs`, for example, "opea_agent". The `HOST_IP` here should be the host ip of the agent microservice. - - Then, enter the dataprep endpoint in the `Icloud File API` section. You first need to enable `Icloud File API` by clicking on the button on the right to turn it into green and then enter the endpoint url, for example, `http://${HOST_IP}:6007/v1`. The `HOST_IP` here should be the host ip of the dataprep microservice. - - You should see screen like the screenshot below when the settings are done. - -![opea-agent-setting](../../../../assets/ui_connections_settings.png) - -3. Upload documents with UI - - Click on the `Workplace` icon in the top left corner. Click `Knowledge`. Click on the "+" sign to the right of `iCloud Knowledge`. You can paste an url in the left hand side of the pop-up window, or upload a local file by click on the cloud icon on the right hand side of the pop-up window. Then click on the `Upload Confirm` button. Wait till the processing is done and the pop-up window will be closed on its own when the data ingestion is done. See the screenshot below. - Then, enter the dataprep endpoint in the `iCloud File API` section. You first need to enable `iCloud File API` by clicking on the button on the right to turn it into green and then enter the endpoint url, for example, `http://${HOST_IP}:6007/v1`. The `HOST_IP` here should be the host ip of the dataprep microservice. - Note: the data ingestion may take a few minutes depending on the length of the document. Please wait patiently and do not close the pop-up window. - -![upload-doc-ui](../../../../assets/upload_doc_ui.png) - -4. Test agent with UI - - After the settings are done and documents are ingested, you can start to ask questions to the agent. Click on the `New Chat` icon in the top left corner, and type in your questions in the text box in the middle of the UI. - - The UI will stream the agent's response tokens. You need to expand the `Thinking` tab to see the agent's reasoning process. After the agent made tool calls, you would also see the tool output after the tool returns output to the agent. Note: it may take a while to get the tool output back if the tool execution takes time. - -![opea-agent-test](../../../../assets/opea-agent-test.png) diff --git a/DeepResearchAgent/docker_compose/intel/hpu/gaudi/compose.yaml b/DeepResearchAgent/docker_compose/intel/hpu/gaudi/compose.yaml index 8c6d579c3c..23da536958 100644 --- a/DeepResearchAgent/docker_compose/intel/hpu/gaudi/compose.yaml +++ b/DeepResearchAgent/docker_compose/intel/hpu/gaudi/compose.yaml @@ -4,22 +4,18 @@ x-common-environment: &common-env - no_proxy: ${NO_PROXY} - http_proxy: ${HTTP_PROXY} - https_proxy: ${HTTPS_PROXY} + no_proxy: ${no_proxy} + http_proxy: ${http_proxy} + https_proxy: ${https_proxy} x-common-agent-environment: &common-agent-env <<: *common-env HF_TOKEN: ${HF_TOKEN} - llm_endpoint_url: ${LLM_ENDPOINT} model: ${LLM_MODEL_ID} - REDIS_URL_VECTOR: ${REDIS_URL_VECTOR} - REDIS_URL_KV: ${REDIS_URL_KV} - TEI_EMBEDDING_ENDPOINT: ${TEI_EMBEDDING_ENDPOINT} - ip_address: ${HOST_IP} - strategy: react_llama - require_human_feedback: false + TAVILY_API_KEY: ${TAVILY_API_KEY} + OPENAI_API_KEY: ${OPENAI_API_KEY} + OPENAI_BASE_URL: ${OPENAI_BASE_URL} services: @@ -27,7 +23,7 @@ services: image: ${REGISTRY:-opea}/vllm-gaudi:${TAG:-latest} container_name: vllm-gaudi-server ports: - - "8086:8000" + - "8000:8000" volumes: - ${HF_CACHE_DIR:-./data}:/data environment: @@ -41,7 +37,7 @@ services: VLLM_SKIP_WARMUP: true PT_HPU_ENABLE_LAZY_COLLECTIVES: true healthcheck: - test: ["CMD-SHELL", "curl -f http://$HOST_IP:8086/health || exit 1"] + test: ["CMD-SHELL", "curl -f http://$HOST_IP:8000/health || exit 1"] interval: 10s timeout: 10s retries: 100 @@ -51,165 +47,11 @@ services: ipc: host command: --model ${LLM_MODEL_ID} --tensor-parallel-size ${NUM_CARDS} --host 0.0.0.0 --port 8000 --max-seq-len-to-capture $MAX_LEN - tei-embedding-serving: - image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.7 - container_name: tei-embedding-serving - entrypoint: /bin/sh -c "apt-get update && apt-get install -y curl && text-embeddings-router --json-output --model-id ${EMBEDDING_MODEL_ID} --auto-truncate" + deep-research-agent-server: + image: ${REGISTRY:-opea}/deep-research-agent:${TAG:-latest} + container_name: deep-research-agent-server ports: - - "${TEI_EMBEDDER_PORT:-10221}:80" - volumes: - - ${HF_CACHE_DIR:-./data}:/data - shm_size: 1g - environment: - <<: *common-env - HF_TOKEN: ${HF_TOKEN} - host_ip: ${HOST_IP} - healthcheck: - test: ["CMD", "curl", "-f", "http://${HOST_IP}:${TEI_EMBEDDER_PORT}/health"] - interval: 10s - timeout: 6s - retries: 48 - - redis-vector-db: - image: redis/redis-stack:7.2.0-v9 - container_name: redis-vector-db - ports: - - "${REDIS_PORT1:-6379}:6379" - - "${REDIS_PORT2:-8001}:8001" - environment: - <<: *common-env - healthcheck: - test: ["CMD", "redis-cli", "ping"] - timeout: 10s - retries: 3 - start_period: 10s - - redis-kv-store: - image: redis/redis-stack:7.2.0-v9 - container_name: redis-kv-store - ports: - - "${REDIS_PORT3:-6380}:6379" - - "${REDIS_PORT4:-8002}:8001" - environment: - <<: *common-env - healthcheck: - test: ["CMD", "redis-cli", "ping"] - timeout: 10s - retries: 3 - start_period: 10s - - dataprep-redis-finance: - image: ${REGISTRY:-opea}/dataprep:${TAG:-latest} - container_name: dataprep-redis-server-finance - depends_on: - redis-vector-db: - condition: service_healthy - redis-kv-store: - condition: service_healthy - tei-embedding-serving: - condition: service_healthy - ports: - - "${DATAPREP_PORT:-6007}:5000" - environment: - <<: *common-env - DATAPREP_COMPONENT_NAME: ${DATAPREP_COMPONENT_NAME} - REDIS_URL_VECTOR: ${REDIS_URL_VECTOR} - REDIS_URL_KV: ${REDIS_URL_KV} - TEI_EMBEDDING_ENDPOINT: ${TEI_EMBEDDING_ENDPOINT} - LLM_ENDPOINT: ${LLM_ENDPOINT} - LLM_MODEL: ${LLM_MODEL_ID} - HF_TOKEN: ${HF_TOKEN} - LOGFLAG: true - - worker-finqa-agent: - image: opea/agent:latest - container_name: finqa-agent-endpoint - volumes: - - ${TOOLSET_PATH}:/home/user/tools/ - - ${PROMPT_PATH}:/home/user/prompts/ + - "8022:8022" ipc: host - ports: - - "9095:9095" - environment: - <<: *common-agent-env - with_memory: false - recursion_limit: ${RECURSION_LIMIT_WORKER} - temperature: ${TEMPERATURE} - max_new_tokens: ${MAX_TOKENS} - stream: false - tools: /home/user/tools/finqa_agent_tools.yaml - custom_prompt: /home/user/prompts/finqa_prompt.py - port: 9095 - - worker-research-agent: - image: opea/agent:latest - container_name: research-agent-endpoint - volumes: - - ${TOOLSET_PATH}:/home/user/tools/ - - ${PROMPT_PATH}:/home/user/prompts/ - ipc: host - ports: - - "9096:9096" environment: <<: *common-agent-env - with_memory: false - recursion_limit: ${RECURSION_LIMIT_WORKER} - stream: false - tools: /home/user/tools/research_agent_tools.yaml - custom_prompt: /home/user/prompts/research_prompt.py - FINNHUB_API_KEY: ${FINNHUB_API_KEY} - FINANCIAL_DATASETS_API_KEY: ${FINANCIAL_DATASETS_API_KEY} - port: 9096 - - docsum-vllm-gaudi: - image: opea/llm-docsum:latest - container_name: docsum-vllm-gaudi - ports: - - ${DOCSUM_PORT:-9000}:9000 - ipc: host - environment: - <<: *common-env - LLM_ENDPOINT: ${LLM_ENDPOINT} - LLM_MODEL_ID: ${LLM_MODEL_ID} - HF_TOKEN: ${HF_TOKEN} - LOGFLAG: ${LOGFLAG:-False} - MAX_INPUT_TOKENS: ${MAX_INPUT_TOKENS} - MAX_TOTAL_TOKENS: ${MAX_TOTAL_TOKENS} - DocSum_COMPONENT_NAME: ${DOCSUM_COMPONENT_NAME:-OpeaDocSumvLLM} - restart: unless-stopped - - supervisor-react-agent: - image: opea/agent:latest - container_name: supervisor-agent-endpoint - volumes: - - ${TOOLSET_PATH}:/home/user/tools/ - - ${PROMPT_PATH}:/home/user/prompts/ - ipc: host - depends_on: - - worker-finqa-agent - - worker-research-agent - ports: - - "9090:9090" - environment: - <<: *common-agent-env - with_memory: "true" - recursion_limit: ${RECURSION_LIMIT_SUPERVISOR} - temperature: ${TEMPERATURE} - max_new_tokens: ${MAX_TOKENS} - stream: "true" - tools: /home/user/tools/supervisor_agent_tools.yaml - custom_prompt: /home/user/prompts/supervisor_prompt.py - WORKER_FINQA_AGENT_URL: ${WORKER_FINQA_AGENT_URL} - WORKER_RESEARCH_AGENT_URL: ${WORKER_RESEARCH_AGENT_URL} - DOCSUM_ENDPOINT: ${DOCSUM_ENDPOINT} - port: 9090 - - agent-ui: - image: opea/agent-ui:latest - container_name: agent-ui - environment: - <<: *common-env - host_ip: ${HOST_IP} - ports: - - "5175:8080" - ipc: host diff --git a/DeepResearchAgent/docker_compose/intel/set_env.sh b/DeepResearchAgent/docker_compose/intel/set_env.sh index c8a36fabb0..5264384d60 100644 --- a/DeepResearchAgent/docker_compose/intel/set_env.sh +++ b/DeepResearchAgent/docker_compose/intel/set_env.sh @@ -20,70 +20,27 @@ check_var() { # Check critical variables check_var "HF_TOKEN" -check_var "HOST_IP" +export ip_address=$(hostname -I | awk '{print $1}') # VLLM configuration -export VLLM_PORT="${VLLM_PORT:-8086}" +export VLLM_PORT="${VLLM_PORT:-8000}" export VLLM_VOLUME="${VLLM_VOLUME:-/data2/huggingface}" export VLLM_IMAGE="${VLLM_IMAGE:-opea/vllm-gaudi:latest}" export LLM_MODEL_ID="${LLM_MODEL_ID:-meta-llama/Llama-3.3-70B-Instruct}" -export LLM_ENDPOINT="http://${HOST_IP}:${VLLM_PORT}" -export MAX_LEN="${MAX_LEN:-16384}" +export MAX_LEN="${MAX_LEN:-131072}" export NUM_CARDS="${NUM_CARDS:-4}" export HF_CACHE_DIR="${HF_CACHE_DIR:-"./data"}" +export OPENAI_BASE_URL="http://${ip_address}:8000/v1" +export OPENAI_API_KEY="empty" +export no_proxy=${no_proxy} +export http_proxy=${http_proxy} +export https_proxy=${https_proxy} -# Data preparation and embedding configuration -export DATAPREP_PORT="${DATAPREP_PORT:-6007}" -export TEI_EMBEDDER_PORT="${TEI_EMBEDDER_PORT:-10221}" -export REDIS_URL_VECTOR="redis://${HOST_IP}:6379" -export REDIS_URL_KV="redis://${HOST_IP}:6380" -export DATAPREP_COMPONENT_NAME="${DATAPREP_COMPONENT_NAME:-OPEA_DATAPREP_REDIS_FINANCE}" -export EMBEDDING_MODEL_ID="${EMBEDDING_MODEL_ID:-BAAI/bge-base-en-v1.5}" -export TEI_EMBEDDING_ENDPOINT="http://${HOST_IP}:${TEI_EMBEDDER_PORT}" # Hugging Face API token export HF_TOKEN="${HF_TOKEN}" -# Recursion limits -export RECURSION_LIMIT_WORKER="${RECURSION_LIMIT_WORKER:-12}" -export RECURSION_LIMIT_SUPERVISOR="${RECURSION_LIMIT_SUPERVISOR:-10}" - -# LLM configuration -export TEMPERATURE="${TEMPERATURE:-0.5}" -export MAX_TOKENS="${MAX_TOKENS:-4096}" -export MAX_INPUT_TOKENS="${MAX_INPUT_TOKENS:-2048}" -export MAX_TOTAL_TOKENS="${MAX_TOTAL_TOKENS:-4096}" - -# Worker URLs -export WORKER_FINQA_AGENT_URL="http://${HOST_IP}:9095/v1/chat/completions" -export WORKER_RESEARCH_AGENT_URL="http://${HOST_IP}:9096/v1/chat/completions" - -# DocSum configuration -export DOCSUM_COMPONENT_NAME="${DOCSUM_COMPONENT_NAME:-"OpeaDocSumvLLM"}" -export DOCSUM_ENDPOINT="http://${HOST_IP}:9000/v1/docsum" - # API keys -check_var "FINNHUB_API_KEY" -check_var "FINANCIAL_DATASETS_API_KEY" -export FINNHUB_API_KEY="${FINNHUB_API_KEY}" -export FINANCIAL_DATASETS_API_KEY="${FINANCIAL_DATASETS_API_KEY}" - - -# Toolset and prompt paths -if check_var "WORKDIR"; then - export TOOLSET_PATH=$WORKDIR/GenAIExamples/FinanceAgent/tools/ - export PROMPT_PATH=$WORKDIR/GenAIExamples/FinanceAgent/prompts/ - - echo "TOOLSET_PATH=${TOOLSET_PATH}" - echo "PROMPT_PATH=${PROMPT_PATH}" - - # Array of directories to check - REQUIRED_DIRS=("${TOOLSET_PATH}" "${PROMPT_PATH}") +check_var "TAVILY_API_KEY" +export TAVILY_API_KEY="${TAVILY_API_KEY}" - for dir in "${REQUIRED_DIRS[@]}"; do - if [ ! -d "${dir}" ]; then - echo "Error: Required directory does not exist: ${dir}" - exit 1 - fi - done -fi diff --git a/DeepResearchAgent/docker_image_build/build.yaml b/DeepResearchAgent/docker_image_build/build.yaml new file mode 100644 index 0000000000..e7e5fb794e --- /dev/null +++ b/DeepResearchAgent/docker_image_build/build.yaml @@ -0,0 +1,13 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +services: + deep_research_agent: + build: + args: + http_proxy: ${http_proxy} + https_proxy: ${https_proxy} + no_proxy: ${no_proxy} + context: ../ + dockerfile: ./Dockerfile + image: ${REGISTRY:-opea}/deep-research-agent:${TAG:-latest} diff --git a/DeepResearchAgent/research_agent.py b/DeepResearchAgent/research_agent.py index 3eda52e06f..a6f66eefcc 100644 --- a/DeepResearchAgent/research_agent.py +++ b/DeepResearchAgent/research_agent.py @@ -30,7 +30,9 @@ async def run(request: SimpleRequest): question = f"Question: {request.question}" - return agent(goal=question) + result = await agent(question) + + return result if __name__ == "__main__": diff --git a/DeepResearchAgent/tests/test_compose_on_gaudi.sh b/DeepResearchAgent/tests/test_compose_on_gaudi.sh new file mode 100644 index 0000000000..e84e68484c --- /dev/null +++ b/DeepResearchAgent/tests/test_compose_on_gaudi.sh @@ -0,0 +1,117 @@ +#!/bin/bash +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +set -xe +IMAGE_REPO=${IMAGE_REPO:-"opea"} +IMAGE_TAG=${IMAGE_TAG:-"latest"} +echo "REGISTRY=IMAGE_REPO=${IMAGE_REPO}" +echo "TAG=IMAGE_TAG=${IMAGE_TAG}" +export REGISTRY=${IMAGE_REPO} +export TAG=${IMAGE_TAG} +export MODEL_CACHE=${model_cache:-"./data"} +export HF_TOKEN=${HF_TOKEN} +export TAVILY_API_KEY=${TAVILY_API_KEY} + +WORKPATH=$(dirname "$PWD") +LOG_PATH="$WORKPATH/tests" +ip_address=$(hostname -I | awk '{print $1}') + +function build_docker_images() { + cd $WORKPATH/docker_image_build + echo "Build all the images with --no-cache, check docker_image_build.log for details..." + docker compose -f build.yaml build --no-cache > ${LOG_PATH}/docker_image_build.log + + docker images && sleep 1s +} + +function start_services() { + cd $WORKPATH/docker_compose/intel/hpu/gaudi + source set_env.sh + + # Start Docker Containers + docker compose -f compose.yaml up -d --quiet-pull > ${LOG_PATH}/start_services_with_compose.log + n=0 + until [[ "$n" -ge 200 ]]; do + echo "n=$n" + docker logs vllm-gaudi-server > vllm_service_start.log 2>&1 + if grep -q "Warmup finished" vllm_service_start.log; then + break + fi + sleep 5s + n=$((n+1)) + done +} + +function validate_service() { + local URL="$1" + local EXPECTED_RESULT="$2" + local SERVICE_NAME="$3" + local DOCKER_NAME="$4" + local INPUT_DATA="$5" + + local HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST -d "$INPUT_DATA" -H 'Content-Type: application/json' "$URL") + if [ "$HTTP_STATUS" -eq 200 ]; then + echo "[ $SERVICE_NAME ] HTTP status is 200. Checking content..." + + local CONTENT=$(curl -s -X POST -d "$INPUT_DATA" -H 'Content-Type: application/json' "$URL" | tee ${LOG_PATH}/${SERVICE_NAME}.log) + + if echo "$CONTENT" | grep -q "$EXPECTED_RESULT"; then + echo "[ $SERVICE_NAME ] Content is as expected." + else + echo "[ $SERVICE_NAME ] Content does not match the expected result: $CONTENT" + docker logs ${DOCKER_NAME} >> ${LOG_PATH}/${SERVICE_NAME}.log + exit 1 + fi + else + echo "[ $SERVICE_NAME ] HTTP status is not 200. Received status was $HTTP_STATUS" + docker logs ${DOCKER_NAME} >> ${LOG_PATH}/${SERVICE_NAME}.log + exit 1 + fi + sleep 1s +} + +function validate_microservices() { + # Check if the microservices are running correctly. + + validate_service \ + "${ip_address}:8022/v1/deep_research_agent" \ + "deep" \ + "deep-research-agent" \ + "deep-research-agent-server" \ + '{"question": "what is the deep learning?"}' +} + + +function stop_docker() { + cd $WORKPATH/docker_compose/intel/hpu/gaudi + docker compose -f compose.yaml down +} + +function main() { + + echo "::group::stop_docker" + stop_docker + echo "::endgroup::" + + echo "::group::build_docker_images" + if [[ "$IMAGE_REPO" == "opea" ]]; then build_docker_images; fi + echo "::endgroup::" + + echo "::group::start_services" + start_services + echo "::endgroup::" + + echo "::group::validate_microservices" + validate_microservices + echo "::endgroup::" + + echo "::group::stop_docker" + stop_docker + echo "::endgroup::" + + docker system prune -f + +} + +main diff --git a/DeepResearchAgent/utils.py b/DeepResearchAgent/utils.py index fb099531ff..083e0d23d9 100644 --- a/DeepResearchAgent/utils.py +++ b/DeepResearchAgent/utils.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: Apache-2.0 from typing import Any - import yaml @@ -23,6 +22,7 @@ def create_agent(config: str, return_instance: bool = False) -> Any: from langgraph.checkpoint.memory import MemorySaver from open_deep_research.graph import builder + from langgraph.types import Command except ImportError as e: raise ImportError( f"Failed to import required modules for langchain deep researcher: {e}. Make sure langgraph and open_deep_research are installed. Also make sure that the benchmark directory is in your path. Also, you might need to install the with-open-deep-research extra dependencies (see README.md)." @@ -53,19 +53,15 @@ def create_agent(config: str, return_instance: bool = False) -> Any: writer_endpoint = agent_config.get("writer_endpoint") max_search_depth = agent_config.get("max_search_depth", 3) - def langchain_wrapper(goal: str): - import asyncio - + async def langchain_wrapper(goal: str): thread = { "configurable": { "thread_id": str(uuid.uuid4()), "search_api": search_api, "planner_provider": planner_provider, "planner_model": planner_model, - "planner_model_kwargs": {"base_url": planner_endpoint}, "writer_provider": writer_provider, "writer_model": writer_model, - "writer_model_kwargs": {"base_url": writer_endpoint}, "max_search_depth": max_search_depth, "report_structure": REPORT_STRUCTURE, } @@ -76,20 +72,15 @@ def langchain_wrapper(goal: str): results = [] - async def run_graph(): - async for event in graph.astream({"topic": goal}, thread, stream_mode="updates"): - results.append(event) - - from langgraph.types import Command - - async for event in graph.astream(Command(resume=True), thread, stream_mode="updates"): - results.append(event) + async for event in graph.astream({"topic": goal}, thread, stream_mode="updates"): + results.append(event) - final_state = graph.get_state(thread) - report = final_state.values.get("final_report") + async for event in graph.astream(Command(resume=True), thread, stream_mode="updates"): + results.append(event) - return report + final_state = graph.get_state(thread) + report = final_state.values.get("final_report") - return asyncio.run(run_graph()) + return report return langchain_wrapper From 04b8dec39be52fc420fde3a2dda7cdd935393801 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 12:12:47 +0000 Subject: [PATCH 04/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- DeepResearchAgent/README.md | 2 -- DeepResearchAgent/docker_compose/intel/set_env.sh | 1 - DeepResearchAgent/utils.py | 3 ++- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/DeepResearchAgent/README.md b/DeepResearchAgent/README.md index eb9a5d732f..fa7cc76b7a 100644 --- a/DeepResearchAgent/README.md +++ b/DeepResearchAgent/README.md @@ -1,9 +1,7 @@ - ## Deep Research Agent Application Deep Research Agents are a new class of autonomous AI systems designed to perform complex, multi-step research tasks that typically require human-level reasoning, planning, and synthesis. - ## Setup Deployment Environment ``` diff --git a/DeepResearchAgent/docker_compose/intel/set_env.sh b/DeepResearchAgent/docker_compose/intel/set_env.sh index 5264384d60..a3f546e68b 100644 --- a/DeepResearchAgent/docker_compose/intel/set_env.sh +++ b/DeepResearchAgent/docker_compose/intel/set_env.sh @@ -43,4 +43,3 @@ export HF_TOKEN="${HF_TOKEN}" # API keys check_var "TAVILY_API_KEY" export TAVILY_API_KEY="${TAVILY_API_KEY}" - diff --git a/DeepResearchAgent/utils.py b/DeepResearchAgent/utils.py index 083e0d23d9..944cfff35e 100644 --- a/DeepResearchAgent/utils.py +++ b/DeepResearchAgent/utils.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 from typing import Any + import yaml @@ -21,8 +22,8 @@ def create_agent(config: str, return_instance: bool = False) -> Any: import uuid from langgraph.checkpoint.memory import MemorySaver - from open_deep_research.graph import builder from langgraph.types import Command + from open_deep_research.graph import builder except ImportError as e: raise ImportError( f"Failed to import required modules for langchain deep researcher: {e}. Make sure langgraph and open_deep_research are installed. Also make sure that the benchmark directory is in your path. Also, you might need to install the with-open-deep-research extra dependencies (see README.md)." From 2a125511182b6452c3f6987069b8ea48bc7fb8ef Mon Sep 17 00:00:00 2001 From: kaokao Date: Tue, 8 Jul 2025 03:05:10 +0000 Subject: [PATCH 05/13] update ut. --- .../docker_compose/intel/hpu/gaudi/compose.yaml | 2 ++ .../docker_compose/intel/{ => hpu/gaudi}/set_env.sh | 5 ++++- DeepResearchAgent/research_agent.py | 6 +++--- DeepResearchAgent/utils.py | 5 ++--- 4 files changed, 11 insertions(+), 7 deletions(-) rename DeepResearchAgent/docker_compose/intel/{ => hpu/gaudi}/set_env.sh (91%) diff --git a/DeepResearchAgent/docker_compose/intel/hpu/gaudi/compose.yaml b/DeepResearchAgent/docker_compose/intel/hpu/gaudi/compose.yaml index 23da536958..dc7a05c270 100644 --- a/DeepResearchAgent/docker_compose/intel/hpu/gaudi/compose.yaml +++ b/DeepResearchAgent/docker_compose/intel/hpu/gaudi/compose.yaml @@ -50,6 +50,8 @@ services: deep-research-agent-server: image: ${REGISTRY:-opea}/deep-research-agent:${TAG:-latest} container_name: deep-research-agent-server + depends_on: + - vllm-service ports: - "8022:8022" ipc: host diff --git a/DeepResearchAgent/docker_compose/intel/set_env.sh b/DeepResearchAgent/docker_compose/intel/hpu/gaudi/set_env.sh similarity index 91% rename from DeepResearchAgent/docker_compose/intel/set_env.sh rename to DeepResearchAgent/docker_compose/intel/hpu/gaudi/set_env.sh index a3f546e68b..84c917b648 100644 --- a/DeepResearchAgent/docker_compose/intel/set_env.sh +++ b/DeepResearchAgent/docker_compose/intel/hpu/gaudi/set_env.sh @@ -4,7 +4,9 @@ # SPDX-License-Identifier: Apache-2.0 # Navigate to the parent directory and source the environment -pushd "../../" > /dev/null +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd) + +pushd "$SCRIPT_DIR/../../../../../" > /dev/null source .set_env.sh popd > /dev/null @@ -43,3 +45,4 @@ export HF_TOKEN="${HF_TOKEN}" # API keys check_var "TAVILY_API_KEY" export TAVILY_API_KEY="${TAVILY_API_KEY}" + diff --git a/DeepResearchAgent/research_agent.py b/DeepResearchAgent/research_agent.py index a6f66eefcc..24f1f9af12 100644 --- a/DeepResearchAgent/research_agent.py +++ b/DeepResearchAgent/research_agent.py @@ -10,10 +10,10 @@ from comps import opea_microservices, register_microservice from comps.cores.telemetry.opea_telemetry import opea_telemetry from pydantic import BaseModel -from utils import * - -agent = create_agent("./deep_researcher.yaml") +from utils import create_agent +config_path = os.path.join(os.path.dirname(__file__), "deep_researcher.yaml") +agent = create_agent(config_path) class SimpleRequest(BaseModel): question: Union[str, List[str]] diff --git a/DeepResearchAgent/utils.py b/DeepResearchAgent/utils.py index 944cfff35e..f85d9267b9 100644 --- a/DeepResearchAgent/utils.py +++ b/DeepResearchAgent/utils.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: Apache-2.0 from typing import Any - import yaml @@ -11,7 +10,7 @@ def load_config(config_path: str): return yaml.safe_load(file) -def create_agent(config: str, return_instance: bool = False) -> Any: +def create_agent(config: str) -> Any: config_dict = load_config(config) @@ -22,8 +21,8 @@ def create_agent(config: str, return_instance: bool = False) -> Any: import uuid from langgraph.checkpoint.memory import MemorySaver - from langgraph.types import Command from open_deep_research.graph import builder + from langgraph.types import Command except ImportError as e: raise ImportError( f"Failed to import required modules for langchain deep researcher: {e}. Make sure langgraph and open_deep_research are installed. Also make sure that the benchmark directory is in your path. Also, you might need to install the with-open-deep-research extra dependencies (see README.md)." From ac67a6ab7561fa9868155d579f7f3a14cb37f087 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 03:01:55 +0000 Subject: [PATCH 06/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- DeepResearchAgent/docker_compose/intel/hpu/gaudi/set_env.sh | 1 - DeepResearchAgent/research_agent.py | 1 + DeepResearchAgent/utils.py | 3 ++- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/DeepResearchAgent/docker_compose/intel/hpu/gaudi/set_env.sh b/DeepResearchAgent/docker_compose/intel/hpu/gaudi/set_env.sh index 84c917b648..e38d0ef378 100644 --- a/DeepResearchAgent/docker_compose/intel/hpu/gaudi/set_env.sh +++ b/DeepResearchAgent/docker_compose/intel/hpu/gaudi/set_env.sh @@ -45,4 +45,3 @@ export HF_TOKEN="${HF_TOKEN}" # API keys check_var "TAVILY_API_KEY" export TAVILY_API_KEY="${TAVILY_API_KEY}" - diff --git a/DeepResearchAgent/research_agent.py b/DeepResearchAgent/research_agent.py index 24f1f9af12..b11138c233 100644 --- a/DeepResearchAgent/research_agent.py +++ b/DeepResearchAgent/research_agent.py @@ -15,6 +15,7 @@ config_path = os.path.join(os.path.dirname(__file__), "deep_researcher.yaml") agent = create_agent(config_path) + class SimpleRequest(BaseModel): question: Union[str, List[str]] diff --git a/DeepResearchAgent/utils.py b/DeepResearchAgent/utils.py index f85d9267b9..5a048df61e 100644 --- a/DeepResearchAgent/utils.py +++ b/DeepResearchAgent/utils.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 from typing import Any + import yaml @@ -21,8 +22,8 @@ def create_agent(config: str) -> Any: import uuid from langgraph.checkpoint.memory import MemorySaver - from open_deep_research.graph import builder from langgraph.types import Command + from open_deep_research.graph import builder except ImportError as e: raise ImportError( f"Failed to import required modules for langchain deep researcher: {e}. Make sure langgraph and open_deep_research are installed. Also make sure that the benchmark directory is in your path. Also, you might need to install the with-open-deep-research extra dependencies (see README.md)." From b8f1ca90d9172ac80273514825872dc7ffd5dc23 Mon Sep 17 00:00:00 2001 From: lkk <33276950+lkk12014402@users.noreply.github.com> Date: Tue, 8 Jul 2025 11:04:20 +0800 Subject: [PATCH 07/13] update image build build.yaml --- DeepResearchAgent/docker_image_build/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DeepResearchAgent/docker_image_build/build.yaml b/DeepResearchAgent/docker_image_build/build.yaml index e7e5fb794e..105538e5cb 100644 --- a/DeepResearchAgent/docker_image_build/build.yaml +++ b/DeepResearchAgent/docker_image_build/build.yaml @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 services: - deep_research_agent: + deep-research-agent: build: args: http_proxy: ${http_proxy} From c31c8fc7e502286e30893ccf3094023b62d90949 Mon Sep 17 00:00:00 2001 From: lkk12014402 Date: Tue, 8 Jul 2025 03:45:09 +0000 Subject: [PATCH 08/13] update ut image. --- DeepResearchAgent/benchmark/accuracy/eval.py | 298 ++++++++++++++++++ .../docker_image_build/build.yaml | 8 + .../tests/test_compose_on_gaudi.sh | 10 + 3 files changed, 316 insertions(+) create mode 100644 DeepResearchAgent/benchmark/accuracy/eval.py diff --git a/DeepResearchAgent/benchmark/accuracy/eval.py b/DeepResearchAgent/benchmark/accuracy/eval.py new file mode 100644 index 0000000000..9f63a61022 --- /dev/null +++ b/DeepResearchAgent/benchmark/accuracy/eval.py @@ -0,0 +1,298 @@ +import argparse +import concurrent.futures +import json +import os +import time +from typing import Any, Tuple +from datasets import load_dataset +import requests + +def load_questions(dataset_names: list[str] | None = None) -> list[dict[str, str]]: + """ + Load questions from the specified Hugging Face dataset configurations. + + Args: + dataset_names: List of dataset configurations to load + Options: + "smolagents:simpleqa", + "hotpotqa", + "simpleqa", + "together-search-bench" + If None, all available configurations except hotpotqa will be loaded + + Returns: + List of question-answer pairs + """ + if dataset_names is None: + dataset_names = ["smolagents:simpleqa"] + + all_questions = [] + + for dataset_name in dataset_names: + print(f"Loading dataset: {dataset_name}") + + try: + if dataset_name == "together-search-bench": + # Load Together-Search-Bench dataset + dataset_path = "togethercomputer/together-search-bench" + ds = load_dataset(dataset_path) + if "test" in ds: + split_data = ds["test"] + else: + print( + f"No 'test' split found in dataset at {dataset_path}") + continue + + for i in range(len(split_data)): + item = split_data[i] + question_data = { + "question": item["question"], + "answer": item["answer"], + "dataset": item.get("dataset", "together-search-bench"), + } + all_questions.append(question_data) + + print( + f"Loaded {len(split_data)} questions from together-search-bench dataset") + continue + + elif dataset_name == "hotpotqa": + # Load HotpotQA dataset (using distractor version for validation) + ds = load_dataset("hotpotqa/hotpot_qa", + "distractor", trust_remote_code=True) + split_name = "validation" + elif dataset_name == "simpleqa": + ds = load_dataset("basicv8vc/SimpleQA") + split_name = "test" + else: + # Strip "smolagents:" prefix when loading the dataset + actual_dataset = dataset_name.split(":")[-1] + ds = load_dataset("smolagents/benchmark-v1", actual_dataset) + split_name = "test" + + except Exception as e: + print(f"Failed to load dataset {dataset_name}: {str(e)}") + continue # Skip this dataset if it fails to load + + + print(f"Dataset structure for {dataset_name}: {ds}") + print(f"Available splits: {list(ds)}") + + split_data = ds[split_name] # type: ignore + + for i in range(len(split_data)): + item = split_data[i] + + if dataset_name == "hotpotqa": + # we remove questions that are easy or medium (if any) just to reduce the number of questions + if item["level"] != "hard": + continue + + question_data = { + "question": item["question"], + "answer": item["answer"], + "dataset": dataset_name, + } + elif dataset_name == "simpleqa": + # Handle SimpleQA dataset format + question_data = { + "question": item["problem"], + "answer": item["answer"], + "dataset": dataset_name, + } + else: + question_data = { + "question": item["question"], + "answer": item["true_answer"], + "dataset": dataset_name, + } + + all_questions.append(question_data) + + print(f"Loaded {len(all_questions)} questions in total") + return all_questions + + +def process_single_question( + question_data: dict[str, str], + idx: int, + total: int, + callback: ScoringFunction, + agent_config: str = "../configs/base_llm_config.yaml", +) -> dict[str, Any]: + """ + Process a single benchmark question with the agent. + + Args: + question_data: Dictionary containing question and answer + idx: Index of the current question + total: Total number of questions + model: LLM model to use + max_steps: Maximum steps for the agent + callback: Optional callback function for evaluation + agent_config: Path to the agent config (default: ../configs/base_llm_config.yaml) + + Returns: + Dictionary with question, answers and evaluation results + """ + question = question_data["question"] + correct_answer = question_data["answer"] + + + """ + + + """ + + result = Result(question=question, agent_answer=agent_answer, + correct_answer=correct_answer) + + evaluation = callback(result) + + single_benchmark_result = { + "question": question, + "correct_answer": correct_answer, + "agent_answer": agent_answer, + "evaluation": evaluation, + "metadata": {k: v for k, v in question_data.items() if k not in ["question", "answer"]}, + } + print(single_benchmark_result) + + return single_benchmark_result + + +def run_benchmark( + questions: list[dict[str, str]], + callback: ScoringFunction, + agent_config: str = "../configs/base_llm_config.yaml", + max_workers: int = 2, +) -> Tuple[float, list[dict[str, Any]]]: + """ + Run the benchmark on a list of questions concurrently. + + Args: + questions: List of question-answer pairs + callback: Function to evaluate agent answers against ground truth + model: LLM model to use for the agent + max_steps: Maximum number of steps the agent can take + max_workers: Number of concurrent threads to use + agent_type: Type of agent to create (default: research_agent) + + Returns: + Tuple of (accuracy score, detailed results) + """ + + results = [] + total_questions = len(questions) + details = [] + + # Use ThreadPoolExecutor to run questions concurrently + with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: + # Create a list of future objects + future_to_idx = { + executor.submit(process_single_question, question_data, idx, total_questions, callback, agent_config): idx + for idx, question_data in enumerate(questions) + } + + # Process results as they complete + for future in concurrent.futures.as_completed(future_to_idx): + idx = future_to_idx[future] + try: + result = future.result() + results.append(result["evaluation"]) + details.append(result) + print(f"Completed question {idx+1}/{total_questions}") + except Exception as exc: + import traceback + traceback.print_exc() + print(f"Question {idx+1} generated an exception: {exc}") + results.append(0) + details.append({"question": questions[idx]["question"], "agent_answer": str( + exc), "evaluation": 0}) + + return sum(results) / len(results), details + + +def main(): + """ + Main function to run the benchmark. + """ + + # Set up argument parser + parser = argparse.ArgumentParser( + description="Run scoring with benchmarking options") + parser.add_argument( + "--datasets", + nargs="+", + choices=["smolagents:simpleqa", "hotpotqa", + "simpleqa", "together-search-bench"], + help="Specific datasets to load (default: all)", + default=["together-search-bench"], + ) + parser.add_argument("--limit", type=int, default=None, + help="Limit number of questions to process (default: all)") + parser.add_argument( + "--service-url", + default="http://localhost:8022/v1/deep_research_agent", + help="the endpoint of deep research agent.", + ) + parser.add_argument( + "--llm-endpoint", + default="http://localhost:8000/v1/", + help="llm service for llm-as-judge.", + ) + parser.add_argument( + "--max-workers", + type=int, + default=1, + help="Number of concurrent workers (default: 1)", + ) + + args = parser.parse_args() + + questions = load_questions(args.datasets) + + + if args.limit is not None: + questions = questions[: args.limit] + print(f"Limited to {len(questions)} questions") + + results, details = run_benchmark( + questions, + callback=llm_as_a_judge_scoring, + max_workers=args.max_workers, + agent_config=args.agent_config, + ) + + print(f"Completed benchmark with {results} accuracy") + + benchmark_results_dir = os.path.join(os.path.dirname( + os.path.dirname(__file__)), "benchmark", "benchmark_results") + os.makedirs(benchmark_results_dir, exist_ok=True) + + output_file = os.path.join( + benchmark_results_dir, + f"benchmark_{'_'.join(args.datasets)}_{time.strftime('%Y-%m-%d_%H-%M-%S', time.localtime())}.json", + ) + + output_data = { + "metadata": { + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), + "datasets": args.datasets, + "agent_config": args.agent_config, + "scoring_method": "llm_as_a_judge_scoring", + "sample_count": len(questions), + }, + "overall_accuracy": results, + "question_details": details, + } + + with open(output_file, "w") as f: + json.dump(output_data, f, indent=2) + + return results + + +if __name__ == "__main__": + main() + diff --git a/DeepResearchAgent/docker_image_build/build.yaml b/DeepResearchAgent/docker_image_build/build.yaml index 105538e5cb..5ac2e17a90 100644 --- a/DeepResearchAgent/docker_image_build/build.yaml +++ b/DeepResearchAgent/docker_image_build/build.yaml @@ -5,9 +5,17 @@ services: deep-research-agent: build: args: + IMAGE_REPO: ${REGISTRY} + BASE_TAG: ${TAG} http_proxy: ${http_proxy} https_proxy: ${https_proxy} no_proxy: ${no_proxy} context: ../ dockerfile: ./Dockerfile image: ${REGISTRY:-opea}/deep-research-agent:${TAG:-latest} + vllm-gaudi: + build: + context: vllm-fork + dockerfile: Dockerfile.hpu + extends: deep-research-agent + image: ${REGISTRY:-opea}/vllm-gaudi:${TAG:-latest} diff --git a/DeepResearchAgent/tests/test_compose_on_gaudi.sh b/DeepResearchAgent/tests/test_compose_on_gaudi.sh index e84e68484c..ab03eb7e32 100644 --- a/DeepResearchAgent/tests/test_compose_on_gaudi.sh +++ b/DeepResearchAgent/tests/test_compose_on_gaudi.sh @@ -18,7 +18,17 @@ LOG_PATH="$WORKPATH/tests" ip_address=$(hostname -I | awk '{print $1}') function build_docker_images() { + opea_branch=${opea_branch:-"main"} cd $WORKPATH/docker_image_build + git clone --depth 1 --branch ${opea_branch} https://github.com/opea-project/GenAIComps.git + pushd GenAIComps + echo "GenAIComps test commit is $(git rev-parse HEAD)" + docker build --no-cache -t ${REGISTRY}/comps-base:${TAG} --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy -f Dockerfile . + popd && sleep 1s + git clone https://github.com/HabanaAI/vllm-fork.git && cd vllm-fork + VLLM_FORK_VER=v0.6.6.post1+Gaudi-1.20.0 + git checkout ${VLLM_FORK_VER} &> /dev/null && cd ../ + echo "Build all the images with --no-cache, check docker_image_build.log for details..." docker compose -f build.yaml build --no-cache > ${LOG_PATH}/docker_image_build.log From 18b6d09f6f85113252194f1e7c9494bb82f166e2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 03:40:13 +0000 Subject: [PATCH 09/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- DeepResearchAgent/benchmark/accuracy/eval.py | 53 ++++++++------------ 1 file changed, 20 insertions(+), 33 deletions(-) diff --git a/DeepResearchAgent/benchmark/accuracy/eval.py b/DeepResearchAgent/benchmark/accuracy/eval.py index 9f63a61022..1f635c38c6 100644 --- a/DeepResearchAgent/benchmark/accuracy/eval.py +++ b/DeepResearchAgent/benchmark/accuracy/eval.py @@ -1,15 +1,19 @@ +# Copyright (C) 2025 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + import argparse import concurrent.futures import json import os import time from typing import Any, Tuple -from datasets import load_dataset + import requests +from datasets import load_dataset + def load_questions(dataset_names: list[str] | None = None) -> list[dict[str, str]]: - """ - Load questions from the specified Hugging Face dataset configurations. + """Load questions from the specified Hugging Face dataset configurations. Args: dataset_names: List of dataset configurations to load @@ -39,8 +43,7 @@ def load_questions(dataset_names: list[str] | None = None) -> list[dict[str, str if "test" in ds: split_data = ds["test"] else: - print( - f"No 'test' split found in dataset at {dataset_path}") + print(f"No 'test' split found in dataset at {dataset_path}") continue for i in range(len(split_data)): @@ -52,14 +55,12 @@ def load_questions(dataset_names: list[str] | None = None) -> list[dict[str, str } all_questions.append(question_data) - print( - f"Loaded {len(split_data)} questions from together-search-bench dataset") + print(f"Loaded {len(split_data)} questions from together-search-bench dataset") continue elif dataset_name == "hotpotqa": # Load HotpotQA dataset (using distractor version for validation) - ds = load_dataset("hotpotqa/hotpot_qa", - "distractor", trust_remote_code=True) + ds = load_dataset("hotpotqa/hotpot_qa", "distractor", trust_remote_code=True) split_name = "validation" elif dataset_name == "simpleqa": ds = load_dataset("basicv8vc/SimpleQA") @@ -74,7 +75,6 @@ def load_questions(dataset_names: list[str] | None = None) -> list[dict[str, str print(f"Failed to load dataset {dataset_name}: {str(e)}") continue # Skip this dataset if it fails to load - print(f"Dataset structure for {dataset_name}: {ds}") print(f"Available splits: {list(ds)}") @@ -120,8 +120,7 @@ def process_single_question( callback: ScoringFunction, agent_config: str = "../configs/base_llm_config.yaml", ) -> dict[str, Any]: - """ - Process a single benchmark question with the agent. + """Process a single benchmark question with the agent. Args: question_data: Dictionary containing question and answer @@ -137,15 +136,12 @@ def process_single_question( """ question = question_data["question"] correct_answer = question_data["answer"] - - """ """ - result = Result(question=question, agent_answer=agent_answer, - correct_answer=correct_answer) + result = Result(question=question, agent_answer=agent_answer, correct_answer=correct_answer) evaluation = callback(result) @@ -167,8 +163,7 @@ def run_benchmark( agent_config: str = "../configs/base_llm_config.yaml", max_workers: int = 2, ) -> Tuple[float, list[dict[str, Any]]]: - """ - Run the benchmark on a list of questions concurrently. + """Run the benchmark on a list of questions concurrently. Args: questions: List of question-answer pairs @@ -204,33 +199,28 @@ def run_benchmark( print(f"Completed question {idx+1}/{total_questions}") except Exception as exc: import traceback + traceback.print_exc() print(f"Question {idx+1} generated an exception: {exc}") results.append(0) - details.append({"question": questions[idx]["question"], "agent_answer": str( - exc), "evaluation": 0}) + details.append({"question": questions[idx]["question"], "agent_answer": str(exc), "evaluation": 0}) return sum(results) / len(results), details def main(): - """ - Main function to run the benchmark. - """ + """Main function to run the benchmark.""" # Set up argument parser - parser = argparse.ArgumentParser( - description="Run scoring with benchmarking options") + parser = argparse.ArgumentParser(description="Run scoring with benchmarking options") parser.add_argument( "--datasets", nargs="+", - choices=["smolagents:simpleqa", "hotpotqa", - "simpleqa", "together-search-bench"], + choices=["smolagents:simpleqa", "hotpotqa", "simpleqa", "together-search-bench"], help="Specific datasets to load (default: all)", default=["together-search-bench"], ) - parser.add_argument("--limit", type=int, default=None, - help="Limit number of questions to process (default: all)") + parser.add_argument("--limit", type=int, default=None, help="Limit number of questions to process (default: all)") parser.add_argument( "--service-url", default="http://localhost:8022/v1/deep_research_agent", @@ -252,7 +242,6 @@ def main(): questions = load_questions(args.datasets) - if args.limit is not None: questions = questions[: args.limit] print(f"Limited to {len(questions)} questions") @@ -266,8 +255,7 @@ def main(): print(f"Completed benchmark with {results} accuracy") - benchmark_results_dir = os.path.join(os.path.dirname( - os.path.dirname(__file__)), "benchmark", "benchmark_results") + benchmark_results_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "benchmark", "benchmark_results") os.makedirs(benchmark_results_dir, exist_ok=True) output_file = os.path.join( @@ -295,4 +283,3 @@ def main(): if __name__ == "__main__": main() - From 596c5676d7b202afd257a2fdac58b76d16a6ccda Mon Sep 17 00:00:00 2001 From: lkk12014402 Date: Tue, 8 Jul 2025 05:20:10 +0000 Subject: [PATCH 10/13] remove benchmark script temporarily --- .../benchmark/accuracy/README.md | 1 - DeepResearchAgent/benchmark/accuracy/eval.py | 285 ------------------ 2 files changed, 286 deletions(-) delete mode 100644 DeepResearchAgent/benchmark/accuracy/README.md delete mode 100644 DeepResearchAgent/benchmark/accuracy/eval.py diff --git a/DeepResearchAgent/benchmark/accuracy/README.md b/DeepResearchAgent/benchmark/accuracy/README.md deleted file mode 100644 index e384971d41..0000000000 --- a/DeepResearchAgent/benchmark/accuracy/README.md +++ /dev/null @@ -1 +0,0 @@ -## Deep Research Agent Benchmarks diff --git a/DeepResearchAgent/benchmark/accuracy/eval.py b/DeepResearchAgent/benchmark/accuracy/eval.py deleted file mode 100644 index 1f635c38c6..0000000000 --- a/DeepResearchAgent/benchmark/accuracy/eval.py +++ /dev/null @@ -1,285 +0,0 @@ -# Copyright (C) 2025 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -import argparse -import concurrent.futures -import json -import os -import time -from typing import Any, Tuple - -import requests -from datasets import load_dataset - - -def load_questions(dataset_names: list[str] | None = None) -> list[dict[str, str]]: - """Load questions from the specified Hugging Face dataset configurations. - - Args: - dataset_names: List of dataset configurations to load - Options: - "smolagents:simpleqa", - "hotpotqa", - "simpleqa", - "together-search-bench" - If None, all available configurations except hotpotqa will be loaded - - Returns: - List of question-answer pairs - """ - if dataset_names is None: - dataset_names = ["smolagents:simpleqa"] - - all_questions = [] - - for dataset_name in dataset_names: - print(f"Loading dataset: {dataset_name}") - - try: - if dataset_name == "together-search-bench": - # Load Together-Search-Bench dataset - dataset_path = "togethercomputer/together-search-bench" - ds = load_dataset(dataset_path) - if "test" in ds: - split_data = ds["test"] - else: - print(f"No 'test' split found in dataset at {dataset_path}") - continue - - for i in range(len(split_data)): - item = split_data[i] - question_data = { - "question": item["question"], - "answer": item["answer"], - "dataset": item.get("dataset", "together-search-bench"), - } - all_questions.append(question_data) - - print(f"Loaded {len(split_data)} questions from together-search-bench dataset") - continue - - elif dataset_name == "hotpotqa": - # Load HotpotQA dataset (using distractor version for validation) - ds = load_dataset("hotpotqa/hotpot_qa", "distractor", trust_remote_code=True) - split_name = "validation" - elif dataset_name == "simpleqa": - ds = load_dataset("basicv8vc/SimpleQA") - split_name = "test" - else: - # Strip "smolagents:" prefix when loading the dataset - actual_dataset = dataset_name.split(":")[-1] - ds = load_dataset("smolagents/benchmark-v1", actual_dataset) - split_name = "test" - - except Exception as e: - print(f"Failed to load dataset {dataset_name}: {str(e)}") - continue # Skip this dataset if it fails to load - - print(f"Dataset structure for {dataset_name}: {ds}") - print(f"Available splits: {list(ds)}") - - split_data = ds[split_name] # type: ignore - - for i in range(len(split_data)): - item = split_data[i] - - if dataset_name == "hotpotqa": - # we remove questions that are easy or medium (if any) just to reduce the number of questions - if item["level"] != "hard": - continue - - question_data = { - "question": item["question"], - "answer": item["answer"], - "dataset": dataset_name, - } - elif dataset_name == "simpleqa": - # Handle SimpleQA dataset format - question_data = { - "question": item["problem"], - "answer": item["answer"], - "dataset": dataset_name, - } - else: - question_data = { - "question": item["question"], - "answer": item["true_answer"], - "dataset": dataset_name, - } - - all_questions.append(question_data) - - print(f"Loaded {len(all_questions)} questions in total") - return all_questions - - -def process_single_question( - question_data: dict[str, str], - idx: int, - total: int, - callback: ScoringFunction, - agent_config: str = "../configs/base_llm_config.yaml", -) -> dict[str, Any]: - """Process a single benchmark question with the agent. - - Args: - question_data: Dictionary containing question and answer - idx: Index of the current question - total: Total number of questions - model: LLM model to use - max_steps: Maximum steps for the agent - callback: Optional callback function for evaluation - agent_config: Path to the agent config (default: ../configs/base_llm_config.yaml) - - Returns: - Dictionary with question, answers and evaluation results - """ - question = question_data["question"] - correct_answer = question_data["answer"] - """ - - - """ - - result = Result(question=question, agent_answer=agent_answer, correct_answer=correct_answer) - - evaluation = callback(result) - - single_benchmark_result = { - "question": question, - "correct_answer": correct_answer, - "agent_answer": agent_answer, - "evaluation": evaluation, - "metadata": {k: v for k, v in question_data.items() if k not in ["question", "answer"]}, - } - print(single_benchmark_result) - - return single_benchmark_result - - -def run_benchmark( - questions: list[dict[str, str]], - callback: ScoringFunction, - agent_config: str = "../configs/base_llm_config.yaml", - max_workers: int = 2, -) -> Tuple[float, list[dict[str, Any]]]: - """Run the benchmark on a list of questions concurrently. - - Args: - questions: List of question-answer pairs - callback: Function to evaluate agent answers against ground truth - model: LLM model to use for the agent - max_steps: Maximum number of steps the agent can take - max_workers: Number of concurrent threads to use - agent_type: Type of agent to create (default: research_agent) - - Returns: - Tuple of (accuracy score, detailed results) - """ - - results = [] - total_questions = len(questions) - details = [] - - # Use ThreadPoolExecutor to run questions concurrently - with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: - # Create a list of future objects - future_to_idx = { - executor.submit(process_single_question, question_data, idx, total_questions, callback, agent_config): idx - for idx, question_data in enumerate(questions) - } - - # Process results as they complete - for future in concurrent.futures.as_completed(future_to_idx): - idx = future_to_idx[future] - try: - result = future.result() - results.append(result["evaluation"]) - details.append(result) - print(f"Completed question {idx+1}/{total_questions}") - except Exception as exc: - import traceback - - traceback.print_exc() - print(f"Question {idx+1} generated an exception: {exc}") - results.append(0) - details.append({"question": questions[idx]["question"], "agent_answer": str(exc), "evaluation": 0}) - - return sum(results) / len(results), details - - -def main(): - """Main function to run the benchmark.""" - - # Set up argument parser - parser = argparse.ArgumentParser(description="Run scoring with benchmarking options") - parser.add_argument( - "--datasets", - nargs="+", - choices=["smolagents:simpleqa", "hotpotqa", "simpleqa", "together-search-bench"], - help="Specific datasets to load (default: all)", - default=["together-search-bench"], - ) - parser.add_argument("--limit", type=int, default=None, help="Limit number of questions to process (default: all)") - parser.add_argument( - "--service-url", - default="http://localhost:8022/v1/deep_research_agent", - help="the endpoint of deep research agent.", - ) - parser.add_argument( - "--llm-endpoint", - default="http://localhost:8000/v1/", - help="llm service for llm-as-judge.", - ) - parser.add_argument( - "--max-workers", - type=int, - default=1, - help="Number of concurrent workers (default: 1)", - ) - - args = parser.parse_args() - - questions = load_questions(args.datasets) - - if args.limit is not None: - questions = questions[: args.limit] - print(f"Limited to {len(questions)} questions") - - results, details = run_benchmark( - questions, - callback=llm_as_a_judge_scoring, - max_workers=args.max_workers, - agent_config=args.agent_config, - ) - - print(f"Completed benchmark with {results} accuracy") - - benchmark_results_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "benchmark", "benchmark_results") - os.makedirs(benchmark_results_dir, exist_ok=True) - - output_file = os.path.join( - benchmark_results_dir, - f"benchmark_{'_'.join(args.datasets)}_{time.strftime('%Y-%m-%d_%H-%M-%S', time.localtime())}.json", - ) - - output_data = { - "metadata": { - "timestamp": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), - "datasets": args.datasets, - "agent_config": args.agent_config, - "scoring_method": "llm_as_a_judge_scoring", - "sample_count": len(questions), - }, - "overall_accuracy": results, - "question_details": details, - } - - with open(output_file, "w") as f: - json.dump(output_data, f, indent=2) - - return results - - -if __name__ == "__main__": - main() From 8b63b1d4422966643b87d855e764761dd6bac0d5 Mon Sep 17 00:00:00 2001 From: lkk12014402 Date: Tue, 8 Jul 2025 06:20:54 +0000 Subject: [PATCH 11/13] update readme --- DeepResearchAgent/README.md | 10 ++++++++-- .../assets/img/opea-deep-research-agent.png | Bin 0 -> 54030 bytes 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 DeepResearchAgent/assets/img/opea-deep-research-agent.png diff --git a/DeepResearchAgent/README.md b/DeepResearchAgent/README.md index fa7cc76b7a..e8b01a2965 100644 --- a/DeepResearchAgent/README.md +++ b/DeepResearchAgent/README.md @@ -1,7 +1,14 @@ -## Deep Research Agent Application +# Deep Research Agent Application Deep Research Agents are a new class of autonomous AI systems designed to perform complex, multi-step research tasks that typically require human-level reasoning, planning, and synthesis. +## Overview + +In this application, we leverage the deep research agent implementation of [langchain-ai/open_deep_research](https://github.com/langchain-ai/open_deep_research), and deploy it on the Intel platform with opea microserice. + +![Architecture Overview](assets/img/opea-deep-research-agent.png) + + ## Setup Deployment Environment ``` @@ -37,4 +44,3 @@ curl http://${host_ip}:8022/v1/deep_research_agent \ -H 'Content-Type: application/json' ``` -## Benchmarks diff --git a/DeepResearchAgent/assets/img/opea-deep-research-agent.png b/DeepResearchAgent/assets/img/opea-deep-research-agent.png new file mode 100644 index 0000000000000000000000000000000000000000..318f461f4668dcd04cb7d6c20d3cc57e4a538a02 GIT binary patch literal 54030 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 literal 0 HcmV?d00001 From 8c3f7b9bfc2d10e28573434db32dcaa623d5316c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 06:15:45 +0000 Subject: [PATCH 12/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- DeepResearchAgent/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/DeepResearchAgent/README.md b/DeepResearchAgent/README.md index e8b01a2965..4b6e9bb144 100644 --- a/DeepResearchAgent/README.md +++ b/DeepResearchAgent/README.md @@ -8,7 +8,6 @@ In this application, we leverage the deep research agent implementation of [lang ![Architecture Overview](assets/img/opea-deep-research-agent.png) - ## Setup Deployment Environment ``` @@ -43,4 +42,3 @@ curl http://${host_ip}:8022/v1/deep_research_agent \ -d '{"question":"What is Deep Learning?"}' \ -H 'Content-Type: application/json' ``` - From 1488b653fb6031c3ae85ec9a7bf0ab0d7c48ee9c Mon Sep 17 00:00:00 2001 From: lkk <33276950+lkk12014402@users.noreply.github.com> Date: Tue, 8 Jul 2025 14:26:55 +0800 Subject: [PATCH 13/13] update test_compose_on_gaudi.sh --- DeepResearchAgent/tests/test_compose_on_gaudi.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DeepResearchAgent/tests/test_compose_on_gaudi.sh b/DeepResearchAgent/tests/test_compose_on_gaudi.sh index ab03eb7e32..c180640201 100644 --- a/DeepResearchAgent/tests/test_compose_on_gaudi.sh +++ b/DeepResearchAgent/tests/test_compose_on_gaudi.sh @@ -45,7 +45,7 @@ function start_services() { until [[ "$n" -ge 200 ]]; do echo "n=$n" docker logs vllm-gaudi-server > vllm_service_start.log 2>&1 - if grep -q "Warmup finished" vllm_service_start.log; then + if grep -q "Application startup complete" vllm_service_start.log; then break fi sleep 5s