项目文件夹

文件
gelei abe3be01b4 feat: 🎸 version 1.0 agentic workflow
Major rewrite of PentestGPT to use an agentic pipeline architecture:
Core Changes: - New event-driven architecture with EventBus for
TUI-agent decoupling - Implemented AgentController with 5-state
lifecycle (IDLE->RUNNING->PAUSED->COMPLETED->ERROR) - Added AgentBackend
interface with ClaudeCodeBackend implementation - Session management
with file-based persistence for resumable pentests - Langfuse
integration for observability and tracing Interface: - New Textual-based
TUI with real-time activity feed - Keyboard shortcuts: F1 help, Ctrl+P
pause, Ctrl+Q quit - Enhanced CLI with --target, --instruction,
--non-interactive, --debug flags Project Structure: - Moved legacy
multi-LLM version (v0.15) to legacy/ directory - New pentestgpt/core/
for agent, controller, events, session modules - New
pentestgpt/interface/ for TUI and CLI components - New
pentestgpt/benchmark/ for xbow benchmark integration - Comprehensive
test suite in tests/ with unit and integration tests DevOps: - Docker
support with Ubuntu 24.04 container - GitHub Actions CI/CD pipeline -
Makefile with dev commands (test, lint, format, typecheck) - Added
xbow-validation-benchmarks as submodule
2025-12-13 00:56:40 +08:00

150 行
4.3 KiB
Python

"""Tool-specific renderers for beautiful output formatting."""
from abc import ABC, abstractmethod
from typing import Any, ClassVar
from rich.markup import escape as rich_escape
class BaseToolRenderer(ABC):
"""Base class for tool-specific renderers."""
tool_name: ClassVar[str] = ""
border_color: ClassVar[str] = "#525252"
@classmethod
def escape_markup(cls, text: str) -> str:
"""Escape Rich markup characters."""
return str(rich_escape(text))
@classmethod
@abstractmethod
def render(cls, tool_data: dict[str, Any]) -> str:
"""Render tool execution data to Rich markup string."""
pass
@classmethod
def get_status_icon(cls, status: str) -> str:
"""Get status icon for tool execution."""
icons = {
"running": "[#f59e0b]●[/] Running",
"completed": "[#10b981]✓[/] Success",
"failed": "[#ef4444]✗[/] Failed",
"error": "[#ef4444]✗[/] Error",
}
return icons.get(status, "[dim]○[/]")
class TerminalRenderer(BaseToolRenderer):
"""Renderer for terminal/command execution."""
tool_name: ClassVar[str] = "terminal_execute"
border_color: ClassVar[str] = "#10b981" # Green
@classmethod
def render(cls, tool_data: dict[str, Any]) -> str:
"""Render terminal command execution."""
args = tool_data.get("args", {})
status = tool_data.get("status", "unknown")
result = tool_data.get("result", "")
command = args.get("command", "")
if not command:
return ""
# Truncate long commands
display_command = cls.escape_markup(command)
if len(display_command) > 120:
display_command = display_command[:117] + "..."
lines = []
lines.append(f"[#10b981]$ {display_command}[/]")
# Add result if available
if status in ("completed", "failed") and result:
result_str = str(result)
if len(result_str) > 500:
result_str = result_str[:497] + "..."
# Format output
if result_str:
lines.append("[dim]Output:[/]")
for line in result_str.split("\n")[:20]: # Max 20 lines
lines.append(f" {cls.escape_markup(line)}")
return "\n".join(lines)
class ThinkingRenderer(BaseToolRenderer):
"""Renderer for AI thinking/planning blocks."""
tool_name: ClassVar[str] = "thinking"
border_color: ClassVar[str] = "#8b5cf6" # Purple
@classmethod
def render(cls, tool_data: dict[str, Any]) -> str:
"""Render thinking content."""
args = tool_data.get("args", {})
content = args.get("content", "")
if not content:
return "[#8b5cf6][italic]Analyzing...[/][/]"
# Truncate long content
display_content = cls.escape_markup(content)
if len(display_content) > 300:
display_content = display_content[:297] + "..."
return f"[#8b5cf6]💭 {display_content}[/]"
class ResultRenderer(BaseToolRenderer):
"""Renderer for generic results."""
tool_name: ClassVar[str] = "result"
border_color: ClassVar[str] = "#6366f1" # Indigo
@classmethod
def render(cls, tool_data: dict[str, Any]) -> str:
"""Render result data."""
result = tool_data.get("result", "")
title = tool_data.get("title", "Result")
if not result:
return ""
result_str = str(result)
if len(result_str) > 500:
result_str = result_str[:497] + "..."
lines = []
lines.append(f"[bold]{cls.escape_markup(title)}[/]")
lines.append(cls.escape_markup(result_str))
return "\n".join(lines)
# Registry of renderers
RENDERER_REGISTRY: dict[str, type[BaseToolRenderer]] = {
"terminal_execute": TerminalRenderer,
"thinking": ThinkingRenderer,
"result": ResultRenderer,
}
def get_renderer(tool_name: str) -> type[BaseToolRenderer] | None:
"""Get renderer for a specific tool name."""
return RENDERER_REGISTRY.get(tool_name)
def render_tool(tool_data: dict[str, Any]) -> str:
"""Render tool data using appropriate renderer."""
tool_name = tool_data.get("tool_name", "")
renderer = get_renderer(tool_name)
if renderer:
return renderer.render(tool_data)
# Fallback: generic rendering
return f"Tool: {tool_name}"