From 836c0be78f3193299cabfdbc5c98a31a5d9a691e Mon Sep 17 00:00:00 2001 From: Joshua Yao Date: Wed, 19 Nov 2025 06:55:33 +0000 Subject: [PATCH 1/2] Transit the implementation of DeepResearchAgent Signed-off-by: Joshua Yao --- DeepResearchAgent/Dockerfile | 4 +- DeepResearchAgent/README.md | 11 +- DeepResearchAgent/agent_factory.py | 68 +++ .../assets/img/opea-deep-research-agent.png | Bin 54030 -> 0 bytes DeepResearchAgent/deep_researcher.yaml | 11 - .../intel/hpu/gaudi/compose.yaml | 30 +- .../docker_compose/intel/hpu/gaudi/set_env.sh | 93 ++++- DeepResearchAgent/requirements.in | 8 + DeepResearchAgent/requirements.txt | 395 +++++++++++++++++- DeepResearchAgent/research_agent.py | 42 +- .../research_agents/deepagents/README.md | 2 + .../research_agents/deepagents/prompts.py | 173 ++++++++ .../research_agents/deepagents/tools.py | 116 +++++ .../research_agents/deepagents/utils.py | 94 +++++ .../tests/test_compose_on_gaudi.sh | 4 +- DeepResearchAgent/utils.py | 90 ---- 16 files changed, 982 insertions(+), 159 deletions(-) create mode 100644 DeepResearchAgent/agent_factory.py delete mode 100644 DeepResearchAgent/assets/img/opea-deep-research-agent.png delete mode 100644 DeepResearchAgent/deep_researcher.yaml create mode 100644 DeepResearchAgent/requirements.in create mode 100644 DeepResearchAgent/research_agents/deepagents/README.md create mode 100644 DeepResearchAgent/research_agents/deepagents/prompts.py create mode 100644 DeepResearchAgent/research_agents/deepagents/tools.py create mode 100644 DeepResearchAgent/research_agents/deepagents/utils.py delete mode 100644 DeepResearchAgent/utils.py diff --git a/DeepResearchAgent/Dockerfile b/DeepResearchAgent/Dockerfile index e84b5e34ae..cbec24bf5a 100644 --- a/DeepResearchAgent/Dockerfile +++ b/DeepResearchAgent/Dockerfile @@ -5,9 +5,9 @@ ARG IMAGE_REPO=opea ARG BASE_TAG=latest FROM opea/comps-base:$BASE_TAG -COPY ./deep_researcher.yaml $HOME/deep_researcher.yaml -COPY ./utils.py $HOME/utils.py +COPY ./research_agents $HOME/research_agents COPY ./requirements.txt $HOME/requirements.txt +COPY ./agent_factory.py $HOME/agent_factory.py COPY ./research_agent.py $HOME/research_agent.py USER root diff --git a/DeepResearchAgent/README.md b/DeepResearchAgent/README.md index 7e88f310f1..a1547d1c43 100644 --- a/DeepResearchAgent/README.md +++ b/DeepResearchAgent/README.md @@ -4,17 +4,15 @@ Deep Research Agents are a new class of autonomous AI systems designed to perfor ## 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. +In this application, we leverage the deep research agent implementation of [langchain-ai/deepagents](https://github.com/langchain-ai/deepagents), and deploy it on the Intel platform with opea microserice. -![Architecture Overview](assets/img/opea-deep-research-agent.png) ## Setup Deployment Environment -``` -# Configure deep_researcher.yaml with your llm model served by the vllm - +```shell # 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="" @@ -31,9 +29,8 @@ source ./set_env.sh To deploy the Deep Research Agent services, execute the docker compose up command with the appropriate arguments. For a default deployment, execute: -``` +```shell docker compose -f docker_compose/intel/hpu/gaudi/compose.yaml up -d - ``` ## Validate Microservice diff --git a/DeepResearchAgent/agent_factory.py b/DeepResearchAgent/agent_factory.py new file mode 100644 index 0000000000..b862901b58 --- /dev/null +++ b/DeepResearchAgent/agent_factory.py @@ -0,0 +1,68 @@ +import os +from datetime import datetime +from typing import Any +from langchain_openai import ChatOpenAI + + +def create_deepagents_research_agent() -> Any: + from deepagents import create_deep_agent + from research_agents.deepagents.prompts import ( + RESEARCHER_INSTRUCTIONS, + RESEARCH_WORKFLOW_INSTRUCTIONS, + SUBAGENT_DELEGATION_INSTRUCTIONS, + ) + from research_agents.deepagents.tools import tavily_search, think_tool + + # Limits + max_concurrent_research_units = os.environ.get("MAX_CONCURRENT_RESEARCH_UNITS", 3) + max_researcher_iterations = os.environ.get("MAX_RESEARCHER_ITERATIONS", 3) + + # Custom instructions (if any) + instructions_researcher=os.environ.get("RESEARCHER_INSTRUCTIONS", RESEARCHER_INSTRUCTIONS) + instructions_research_workflow=os.environ.get("RESEARCH_WORKFLOW_INSTRUCTIONS", RESEARCH_WORKFLOW_INSTRUCTIONS) + instructions_subagent_delegation=os.environ.get("SUBAGENT_DELEGATION_INSTRUCTIONS", SUBAGENT_DELEGATION_INSTRUCTIONS) + + # Combine orchestrator instructions (RESEARCHER_INSTRUCTIONS only for sub-agents) + INSTRUCTIONS = ( + instructions_research_workflow + + "\n\n" + + "=" * 80 + + "\n\n" + + instructions_subagent_delegation.format( + max_concurrent_research_units=max_concurrent_research_units, + max_researcher_iterations=max_researcher_iterations, + ) + ) + + # Get current date + current_date = datetime.now().strftime("%Y-%m-%d") + + # Research agent definition + research_sub_agent = { + "name": "research-agent", + "description": "Delegate research to the sub-agent researcher. Only give this researcher one topic at a time.", + "system_prompt": instructions_researcher.format(date=current_date), + "tools": [tavily_search, think_tool], + } + + # LLM serving endpoint + model = ChatOpenAI( + openai_api_base=os.environ.get("OPENAI_BASE_URL", "http://0.0.0.0:8000/v1/"), + openai_api_key=os.environ.get("OPENAI_API_KEY", "empty-api-key"), + model_name=os.environ.get("LLM_MODEL_ID", "meta-llama/Llama-3.3-70B-Instruct"), + temperature=0.0 + ) + + # Create the agent + return create_deep_agent( + model=model, + tools=[tavily_search, think_tool], + system_prompt=INSTRUCTIONS, + subagents=[research_sub_agent], + ) + +def create_agent(impl="DeepAgents") -> Any: + if impl == "DeepAgents": + return create_deepagents_research_agent() + else: + raise ValueError(f"Unknown agent implementation: {impl}") diff --git a/DeepResearchAgent/assets/img/opea-deep-research-agent.png b/DeepResearchAgent/assets/img/opea-deep-research-agent.png deleted file mode 100644 index 318f461f4668dcd04cb7d6c20d3cc57e4a538a02..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54030 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 diff --git a/DeepResearchAgent/deep_researcher.yaml b/DeepResearchAgent/deep_researcher.yaml deleted file mode 100644 index eb5b9c1640..0000000000 --- a/DeepResearchAgent/deep_researcher.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (C) 2025 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -agent: - type: langchain_deep_researcher - search_api: "tavily" - planner_provider: "openai" - planner_model: "meta-llama/Llama-3.3-70B-Instruct" - writer_provider: "openai" - writer_model: "meta-llama/Llama-3.3-70B-Instruct" - max_search_depth: 2 diff --git a/DeepResearchAgent/docker_compose/intel/hpu/gaudi/compose.yaml b/DeepResearchAgent/docker_compose/intel/hpu/gaudi/compose.yaml index d49af13a94..307c29760c 100644 --- a/DeepResearchAgent/docker_compose/intel/hpu/gaudi/compose.yaml +++ b/DeepResearchAgent/docker_compose/intel/hpu/gaudi/compose.yaml @@ -8,22 +8,12 @@ x-common-environment: http_proxy: ${http_proxy} https_proxy: ${https_proxy} -x-common-agent-environment: - &common-agent-env - <<: *common-env - HF_TOKEN: ${HF_TOKEN} - model: ${LLM_MODEL_ID} - TAVILY_API_KEY: ${TAVILY_API_KEY} - OPENAI_API_KEY: ${OPENAI_API_KEY} - OPENAI_BASE_URL: ${OPENAI_BASE_URL} - services: - vllm-service: image: opea/vllm-gaudi:1.22.0 container_name: vllm-gaudi-server ports: - - "8000:8000" + - ${VLLM_PORT:-8000}:8000 volumes: - ${HF_CACHE_DIR:-./data}:/data environment: @@ -34,10 +24,10 @@ services: OMPI_MCA_btl_vader_single_copy_mechanism: none LLM_MODEL_ID: ${LLM_MODEL_ID} VLLM_TORCH_PROFILER_DIR: "/mnt" - VLLM_SKIP_WARMUP: true + VLLM_SKIP_WARMUP: ${VLLM_SKIP_WARMUP:-true} PT_HPU_ENABLE_LAZY_COLLECTIVES: true healthcheck: - test: ["CMD-SHELL", "curl -f http://$HOST_IP:8000/health || exit 1"] + test: ["CMD-SHELL", "curl -f http://${HOST_IP}:${VLLM_PORT:-8000}/health || exit 1"] interval: 10s timeout: 10s retries: 100 @@ -45,7 +35,7 @@ services: 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 + command: --model ${LLM_MODEL_ID} --tensor-parallel-size ${NUM_CARDS} --enable-auto-tool-choice --tool-call-parser ${TOOL_CALL_PARSER} --host 0.0.0.0 --port 8000 --max-seq-len-to-capture ${MAX_LEN} deep-research-agent-server: image: ${REGISTRY:-opea}/deep-research-agent:${TAG:-latest} @@ -56,4 +46,14 @@ services: - "8022:8022" ipc: host environment: - <<: *common-agent-env + <<: *common-env + HF_TOKEN: ${HF_TOKEN} + model: ${LLM_MODEL_ID} + TAVILY_API_KEY: ${TAVILY_API_KEY} + OPENAI_API_KEY: ${OPENAI_API_KEY} + OPENAI_BASE_URL: ${OPENAI_BASE_URL} + MAX_CONCURRENT_RESEARCH_UNITS: ${MAX_CONCURRENT_RESEARCH_UNITS:-3} + MAX_RESEARCHER_ITERATIONS: ${MAX_RESEARCHER_ITERATIONS:-3} + RESEARCHER_INSTRUCTIONS: ${RESEARCHER_INSTRUCTIONS:-""} + RESEARCH_WORKFLOW_INSTRUCTIONS: ${RESEARCH_WORKFLOW_INSTRUCTIONS:-""} + SUBAGENT_DELEGATION_INSTRUCTIONS: ${SUBAGENT_DELEGATION_INSTRUCTIONS:-""} \ No newline at end of file diff --git a/DeepResearchAgent/docker_compose/intel/hpu/gaudi/set_env.sh b/DeepResearchAgent/docker_compose/intel/hpu/gaudi/set_env.sh index 9df0330f46..6cf90ca6af 100644 --- a/DeepResearchAgent/docker_compose/intel/hpu/gaudi/set_env.sh +++ b/DeepResearchAgent/docker_compose/intel/hpu/gaudi/set_env.sh @@ -3,45 +3,102 @@ # Copyright (C) 2024 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -# Navigate to the parent directory and source the environment +# ============================================================================== +# Environment Configuration for DeepResearchAgent on Intel Gaudi HPU +# ============================================================================== + +# Get the directory where this script is located SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)" +# Source the parent environment configuration file pushd "$SCRIPT_DIR/../../../../../" > /dev/null source .set_env.sh popd > /dev/null -# Function to check if a variable is set +# ------------------------------------------------------------------------------ +# Helper Functions +# ------------------------------------------------------------------------------ + +# Validates that a required environment 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 + return 1 # Return error but don't exit to allow other checks to run fi } -# Check critical variables -check_var "HF_TOKEN" +# ------------------------------------------------------------------------------ +# Validate Required API Keys +# ------------------------------------------------------------------------------ + +check_var "HF_TOKEN" # HuggingFace token for model access +check_var "TAVILY_API_KEY" # Tavily API key for web search functionality + +# ------------------------------------------------------------------------------ +# Network Configuration +# ------------------------------------------------------------------------------ + +# Detect the primary IP address of the host machine export ip_address=$(hostname -I | awk '{print $1}') +export HOST_IP=${ip_address} -# VLLM configuration +# Update proxy settings to include the host IP +export no_proxy=${no_proxy},${ip_address} +export http_proxy=${http_proxy} +export https_proxy=${https_proxy} + +# ------------------------------------------------------------------------------ +# vLLM Service Configuration +# ------------------------------------------------------------------------------ + +# Port where vLLM service will be accessible export VLLM_PORT="${VLLM_PORT:-8000}" -export VLLM_VOLUME="${VLLM_VOLUME:-/data2/huggingface}" -export VLLM_IMAGE="${VLLM_IMAGE:-opea/vllm-gaudi:latest}" + +# ------------------------------------------------------------------------------ +# Language Model Configuration +# ------------------------------------------------------------------------------ + +# LLM model to use for the Deep Research Agent +# See supported models and tool call parsers at: +# https://docs.vllm.ai/en/stable/features/tool_calling/#automatic-function-calling export LLM_MODEL_ID="${LLM_MODEL_ID:-meta-llama/Llama-3.3-70B-Instruct}" + +# Parser for handling function/tool calls (must match the model) +export TOOL_CALL_PARSER="${TOOL_CALL_PARSER:-llama3_json}" + +# Maximum sequence length for model context (131072 = ~128K tokens) export MAX_LEN="${MAX_LEN:-131072}" + +# Number of Gaudi accelerator cards to use export NUM_CARDS="${NUM_CARDS:-4}" + +# Directory for caching HuggingFace models 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} +# OpenAI-compatible API endpoint URL for vLLM +export OPENAI_BASE_URL="http://${ip_address}:${VLLM_PORT}/v1" + +# ------------------------------------------------------------------------------ +# API Keys and Authentication +# ------------------------------------------------------------------------------ + +export HF_TOKEN="${HF_TOKEN}" # HuggingFace authentication token +export OPENAI_API_KEY="empty-api-key" # Placeholder for vLLM compatibility +export TAVILY_API_KEY="${TAVILY_API_KEY}" # Tavily search API key + +# ------------------------------------------------------------------------------ +# Deep Research Agent Configuration +# ------------------------------------------------------------------------------ + +# Maximum number of research units that can run concurrently +export MAX_CONCURRENT_RESEARCH_UNITS="${MAX_CONCURRENT_RESEARCH_UNITS:-3}" -# Hugging Face API token -export HF_TOKEN="${HF_TOKEN}" +# Maximum iterations per researcher before stopping +export MAX_RESEARCHER_ITERATIONS="${MAX_RESEARCHER_ITERATIONS:-3}" -# API keys -check_var "TAVILY_API_KEY" -export TAVILY_API_KEY="${TAVILY_API_KEY}" +# Custom instructions for agent behavior (leave empty for defaults) +export RESEARCHER_INSTRUCTIONS="" # Instructions for individual researchers +export RESEARCH_WORKFLOW_INSTRUCTIONS="" # Instructions for overall research workflow +export SUBAGENT_DELEGATION_INSTRUCTIONS="" # Instructions for task delegation between agents \ No newline at end of file diff --git a/DeepResearchAgent/requirements.in b/DeepResearchAgent/requirements.in new file mode 100644 index 0000000000..8042efea5e --- /dev/null +++ b/DeepResearchAgent/requirements.in @@ -0,0 +1,8 @@ +deepagents +httpx +langchain_openai +langchain-tavily +langgraph-cli[inmem] +markdownify +rich +tavily-python \ No newline at end of file diff --git a/DeepResearchAgent/requirements.txt b/DeepResearchAgent/requirements.txt index ac50164672..7d11b0bde6 100644 --- a/DeepResearchAgent/requirements.txt +++ b/DeepResearchAgent/requirements.txt @@ -1 +1,394 @@ -open-deep-research==0.0.16 +# This file was autogenerated by uv via the following command: +# uv pip compile ./DeepResearchAgent/requirements.in --universal -o ./DeepResearchAgent/requirements.txt +aiofiles==24.1.0 + # via daytona +aiohappyeyeballs==2.6.1 + # via aiohttp +aiohttp==3.13.2 + # via + # aiohttp-retry + # daytona-api-client-async + # daytona-toolbox-api-client-async + # langchain-tavily + # tavily +aiohttp-retry==2.9.1 + # via + # daytona-api-client-async + # daytona-toolbox-api-client-async +aiosignal==1.4.0 + # via aiohttp +annotated-types==0.7.0 + # via pydantic +anthropic==0.74.0 + # via langchain-anthropic +anyio==4.11.0 + # via + # anthropic + # httpx + # openai + # runloop-api-client + # sse-starlette + # starlette + # watchfiles +attrs==25.4.0 + # via aiohttp +beautifulsoup4==4.14.2 + # via markdownify +blockbuster==1.5.25 + # via langgraph-runtime-inmem +bracex==2.6 + # via wcmatch +certifi==2025.11.12 + # via + # httpcore + # httpx + # requests +cffi==2.0.0 ; platform_python_implementation != 'PyPy' + # via cryptography +charset-normalizer==3.4.4 + # via requests +click==8.3.1 + # via + # langgraph-cli + # uvicorn +cloudpickle==3.1.2 + # via langgraph-api +colorama==0.4.6 ; sys_platform == 'win32' + # via + # click + # tqdm +cryptography==44.0.3 + # via langgraph-api +daytona==0.115.0 + # via deepagents +daytona-api-client==0.115.0 + # via daytona +daytona-api-client-async==0.115.0 + # via daytona +daytona-toolbox-api-client==0.115.0 + # via daytona +daytona-toolbox-api-client-async==0.115.0 + # via daytona +deepagents==0.2.7 + # via -r ./DeepResearchAgent/requirements.in +deprecated==1.3.1 + # via daytona +distro==1.9.0 + # via + # anthropic + # openai + # runloop-api-client +docstring-parser==0.17.0 + # via anthropic +environs==14.5.0 + # via daytona +forbiddenfruit==0.1.4 ; implementation_name == 'cpython' + # via blockbuster +frozenlist==1.8.0 + # via + # aiohttp + # aiosignal +googleapis-common-protos==1.72.0 + # via opentelemetry-exporter-otlp-proto-http +grpcio==1.76.0 + # via + # grpcio-tools + # langgraph-api +grpcio-tools==1.75.1 + # via langgraph-api +h11==0.16.0 + # via + # httpcore + # uvicorn +httpcore==1.0.9 + # via httpx +httpx==0.28.1 + # via + # -r ./DeepResearchAgent/requirements.in + # anthropic + # daytona + # langgraph-api + # langgraph-sdk + # langsmith + # openai + # runloop-api-client + # tavily-python +idna==3.11 + # via + # anyio + # httpx + # requests + # yarl +importlib-metadata==8.7.0 + # via opentelemetry-api +jiter==0.12.0 + # via + # anthropic + # openai +jsonpatch==1.33 + # via langchain-core +jsonpointer==3.0.0 + # via jsonpatch +jsonschema-rs==0.29.1 + # via langgraph-api +langchain==1.0.7 + # via + # deepagents + # langchain-tavily +langchain-anthropic==1.1.0 + # via deepagents +langchain-core==1.0.5 + # via + # deepagents + # langchain + # langchain-anthropic + # langchain-openai + # langchain-tavily + # langgraph + # langgraph-api + # langgraph-checkpoint + # langgraph-prebuilt +langchain-openai==1.0.3 + # via -r ./DeepResearchAgent/requirements.in +langchain-tavily==0.2.13 + # via -r ./DeepResearchAgent/requirements.in +langgraph==1.0.3 + # via + # langchain + # langgraph-api + # langgraph-runtime-inmem +langgraph-api==0.5.16 + # via langgraph-cli +langgraph-checkpoint==3.0.1 + # via + # langgraph + # langgraph-api + # langgraph-prebuilt + # langgraph-runtime-inmem +langgraph-cli==0.4.7 + # via -r ./DeepResearchAgent/requirements.in +langgraph-prebuilt==1.0.4 + # via langgraph +langgraph-runtime-inmem==0.18.0 + # via + # langgraph-api + # langgraph-cli +langgraph-sdk==0.2.9 + # via + # langgraph + # langgraph-api + # langgraph-cli +langsmith==0.4.43 + # via + # langchain-core + # langgraph-api +markdown-it-py==4.0.0 + # via rich +markdownify==1.2.2 + # via -r ./DeepResearchAgent/requirements.in +marshmallow==4.1.0 + # via environs +mdurl==0.1.2 + # via markdown-it-py +multidict==6.7.0 + # via + # aiohttp + # yarl +multipart==1.3.0 + # via daytona +obstore==0.7.3 + # via daytona +openai==2.8.1 + # via langchain-openai +opentelemetry-api==1.38.0 + # via + # langgraph-api + # opentelemetry-exporter-otlp-proto-http + # opentelemetry-sdk + # opentelemetry-semantic-conventions +opentelemetry-exporter-otlp-proto-common==1.38.0 + # via opentelemetry-exporter-otlp-proto-http +opentelemetry-exporter-otlp-proto-http==1.38.0 + # via langgraph-api +opentelemetry-proto==1.38.0 + # via + # opentelemetry-exporter-otlp-proto-common + # opentelemetry-exporter-otlp-proto-http +opentelemetry-sdk==1.38.0 + # via + # langgraph-api + # opentelemetry-exporter-otlp-proto-http +opentelemetry-semantic-conventions==0.59b0 + # via opentelemetry-sdk +orjson==3.11.4 + # via + # langgraph-api + # langgraph-sdk + # langsmith +ormsgpack==1.12.0 + # via langgraph-checkpoint +packaging==25.0 + # via + # langchain-core + # langsmith +propcache==0.4.1 + # via + # aiohttp + # yarl +protobuf==6.33.1 + # via + # googleapis-common-protos + # grpcio-tools + # langgraph-api + # opentelemetry-proto +pycparser==2.23 ; implementation_name != 'PyPy' and platform_python_implementation != 'PyPy' + # via cffi +pydantic==2.12.4 + # via + # anthropic + # daytona + # daytona-api-client + # daytona-api-client-async + # daytona-toolbox-api-client + # daytona-toolbox-api-client-async + # langchain + # langchain-anthropic + # langchain-core + # langgraph + # langsmith + # openai + # runloop-api-client +pydantic-core==2.41.5 + # via pydantic +pygments==2.19.2 + # via rich +pyjwt==2.10.1 + # via langgraph-api +python-dateutil==2.9.0.post0 + # via + # daytona-api-client + # daytona-api-client-async + # daytona-toolbox-api-client + # daytona-toolbox-api-client-async +python-dotenv==1.2.1 + # via + # environs + # langgraph-cli +pyyaml==6.0.3 + # via langchain-core +regex==2025.11.3 + # via tiktoken +requests==2.32.5 + # via + # langchain-tavily + # langsmith + # opentelemetry-exporter-otlp-proto-http + # requests-toolbelt + # tavily + # tavily-python + # tiktoken +requests-toolbelt==1.0.0 + # via langsmith +rich==14.2.0 + # via -r ./DeepResearchAgent/requirements.in +runloop-api-client==0.68.0 + # via deepagents +setuptools==80.9.0 + # via grpcio-tools +six==1.17.0 + # via + # markdownify + # python-dateutil +sniffio==1.3.1 + # via + # anthropic + # anyio + # openai + # runloop-api-client +soupsieve==2.8 + # via beautifulsoup4 +sse-starlette==2.1.3 + # via + # langgraph-api + # langgraph-runtime-inmem +starlette==0.50.0 + # via + # langgraph-api + # langgraph-runtime-inmem + # sse-starlette +structlog==25.5.0 + # via + # langgraph-api + # langgraph-runtime-inmem +tavily==1.1.0 + # via deepagents +tavily-python==0.7.13 + # via -r ./DeepResearchAgent/requirements.in +tenacity==9.1.2 + # via + # langchain-core + # langgraph-api +tiktoken==0.12.0 + # via + # langchain-openai + # tavily-python +toml==0.10.2 + # via daytona +tqdm==4.67.1 + # via openai +truststore==0.10.4 + # via langgraph-api +typing-extensions==4.15.0 + # via + # aiosignal + # anthropic + # anyio + # beautifulsoup4 + # daytona-api-client + # daytona-api-client-async + # daytona-toolbox-api-client + # daytona-toolbox-api-client-async + # grpcio + # langchain-core + # obstore + # openai + # opentelemetry-api + # opentelemetry-exporter-otlp-proto-http + # opentelemetry-sdk + # opentelemetry-semantic-conventions + # pydantic + # pydantic-core + # runloop-api-client + # starlette + # typing-inspection +typing-inspection==0.4.2 + # via pydantic +urllib3==2.5.0 + # via + # daytona-api-client + # daytona-api-client-async + # daytona-toolbox-api-client + # daytona-toolbox-api-client-async + # requests +uuid-utils==0.11.1 + # via runloop-api-client +uvicorn==0.38.0 + # via + # langgraph-api + # sse-starlette +watchfiles==1.1.1 + # via langgraph-api +wcmatch==10.1 + # via deepagents +websockets==15.0.1 + # via daytona +wrapt==2.0.1 + # via deprecated +xxhash==3.6.0 + # via langgraph +yarl==1.22.0 + # via aiohttp +zipp==3.23.0 + # via importlib-metadata +zstandard==0.25.0 + # via langsmith diff --git a/DeepResearchAgent/research_agent.py b/DeepResearchAgent/research_agent.py index 5964b82853..9259b20a50 100644 --- a/DeepResearchAgent/research_agent.py +++ b/DeepResearchAgent/research_agent.py @@ -1,19 +1,19 @@ # Copyright (C) 2024 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -import argparse -import json import os -import re +import logging from typing import List, Union - -from comps import opea_microservices, register_microservice +from comps import opea_microservices, register_microservice, CustomLogger from comps.cores.telemetry.opea_telemetry import opea_telemetry from pydantic import BaseModel -from utils import create_agent -config_path = os.path.join(os.path.dirname(__file__), "deep_researcher.yaml") -agent = create_agent(config_path) +from agent_factory import create_agent +from research_agents.deepagents.utils import format_message + +logger = CustomLogger(__name__) +log_level = logging.DEBUG if os.getenv("LOGFLAG", "").lower() == "true" else logging.INFO +logging.basicConfig(level=log_level, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") class SimpleRequest(BaseModel): @@ -29,11 +29,27 @@ class SimpleRequest(BaseModel): @opea_telemetry async def run(request: SimpleRequest): - question = f"Question: {request.question}" - - result = await agent(question) - - return {"answer": result} + logger.debug(f"Received question: {request.question}") + + logger.debug("Creating DeepAgents research agent...") + agent = create_agent(impl="DeepAgents") + + logger.debug("Invoking agent with the provided question...") + result = agent.invoke( + { + "messages": [ + { + "role": "user", + "content": f"Question: {request.question}", + } + ], + }, + ) + logger.debug("Agent invocation completed.") + if os.getenv("LOGFLAG", "").lower() == "true": + format_message(result["messages"]) + + return {"answer": result["messages"][-1].content} if __name__ == "__main__": diff --git a/DeepResearchAgent/research_agents/deepagents/README.md b/DeepResearchAgent/research_agents/deepagents/README.md new file mode 100644 index 0000000000..13c8196b7e --- /dev/null +++ b/DeepResearchAgent/research_agents/deepagents/README.md @@ -0,0 +1,2 @@ +# Deep Research Agent of DeepAgents +The code is from LangChain [DeepAgents](https://github.com/langchain-ai/deepagents-quickstarts/tree/main/deep_research). \ No newline at end of file diff --git a/DeepResearchAgent/research_agents/deepagents/prompts.py b/DeepResearchAgent/research_agents/deepagents/prompts.py new file mode 100644 index 0000000000..588bec0a5e --- /dev/null +++ b/DeepResearchAgent/research_agents/deepagents/prompts.py @@ -0,0 +1,173 @@ +"""Prompt templates and tool descriptions for the research deepagent.""" + +RESEARCH_WORKFLOW_INSTRUCTIONS = """# Research Workflow + +Follow this workflow for all research requests: + +1. **Plan**: Create a todo list with write_todos to break down the research into focused tasks +2. **Save the request**: Use write_file() to save the user's research question to `/research_request.md` +3. **Research**: Delegate research tasks to sub-agents using the task() tool - ALWAYS use sub-agents for research, never conduct research yourself +4. **Synthesize**: Review all sub-agent findings and consolidate citations (each unique URL gets one number across all findings) +5. **Write Report**: Write a comprehensive final report to `/final_report.md` (see Report Writing Guidelines below) +6. **Verify**: Read `/research_request.md` and confirm you've addressed all aspects with proper citations and structure + +## Research Planning Guidelines +- Batch similar research tasks into a single TODO to minimize overhead +- For simple fact-finding questions, use 1 sub-agent +- For comparisons or multi-faceted topics, delegate to multiple parallel sub-agents +- Each sub-agent should research one specific aspect and return findings + +## Report Writing Guidelines + +When writing the final report to `/final_report.md`, follow these structure patterns: + +**For comparisons:** +1. Introduction +2. Overview of topic A +3. Overview of topic B +4. Detailed comparison +5. Conclusion + +**For lists/rankings:** +Simply list items with details - no introduction needed: +1. Item 1 with explanation +2. Item 2 with explanation +3. Item 3 with explanation + +**For summaries/overviews:** +1. Overview of topic +2. Key concept 1 +3. Key concept 2 +4. Key concept 3 +5. Conclusion + +**General guidelines:** +- Use clear section headings (## for sections, ### for subsections) +- Write in paragraph form by default - be text-heavy, not just bullet points +- Do NOT use self-referential language ("I found...", "I researched...") +- Write as a professional report without meta-commentary +- Each section should be comprehensive and detailed +- Use bullet points only when listing is more appropriate than prose + +**Citation format:** +- Cite sources inline using [1], [2], [3] format +- Assign each unique URL a single citation number across ALL sub-agent findings +- End report with ### Sources section listing each numbered source +- Number sources sequentially without gaps (1,2,3,4...) +- Format: [1] Source Title: URL (each on separate line for proper list rendering) +- Example: + + Some important finding [1]. Another key insight [2]. + + ### Sources + [1] AI Research Paper: https://example.com/paper + [2] Industry Analysis: https://example.com/analysis +""" + +RESEARCHER_INSTRUCTIONS = """You are a research assistant conducting research on the user's input topic. For context, today's date is {date}. + + +Your job is to use tools to gather information about the user's input topic. +You can use any of the research tools provided to you to find resources that can help answer the research question. +You can call these tools in series or in parallel, your research is conducted in a tool-calling loop. + + + +You have access to two specific research tools: +1. **tavily_search**: For conducting web searches to gather information +2. **think_tool**: For reflection and strategic planning during research +**CRITICAL: Use think_tool after each search to reflect on results and plan next steps** + + + +Think like a human researcher with limited time. Follow these steps: + +1. **Read the question carefully** - What specific information does the user need? +2. **Start with broader searches** - Use broad, comprehensive queries first +3. **After each search, pause and assess** - Do I have enough to answer? What's still missing? +4. **Execute narrower searches as you gather information** - Fill in the gaps +5. **Stop when you can answer confidently** - Don't keep searching for perfection + + + +**Tool Call Budgets** (Prevent excessive searching): +- **Simple queries**: Use 2-3 search tool calls maximum +- **Complex queries**: Use up to 5 search tool calls maximum +- **Always stop**: After 5 search tool calls if you cannot find the right sources + +**Stop Immediately When**: +- You can answer the user's question comprehensively +- You have 3+ relevant examples/sources for the question +- Your last 2 searches returned similar information + + + +After each search tool call, use think_tool to analyze the results: +- What key information did I find? +- What's missing? +- Do I have enough to answer the question comprehensively? +- Should I search more or provide my answer? + + + +When providing your findings back to the orchestrator: + +1. **Structure your response**: Organize findings with clear headings and detailed explanations +2. **Cite sources inline**: Use [1], [2], [3] format when referencing information from your searches +3. **Include Sources section**: End with ### Sources listing each numbered source with title and URL + +Example: +``` +## Key Findings + +Context engineering is a critical technique for AI agents [1]. Studies show that proper context management can improve performance by 40% [2]. + +### Sources +[1] Context Engineering Guide: https://example.com/context-guide +[2] AI Performance Study: https://example.com/study +``` + +The orchestrator will consolidate citations from all sub-agents into the final report. + +""" + +TASK_DESCRIPTION_PREFIX = """Delegate a task to a specialized sub-agent with isolated context. Available agents for delegation are: +{other_agents} +""" + +SUBAGENT_DELEGATION_INSTRUCTIONS = """# Sub-Agent Research Coordination + +Your role is to coordinate research by delegating tasks from your TODO list to specialized research sub-agents. + +## Delegation Strategy + +**DEFAULT: Start with 1 sub-agent** for most queries: +- "What is quantum computing?" → 1 sub-agent (general overview) +- "List the top 10 coffee shops in San Francisco" → 1 sub-agent +- "Summarize the history of the internet" → 1 sub-agent +- "Research context engineering for AI agents" → 1 sub-agent (covers all aspects) + +**ONLY parallelize when the query EXPLICITLY requires comparison or has clearly independent aspects:** + +**Explicit comparisons** → 1 sub-agent per element: +- "Compare OpenAI vs Anthropic vs DeepMind AI safety approaches" → 3 parallel sub-agents +- "Compare Python vs JavaScript for web development" → 2 parallel sub-agents + +**Clearly separated aspects** → 1 sub-agent per aspect (use sparingly): +- "Research renewable energy adoption in Europe, Asia, and North America" → 3 parallel sub-agents (geographic separation) +- Only use this pattern when aspects cannot be covered efficiently by a single comprehensive search + +## Key Principles +- **Bias towards single sub-agent**: One comprehensive research task is more token-efficient than multiple narrow ones +- **Avoid premature decomposition**: Don't break "research X" into "research X overview", "research X techniques", "research X applications" - just use 1 sub-agent for all of X +- **Parallelize only for clear comparisons**: Use multiple sub-agents when comparing distinct entities or geographically separated data + +## Parallel Execution Limits +- Use at most {max_concurrent_research_units} parallel sub-agents per iteration +- Make multiple task() calls in a single response to enable parallel execution +- Each sub-agent returns findings independently + +## Research Limits +- Stop after {max_researcher_iterations} delegation rounds if you haven't found adequate sources +- Stop when you have sufficient information to answer comprehensively +- Bias towards focused research over exhaustive exploration""" \ No newline at end of file diff --git a/DeepResearchAgent/research_agents/deepagents/tools.py b/DeepResearchAgent/research_agents/deepagents/tools.py new file mode 100644 index 0000000000..5a97cdcdd2 --- /dev/null +++ b/DeepResearchAgent/research_agents/deepagents/tools.py @@ -0,0 +1,116 @@ +"""Research Tools. + +This module provides search and content processing utilities for the research agent, +using Tavily for URL discovery and fetching full webpage content. +""" + +import httpx +from langchain_core.tools import InjectedToolArg, tool +from markdownify import markdownify +from tavily import TavilyClient +from typing_extensions import Annotated, Literal + +tavily_client = TavilyClient() + + +def fetch_webpage_content(url: str, timeout: float = 10.0) -> str: + """Fetch and convert webpage content to markdown. + + Args: + url: URL to fetch + timeout: Request timeout in seconds + + Returns: + Webpage content as markdown + """ + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" + } + + try: + response = httpx.get(url, headers=headers, timeout=timeout) + response.raise_for_status() + return markdownify(response.text) + except Exception as e: + return f"Error fetching content from {url}: {str(e)}" + + +@tool(parse_docstring=True) +def tavily_search( + query: str, + max_results: Annotated[int, InjectedToolArg] = 1, + topic: Annotated[ + Literal["general", "news", "finance"], InjectedToolArg + ] = "general", +) -> str: + """Search the web for information on a given query. + + Uses Tavily to discover relevant URLs, then fetches and returns full webpage content as markdown. + + Args: + query: Search query to execute + max_results: Maximum number of results to return (default: 1) + topic: Topic filter - 'general', 'news', or 'finance' (default: 'general') + + Returns: + Formatted search results with full webpage content + """ + # Use Tavily to discover URLs + search_results = tavily_client.search( + query, + max_results=max_results, + topic=topic, + ) + + # Fetch full content for each URL + result_texts = [] + for result in search_results.get("results", []): + url = result["url"] + title = result["title"] + + # Fetch webpage content + content = fetch_webpage_content(url) + + result_text = f"""## {title} +**URL:** {url} + +{content} + +--- +""" + result_texts.append(result_text) + + # Format final response + response = f"""🔍 Found {len(result_texts)} result(s) for '{query}': + +{chr(10).join(result_texts)}""" + + return response + + +@tool(parse_docstring=True) +def think_tool(reflection: str) -> str: + """Tool for strategic reflection on research progress and decision-making. + + Use this tool after each search to analyze results and plan next steps systematically. + This creates a deliberate pause in the research workflow for quality decision-making. + + When to use: + - After receiving search results: What key information did I find? + - Before deciding next steps: Do I have enough to answer comprehensively? + - When assessing research gaps: What specific information am I still missing? + - Before concluding research: Can I provide a complete answer now? + + Reflection should address: + 1. Analysis of current findings - What concrete information have I gathered? + 2. Gap assessment - What crucial information is still missing? + 3. Quality evaluation - Do I have sufficient evidence/examples for a good answer? + 4. Strategic decision - Should I continue searching or provide my answer? + + Args: + reflection: Your detailed reflection on research progress, findings, gaps, and next steps + + Returns: + Confirmation that reflection was recorded for decision-making + """ + return f"Reflection recorded: {reflection}" diff --git a/DeepResearchAgent/research_agents/deepagents/utils.py b/DeepResearchAgent/research_agents/deepagents/utils.py new file mode 100644 index 0000000000..48aecfbb46 --- /dev/null +++ b/DeepResearchAgent/research_agents/deepagents/utils.py @@ -0,0 +1,94 @@ +"""Utility functions for displaying messages and prompts in Jupyter notebooks.""" + +import json + +from rich.console import Console +from rich.panel import Panel +from rich.text import Text + +console = Console() + + +def format_message_content(message): + """Convert message content to displayable string.""" + parts = [] + tool_calls_processed = False + + # Handle main content + if isinstance(message.content, str): + parts.append(message.content) + elif isinstance(message.content, list): + # Handle complex content like tool calls (Anthropic format) + for item in message.content: + if item.get("type") == "text": + parts.append(item["text"]) + elif item.get("type") == "tool_use": + parts.append(f"\n🔧 Tool Call: {item['name']}") + parts.append(f" Args: {json.dumps(item['input'], indent=2)}") + parts.append(f" ID: {item.get('id', 'N/A')}") + tool_calls_processed = True + else: + parts.append(str(message.content)) + + # Handle tool calls attached to the message (OpenAI format) - only if not already processed + if ( + not tool_calls_processed + and hasattr(message, "tool_calls") + and message.tool_calls + ): + for tool_call in message.tool_calls: + parts.append(f"\n🔧 Tool Call: {tool_call['name']}") + parts.append(f" Args: {json.dumps(tool_call['args'], indent=2)}") + parts.append(f" ID: {tool_call['id']}") + + return "\n".join(parts) + + +def format_messages(messages): + """Format and display a list of messages with Rich formatting.""" + for m in messages: + msg_type = m.__class__.__name__.replace("Message", "") + content = format_message_content(m) + + if msg_type == "Human": + console.print(Panel(content, title="🧑 Human", border_style="blue")) + elif msg_type == "Ai": + console.print(Panel(content, title="🤖 Assistant", border_style="green")) + elif msg_type == "Tool": + console.print(Panel(content, title="🔧 Tool Output", border_style="yellow")) + else: + console.print(Panel(content, title=f"📝 {msg_type}", border_style="white")) + + +def format_message(messages): + """Alias for format_messages for backward compatibility.""" + return format_messages(messages) + + +def show_prompt(prompt_text: str, title: str = "Prompt", border_style: str = "blue"): + """Display a prompt with rich formatting and XML tag highlighting. + + Args: + prompt_text: The prompt string to display + title: Title for the panel (default: "Prompt") + border_style: Border color style (default: "blue") + """ + # Create a formatted display of the prompt + formatted_text = Text(prompt_text) + formatted_text.highlight_regex(r"<[^>]+>", style="bold blue") # Highlight XML tags + formatted_text.highlight_regex( + r"##[^#\n]+", style="bold magenta" + ) # Highlight headers + formatted_text.highlight_regex( + r"###[^#\n]+", style="bold cyan" + ) # Highlight sub-headers + + # Display in a panel for better presentation + console.print( + Panel( + formatted_text, + title=f"[bold green]{title}[/bold green]", + border_style=border_style, + padding=(1, 2), + ) + ) diff --git a/DeepResearchAgent/tests/test_compose_on_gaudi.sh b/DeepResearchAgent/tests/test_compose_on_gaudi.sh index e76a66b9cc..2d5d48e702 100644 --- a/DeepResearchAgent/tests/test_compose_on_gaudi.sh +++ b/DeepResearchAgent/tests/test_compose_on_gaudi.sh @@ -64,7 +64,7 @@ function validate_service() { 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 + if echo "$CONTENT" | grep -iq "$EXPECTED_RESULT"; then echo "[ $SERVICE_NAME ] Content is as expected." else echo "[ $SERVICE_NAME ] Content does not match the expected result: $CONTENT" @@ -84,7 +84,7 @@ function validate_microservices() { validate_service \ "${ip_address}:8022/v1/deep_research_agent" \ - "deep" \ + "deep" \ "deep-research-agent" \ "deep-research-agent-server" \ '{"question": "what is the deep learning?"}' diff --git a/DeepResearchAgent/utils.py b/DeepResearchAgent/utils.py deleted file mode 100644 index 036747bbeb..0000000000 --- a/DeepResearchAgent/utils.py +++ /dev/null @@ -1,90 +0,0 @@ -# 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) -> 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 langgraph.types import Command - - # from open_deep_research.graph import builder - # TODO - from legacy.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) - - 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, - "writer_provider": writer_provider, - "writer_model": writer_model, - "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 for event in graph.astream({"topic": goal}, thread, stream_mode="updates"): - results.append(event) - - 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 langchain_wrapper From f13cd21ec7726afc0ab90416840013558736ea9c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 06:57:36 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- DeepResearchAgent/README.md | 1 - DeepResearchAgent/agent_factory.py | 23 ++++++++++++------- .../intel/hpu/gaudi/compose.yaml | 2 +- .../docker_compose/intel/hpu/gaudi/set_env.sh | 2 +- DeepResearchAgent/research_agent.py | 16 ++++++------- .../research_agents/deepagents/README.md | 3 ++- .../research_agents/deepagents/prompts.py | 6 +++-- .../research_agents/deepagents/tools.py | 6 ++--- .../research_agents/deepagents/utils.py | 16 ++++--------- 9 files changed, 39 insertions(+), 36 deletions(-) diff --git a/DeepResearchAgent/README.md b/DeepResearchAgent/README.md index a1547d1c43..38ec10f72f 100644 --- a/DeepResearchAgent/README.md +++ b/DeepResearchAgent/README.md @@ -6,7 +6,6 @@ Deep Research Agents are a new class of autonomous AI systems designed to perfor In this application, we leverage the deep research agent implementation of [langchain-ai/deepagents](https://github.com/langchain-ai/deepagents), and deploy it on the Intel platform with opea microserice. - ## Setup Deployment Environment ```shell diff --git a/DeepResearchAgent/agent_factory.py b/DeepResearchAgent/agent_factory.py index b862901b58..1361e58179 100644 --- a/DeepResearchAgent/agent_factory.py +++ b/DeepResearchAgent/agent_factory.py @@ -1,14 +1,18 @@ +# Copyright (C) 2025 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + import os from datetime import datetime from typing import Any + from langchain_openai import ChatOpenAI def create_deepagents_research_agent() -> Any: from deepagents import create_deep_agent from research_agents.deepagents.prompts import ( - RESEARCHER_INSTRUCTIONS, RESEARCH_WORKFLOW_INSTRUCTIONS, + RESEARCHER_INSTRUCTIONS, SUBAGENT_DELEGATION_INSTRUCTIONS, ) from research_agents.deepagents.tools import tavily_search, think_tool @@ -18,9 +22,11 @@ def create_deepagents_research_agent() -> Any: max_researcher_iterations = os.environ.get("MAX_RESEARCHER_ITERATIONS", 3) # Custom instructions (if any) - instructions_researcher=os.environ.get("RESEARCHER_INSTRUCTIONS", RESEARCHER_INSTRUCTIONS) - instructions_research_workflow=os.environ.get("RESEARCH_WORKFLOW_INSTRUCTIONS", RESEARCH_WORKFLOW_INSTRUCTIONS) - instructions_subagent_delegation=os.environ.get("SUBAGENT_DELEGATION_INSTRUCTIONS", SUBAGENT_DELEGATION_INSTRUCTIONS) + instructions_researcher = os.environ.get("RESEARCHER_INSTRUCTIONS", RESEARCHER_INSTRUCTIONS) + instructions_research_workflow = os.environ.get("RESEARCH_WORKFLOW_INSTRUCTIONS", RESEARCH_WORKFLOW_INSTRUCTIONS) + instructions_subagent_delegation = os.environ.get( + "SUBAGENT_DELEGATION_INSTRUCTIONS", SUBAGENT_DELEGATION_INSTRUCTIONS + ) # Combine orchestrator instructions (RESEARCHER_INSTRUCTIONS only for sub-agents) INSTRUCTIONS = ( @@ -33,7 +39,7 @@ def create_deepagents_research_agent() -> Any: max_researcher_iterations=max_researcher_iterations, ) ) - + # Get current date current_date = datetime.now().strftime("%Y-%m-%d") @@ -50,9 +56,9 @@ def create_deepagents_research_agent() -> Any: openai_api_base=os.environ.get("OPENAI_BASE_URL", "http://0.0.0.0:8000/v1/"), openai_api_key=os.environ.get("OPENAI_API_KEY", "empty-api-key"), model_name=os.environ.get("LLM_MODEL_ID", "meta-llama/Llama-3.3-70B-Instruct"), - temperature=0.0 + temperature=0.0, ) - + # Create the agent return create_deep_agent( model=model, @@ -60,7 +66,8 @@ def create_deepagents_research_agent() -> Any: system_prompt=INSTRUCTIONS, subagents=[research_sub_agent], ) - + + def create_agent(impl="DeepAgents") -> Any: if impl == "DeepAgents": return create_deepagents_research_agent() diff --git a/DeepResearchAgent/docker_compose/intel/hpu/gaudi/compose.yaml b/DeepResearchAgent/docker_compose/intel/hpu/gaudi/compose.yaml index 307c29760c..77ef688502 100644 --- a/DeepResearchAgent/docker_compose/intel/hpu/gaudi/compose.yaml +++ b/DeepResearchAgent/docker_compose/intel/hpu/gaudi/compose.yaml @@ -56,4 +56,4 @@ services: MAX_RESEARCHER_ITERATIONS: ${MAX_RESEARCHER_ITERATIONS:-3} RESEARCHER_INSTRUCTIONS: ${RESEARCHER_INSTRUCTIONS:-""} RESEARCH_WORKFLOW_INSTRUCTIONS: ${RESEARCH_WORKFLOW_INSTRUCTIONS:-""} - SUBAGENT_DELEGATION_INSTRUCTIONS: ${SUBAGENT_DELEGATION_INSTRUCTIONS:-""} \ No newline at end of file + SUBAGENT_DELEGATION_INSTRUCTIONS: ${SUBAGENT_DELEGATION_INSTRUCTIONS:-""} diff --git a/DeepResearchAgent/docker_compose/intel/hpu/gaudi/set_env.sh b/DeepResearchAgent/docker_compose/intel/hpu/gaudi/set_env.sh index 6cf90ca6af..facb46f085 100644 --- a/DeepResearchAgent/docker_compose/intel/hpu/gaudi/set_env.sh +++ b/DeepResearchAgent/docker_compose/intel/hpu/gaudi/set_env.sh @@ -101,4 +101,4 @@ export MAX_RESEARCHER_ITERATIONS="${MAX_RESEARCHER_ITERATIONS:-3}" # Custom instructions for agent behavior (leave empty for defaults) export RESEARCHER_INSTRUCTIONS="" # Instructions for individual researchers export RESEARCH_WORKFLOW_INSTRUCTIONS="" # Instructions for overall research workflow -export SUBAGENT_DELEGATION_INSTRUCTIONS="" # Instructions for task delegation between agents \ No newline at end of file +export SUBAGENT_DELEGATION_INSTRUCTIONS="" # Instructions for task delegation between agents diff --git a/DeepResearchAgent/research_agent.py b/DeepResearchAgent/research_agent.py index 9259b20a50..beb0e77b7f 100644 --- a/DeepResearchAgent/research_agent.py +++ b/DeepResearchAgent/research_agent.py @@ -1,14 +1,14 @@ # Copyright (C) 2024 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -import os import logging +import os from typing import List, Union -from comps import opea_microservices, register_microservice, CustomLogger -from comps.cores.telemetry.opea_telemetry import opea_telemetry -from pydantic import BaseModel from agent_factory import create_agent +from comps import CustomLogger, opea_microservices, register_microservice +from comps.cores.telemetry.opea_telemetry import opea_telemetry +from pydantic import BaseModel from research_agents.deepagents.utils import format_message logger = CustomLogger(__name__) @@ -30,10 +30,10 @@ class SimpleRequest(BaseModel): async def run(request: SimpleRequest): logger.debug(f"Received question: {request.question}") - + logger.debug("Creating DeepAgents research agent...") agent = create_agent(impl="DeepAgents") - + logger.debug("Invoking agent with the provided question...") result = agent.invoke( { @@ -43,12 +43,12 @@ async def run(request: SimpleRequest): "content": f"Question: {request.question}", } ], - }, + }, ) logger.debug("Agent invocation completed.") if os.getenv("LOGFLAG", "").lower() == "true": format_message(result["messages"]) - + return {"answer": result["messages"][-1].content} diff --git a/DeepResearchAgent/research_agents/deepagents/README.md b/DeepResearchAgent/research_agents/deepagents/README.md index 13c8196b7e..5b0c279d94 100644 --- a/DeepResearchAgent/research_agents/deepagents/README.md +++ b/DeepResearchAgent/research_agents/deepagents/README.md @@ -1,2 +1,3 @@ # Deep Research Agent of DeepAgents -The code is from LangChain [DeepAgents](https://github.com/langchain-ai/deepagents-quickstarts/tree/main/deep_research). \ No newline at end of file + +The code is from LangChain [DeepAgents](https://github.com/langchain-ai/deepagents-quickstarts/tree/main/deep_research). diff --git a/DeepResearchAgent/research_agents/deepagents/prompts.py b/DeepResearchAgent/research_agents/deepagents/prompts.py index 588bec0a5e..10451a8265 100644 --- a/DeepResearchAgent/research_agents/deepagents/prompts.py +++ b/DeepResearchAgent/research_agents/deepagents/prompts.py @@ -1,3 +1,5 @@ +# Copyright (C) 2025 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 """Prompt templates and tool descriptions for the research deepagent.""" RESEARCH_WORKFLOW_INSTRUCTIONS = """# Research Workflow @@ -68,7 +70,7 @@ Your job is to use tools to gather information about the user's input topic. -You can use any of the research tools provided to you to find resources that can help answer the research question. +You can use any of the research tools provided to you to find resources that can help answer the research question. You can call these tools in series or in parallel, your research is conducted in a tool-calling loop. @@ -170,4 +172,4 @@ ## Research Limits - Stop after {max_researcher_iterations} delegation rounds if you haven't found adequate sources - Stop when you have sufficient information to answer comprehensively -- Bias towards focused research over exhaustive exploration""" \ No newline at end of file +- Bias towards focused research over exhaustive exploration""" diff --git a/DeepResearchAgent/research_agents/deepagents/tools.py b/DeepResearchAgent/research_agents/deepagents/tools.py index 5a97cdcdd2..6a6ca25c05 100644 --- a/DeepResearchAgent/research_agents/deepagents/tools.py +++ b/DeepResearchAgent/research_agents/deepagents/tools.py @@ -1,3 +1,5 @@ +# Copyright (C) 2025 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 """Research Tools. This module provides search and content processing utilities for the research agent, @@ -39,9 +41,7 @@ def fetch_webpage_content(url: str, timeout: float = 10.0) -> str: def tavily_search( query: str, max_results: Annotated[int, InjectedToolArg] = 1, - topic: Annotated[ - Literal["general", "news", "finance"], InjectedToolArg - ] = "general", + topic: Annotated[Literal["general", "news", "finance"], InjectedToolArg] = "general", ) -> str: """Search the web for information on a given query. diff --git a/DeepResearchAgent/research_agents/deepagents/utils.py b/DeepResearchAgent/research_agents/deepagents/utils.py index 48aecfbb46..3a103e244f 100644 --- a/DeepResearchAgent/research_agents/deepagents/utils.py +++ b/DeepResearchAgent/research_agents/deepagents/utils.py @@ -1,3 +1,5 @@ +# Copyright (C) 2025 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 """Utility functions for displaying messages and prompts in Jupyter notebooks.""" import json @@ -31,11 +33,7 @@ def format_message_content(message): parts.append(str(message.content)) # Handle tool calls attached to the message (OpenAI format) - only if not already processed - if ( - not tool_calls_processed - and hasattr(message, "tool_calls") - and message.tool_calls - ): + if not tool_calls_processed and hasattr(message, "tool_calls") and message.tool_calls: for tool_call in message.tool_calls: parts.append(f"\n🔧 Tool Call: {tool_call['name']}") parts.append(f" Args: {json.dumps(tool_call['args'], indent=2)}") @@ -76,12 +74,8 @@ def show_prompt(prompt_text: str, title: str = "Prompt", border_style: str = "bl # Create a formatted display of the prompt formatted_text = Text(prompt_text) formatted_text.highlight_regex(r"<[^>]+>", style="bold blue") # Highlight XML tags - formatted_text.highlight_regex( - r"##[^#\n]+", style="bold magenta" - ) # Highlight headers - formatted_text.highlight_regex( - r"###[^#\n]+", style="bold cyan" - ) # Highlight sub-headers + formatted_text.highlight_regex(r"##[^#\n]+", style="bold magenta") # Highlight headers + formatted_text.highlight_regex(r"###[^#\n]+", style="bold cyan") # Highlight sub-headers # Display in a panel for better presentation console.print(