Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions libs/deepagents-cli/deepagents_cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ cli/
├── __main__.py # Entry point for `python -m deepagents.cli`
├── main.py # CLI loop, argument parsing, main orchestration
├── config.py # Configuration, constants, colors, model creation
├── tools.py # Custom tools (http_request, web_search)
├── tools.py # Custom tools (http_request, parallel_search, tavily_search)
├── ui.py # Display logic, TokenTracker, help screens
├── input.py # Input handling, completers, prompt session
├── commands.py # Slash command and bash command handlers
Expand Down Expand Up @@ -44,9 +44,10 @@ cli/
### `tools.py` - Custom Agent Tools
- **Purpose**: Additional tools for the agent beyond built-in filesystem operations
- **Tools**:
- `fetch_url()` - Fetch and convert web content to markdown
- `http_request()` - Make HTTP requests to APIs
- `web_search()` - Search the web using Tavily API
- `tavily_client` - Initialized Tavily client (if API key available)
- `parallel_search()` - Search the web using Parallel API (if PARALLEL_API_KEY is set)
- `tavily_search()` - Search the web using Tavily API (if TAVILY_API_KEY is set)

### `ui.py` - Display & Rendering
- **Purpose**: All UI rendering and display logic
Expand Down
70 changes: 56 additions & 14 deletions libs/deepagents-cli/deepagents_cli/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,15 +160,27 @@ def get_system_prompt(assistant_id: str, sandbox_type: str | None = None) -> str

### Web Search Tool Usage

When you use the web_search tool:
1. The tool will return search results with titles, URLs, and content excerpts
2. You MUST read and process these results, then respond naturally to the user
3. NEVER show raw JSON or tool results directly to the user
4. Synthesize the information from multiple sources into a coherent answer
5. Cite your sources by mentioning page titles or URLs when relevant
6. If the search doesn't find what you need, explain what you found and ask clarifying questions
You have access to web search tools based on available API keys:

The user only sees your text responses - not tool results. Always provide a complete, natural language answer after using web_search.
**tavily_search**:
- Returns results with 'content' field (string) containing excerpts
- Includes 'score' field for relevance (0-1)
- Supports 'topic' parameter: "general", "news", or "finance"

**parallel_search**:
- Returns results with 'excerpts' field (array of strings) in markdown format
- Includes 'search_id' for tracking
- Supports 'objective' parameter for natural language search goals and multiple queries

When using either tool:
1. You MUST read and process the results, then respond naturally to the user
2. NEVER show raw JSON or tool results directly to the user
3. Synthesize the information from multiple sources into a coherent answer
4. Cite your sources by mentioning page titles or URLs when relevant
5. If the search doesn't find what you need, explain what you found and ask clarifying questions

The user only sees your text responses - not tool results.
Always provide a complete, natural language answer after searching.

### Todo List Management

Expand Down Expand Up @@ -211,13 +223,37 @@ def _format_edit_file_description(tool_call: ToolCall, state: AgentState, runtim
)


def _format_web_search_description(tool_call: ToolCall, state: AgentState, runtime: Runtime) -> str:
"""Format web_search tool call for approval prompt."""
def _format_tavily_search_description(
tool_call: ToolCall, state: AgentState, runtime: Runtime
) -> str:
"""Format tavily_search tool call for approval prompt."""
args = tool_call["args"]
query = args.get("query", "unknown")
max_results = args.get("max_results", 5)
topic = args.get("topic", "general")

return (
f"Query: {query}\n"
f"Max results: {max_results}\n"
f"Topic: {topic}\n\n"
"⚠️ This will use Tavily API credits"
)


def _format_parallel_search_description(
tool_call: ToolCall, state: AgentState, runtime: Runtime
) -> str:
"""Format parallel_search tool call for approval prompt."""
args = tool_call["args"]
queries = args.get("queries", [])
max_results = args.get("max_results", 5)
objective = args.get("objective")

description = f"Queries: {queries}\nMax results: {max_results}\n"
description += f"Objective: {objective}\n"
description += "\n⚠️ This will use Parallel API credits"

return f"Query: {query}\nMax results: {max_results}\n\n⚠️ This will use Tavily API credits"
return description


def _format_fetch_url_description(tool_call: ToolCall, state: AgentState, runtime: Runtime) -> str:
Expand Down Expand Up @@ -354,9 +390,14 @@ def create_agent_with_config(
"description": _format_edit_file_description,
}

web_search_interrupt_config: InterruptOnConfig = {
tavily_search_interrupt_config: InterruptOnConfig = {
"allowed_decisions": ["approve", "reject"],
"description": _format_tavily_search_description,
}

parallel_search_interrupt_config: InterruptOnConfig = {
"allowed_decisions": ["approve", "reject"],
"description": _format_web_search_description,
"description": _format_parallel_search_description,
}

fetch_url_interrupt_config: InterruptOnConfig = {
Expand All @@ -380,7 +421,8 @@ def create_agent_with_config(
"execute": execute_interrupt_config,
"write_file": write_file_interrupt_config,
"edit_file": edit_file_interrupt_config,
"web_search": web_search_interrupt_config,
"tavily_search": tavily_search_interrupt_config,
"parallel_search": parallel_search_interrupt_config,
"fetch_url": fetch_url_interrupt_config,
"task": task_interrupt_config,
},
Expand Down
9 changes: 9 additions & 0 deletions libs/deepagents-cli/deepagents_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,14 @@ class Settings:
openai_api_key: OpenAI API key if available
anthropic_api_key: Anthropic API key if available
tavily_api_key: Tavily API key if available
parallel_api_key: Parallel API key if available
"""

# API keys
openai_api_key: str | None
anthropic_api_key: str | None
tavily_api_key: str | None
parallel_api_key: str | None

# Project information
project_root: Path | None
Expand All @@ -152,6 +154,7 @@ def from_environment(cls, *, start_path: Path | None = None) -> "Settings":
openai_key = os.environ.get("OPENAI_API_KEY")
anthropic_key = os.environ.get("ANTHROPIC_API_KEY")
tavily_key = os.environ.get("TAVILY_API_KEY")
parallel_key = os.environ.get("PARALLEL_API_KEY")

# Detect project
project_root = _find_project_root(start_path)
Expand All @@ -160,6 +163,7 @@ def from_environment(cls, *, start_path: Path | None = None) -> "Settings":
openai_api_key=openai_key,
anthropic_api_key=anthropic_key,
tavily_api_key=tavily_key,
parallel_api_key=parallel_key,
project_root=project_root,
)

Expand All @@ -178,6 +182,11 @@ def has_tavily(self) -> bool:
"""Check if Tavily API key is configured."""
return self.tavily_api_key is not None

@property
def has_parallel(self) -> bool:
"""Check if Parallel API key is configured."""
return self.parallel_api_key is not None

@property
def has_project(self) -> bool:
"""Check if currently in a git project."""
Expand Down
7 changes: 5 additions & 2 deletions libs/deepagents-cli/deepagents_cli/default_agent_prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,11 @@ Examples: `pytest /foo/bar/tests` (good), `cd /foo/bar && pytest tests` (bad)

Always use absolute paths starting with /.

### web_search
Search for documentation, error solutions, and code examples.
### parallel_search
Search for documentation, error solutions, and code examples using Parallel.

### tavily_search
Search for documentation, error solutions, and code examples using Tavily.

### http_request
Make HTTP requests to APIs (GET, POST, etc.).
Expand Down
5 changes: 3 additions & 2 deletions libs/deepagents-cli/deepagents_cli/execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,8 @@ async def execute_task(
"grep": "🔎",
"shell": "⚡",
"execute": "🔧",
"web_search": "🌐",
"parallel_search": "🌐",
"tavily_search": "🌐",
"http_request": "🌍",
"task": "🤖",
"write_todos": "📋",
Expand Down Expand Up @@ -372,7 +373,7 @@ def flush_text_buffer(*, final: bool = False) -> None:
status.start()
spinner_active = True

# For all other tools (web_search, http_request, etc.),
# For all other tools (parallel_search, http_request, etc.),
# results are hidden from user - agent will process and respond
continue

Expand Down
21 changes: 15 additions & 6 deletions libs/deepagents-cli/deepagents_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
get_default_working_dir,
)
from deepagents_cli.skills import execute_skills_command, setup_skills_parser
from deepagents_cli.tools import fetch_url, http_request, web_search
from deepagents_cli.tools import fetch_url, http_request, parallel_search, tavily_search
from deepagents_cli.ui import TokenTracker, show_help


Expand Down Expand Up @@ -52,6 +52,11 @@ def check_cli_dependencies() -> None:
except ImportError:
missing.append("tavily-python")

try:
import parallel
except ImportError:
missing.append("parallel-web")

try:
import prompt_toolkit
except ImportError:
Expand Down Expand Up @@ -167,15 +172,17 @@ async def simple_cli(
)
console.print()

if not settings.has_tavily:
if not settings.has_tavily and not settings.has_parallel:
console.print(
"[yellow]⚠ Web search disabled:[/yellow] TAVILY_API_KEY not found.",
"[yellow]⚠ Web search disabled:[/yellow] No API key found.",
style=COLORS["dim"],
)
console.print(" To enable web search, set your Tavily API key:", style=COLORS["dim"])
console.print(" To enable web search, set one of these API keys:", style=COLORS["dim"])
console.print(" export TAVILY_API_KEY=your_api_key_here", style=COLORS["dim"])
console.print(" export PARALLEL_API_KEY=your_api_key_here", style=COLORS["dim"])
console.print(" Or add them to your .env file.", style=COLORS["dim"])
console.print(
" Or add it to your .env file. Get your key at: https://tavily.com",
" Get keys at: https://tavily.com or https://www.parallel.ai",
style=COLORS["dim"],
)
console.print()
Expand Down Expand Up @@ -273,7 +280,9 @@ async def _run_agent_session(
# Create agent with conditional tools
tools = [http_request, fetch_url]
if settings.has_tavily:
tools.append(web_search)
tools.append(tavily_search)
if settings.has_parallel:
tools.append(parallel_search)

agent, composite_backend = create_agent_with_config(
model, assistant_id, tools, sandbox=sandbox_backend, sandbox_type=sandbox_type
Expand Down
107 changes: 104 additions & 3 deletions libs/deepagents-cli/deepagents_cli/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@

import requests
from markdownify import markdownify
from parallel import Parallel
from parallel.types.beta.search_result import SearchResult
from tavily import TavilyClient

from deepagents_cli.config import settings

# Initialize Tavily client if API key is available
tavily_client = TavilyClient(api_key=settings.tavily_api_key) if settings.has_tavily else None

# Initialize Parallel client if API key is available
parallel_client = Parallel(api_key=settings.parallel_api_key) if settings.has_parallel else None


def http_request(
url: str,
Expand Down Expand Up @@ -87,12 +92,12 @@ def http_request(
}


def web_search(
def tavily_search(
query: str,
max_results: int = 5,
topic: Literal["general", "news", "finance"] = "general",
include_raw_content: bool = False,
):
) -> dict[str, Any]:
"""Search the web using Tavily for current information and documentation.

This tool searches the web and returns relevant results. After receiving results,
Expand Down Expand Up @@ -134,7 +139,103 @@ def web_search(
topic=topic,
)
except Exception as e:
return {"error": f"Web search error: {e!s}", "query": query}
return {"error": f"Tavily search error: {e!s}", "query": query}


def parallel_search(
objective: str,
queries: list[str],
max_results: int = 5,
max_chars_per_excerpt: int = 1000,
) -> SearchResult:
"""Search the web using Parallel for current information and documentation.

This tool searches the web and returns relevant results with excerpts.
After receiving results, you MUST synthesize the information into a natural,
helpful response for the user.

Args:
objective: Natural-language description of what the web search is trying to find.
May include guidance about preferred sources or freshness.
queries: A list of search queries (be specific and detailed)
max_results: Number of results to return (default: 5)
max_chars_per_excerpt: Maximum characters per excerpt (default: 1000)

Returns:
Dictionary containing:
- results: A list of WebSearchResult objects, ordered by decreasing relevance:
- title: Title of the webpage, if available.
- url: URL associated with the search result.
- excerpts: List of relevant excerpted content from the URL, formatted as markdown.
- publish_date: Publish date of the webpage in YYYY-MM-DD format, if available.
- search_id: Unique search identifier

IMPORTANT: After using this tool:
1. Read through the 'excerpts' field of each result (array of strings)
2. Extract relevant information that answers the user's question
3. Synthesize this into a clear, natural language response
4. Cite sources by mentioning the page titles or URLs
5. NEVER show the raw JSON to the user - always provide a formatted response
"""
if parallel_client is None:
return {
"error": "Parallel API key not configured. Please set PARALLEL_API_KEY environment variable.",
"queries": queries,
"objective": objective,
}

try:
return parallel_client.beta.search(
objective=objective,
search_queries=queries,
max_results=max_results,
max_chars_per_result=max_chars_per_excerpt,
)
except Exception as e:
return {
"error": f"Parallel search error: {e!s}",
"queries": queries,
"objective": objective,
}


def web_search(
query: str,
max_results: int = 5,
topic: Literal["general", "news", "finance"] = "general",
include_raw_content: bool = False,
):
"""Search the web using Tavily for current information and documentation.

This is a backward compatibility wrapper for tavily_search.
For new code, use tavily_search or parallel_search directly.

This tool searches the web and returns relevant results. After receiving results,
you MUST synthesize the information into a natural, helpful response for the user.

Args:
query: The search query (be specific and detailed)
max_results: Number of results to return (default: 5)
topic: Search topic type - "general" for most queries, "news" for current events
include_raw_content: Include full page content (warning: uses more tokens)

Returns:
Dictionary containing:
- results: List of search results, each with:
- title: Page title
- url: Page URL
- content: Relevant excerpt from the page
- score: Relevance score (0-1)
- query: The original search query

IMPORTANT: After using this tool:
1. Read through the 'content' field of each result
2. Extract relevant information that answers the user's question
3. Synthesize this into a clear, natural language response
4. Cite sources by mentioning the page titles or URLs
5. NEVER show the raw JSON to the user - always provide a formatted response
"""
return tavily_search(query, max_results, topic, include_raw_content)


def fetch_url(url: str, timeout: int = 30) -> dict[str, Any]:
Expand Down
Loading