Upload deepdiver_v2/cli/demo.py with huggingface_hub
Browse files- deepdiver_v2/cli/demo.py +668 -0
deepdiver_v2/cli/demo.py
ADDED
|
@@ -0,0 +1,668 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
|
| 2 |
+
#!/usr/bin/env python3
|
| 3 |
+
"""
|
| 4 |
+
CLI Demo for DeepDiver Long Writer Multi-Agent System
|
| 5 |
+
|
| 6 |
+
This demo showcases the multi-agent system that includes:
|
| 7 |
+
- PlannerAgent: Coordinates and orchestrates the entire process
|
| 8 |
+
- InformationSeekerAgent: Gathers and researches information
|
| 9 |
+
- WriterAgent: Creates long-form content
|
| 10 |
+
|
| 11 |
+
Features:
|
| 12 |
+
- Loads configuration from config/.env file
|
| 13 |
+
- Shows real-time tool calls and reasoning traces
|
| 14 |
+
- Displays sub-agent responses and interactions
|
| 15 |
+
- Visualizes the complete execution flow
|
| 16 |
+
- Query preprocessing for safety and task suitability check
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
import os
|
| 20 |
+
import sys
|
| 21 |
+
import json
|
| 22 |
+
import time
|
| 23 |
+
import logging
|
| 24 |
+
import argparse
|
| 25 |
+
import requests
|
| 26 |
+
from pathlib import Path
|
| 27 |
+
from typing import Dict, Any, List, Optional
|
| 28 |
+
from rich.console import Console
|
| 29 |
+
from rich.table import Table
|
| 30 |
+
from rich.panel import Panel
|
| 31 |
+
from rich.syntax import Syntax
|
| 32 |
+
from rich.markdown import Markdown
|
| 33 |
+
|
| 34 |
+
# Add project root to Python path
|
| 35 |
+
project_root = Path(__file__).parent.parent
|
| 36 |
+
sys.path.insert(0, str(project_root))
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
# Configure logging to keep the CLI clean
|
| 40 |
+
def setup_clean_logging(debug_mode: bool = False):
|
| 41 |
+
"""Configure logging to show only relevant information for the demo"""
|
| 42 |
+
if debug_mode:
|
| 43 |
+
# Debug mode: show all logs with timestamps
|
| 44 |
+
logging.basicConfig(
|
| 45 |
+
level=logging.DEBUG,
|
| 46 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
| 47 |
+
datefmt='%H:%M:%S'
|
| 48 |
+
)
|
| 49 |
+
else:
|
| 50 |
+
# Clean demo mode: suppress verbose logs
|
| 51 |
+
|
| 52 |
+
# Suppress specific noisy loggers
|
| 53 |
+
noisy_loggers = [
|
| 54 |
+
'httpx',
|
| 55 |
+
'httpcore',
|
| 56 |
+
'urllib3',
|
| 57 |
+
'src.tools.mcp_client',
|
| 58 |
+
# 'src.agents.base_agent',
|
| 59 |
+
'config.config' # Also suppress config messages in quiet mode
|
| 60 |
+
]
|
| 61 |
+
|
| 62 |
+
for logger_name in noisy_loggers:
|
| 63 |
+
logging.getLogger(logger_name).setLevel(logging.ERROR)
|
| 64 |
+
|
| 65 |
+
# Set up default clean logging before any imports
|
| 66 |
+
setup_clean_logging(debug_mode=False)
|
| 67 |
+
|
| 68 |
+
# Import the multi-agent system components
|
| 69 |
+
from config.config import get_config, reload_config
|
| 70 |
+
from src.agents.planner_agent import create_planner_agent
|
| 71 |
+
from src.agents.base_agent import AgentResponse
|
| 72 |
+
|
| 73 |
+
console = Console()
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
class DemoVisualizer:
|
| 77 |
+
"""Visualizes the execution of the multi-agent system"""
|
| 78 |
+
|
| 79 |
+
def __init__(self, quiet_mode: bool = False):
|
| 80 |
+
self.console = console
|
| 81 |
+
self.execution_log = []
|
| 82 |
+
self.quiet_mode = quiet_mode
|
| 83 |
+
|
| 84 |
+
def _should_display(self, force: bool = False) -> bool:
|
| 85 |
+
"""Check if output should be displayed based on quiet mode"""
|
| 86 |
+
return not self.quiet_mode or force
|
| 87 |
+
|
| 88 |
+
def show_welcome(self):
|
| 89 |
+
"""Display welcome message and system info"""
|
| 90 |
+
if not self._should_display():
|
| 91 |
+
return
|
| 92 |
+
|
| 93 |
+
welcome_text = """
|
| 94 |
+
# 🤖 DeepDiver Long Writer Multi-Agent System Demo
|
| 95 |
+
|
| 96 |
+
This demo showcases an advanced multi-agent system for research and long-form content generation.
|
| 97 |
+
|
| 98 |
+
## System Components:
|
| 99 |
+
- **🧠 PlannerAgent**: Orchestrates the entire process and coordinates sub-agents
|
| 100 |
+
- **🔍 InformationSeekerAgent**: Performs web research and gathers information
|
| 101 |
+
- **✍️ WriterAgent**: Creates comprehensive long-form content
|
| 102 |
+
|
| 103 |
+
## Features:
|
| 104 |
+
- Real-time tool execution visualization
|
| 105 |
+
- Sub-agent response tracking
|
| 106 |
+
- Complete reasoning trace display
|
| 107 |
+
- Configuration management
|
| 108 |
+
- Query safety and suitability pre-check
|
| 109 |
+
"""
|
| 110 |
+
self.console.print(Panel(Markdown(welcome_text), title="[bold blue]Welcome", border_style="blue"))
|
| 111 |
+
|
| 112 |
+
def show_config(self, config):
|
| 113 |
+
"""Display current configuration"""
|
| 114 |
+
if not self._should_display():
|
| 115 |
+
return
|
| 116 |
+
|
| 117 |
+
config_table = Table(title="📋 System Configuration", show_header=True, header_style="bold magenta")
|
| 118 |
+
config_table.add_column("Setting", style="cyan", no_wrap=True)
|
| 119 |
+
config_table.add_column("Value", style="green")
|
| 120 |
+
|
| 121 |
+
# Safe config display (hide sensitive values)
|
| 122 |
+
safe_config = config.to_dict()
|
| 123 |
+
for key, value in safe_config.items():
|
| 124 |
+
if value is not None and str(value) != "None":
|
| 125 |
+
display_value = str(value)
|
| 126 |
+
if len(display_value) > 60:
|
| 127 |
+
display_value = display_value[:57] + "..."
|
| 128 |
+
config_table.add_row(key, display_value)
|
| 129 |
+
|
| 130 |
+
self.console.print(config_table)
|
| 131 |
+
|
| 132 |
+
def show_planner_start(self, query: str):
|
| 133 |
+
"""Show planner starting execution"""
|
| 134 |
+
self.console.print(Panel(
|
| 135 |
+
f"[bold yellow]User Query:[/bold yellow] {query}\n\n"
|
| 136 |
+
f"[bold green]🚀 Starting PlannerAgent execution...[/bold green]",
|
| 137 |
+
title="[bold blue]Task Initiation",
|
| 138 |
+
border_style="green"
|
| 139 |
+
))
|
| 140 |
+
|
| 141 |
+
def show_reasoning_step(self, iteration: int, reasoning: str):
|
| 142 |
+
"""Display reasoning step"""
|
| 143 |
+
self.console.print(Panel(
|
| 144 |
+
Markdown(f"**Iteration {iteration} - Reasoning:**\n\n{reasoning}"),
|
| 145 |
+
title=f"[bold yellow]🧠 Agent Reasoning (Step {iteration})",
|
| 146 |
+
border_style="yellow"
|
| 147 |
+
))
|
| 148 |
+
|
| 149 |
+
def show_tool_call(self, iteration: int, tool_name: str, arguments: Dict[str, Any]):
|
| 150 |
+
"""Display tool call"""
|
| 151 |
+
args_json = json.dumps(arguments, indent=2, ensure_ascii=False)
|
| 152 |
+
|
| 153 |
+
self.console.print(Panel(
|
| 154 |
+
f"[bold cyan]Tool:[/bold cyan] {tool_name}\n\n"
|
| 155 |
+
f"[bold cyan]Arguments:[/bold cyan]\n{Syntax(args_json, 'json', theme='monokai', line_numbers=True)}",
|
| 156 |
+
title=f"[bold cyan]🔧 Tool Call (Step {iteration})",
|
| 157 |
+
border_style="cyan"
|
| 158 |
+
))
|
| 159 |
+
|
| 160 |
+
def show_tool_result(self, iteration: int, tool_name: str, result: Dict[str, Any]):
|
| 161 |
+
"""Display tool result"""
|
| 162 |
+
success = result.get("success", True)
|
| 163 |
+
status_icon = "✅" if success else "❌"
|
| 164 |
+
status_color = "green" if success else "red"
|
| 165 |
+
|
| 166 |
+
# Format result for display
|
| 167 |
+
if success and "data" in result:
|
| 168 |
+
display_result = result["data"]
|
| 169 |
+
elif "error" in result:
|
| 170 |
+
display_result = {"error": result["error"]}
|
| 171 |
+
else:
|
| 172 |
+
display_result = result
|
| 173 |
+
|
| 174 |
+
result_text = json.dumps(display_result, indent=2, ensure_ascii=False)
|
| 175 |
+
if len(result_text) > 1000:
|
| 176 |
+
result_text = result_text[:997] + "..."
|
| 177 |
+
|
| 178 |
+
self.console.print(Panel(
|
| 179 |
+
f"[bold {status_color}]Status:[/bold {status_color}] {status_icon} {'Success' if success else 'Failed'}\n\n"
|
| 180 |
+
f"[bold {status_color}]Result:[/bold {status_color}]\n{Syntax(result_text, 'json', theme='monokai', line_numbers=True)}",
|
| 181 |
+
title=f"[bold {status_color}]📋 Tool Result: {tool_name} (Step {iteration})",
|
| 182 |
+
border_style=status_color
|
| 183 |
+
))
|
| 184 |
+
|
| 185 |
+
def show_sub_agent_execution(self, agent_name: str, task_content: str):
|
| 186 |
+
"""Show sub-agent starting execution"""
|
| 187 |
+
self.console.print(Panel(
|
| 188 |
+
f"[bold magenta]Agent:[/bold magenta] {agent_name}\n\n"
|
| 189 |
+
f"[bold magenta]Task:[/bold magenta] {task_content[:500]}{'...' if len(task_content) > 500 else ''}",
|
| 190 |
+
title="[bold magenta]🤝 Sub-Agent Execution",
|
| 191 |
+
border_style="magenta"
|
| 192 |
+
))
|
| 193 |
+
|
| 194 |
+
def show_sub_agent_result(self, agent_name: str, result: Dict[str, Any]):
|
| 195 |
+
"""Show sub-agent execution result"""
|
| 196 |
+
success = result.get("success", True)
|
| 197 |
+
status_icon = "✅" if success else "❌"
|
| 198 |
+
status_color = "green" if success else "red"
|
| 199 |
+
|
| 200 |
+
# Extract key information
|
| 201 |
+
iterations = result.get("iterations", 0)
|
| 202 |
+
execution_time = result.get("execution_time", 0)
|
| 203 |
+
|
| 204 |
+
summary = f"[bold {status_color}]Status:[/bold {status_color}] {status_icon} {'Success' if success else 'Failed'}\n"
|
| 205 |
+
summary += f"[bold blue]Iterations:[/bold blue] {iterations}\n"
|
| 206 |
+
summary += f"[bold blue]Execution Time:[/bold blue] {execution_time:.2f}s\n\n"
|
| 207 |
+
|
| 208 |
+
if success and "data" in result:
|
| 209 |
+
data = result["data"]
|
| 210 |
+
if isinstance(data, dict):
|
| 211 |
+
for key, value in data.items():
|
| 212 |
+
if isinstance(value, str) and len(value) > 200:
|
| 213 |
+
summary += f"[bold blue]{key}:[/bold blue] {value[:197]}...\n"
|
| 214 |
+
else:
|
| 215 |
+
summary += f"[bold blue]{key}:[/bold blue] {value}\n"
|
| 216 |
+
elif "error" in result:
|
| 217 |
+
summary += f"[bold red]Error:[/bold red] {result['error']}\n"
|
| 218 |
+
|
| 219 |
+
self.console.print(Panel(
|
| 220 |
+
summary,
|
| 221 |
+
title=f"[bold {status_color}]📊 Sub-Agent Result: {agent_name}",
|
| 222 |
+
border_style=status_color
|
| 223 |
+
))
|
| 224 |
+
|
| 225 |
+
def show_final_result(self, response: AgentResponse):
|
| 226 |
+
"""Display final execution result"""
|
| 227 |
+
# Always show final results, even in quiet mode
|
| 228 |
+
if not self._should_display(force=True):
|
| 229 |
+
return
|
| 230 |
+
|
| 231 |
+
success = response.success
|
| 232 |
+
status_icon = "✅" if success else "❌"
|
| 233 |
+
status_color = "green" if success else "red"
|
| 234 |
+
|
| 235 |
+
summary = f"[bold {status_color}]Final Status:[/bold {status_color}] {status_icon} {'Completed Successfully' if success else 'Failed'}\n"
|
| 236 |
+
summary += f"[bold blue]Total Iterations:[/bold blue] {response.iterations}\n"
|
| 237 |
+
summary += f"[bold blue]Total Execution Time:[/bold blue] {response.execution_time:.2f}s\n"
|
| 238 |
+
summary += f"[bold blue]Agent:[/bold blue] {response.agent_name}\n\n"
|
| 239 |
+
|
| 240 |
+
if success and response.result:
|
| 241 |
+
if isinstance(response.result, dict):
|
| 242 |
+
for key, value in response.result.items():
|
| 243 |
+
if isinstance(value, str) and len(value) > 3000:
|
| 244 |
+
summary += f"[bold blue]{key}:[/bold blue] {value[:2997]}...\n\n"
|
| 245 |
+
else:
|
| 246 |
+
summary += f"[bold blue]{key}:[/bold blue] {value}\n\n"
|
| 247 |
+
elif response.error:
|
| 248 |
+
summary += f"[bold red]Error:[/bold red] {response.error}\n"
|
| 249 |
+
|
| 250 |
+
self.console.print(Panel(
|
| 251 |
+
summary,
|
| 252 |
+
title=f"[bold {status_color}]🏁 Final Result",
|
| 253 |
+
border_style=status_color
|
| 254 |
+
))
|
| 255 |
+
|
| 256 |
+
def show_reasoning_trace(self, trace: List[Dict[str, Any]]):
|
| 257 |
+
"""Display detailed reasoning trace"""
|
| 258 |
+
if not trace:
|
| 259 |
+
return
|
| 260 |
+
|
| 261 |
+
trace_table = Table(title="🔍 Detailed Execution Trace", show_header=True, header_style="bold cyan")
|
| 262 |
+
trace_table.add_column("Step", style="cyan", width=8)
|
| 263 |
+
trace_table.add_column("Type", style="magenta", width=12)
|
| 264 |
+
trace_table.add_column("Details", style="white")
|
| 265 |
+
|
| 266 |
+
for i, step in enumerate(trace, 1):
|
| 267 |
+
step_type = step.get("type", "unknown")
|
| 268 |
+
|
| 269 |
+
if step_type == "reasoning":
|
| 270 |
+
content = step.get("content", "")[:100] + ("..." if len(step.get("content", "")) > 100 else "")
|
| 271 |
+
trace_table.add_row(str(i), "🧠 Reasoning", content)
|
| 272 |
+
|
| 273 |
+
elif step_type == "action":
|
| 274 |
+
tool = step.get("tool", "")
|
| 275 |
+
result_status = "✅" if step.get("result", {}).get("success", True) else "❌"
|
| 276 |
+
trace_table.add_row(str(i), "🔧 Tool Call", f"{result_status} {tool}")
|
| 277 |
+
|
| 278 |
+
elif step_type == "error":
|
| 279 |
+
error = step.get("error", "")[:100] + ("..." if len(step.get("error", "")) > 100 else "")
|
| 280 |
+
trace_table.add_row(str(i), "❌ Error", error)
|
| 281 |
+
|
| 282 |
+
self.console.print(trace_table)
|
| 283 |
+
|
| 284 |
+
def show_unsupported_response(self):
|
| 285 |
+
"""Display the fixed response for unsupported queries"""
|
| 286 |
+
# Always show this response, even in quiet mode
|
| 287 |
+
self.console.print(Panel(
|
| 288 |
+
"Sorry, your question is not within the current scope of tasks for DeepDiver-V2. Please try asking a question related to long-form writing or complex knowledge Q&A instead.",
|
| 289 |
+
title="[bold yellow]❌ Unsupported Query",
|
| 290 |
+
border_style="yellow"
|
| 291 |
+
))
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
class AgentExecutionMonitor:
|
| 295 |
+
"""Monitors agent execution and provides real-time feedback"""
|
| 296 |
+
|
| 297 |
+
def __init__(self, visualizer: DemoVisualizer):
|
| 298 |
+
self.visualizer = visualizer
|
| 299 |
+
self.current_iteration = 0
|
| 300 |
+
|
| 301 |
+
def on_reasoning_step(self, iteration: int, reasoning: str):
|
| 302 |
+
"""Called when agent performs reasoning"""
|
| 303 |
+
self.visualizer.show_reasoning_step(iteration, reasoning)
|
| 304 |
+
|
| 305 |
+
def on_tool_call(self, iteration: int, tool_name: str, arguments: Dict[str, Any]):
|
| 306 |
+
"""Called when agent makes a tool call"""
|
| 307 |
+
self.visualizer.show_tool_call(iteration, tool_name, arguments)
|
| 308 |
+
|
| 309 |
+
# Check for sub-agent assignments
|
| 310 |
+
if "assign_" in tool_name and "task" in tool_name:
|
| 311 |
+
if "tasks" in arguments:
|
| 312 |
+
for task in arguments.get("tasks", []):
|
| 313 |
+
task_content = task.get("task_content", "")
|
| 314 |
+
self.visualizer.show_sub_agent_execution("InformationSeeker", task_content)
|
| 315 |
+
elif "task_content" in arguments:
|
| 316 |
+
task_content = arguments.get("task_content", "")
|
| 317 |
+
self.visualizer.show_sub_agent_execution("Writer", task_content)
|
| 318 |
+
|
| 319 |
+
def on_tool_result(self, iteration: int, tool_name: str, result: Dict[str, Any]):
|
| 320 |
+
"""Called when tool execution completes"""
|
| 321 |
+
self.visualizer.show_tool_result(iteration, tool_name, result)
|
| 322 |
+
|
| 323 |
+
# Show sub-agent results if this was an assignment
|
| 324 |
+
if "assign_" in tool_name and "task" in tool_name:
|
| 325 |
+
if "data" in result and "tasks" in result["data"]:
|
| 326 |
+
for task_result in result["data"]["tasks"]:
|
| 327 |
+
agent_name = task_result.get("agent_name", "InformationSeeker")
|
| 328 |
+
self.visualizer.show_sub_agent_result(agent_name, task_result)
|
| 329 |
+
elif "data" in result:
|
| 330 |
+
agent_name = result["data"].get("agent_name", "Writer")
|
| 331 |
+
self.visualizer.show_sub_agent_result(agent_name, result["data"])
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
def classify_query(query: str, config) -> Dict[str, Any]:
|
| 335 |
+
"""
|
| 336 |
+
Classify user query into one of three categories using LLM:
|
| 337 |
+
1. SAFE_SENSITIVE: Contains unsafe content (insults, political risks, etc.)
|
| 338 |
+
2. NON_KNOWLEDGE: Non-knowledge intensive (no need for research, e.g., greetings, simple calculations)
|
| 339 |
+
3. NORMAL: Requires processing (long-form writing or complex knowledge Q&A)
|
| 340 |
+
|
| 341 |
+
Returns:
|
| 342 |
+
Dict with 'category' (str) and 'reasoning' (str)
|
| 343 |
+
"""
|
| 344 |
+
logger = logging.getLogger(__name__)
|
| 345 |
+
|
| 346 |
+
# Get model configuration
|
| 347 |
+
model_config = config.get_custom_llm_config()
|
| 348 |
+
pangu_url = model_config.get('url') or os.getenv('MODEL_REQUEST_URL', '')
|
| 349 |
+
model_token = model_config.get('token') or os.getenv('MODEL_REQUEST_TOKEN', '')
|
| 350 |
+
|
| 351 |
+
# Validate model configuration
|
| 352 |
+
if not pangu_url:
|
| 353 |
+
logger.error("Model URL not configured for query classification")
|
| 354 |
+
# Fallback to NORMAL category if model config is missing
|
| 355 |
+
return {
|
| 356 |
+
"category": "NORMAL",
|
| 357 |
+
"reasoning": "模型配置不完整,跳过分类检查,默认按正常任务处理"
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
headers = {'Content-Type': 'application/json', 'csb-token': model_token}
|
| 361 |
+
|
| 362 |
+
# Classification prompt (detailed instructions for accurate categorization)
|
| 363 |
+
prompt_template = """
|
| 364 |
+
你是一个Query分类器,需要将用户输入的查询分为以下三类,并给出明确的分类理由:
|
| 365 |
+
|
| 366 |
+
1. 【SAFE_SENSITIVE - 安全敏感内容】:包含以下任何一种情况的查询
|
| 367 |
+
- 辱骂、侮辱性语言(如脏话、人身攻击)
|
| 368 |
+
- 涉及政治敏感内容(如国家领导人、敏感政治事件、舆情风险话题)
|
| 369 |
+
- 违法违规内容(如暴力、色情、恐怖主义相关)
|
| 370 |
+
- 歧视性言论(种族、性别、宗教等歧视)
|
| 371 |
+
|
| 372 |
+
2. 【NON_KNOWLEDGE - 非知识密集型任务】:不需要进行信息搜索的简单查询
|
| 373 |
+
- 问候语(如"你好"、"早上好"、"嗨")
|
| 374 |
+
- 简单计算(如"1+1等于几"、"25乘以4是多少")
|
| 375 |
+
- 基础闲聊(如"你是谁")
|
| 376 |
+
- 指令性语句(如"退出"、"帮助"、"开始")
|
| 377 |
+
- 不需要信息收集的简单问题
|
| 378 |
+
|
| 379 |
+
3. 【NORMAL - 正常任务】:不包含安全敏感内容,需要进行信息搜索或长文写作的任务
|
| 380 |
+
- 简单的信息收集任务 (如"华为成立时间是什么时候")
|
| 381 |
+
- 复杂知识问答(如"ACL2025举办地有什么美食推荐")
|
| 382 |
+
- 长文写作任务(如"写一篇关于气候变化影响的5000字报告")
|
| 383 |
+
- 需要数据支持的分析(如"2023年全球经济增长数据及分析")
|
| 384 |
+
- 专业领域研究(如"机器学习在医疗诊断中的应用案例")
|
| 385 |
+
|
| 386 |
+
分类要求:
|
| 387 |
+
- 严格按照上述定义进行分类,不要遗漏任何关键特征
|
| 388 |
+
- 优先判断是否为SAFE_SENSITIVE,其次判断是否为NON_KNOWLEDGE,最后才是NORMAL
|
| 389 |
+
- 必须提供清晰的分类理由,说明为什么属于该类别
|
| 390 |
+
- 输出格式必须严格遵循:先输出分类理由的思考,然后换行输出分类结果(SAFE_SENSITIVE/NON_KNOWLEDGE/NORMAL)
|
| 391 |
+
|
| 392 |
+
示例1(SAFE_SENSITIVE):
|
| 393 |
+
该查询包含辱骂性语言"XXX",符合安全敏感内容的定义,属于需要拦截的内容
|
| 394 |
+
SAFE_SENSITIVE
|
| 395 |
+
|
| 396 |
+
示例2(NON_KNOWLEDGE):
|
| 397 |
+
该查询是简单的问候语"你好",不需要进行信息搜索,属于非知识密集型任务
|
| 398 |
+
NON_KNOWLEDGE
|
| 399 |
+
|
| 400 |
+
示例3(NORMAL):
|
| 401 |
+
该查询不包含安全敏感内容,要求撰写关于"区块链技术在金融领域的应用"的长文,需要进行信息收集、案例研究和深度分析,属于正常的长文写作任务
|
| 402 |
+
NORMAL
|
| 403 |
+
|
| 404 |
+
用户输入query:$query"""
|
| 405 |
+
|
| 406 |
+
# Prepare conversation history
|
| 407 |
+
conversation_history = [
|
| 408 |
+
{"role": "user", "content": prompt_template.replace("$query", query) + " /no_think"}
|
| 409 |
+
]
|
| 410 |
+
|
| 411 |
+
try:
|
| 412 |
+
# Call LLM with retry logic
|
| 413 |
+
retry_num = 1
|
| 414 |
+
max_retry_num = 3
|
| 415 |
+
while retry_num <= max_retry_num:
|
| 416 |
+
try:
|
| 417 |
+
response = requests.post(
|
| 418 |
+
url=pangu_url,
|
| 419 |
+
headers=headers,
|
| 420 |
+
json={
|
| 421 |
+
"model": config.model_name,
|
| 422 |
+
"chat_template": "{% for message in messages %}{% if loop.first and messages[0]['role'] != 'system' %}{{ '<s>[unused9]系统:[unused10]' }}{% endif %}{% if message['role'] == 'system' %}{{'<s>[unused9]系统:' + message['content'] + '[unused10]'}}{% endif %}{% if message['role'] == 'assistant' %}{{'[unused9]助手:' + message['content'] + '[unused10]'}}{% endif %}{% if message['role'] == 'tool' %}{{'[unused9]工具:' + message['content'] + '[unused10]'}}{% endif %}{% if message['role'] == 'function' %}{{'[unused9]方法:' + message['content'] + '[unused10]'}}{% endif %}{% if message['role'] == 'user' %}{{'[unused9]用户:' + message['content'] + '[unused10]'}}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '[unused9]助手:' }}{% endif %}",
|
| 423 |
+
"spaces_between_special_tokens": False,
|
| 424 |
+
"messages": conversation_history,
|
| 425 |
+
"temperature": 0.1, # Low temperature for deterministic classification
|
| 426 |
+
"max_tokens": 5000,
|
| 427 |
+
},
|
| 428 |
+
timeout=model_config.get("timeout", 60)
|
| 429 |
+
)
|
| 430 |
+
|
| 431 |
+
response_json = response.json()
|
| 432 |
+
logger.debug(f"Classification API response: {json.dumps(response_json, indent=2)}")
|
| 433 |
+
|
| 434 |
+
# Extract and parse result
|
| 435 |
+
assistant_message = response_json["choices"][0]["message"]["content"].strip()
|
| 436 |
+
lines = assistant_message.split('\n', 1)
|
| 437 |
+
|
| 438 |
+
if len(lines) < 2:
|
| 439 |
+
raise ValueError(f"Invalid response format: {assistant_message}")
|
| 440 |
+
|
| 441 |
+
reasoning = lines[0].strip()
|
| 442 |
+
category = lines[1].strip() if len(lines) > 1 else "NORMAL"
|
| 443 |
+
|
| 444 |
+
# Validate category
|
| 445 |
+
valid_categories = ["SAFE_SENSITIVE", "NON_KNOWLEDGE", "NORMAL"]
|
| 446 |
+
if category not in valid_categories:
|
| 447 |
+
logger.warning(f"Invalid category '{category}', using fallback NORMAL")
|
| 448 |
+
category = "NORMAL"
|
| 449 |
+
reasoning = f"模型返回无效分类 '{category}',默认按正常任务处理。原始理由:{reasoning}"
|
| 450 |
+
|
| 451 |
+
return {
|
| 452 |
+
"category": category,
|
| 453 |
+
"reasoning": reasoning
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
except Exception as e:
|
| 457 |
+
logger.error(f"Classification attempt {retry_num} failed: {str(e)}")
|
| 458 |
+
if retry_num == max_retry_num:
|
| 459 |
+
raise
|
| 460 |
+
time.sleep(2) # Wait before retry
|
| 461 |
+
retry_num += 1
|
| 462 |
+
|
| 463 |
+
except Exception as e:
|
| 464 |
+
logger.error(f"Query classification failed: {str(e)}")
|
| 465 |
+
# Fallback to NORMAL category if classification fails
|
| 466 |
+
return {
|
| 467 |
+
"category": "NORMAL",
|
| 468 |
+
"reasoning": f"分类服务暂时不可用(错误:{str(e)[:100]}...),默认按正常任务处理"
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
|
| 472 |
+
def load_environment_config(quiet: bool = False):
|
| 473 |
+
"""Load configuration from .env file"""
|
| 474 |
+
try:
|
| 475 |
+
# Check for .env file in config directory
|
| 476 |
+
config_dir = Path(__file__).parent.parent / "config"
|
| 477 |
+
env_file = config_dir / ".env"
|
| 478 |
+
|
| 479 |
+
if not env_file.exists():
|
| 480 |
+
if not quiet:
|
| 481 |
+
console.print(f"[yellow]⚠️ No .env file found at {env_file}[/yellow]")
|
| 482 |
+
console.print(f"[yellow]💡 Please copy env.template to config/.env and configure your settings[/yellow]")
|
| 483 |
+
return None
|
| 484 |
+
|
| 485 |
+
# Reload configuration to pick up .env file
|
| 486 |
+
reload_config()
|
| 487 |
+
config = get_config()
|
| 488 |
+
|
| 489 |
+
if not quiet:
|
| 490 |
+
console.print("[green]✅ Configuration loaded successfully[/green]")
|
| 491 |
+
return config
|
| 492 |
+
|
| 493 |
+
except Exception as e:
|
| 494 |
+
if not quiet:
|
| 495 |
+
console.print(f"[red]❌ Failed to load configuration: {e}[/red]")
|
| 496 |
+
return None
|
| 497 |
+
|
| 498 |
+
|
| 499 |
+
def create_sample_env_file():
|
| 500 |
+
"""Create a sample .env file for demo purposes"""
|
| 501 |
+
config_dir = Path(__file__).parent.parent / "config"
|
| 502 |
+
env_file = config_dir / ".env"
|
| 503 |
+
|
| 504 |
+
if env_file.exists():
|
| 505 |
+
return
|
| 506 |
+
|
| 507 |
+
# Copy from template
|
| 508 |
+
template_file = Path(__file__).parent.parent / "env.template"
|
| 509 |
+
if template_file.exists():
|
| 510 |
+
import shutil
|
| 511 |
+
shutil.copy2(template_file, env_file)
|
| 512 |
+
console.print(f"[green]✅ Created .env file from template at {env_file}[/green]")
|
| 513 |
+
console.print("[yellow]⚠️ Please edit the .env file with your actual configuration values[/yellow]")
|
| 514 |
+
else:
|
| 515 |
+
console.print(f"[red]❌ Could not find env.template to copy[/red]")
|
| 516 |
+
|
| 517 |
+
|
| 518 |
+
def run_demo_query(planner, query: str, visualizer: DemoVisualizer, config) -> Optional[AgentResponse]:
|
| 519 |
+
"""Run a demo query through the planner with preprocessing"""
|
| 520 |
+
|
| 521 |
+
# Step 1: Show query information
|
| 522 |
+
visualizer.show_planner_start(query)
|
| 523 |
+
|
| 524 |
+
# Step 2: Query classification (preprocessing)
|
| 525 |
+
classification_result = classify_query(query, config)
|
| 526 |
+
|
| 527 |
+
# Step 3: Branch processing based on classification
|
| 528 |
+
unsupported_categories = ["SAFE_SENSITIVE", "NON_KNOWLEDGE"]
|
| 529 |
+
if classification_result["category"] in unsupported_categories:
|
| 530 |
+
# Show fixed response for unsupported queries
|
| 531 |
+
visualizer.show_unsupported_response()
|
| 532 |
+
return None
|
| 533 |
+
|
| 534 |
+
# Step 4: Process normal query (original flow)
|
| 535 |
+
try:
|
| 536 |
+
# Execute the query
|
| 537 |
+
with console.status("[bold green]Executing planner task...", spinner="dots"):
|
| 538 |
+
response = planner.execute_task(query)
|
| 539 |
+
|
| 540 |
+
# Show final results
|
| 541 |
+
visualizer.show_final_result(response)
|
| 542 |
+
|
| 543 |
+
# Show detailed trace if available
|
| 544 |
+
if hasattr(response, 'reasoning_trace') and response.reasoning_trace:
|
| 545 |
+
visualizer.show_reasoning_trace(response.reasoning_trace)
|
| 546 |
+
|
| 547 |
+
return response
|
| 548 |
+
|
| 549 |
+
except Exception as e:
|
| 550 |
+
console.print(f"[red]❌ Error during execution: {e}[/red]")
|
| 551 |
+
return None
|
| 552 |
+
|
| 553 |
+
|
| 554 |
+
def main():
|
| 555 |
+
"""Main CLI demo function"""
|
| 556 |
+
parser = argparse.ArgumentParser(description="DeepDiver Multi-Agent System Demo")
|
| 557 |
+
parser.add_argument("--query", "-q", type=str, help="Query to execute (interactive mode if not provided)")
|
| 558 |
+
parser.add_argument("--config-only", "-c", action="store_true", help="Only show configuration and exit")
|
| 559 |
+
parser.add_argument("--create-env", "-e", action="store_true", help="Create sample .env file from template")
|
| 560 |
+
parser.add_argument("--debug", "-d", action="store_true", help="Enable debug mode with verbose logging")
|
| 561 |
+
parser.add_argument("--quiet", help="Suppress all non-essential output")
|
| 562 |
+
|
| 563 |
+
args = parser.parse_args()
|
| 564 |
+
|
| 565 |
+
# Setup logging based on arguments (re-configure if debug mode is requested)
|
| 566 |
+
if args.debug:
|
| 567 |
+
setup_clean_logging(debug_mode=True)
|
| 568 |
+
|
| 569 |
+
# Initialize visualizer
|
| 570 |
+
visualizer = DemoVisualizer(quiet_mode=args.quiet)
|
| 571 |
+
if not args.quiet:
|
| 572 |
+
visualizer.show_welcome()
|
| 573 |
+
|
| 574 |
+
# Create sample .env file if requested
|
| 575 |
+
if args.create_env:
|
| 576 |
+
create_sample_env_file()
|
| 577 |
+
return 0
|
| 578 |
+
|
| 579 |
+
# Load configuration
|
| 580 |
+
config = load_environment_config(quiet=args.quiet)
|
| 581 |
+
if not config:
|
| 582 |
+
if not args.quiet:
|
| 583 |
+
console.print("[red]❌ Cannot proceed without valid configuration[/red]")
|
| 584 |
+
console.print("[yellow]💡 Use --create-env to create a sample configuration file[/yellow]")
|
| 585 |
+
return 1
|
| 586 |
+
|
| 587 |
+
# Show configuration
|
| 588 |
+
visualizer.show_config(config)
|
| 589 |
+
|
| 590 |
+
if args.config_only:
|
| 591 |
+
return 0
|
| 592 |
+
|
| 593 |
+
# Initialize planner agent
|
| 594 |
+
try:
|
| 595 |
+
if not args.quiet:
|
| 596 |
+
console.print("[blue]🔄 Initializing PlannerAgent...[/blue]")
|
| 597 |
+
|
| 598 |
+
# Create planner with sub-agent configurations
|
| 599 |
+
sub_agent_configs = {
|
| 600 |
+
"information_seeker": {
|
| 601 |
+
"model": config.model_name,
|
| 602 |
+
"max_iterations": config.information_seeker_max_iterations or 30,
|
| 603 |
+
},
|
| 604 |
+
"writer": {
|
| 605 |
+
"model": config.model_name,
|
| 606 |
+
"max_iterations": config.writer_max_iterations or 30,
|
| 607 |
+
"temperature": config.model_temperature,
|
| 608 |
+
"max_tokens": config.model_max_tokens
|
| 609 |
+
}
|
| 610 |
+
}
|
| 611 |
+
|
| 612 |
+
planner = create_planner_agent(
|
| 613 |
+
model=config.model_name,
|
| 614 |
+
max_iterations=config.planner_max_iterations or 40,
|
| 615 |
+
sub_agent_configs=sub_agent_configs
|
| 616 |
+
)
|
| 617 |
+
|
| 618 |
+
if not args.quiet:
|
| 619 |
+
console.print("[green]✅ PlannerAgent initialized successfully[/green]")
|
| 620 |
+
|
| 621 |
+
except Exception as e:
|
| 622 |
+
if not args.quiet:
|
| 623 |
+
console.print(f"[red]❌ Failed to initialize PlannerAgent: {e}[/red]")
|
| 624 |
+
return 1
|
| 625 |
+
|
| 626 |
+
# Handle query execution
|
| 627 |
+
if args.query:
|
| 628 |
+
# Single query mode
|
| 629 |
+
run_demo_query(planner, args.query, visualizer, config)
|
| 630 |
+
else:
|
| 631 |
+
# Interactive mode
|
| 632 |
+
if not args.quiet:
|
| 633 |
+
console.print("\n[bold blue]🎯 Interactive Mode[/bold blue]")
|
| 634 |
+
console.print("Enter your queries below. Type 'quit' or 'exit' to leave.")
|
| 635 |
+
|
| 636 |
+
while True:
|
| 637 |
+
try:
|
| 638 |
+
prompt_text = "\n[bold cyan]Enter your query:[/bold cyan] " if not args.quiet else "Query: "
|
| 639 |
+
query = console.input(prompt_text).strip()
|
| 640 |
+
|
| 641 |
+
if query.lower() in ['quit', 'exit', 'q']:
|
| 642 |
+
if not args.quiet:
|
| 643 |
+
console.print("[green]👋 Goodbye![/green]")
|
| 644 |
+
break
|
| 645 |
+
|
| 646 |
+
if not query:
|
| 647 |
+
continue
|
| 648 |
+
|
| 649 |
+
if not args.quiet:
|
| 650 |
+
console.print("\n" + "="*80 + "\n")
|
| 651 |
+
run_demo_query(planner, query, visualizer, config)
|
| 652 |
+
if not args.quiet:
|
| 653 |
+
console.print("\n" + "="*80 + "\n")
|
| 654 |
+
|
| 655 |
+
except KeyboardInterrupt:
|
| 656 |
+
if not args.quiet:
|
| 657 |
+
console.print("\n[yellow]⚠️ Interrupted by user[/yellow]")
|
| 658 |
+
break
|
| 659 |
+
except EOFError:
|
| 660 |
+
if not args.quiet:
|
| 661 |
+
console.print("\n[green]👋 Goodbye![/green]")
|
| 662 |
+
break
|
| 663 |
+
|
| 664 |
+
return 0
|
| 665 |
+
|
| 666 |
+
|
| 667 |
+
if __name__ == "__main__":
|
| 668 |
+
sys.exit(main())
|