Spaces:
Running
Running
Upload 56 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- AlphaGenome_mcp.py +74 -0
- README.md +6 -5
- __pycache__/agent_v3.cpython-311.pyc +0 -0
- agent_v3.py +587 -0
- app.py +649 -62
- core/__init__.py +8 -0
- core/__pycache__/__init__.cpython-311.pyc +0 -0
- core/__pycache__/__init__.cpython-312.pyc +0 -0
- core/__pycache__/constants.cpython-311.pyc +0 -0
- core/__pycache__/constants.cpython-312.pyc +0 -0
- core/__pycache__/types.cpython-311.pyc +0 -0
- core/__pycache__/types.cpython-312.pyc +0 -0
- core/constants.py +259 -0
- core/types.py +29 -0
- images.png +0 -0
- managers/.DS_Store +0 -0
- managers/__init__.py +41 -0
- managers/__pycache__/__init__.cpython-311.pyc +0 -0
- managers/execution/__init__.py +8 -0
- managers/execution/__pycache__/__init__.cpython-311.pyc +0 -0
- managers/execution/__pycache__/monitoring.cpython-311.pyc +0 -0
- managers/execution/__pycache__/python_executor.cpython-311.pyc +0 -0
- managers/execution/monitoring.py +52 -0
- managers/execution/python_executor.py +138 -0
- managers/support/__init__.py +8 -0
- managers/support/__pycache__/__init__.cpython-311.pyc +0 -0
- managers/support/__pycache__/console_display.cpython-311.pyc +0 -0
- managers/support/__pycache__/package_manager.cpython-311.pyc +0 -0
- managers/support/console_display.py +140 -0
- managers/support/package_manager.py +58 -0
- managers/tools/__init__.py +23 -0
- managers/tools/__pycache__/__init__.cpython-311.pyc +0 -0
- managers/tools/__pycache__/builtin_tools.cpython-311.pyc +0 -0
- managers/tools/__pycache__/mcp_manager.cpython-311.pyc +0 -0
- managers/tools/__pycache__/tool_manager.cpython-311.pyc +0 -0
- managers/tools/__pycache__/tool_registry.cpython-311.pyc +0 -0
- managers/tools/__pycache__/tool_selector.cpython-311.pyc +0 -0
- managers/tools/builtin_tools.py +34 -0
- managers/tools/mcp_manager.py +588 -0
- managers/tools/tool_manager.py +521 -0
- managers/tools/tool_registry.py +314 -0
- managers/tools/tool_selector.py +109 -0
- managers/workflow/__init__.py +9 -0
- managers/workflow/__pycache__/__init__.cpython-311.pyc +0 -0
- managers/workflow/__pycache__/plan_manager.cpython-311.pyc +0 -0
- managers/workflow/__pycache__/state_manager.cpython-311.pyc +0 -0
- managers/workflow/__pycache__/workflow_engine.cpython-311.pyc +0 -0
- managers/workflow/plan_manager.py +78 -0
- managers/workflow/state_manager.py +24 -0
- managers/workflow/workflow_engine.py +303 -0
AlphaGenome_mcp.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Model Context Protocol (MCP) for AlphaGenome
|
| 3 |
+
|
| 4 |
+
AlphaGenome provides state-of-the-art AI-driven tools for genomic sequence analysis, variant effect prediction, and functional genomics visualization. The platform enables researchers to predict regulatory elements, assess variant impacts, and explore molecular mechanisms across diverse cell types and tissues.
|
| 5 |
+
|
| 6 |
+
This MCP Server contains the tools extracted from the following tutorials with their tools:
|
| 7 |
+
1. batch_variant_scoring
|
| 8 |
+
- score_variants_batch: Score multiple genetic variants in batch using configurable variant scorers
|
| 9 |
+
- filter_variant_scores: Filter variant scores by ontology criteria (e.g., cell types)
|
| 10 |
+
2. essential_commands
|
| 11 |
+
- create_genomic_interval: Create genomic intervals for DNA regions
|
| 12 |
+
- create_genomic_variant: Create genomic variants for genetic changes
|
| 13 |
+
- create_track_data: Create TrackData objects from arrays and metadata
|
| 14 |
+
- create_variant_scores: Create AnnData objects for variant scoring results
|
| 15 |
+
- genomic_interval_operations: Perform operations on genomic intervals
|
| 16 |
+
- variant_interval_operations: Check variant overlaps with intervals
|
| 17 |
+
- track_data_operations: Filter, resize, slice and transform TrackData
|
| 18 |
+
- track_data_resolution_conversion: Convert between track data resolutions
|
| 19 |
+
3. example_analysis_workflow
|
| 20 |
+
# REMOVED - Too TAL1-specific:
|
| 21 |
+
# - visualize_tal1_variant_positions: Visualize genomic positions of oncogenic TAL1 variants
|
| 22 |
+
# - predict_variant_functional_impact: Predict functional effects of specific TAL1 variants
|
| 23 |
+
# - compare_oncogenic_vs_background_variants: Compare predicted effects of disease variants vs background
|
| 24 |
+
4. quick_start
|
| 25 |
+
- predict_dna_sequence: Predict genomic tracks from DNA sequence
|
| 26 |
+
- predict_genome_interval: Predict genomic tracks for reference genome intervals
|
| 27 |
+
# REMOVED - Redundant with visualize_variant_effects:
|
| 28 |
+
# - predict_variant_effects: Predict and visualize genetic variant effects
|
| 29 |
+
# REMOVED - Redundant with score_variants_batch:
|
| 30 |
+
# - score_variant_effect: Score genetic variant effects using variant scorers
|
| 31 |
+
- ism_analysis: Perform in silico mutagenesis analysis with sequence logos
|
| 32 |
+
# REMOVED - Should be parameter in other functions:
|
| 33 |
+
# - mouse_predictions: Make predictions for mouse sequences and intervals
|
| 34 |
+
5. tissue_ontology_mapping
|
| 35 |
+
- explore_output_metadata: Explore and filter output metadata for specific organisms and search terms
|
| 36 |
+
- count_tracks_by_output_type: Count tracks by output type for human and mouse organisms
|
| 37 |
+
6. variant_scoring_ui
|
| 38 |
+
# REMOVED - Redundant with score_variants_batch:
|
| 39 |
+
# - score_variant: Score a single variant with multiple variant scorers and save results
|
| 40 |
+
- visualize_variant_effects: Generate comprehensive variant effect visualization across multiple modalities
|
| 41 |
+
7. visualization_modality_tour
|
| 42 |
+
- visualize_gene_expression: Visualize RNA_SEQ and CAGE gene expression predictions
|
| 43 |
+
# REMOVED - Redundant with visualize_variant_effects:
|
| 44 |
+
# - visualize_variant_expression_effects: Show REF vs ALT variant effects on gene expression
|
| 45 |
+
# REMOVED - Too specific:
|
| 46 |
+
# - visualize_custom_annotations: Plot custom annotations like polyadenylation sites
|
| 47 |
+
- visualize_chromatin_accessibility: Visualize DNASE and ATAC chromatin accessibility
|
| 48 |
+
- visualize_splicing_effects: Visualize splicing predictions with SPLICE_SITES, SPLICE_SITE_USAGE, SPLICE_JUNCTIONS
|
| 49 |
+
# REMOVED - Redundant with visualize_variant_effects:
|
| 50 |
+
# - visualize_variant_splicing_effects: Show REF vs ALT variant effects on splicing with sashimi plots
|
| 51 |
+
- visualize_histone_modifications: Visualize CHIP_HISTONE predictions with custom colors
|
| 52 |
+
- visualize_tf_binding: Visualize CHIP_TF transcription factor binding predictions
|
| 53 |
+
- visualize_contact_maps: Visualize CONTACT_MAPS DNA-DNA contact predictions
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
import sys
|
| 57 |
+
from pathlib import Path
|
| 58 |
+
from fastmcp import FastMCP
|
| 59 |
+
import requests
|
| 60 |
+
|
| 61 |
+
# Import the MCP tools from the tools folder
|
| 62 |
+
from tools.batch_variant_scoring import batch_variant_scoring_mcp
|
| 63 |
+
from tools.essential_commands import essential_commands_mcp
|
| 64 |
+
|
| 65 |
+
# Define the MCP server
|
| 66 |
+
mcp = FastMCP(name = "AlphaGenome")
|
| 67 |
+
|
| 68 |
+
# Mount the tools
|
| 69 |
+
mcp.mount(batch_variant_scoring_mcp)
|
| 70 |
+
mcp.mount(essential_commands_mcp)
|
| 71 |
+
|
| 72 |
+
# Run the MCP server
|
| 73 |
+
if __name__ == "__main__":
|
| 74 |
+
mcp.run(show_banner=False)
|
README.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
|
|
|
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 5.
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
---
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Alpha
|
| 3 |
+
emoji: 👁
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
python_version: 3.11
|
| 6 |
+
colorTo: gray
|
| 7 |
sdk: gradio
|
| 8 |
+
sdk_version: 5.49.0
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
---
|
__pycache__/agent_v3.cpython-311.pyc
ADDED
|
Binary file (28.6 kB). View file
|
|
|
agent_v3.py
ADDED
|
@@ -0,0 +1,587 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
CodeAgent: A LangGraph-based agent for executing Python code and using tools.
|
| 3 |
+
Fully modular version with unified tool management.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import re
|
| 8 |
+
import time
|
| 9 |
+
from typing import Dict, List, Optional
|
| 10 |
+
from jinja2 import Template
|
| 11 |
+
from langchain_core.language_models.chat_models import BaseChatModel
|
| 12 |
+
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage
|
| 13 |
+
from langchain_openai import ChatOpenAI
|
| 14 |
+
from dotenv import load_dotenv
|
| 15 |
+
|
| 16 |
+
# Import core types and constants
|
| 17 |
+
from core.types import AgentState, AgentConfig
|
| 18 |
+
from core.constants import SYSTEM_PROMPT_TEMPLATE
|
| 19 |
+
|
| 20 |
+
# Import managers (organized by subsystem)
|
| 21 |
+
from managers import (
|
| 22 |
+
# Support
|
| 23 |
+
PackageManager,
|
| 24 |
+
ConsoleDisplay,
|
| 25 |
+
# Workflow
|
| 26 |
+
PlanManager,
|
| 27 |
+
StateManager,
|
| 28 |
+
WorkflowEngine,
|
| 29 |
+
# Tools
|
| 30 |
+
ToolManager,
|
| 31 |
+
ToolSource,
|
| 32 |
+
ToolSelector,
|
| 33 |
+
# Execution
|
| 34 |
+
Timing,
|
| 35 |
+
PythonExecutor
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
# Load environment variables
|
| 39 |
+
load_dotenv("./.env")
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def get_system_prompt(functions: Dict[str, dict], packages: Dict[str, str] = None) -> str:
|
| 43 |
+
"""Generate system prompt using template and functions."""
|
| 44 |
+
if packages is None:
|
| 45 |
+
from core.constants import LIBRARY_CONTENT_DICT
|
| 46 |
+
packages = LIBRARY_CONTENT_DICT
|
| 47 |
+
return Template(SYSTEM_PROMPT_TEMPLATE).render(functions=functions, packages=packages)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class CodeAgent:
|
| 51 |
+
"""A code-based agent that can execute Python code and use tools to solve tasks."""
|
| 52 |
+
|
| 53 |
+
def __init__(self, model: BaseChatModel,
|
| 54 |
+
config: Optional[AgentConfig] = None,
|
| 55 |
+
use_tool_manager: bool = True,
|
| 56 |
+
use_tool_selection: bool = True):
|
| 57 |
+
"""
|
| 58 |
+
Initialize the CodeAgent with unified tool management.
|
| 59 |
+
|
| 60 |
+
Args:
|
| 61 |
+
model: The language model to use for generation
|
| 62 |
+
config: Configuration for the agent
|
| 63 |
+
use_tool_manager: Whether to use the unified ToolManager (recommended)
|
| 64 |
+
use_tool_selection: Whether to use LLM-based tool selection (like Biomni)
|
| 65 |
+
"""
|
| 66 |
+
self.model = model
|
| 67 |
+
self.config = config or AgentConfig()
|
| 68 |
+
self.use_tool_manager = use_tool_manager
|
| 69 |
+
self.use_tool_selection = use_tool_selection
|
| 70 |
+
|
| 71 |
+
# Cache selected tools to avoid re-selection at each step
|
| 72 |
+
self._selected_tools_cache = None
|
| 73 |
+
|
| 74 |
+
# Initialize modular components
|
| 75 |
+
self.package_manager = PackageManager()
|
| 76 |
+
self.console = ConsoleDisplay()
|
| 77 |
+
self.state_manager = StateManager()
|
| 78 |
+
self.plan_manager = PlanManager()
|
| 79 |
+
|
| 80 |
+
# Initialize unified tool management
|
| 81 |
+
if not self.use_tool_manager:
|
| 82 |
+
raise ValueError("ToolManager is required. Legacy mode (use_tool_manager=False) has been removed.")
|
| 83 |
+
|
| 84 |
+
self.tool_manager = ToolManager(self.console)
|
| 85 |
+
|
| 86 |
+
# Initialize tool selector for LLM-based tool selection
|
| 87 |
+
if self.use_tool_selection:
|
| 88 |
+
self.tool_selector = ToolSelector(self.model)
|
| 89 |
+
else:
|
| 90 |
+
self.tool_selector = None
|
| 91 |
+
|
| 92 |
+
# Initialize workflow engine
|
| 93 |
+
self.workflow_engine = WorkflowEngine(model, self.config, self.console, self.state_manager)
|
| 94 |
+
|
| 95 |
+
# Initialize Python executor
|
| 96 |
+
self.python_executor = PythonExecutor()
|
| 97 |
+
|
| 98 |
+
# Setup workflow
|
| 99 |
+
self._setup_workflow()
|
| 100 |
+
|
| 101 |
+
# ====================
|
| 102 |
+
# WORKFLOW SETUP
|
| 103 |
+
# ====================
|
| 104 |
+
|
| 105 |
+
def _setup_workflow(self):
|
| 106 |
+
"""Setup the LangGraph workflow using WorkflowEngine."""
|
| 107 |
+
self.workflow_engine.setup_workflow(
|
| 108 |
+
self.generate,
|
| 109 |
+
self.execute,
|
| 110 |
+
self.should_continue
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
# ====================
|
| 114 |
+
# WORKFLOW NODES
|
| 115 |
+
# ====================
|
| 116 |
+
|
| 117 |
+
def generate(self, state: AgentState) -> AgentState:
|
| 118 |
+
"""Generate response using LLM with tool-aware prompt."""
|
| 119 |
+
|
| 120 |
+
# Get all available tools first
|
| 121 |
+
all_schemas = self.tool_manager.get_tool_schemas(openai_format=True)
|
| 122 |
+
all_functions_dict = {schema['function']['name']: schema for schema in all_schemas}
|
| 123 |
+
|
| 124 |
+
# Use tool selection if enabled and not cached
|
| 125 |
+
if self.use_tool_selection and self.tool_selector and state.get("messages") and self._selected_tools_cache is None:
|
| 126 |
+
# Get the user's query from the first message
|
| 127 |
+
user_query = ""
|
| 128 |
+
for msg in state["messages"]:
|
| 129 |
+
if hasattr(msg, 'content') and msg.content:
|
| 130 |
+
user_query = msg.content
|
| 131 |
+
break
|
| 132 |
+
|
| 133 |
+
if user_query:
|
| 134 |
+
# Prepare tools for selection (convert schemas to tool info format)
|
| 135 |
+
available_tools = {}
|
| 136 |
+
for tool_name, schema in all_functions_dict.items():
|
| 137 |
+
available_tools[tool_name] = {
|
| 138 |
+
'description': schema['function'].get('description', 'No description'),
|
| 139 |
+
'source': 'tool_manager' # Could be enhanced to show actual source
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
# Select relevant tools using LLM (only once)
|
| 143 |
+
selected_tool_names = self.tool_selector.select_tools_for_task(
|
| 144 |
+
user_query, available_tools, max_tools=15
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
# Cache the selected tools
|
| 148 |
+
self._selected_tools_cache = {name: all_functions_dict[name]
|
| 149 |
+
for name in selected_tool_names
|
| 150 |
+
if name in all_functions_dict}
|
| 151 |
+
|
| 152 |
+
self.console.console.print(f"🎯 Selected {len(self._selected_tools_cache)} tools from {len(all_functions_dict)} available tools (cached for session)")
|
| 153 |
+
functions_dict = self._selected_tools_cache
|
| 154 |
+
else:
|
| 155 |
+
functions_dict = all_functions_dict
|
| 156 |
+
elif self.use_tool_selection and self._selected_tools_cache is not None:
|
| 157 |
+
# Use cached selected tools
|
| 158 |
+
functions_dict = self._selected_tools_cache
|
| 159 |
+
else:
|
| 160 |
+
# No tool selection or selection disabled
|
| 161 |
+
functions_dict = all_functions_dict
|
| 162 |
+
|
| 163 |
+
all_packages = self.package_manager.get_all_packages()
|
| 164 |
+
system_prompt = get_system_prompt(functions_dict, all_packages)
|
| 165 |
+
|
| 166 |
+
# Truncate conversation history to prevent context overflow
|
| 167 |
+
messages = [SystemMessage(content=system_prompt)] + state["messages"]
|
| 168 |
+
|
| 169 |
+
response = self.model.invoke(messages)
|
| 170 |
+
|
| 171 |
+
# Cut the text after the </execute> tag, while keeping the </execute> tag
|
| 172 |
+
if "</execute>" in response.content:
|
| 173 |
+
response.content = response.content.split("</execute>")[0] + "</execute>"
|
| 174 |
+
|
| 175 |
+
# Parse the response
|
| 176 |
+
msg = str(response.content)
|
| 177 |
+
llm_reply = AIMessage(content=msg.strip())
|
| 178 |
+
|
| 179 |
+
# Update step count
|
| 180 |
+
new_step_count = state.get("step_count", 0) + 1
|
| 181 |
+
|
| 182 |
+
return self.state_manager.create_state_dict(
|
| 183 |
+
messages=[llm_reply],
|
| 184 |
+
step_count=new_step_count,
|
| 185 |
+
error_count=state.get("error_count", 0),
|
| 186 |
+
start_time=state.get("start_time", time.time()),
|
| 187 |
+
current_plan=self._extract_current_plan(msg)
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
def _extract_current_plan(self, content: str) -> Optional[str]:
|
| 191 |
+
"""Extract the current plan from the agent's response."""
|
| 192 |
+
return self.plan_manager.extract_plan_from_content(content)
|
| 193 |
+
|
| 194 |
+
def execute(self, state: AgentState) -> AgentState:
|
| 195 |
+
"""Execute code using persistent Python executor."""
|
| 196 |
+
try:
|
| 197 |
+
last_message = state["messages"][-1].content
|
| 198 |
+
execute_match = re.search(r"<execute>(.*?)</execute>", last_message, re.DOTALL)
|
| 199 |
+
|
| 200 |
+
if execute_match:
|
| 201 |
+
code = execute_match.group(1).strip()
|
| 202 |
+
|
| 203 |
+
# Execute regular code in persistent environment (tools already injected)
|
| 204 |
+
result = self.python_executor(code)
|
| 205 |
+
|
| 206 |
+
# Include both the code and result in the observation
|
| 207 |
+
obs = f"\n<observation>\nCode Output:\n{result}</observation>"
|
| 208 |
+
return self.state_manager.create_state_dict(
|
| 209 |
+
messages=[AIMessage(content=obs.strip())],
|
| 210 |
+
step_count=state.get("step_count", 0),
|
| 211 |
+
error_count=state.get("error_count", 0),
|
| 212 |
+
start_time=state.get("start_time", time.time()),
|
| 213 |
+
current_plan=state.get("current_plan")
|
| 214 |
+
)
|
| 215 |
+
else:
|
| 216 |
+
return self.state_manager.create_state_dict(
|
| 217 |
+
messages=[AIMessage(content="<error>No executable code found</error>")],
|
| 218 |
+
step_count=state.get("step_count", 0),
|
| 219 |
+
error_count=state.get("error_count", 0) + 1,
|
| 220 |
+
start_time=state.get("start_time", time.time()),
|
| 221 |
+
current_plan=state.get("current_plan")
|
| 222 |
+
)
|
| 223 |
+
except Exception as e:
|
| 224 |
+
return self.state_manager.create_state_dict(
|
| 225 |
+
messages=[AIMessage(content=f"<error>Execution error: {str(e)}</error>")],
|
| 226 |
+
step_count=state.get("step_count", 0),
|
| 227 |
+
error_count=state.get("error_count", 0) + 1,
|
| 228 |
+
start_time=state.get("start_time", time.time()),
|
| 229 |
+
current_plan=state.get("current_plan")
|
| 230 |
+
)
|
| 231 |
+
|
| 232 |
+
def should_continue(self, state: AgentState) -> str:
|
| 233 |
+
"""Decide whether to continue executing or end the workflow."""
|
| 234 |
+
last_message = state["messages"][-1].content
|
| 235 |
+
step_count = state.get("step_count", 0)
|
| 236 |
+
error_count = state.get("error_count", 0)
|
| 237 |
+
start_time = state.get("start_time", time.time())
|
| 238 |
+
|
| 239 |
+
# Check for timeout
|
| 240 |
+
if time.time() - start_time > self.config.timeout_seconds:
|
| 241 |
+
return "end"
|
| 242 |
+
|
| 243 |
+
# Check for maximum steps
|
| 244 |
+
if step_count >= self.config.max_steps:
|
| 245 |
+
return "end"
|
| 246 |
+
|
| 247 |
+
# Check for too many errors
|
| 248 |
+
if error_count >= self.config.retry_attempts:
|
| 249 |
+
return "end"
|
| 250 |
+
|
| 251 |
+
# Check if the finish() tool has been called
|
| 252 |
+
if "<solution>" in last_message and "</solution>" in last_message:
|
| 253 |
+
return "end"
|
| 254 |
+
|
| 255 |
+
# Check if there's an execute tag in the last message
|
| 256 |
+
elif "<execute>" in last_message and "</execute>" in last_message:
|
| 257 |
+
return "execute"
|
| 258 |
+
|
| 259 |
+
else:
|
| 260 |
+
return "end"
|
| 261 |
+
|
| 262 |
+
# ====================
|
| 263 |
+
# PACKAGE MANAGEMENT - Delegated to PackageManager
|
| 264 |
+
# ====================
|
| 265 |
+
|
| 266 |
+
def add_packages(self, packages: Dict[str, str]) -> bool:
|
| 267 |
+
"""Add new packages to the available packages."""
|
| 268 |
+
return self.package_manager.add_packages(packages)
|
| 269 |
+
|
| 270 |
+
def get_all_packages(self) -> Dict[str, str]:
|
| 271 |
+
"""Get all available packages (default + custom)."""
|
| 272 |
+
return self.package_manager.get_all_packages()
|
| 273 |
+
|
| 274 |
+
# ====================
|
| 275 |
+
# UNIFIED TOOL MANAGEMENT - Delegated to ToolManager
|
| 276 |
+
# ====================
|
| 277 |
+
|
| 278 |
+
def add_tool(self, function: callable, name: str = None, description: str = None) -> bool:
|
| 279 |
+
"""Add a tool function to the manager."""
|
| 280 |
+
return self.tool_manager.add_tool(function, name, description, ToolSource.LOCAL)
|
| 281 |
+
|
| 282 |
+
def remove_tool(self, name: str) -> bool:
|
| 283 |
+
"""Remove a tool by name."""
|
| 284 |
+
return self.tool_manager.remove_tool(name)
|
| 285 |
+
|
| 286 |
+
def list_tools(self, source: str = "all", include_details: bool = False) -> List[Dict]:
|
| 287 |
+
"""List all available tools with optional filtering."""
|
| 288 |
+
source_enum = ToolSource.ALL
|
| 289 |
+
if source.lower() in ["local", "decorated", "mcp"]:
|
| 290 |
+
source_enum = ToolSource(source.lower())
|
| 291 |
+
|
| 292 |
+
return self.tool_manager.list_tools(source_enum, include_details)
|
| 293 |
+
|
| 294 |
+
def search_tools(self, query: str) -> List[Dict]:
|
| 295 |
+
"""Search tools by name and description."""
|
| 296 |
+
return self.tool_manager.search_tools(query)
|
| 297 |
+
|
| 298 |
+
def get_tool_info(self, name: str) -> Optional[Dict]:
|
| 299 |
+
"""Get detailed information about a specific tool."""
|
| 300 |
+
tool_info = self.tool_manager.get_tool(name)
|
| 301 |
+
if tool_info:
|
| 302 |
+
return {
|
| 303 |
+
"name": tool_info.name,
|
| 304 |
+
"description": tool_info.description,
|
| 305 |
+
"source": tool_info.source.value,
|
| 306 |
+
"server": tool_info.server,
|
| 307 |
+
"module": tool_info.module,
|
| 308 |
+
"has_function": tool_info.function is not None,
|
| 309 |
+
"required_parameters": tool_info.required_parameters,
|
| 310 |
+
"optional_parameters": tool_info.optional_parameters
|
| 311 |
+
}
|
| 312 |
+
return None
|
| 313 |
+
|
| 314 |
+
def get_all_tool_functions(self) -> Dict[str, callable]:
|
| 315 |
+
"""Get all tool functions as a dictionary."""
|
| 316 |
+
return self.tool_manager.get_all_functions()
|
| 317 |
+
|
| 318 |
+
# ====================
|
| 319 |
+
# MCP METHODS - Now delegated to ToolManager
|
| 320 |
+
# ====================
|
| 321 |
+
|
| 322 |
+
def add_mcp(self, config_path: str = "./mcp_config.yaml") -> None:
|
| 323 |
+
"""Add MCP tools from configuration file."""
|
| 324 |
+
self.tool_manager.add_mcp_server(config_path)
|
| 325 |
+
|
| 326 |
+
def list_mcp_tools(self) -> List[Dict]:
|
| 327 |
+
"""List all loaded MCP tools."""
|
| 328 |
+
return self.tool_manager.list_tools(self.tool_manager.ToolSource.MCP)
|
| 329 |
+
|
| 330 |
+
def list_mcp_servers(self) -> Dict[str, List[str]]:
|
| 331 |
+
"""List all MCP servers and their tools."""
|
| 332 |
+
return self.tool_manager.list_mcp_servers()
|
| 333 |
+
|
| 334 |
+
def show_mcp_status(self) -> None:
|
| 335 |
+
"""Display detailed MCP status information to the user."""
|
| 336 |
+
self.tool_manager.show_mcp_status()
|
| 337 |
+
|
| 338 |
+
def get_mcp_summary(self) -> Dict[str, any]:
|
| 339 |
+
"""Get a summary of MCP tools for programmatic access."""
|
| 340 |
+
return self.tool_manager.get_mcp_summary()
|
| 341 |
+
|
| 342 |
+
# ====================
|
| 343 |
+
# ENHANCED TOOL FEATURES
|
| 344 |
+
# ====================
|
| 345 |
+
|
| 346 |
+
def get_tool_statistics(self) -> Dict[str, any]:
|
| 347 |
+
"""Get comprehensive tool statistics."""
|
| 348 |
+
return self.tool_manager.get_tool_statistics()
|
| 349 |
+
|
| 350 |
+
def validate_tools(self) -> Dict[str, List[str]]:
|
| 351 |
+
"""Validate all tools and return any issues."""
|
| 352 |
+
return self.tool_manager.validate_tools()
|
| 353 |
+
|
| 354 |
+
# ====================
|
| 355 |
+
# TOOL SELECTION MANAGEMENT
|
| 356 |
+
# ====================
|
| 357 |
+
|
| 358 |
+
def reset_tool_selection(self):
|
| 359 |
+
"""Reset the cached tool selection to allow re-selection on next query."""
|
| 360 |
+
self._selected_tools_cache = None
|
| 361 |
+
if self.use_tool_selection:
|
| 362 |
+
self.console.console.print("🔄 Tool selection cache cleared - will re-select tools on next query")
|
| 363 |
+
|
| 364 |
+
def get_selected_tools(self):
|
| 365 |
+
"""Get the currently selected tools (if any)."""
|
| 366 |
+
return list(self._selected_tools_cache.keys()) if self._selected_tools_cache else None
|
| 367 |
+
|
| 368 |
+
# ====================
|
| 369 |
+
# TRACE AND SUMMARY METHODS
|
| 370 |
+
# ====================
|
| 371 |
+
|
| 372 |
+
def get_trace(self) -> Dict:
|
| 373 |
+
"""Get the complete trace of the last execution."""
|
| 374 |
+
if not self.workflow_engine:
|
| 375 |
+
return {}
|
| 376 |
+
|
| 377 |
+
return {
|
| 378 |
+
"execution_time": time.strftime('%Y-%m-%d %H:%M:%S'),
|
| 379 |
+
"config": {
|
| 380 |
+
"max_steps": self.config.max_steps,
|
| 381 |
+
"timeout_seconds": self.config.timeout_seconds,
|
| 382 |
+
"verbose": self.config.verbose
|
| 383 |
+
},
|
| 384 |
+
"messages": self.workflow_engine.message_history,
|
| 385 |
+
"trace_logs": self.workflow_engine.trace_logs
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
def get_summary(self) -> Dict:
|
| 389 |
+
"""Get a summary of the last execution."""
|
| 390 |
+
if not self.workflow_engine:
|
| 391 |
+
return {}
|
| 392 |
+
return self.workflow_engine.generate_summary()
|
| 393 |
+
|
| 394 |
+
def save_trace(self, filepath: str = None) -> str:
|
| 395 |
+
"""Save the trace of the last execution to a file."""
|
| 396 |
+
if not self.workflow_engine:
|
| 397 |
+
raise RuntimeError("No workflow engine available")
|
| 398 |
+
return self.workflow_engine.save_trace_to_file(filepath)
|
| 399 |
+
|
| 400 |
+
def save_summary(self, filepath: str = None) -> str:
|
| 401 |
+
"""Save the summary of the last execution to a file."""
|
| 402 |
+
if not self.workflow_engine:
|
| 403 |
+
raise RuntimeError("No workflow engine available")
|
| 404 |
+
return self.workflow_engine.save_summary_to_file(filepath)
|
| 405 |
+
|
| 406 |
+
# ====================
|
| 407 |
+
# PUBLIC INTERFACE
|
| 408 |
+
# ====================
|
| 409 |
+
|
| 410 |
+
def run(self, query: str, save_trace: bool = False, save_summary: bool = False,
|
| 411 |
+
trace_dir: str = "traces") -> str:
|
| 412 |
+
"""
|
| 413 |
+
Run the agent with a given query using modular components.
|
| 414 |
+
|
| 415 |
+
Args:
|
| 416 |
+
query: The task/question to solve
|
| 417 |
+
save_trace: Whether to save the complete trace to a file
|
| 418 |
+
save_summary: Whether to save the execution summary to a file
|
| 419 |
+
trace_dir: Directory to save trace and summary files
|
| 420 |
+
|
| 421 |
+
Returns:
|
| 422 |
+
The final response content
|
| 423 |
+
"""
|
| 424 |
+
# Start timing the overall execution
|
| 425 |
+
overall_timing = Timing(start_time=time.time())
|
| 426 |
+
|
| 427 |
+
# Display task header
|
| 428 |
+
self.console.print_task_header(query)
|
| 429 |
+
|
| 430 |
+
# Initialize agent with functions using ToolManager
|
| 431 |
+
functions_dict = self.get_all_tool_functions()
|
| 432 |
+
|
| 433 |
+
# Display enhanced tool information
|
| 434 |
+
# Get detailed tool statistics
|
| 435 |
+
stats = self.tool_manager.get_tool_statistics()
|
| 436 |
+
mcp_servers = self.tool_manager.list_mcp_servers()
|
| 437 |
+
|
| 438 |
+
self.console.console.print(f"🛠️ Loaded {stats['total_tools']} total tools:")
|
| 439 |
+
if stats['by_source']['local'] > 0:
|
| 440 |
+
self.console.console.print(f" 📋 Local tools: {stats['by_source']['local']}")
|
| 441 |
+
if stats['by_source']['decorated'] > 0:
|
| 442 |
+
self.console.console.print(f" 🎯 Decorated tools: {stats['by_source']['decorated']}")
|
| 443 |
+
if stats['by_source']['mcp'] > 0:
|
| 444 |
+
self.console.console.print(f" 🔗 MCP tools: {stats['by_source']['mcp']} from {len(mcp_servers)} servers")
|
| 445 |
+
for server_name, tools in mcp_servers.items():
|
| 446 |
+
self.console.console.print(f" • {server_name}: {len(tools)} tools")
|
| 447 |
+
|
| 448 |
+
# Inject functions into Python executor
|
| 449 |
+
self.python_executor.send_functions(functions_dict)
|
| 450 |
+
|
| 451 |
+
# Import available packages using PackageManager
|
| 452 |
+
imported_packages, failed_packages = self.package_manager.import_packages(self.python_executor)
|
| 453 |
+
self.console.print_packages_info(imported_packages, failed_packages)
|
| 454 |
+
|
| 455 |
+
# Inject any initial variables
|
| 456 |
+
state_variables = {}
|
| 457 |
+
self.python_executor.send_variables(state_variables)
|
| 458 |
+
|
| 459 |
+
# Create initial state using StateManager
|
| 460 |
+
input_state = self.state_manager.create_state_dict(
|
| 461 |
+
messages=[HumanMessage(content=query)],
|
| 462 |
+
step_count=0,
|
| 463 |
+
error_count=0,
|
| 464 |
+
start_time=time.time(),
|
| 465 |
+
current_plan=None
|
| 466 |
+
)
|
| 467 |
+
|
| 468 |
+
# Execute workflow using WorkflowEngine and get result with final state
|
| 469 |
+
result, final_state = self.workflow_engine.run_workflow(input_state)
|
| 470 |
+
|
| 471 |
+
# Complete overall timing and display summary
|
| 472 |
+
overall_timing.end_time = time.time()
|
| 473 |
+
|
| 474 |
+
# Extract final state information for summary
|
| 475 |
+
final_step_count = final_state.get("step_count", 0) if final_state else 0
|
| 476 |
+
final_error_count = final_state.get("error_count", 0) if final_state else 0
|
| 477 |
+
|
| 478 |
+
self.console.print_execution_summary(final_step_count, final_error_count, overall_timing.duration)
|
| 479 |
+
|
| 480 |
+
# Save trace and summary if requested
|
| 481 |
+
if save_trace or save_summary:
|
| 482 |
+
# Create trace directory if it doesn't exist
|
| 483 |
+
from pathlib import Path
|
| 484 |
+
trace_path = Path(trace_dir)
|
| 485 |
+
trace_path.mkdir(parents=True, exist_ok=True)
|
| 486 |
+
|
| 487 |
+
if save_trace:
|
| 488 |
+
trace_file = trace_path / f"agent_trace_{time.strftime('%Y%m%d_%H%M%S')}.json"
|
| 489 |
+
saved_trace = self.workflow_engine.save_trace_to_file(str(trace_file))
|
| 490 |
+
self.console.console.print(f"💾 Trace saved to: {saved_trace}")
|
| 491 |
+
|
| 492 |
+
if save_summary:
|
| 493 |
+
summary_file = trace_path / f"agent_summary_{time.strftime('%Y%m%d_%H%M%S')}.json"
|
| 494 |
+
saved_summary = self.workflow_engine.save_summary_to_file(str(summary_file))
|
| 495 |
+
self.console.console.print(f"📊 Summary saved to: {saved_summary}")
|
| 496 |
+
|
| 497 |
+
return result
|
| 498 |
+
|
| 499 |
+
|
| 500 |
+
# ====================
|
| 501 |
+
# EXAMPLE USAGE
|
| 502 |
+
# ====================
|
| 503 |
+
|
| 504 |
+
if __name__ == "__main__":
|
| 505 |
+
# Example usage of the fully modular CodeAgent architecture
|
| 506 |
+
# Create LLM client
|
| 507 |
+
model = ChatOpenAI(
|
| 508 |
+
model="google/gemini-2.5-flash",
|
| 509 |
+
base_url="https://openrouter.ai/api/v1",
|
| 510 |
+
temperature=0.7,
|
| 511 |
+
api_key=os.environ["OPENROUTER_API_KEY"],
|
| 512 |
+
)
|
| 513 |
+
|
| 514 |
+
model = ChatAnthropic(model='claude-sonnet-4-5-20250929')
|
| 515 |
+
|
| 516 |
+
|
| 517 |
+
|
| 518 |
+
# Create configuration
|
| 519 |
+
config = AgentConfig(
|
| 520 |
+
max_steps=15,
|
| 521 |
+
max_conversation_length=30,
|
| 522 |
+
retry_attempts=3,
|
| 523 |
+
timeout_seconds=1200,
|
| 524 |
+
verbose=True
|
| 525 |
+
)
|
| 526 |
+
|
| 527 |
+
# Create agent with unified tool management and LLM-based tool selection
|
| 528 |
+
agent = CodeAgent(model=model, config=config, use_tool_manager=True, use_tool_selection=True)
|
| 529 |
+
|
| 530 |
+
# Demonstrate tool management capabilities
|
| 531 |
+
print("\n🔧 Tool Management Demo:")
|
| 532 |
+
|
| 533 |
+
# Show tool statistics
|
| 534 |
+
stats = agent.get_tool_statistics()
|
| 535 |
+
print(f"📊 Tool Statistics: {stats}")
|
| 536 |
+
|
| 537 |
+
# Add MCP tools
|
| 538 |
+
try:
|
| 539 |
+
print("🔧 Loading MCP tools...")
|
| 540 |
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
| 541 |
+
config_path = os.path.join(script_dir, "mcp_config.yaml")
|
| 542 |
+
agent.add_mcp(config_path)
|
| 543 |
+
print("✅ MCP tools loaded successfully!")
|
| 544 |
+
|
| 545 |
+
# Show enhanced MCP status
|
| 546 |
+
agent.show_mcp_status()
|
| 547 |
+
|
| 548 |
+
# Show all available tools by source
|
| 549 |
+
all_tools = agent.list_tools("all", include_details=True)
|
| 550 |
+
print(f"\n📋 All tools loaded:")
|
| 551 |
+
for tool in all_tools:
|
| 552 |
+
print(f" • {tool['name']} ({tool['source']}) - {tool['description'][:50]}...")
|
| 553 |
+
|
| 554 |
+
except Exception as e:
|
| 555 |
+
print(f"⚠️ Could not load MCP tools: {e}")
|
| 556 |
+
|
| 557 |
+
# Validate tools
|
| 558 |
+
issues = agent.validate_tools()
|
| 559 |
+
if any(issues.values()):
|
| 560 |
+
print(f"⚠️ Tool validation issues: {issues}")
|
| 561 |
+
else:
|
| 562 |
+
print("✅ All tools validated successfully!")
|
| 563 |
+
# List available packages
|
| 564 |
+
print(f"\n📦 Available packages: {list(agent.get_all_packages().keys())}")
|
| 565 |
+
# Run the agent with a query and save trace/summary
|
| 566 |
+
print("\n🚀 Running agent with trace and summary saving...")
|
| 567 |
+
result = agent.run(
|
| 568 |
+
query="""
|
| 569 |
+
Use AlphaGenome MCP to analyze heart gene expression data to identify the causal gene
|
| 570 |
+
for the variant chr11:116837649:T>G, associated with Hypoalphalipoproteinemia. My API key is: AIzaSyD1USDNy9WqfIROICB3FWI1wJHmkO2z21U.
|
| 571 |
+
""",
|
| 572 |
+
save_trace=True, # Save complete execution trace
|
| 573 |
+
save_summary=True, # Save execution summary
|
| 574 |
+
trace_dir="traces" # Directory to save files
|
| 575 |
+
)
|
| 576 |
+
|
| 577 |
+
# You can also access trace and summary programmatically
|
| 578 |
+
print("\n📊 Execution Summary:")
|
| 579 |
+
summary = agent.get_summary()
|
| 580 |
+
print(f" Total steps: {summary.get('total_steps', 0)}")
|
| 581 |
+
print(f" Code executions: {len(summary.get('code_executions', []))}")
|
| 582 |
+
print(f" Observations: {len(summary.get('observations', []))}")
|
| 583 |
+
print(f" Errors: {len(summary.get('errors', []))}")
|
| 584 |
+
|
| 585 |
+
# You can save trace/summary manually after execution
|
| 586 |
+
# agent.save_trace("custom_trace.json")
|
| 587 |
+
# agent.save_summary("custom_summary.json")
|
app.py
CHANGED
|
@@ -1,69 +1,656 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import gradio as gr
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
allow="accelerometer; autoplay; clipboard-write; encrypted-media;
|
| 8 |
-
gyroscope; picture-in-picture; web-share"
|
| 9 |
-
referrerpolicy="strict-origin-when-cross-origin"
|
| 10 |
-
allowfullscreen></iframe>
|
| 11 |
-
"""
|
| 12 |
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
src="https://www.youtube.com/embed/QMNqvybf_9U?si=Y1AAy5V_S7Nht4Vs"
|
| 16 |
-
title="YouTube video player" frameborder="0"
|
| 17 |
-
allow="accelerometer; autoplay; clipboard-write; encrypted-media;
|
| 18 |
-
gyroscope; picture-in-picture; web-share"
|
| 19 |
-
referrerpolicy="strict-origin-when-cross-origin"
|
| 20 |
-
allowfullscreen></iframe>
|
| 21 |
-
"""
|
| 22 |
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
"""
|
| 26 |
-
# Demos
|
| 27 |
-
Below, we showcase demos of AI agents created by Paper2Agent, illustrating how each agent applies the tools from its source paper to tackle scientific tasks.
|
| 28 |
|
| 29 |
-
## 🧬 AlphaGenome Agent for Genomic Data Interpretation
|
| 30 |
-
**Example query:**
|
| 31 |
-
```
|
| 32 |
-
Analyze heart gene expression data with AlphaGenome MCP to identify the causal genefor the variant chr11:116837649:T>G, associated with Hypoalphalipoproteinemia.
|
| 33 |
-
```
|
| 34 |
-
"""
|
| 35 |
-
)
|
| 36 |
-
gr.HTML(embed_html1)
|
| 37 |
-
gr.Markdown(
|
| 38 |
-
"""
|
| 39 |
-
# 🤖 How to Create AlphaGenome and other Paper Agents?
|
| 40 |
-
|
| 41 |
-
To streamline usage, we recommend creating Paper Agents by connecting Paper MCP servers to an AI coding agent, such as [Claude Code](https://www.anthropic.com/claude-code) or the [Google Gemini CLI](https://google-gemini.github.io/gemini-cli/) (it's free with a Google account!).
|
| 42 |
-
|
| 43 |
-
We are also actively developing our own Chatbot-based agent, which will be released soon.
|
| 44 |
-
|
| 45 |
-
## ⚙️ Using Claude Code
|
| 46 |
-
First, install and set up Claude Code:
|
| 47 |
-
```bash
|
| 48 |
-
npm install -g @anthropic-ai/claude-code
|
| 49 |
-
claude
|
| 50 |
-
```
|
| 51 |
-
After setup, link Claude Code with the Paper MCP server of interest.
|
| 52 |
-
For example, to create an AlphaGenome Agent, run:
|
| 53 |
-
```
|
| 54 |
-
claude mcp add \
|
| 55 |
-
--transport http \
|
| 56 |
-
alphagenome \
|
| 57 |
-
https://Paper2Agent-alphagenome-mcp.hf.space/mcp
|
| 58 |
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
```
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Gradio Interface for LangGraph ReAct Agent
|
| 3 |
+
A production-ready web interface with real-time streaming output
|
| 4 |
+
Styled similar to gradio_ui.py with sidebar layout
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import re
|
| 9 |
+
import sys
|
| 10 |
+
import time
|
| 11 |
+
from typing import Generator, Optional
|
| 12 |
+
from dataclasses import dataclass
|
| 13 |
+
|
| 14 |
import gradio as gr
|
| 15 |
+
from gradio.themes.utils import fonts
|
| 16 |
+
from dotenv import load_dotenv
|
| 17 |
+
from langchain_openai import ChatOpenAI
|
| 18 |
+
from langchain_core.messages import HumanMessage, AIMessage
|
| 19 |
+
from langchain_anthropic import ChatAnthropic
|
| 20 |
|
| 21 |
+
# Import the agent from agent_v3
|
| 22 |
+
sys.path.append(os.path.dirname(__file__))
|
| 23 |
+
from agent_v3 import CodeAgent
|
| 24 |
+
from core.types import AgentConfig
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
+
# Import support modules
|
| 27 |
+
from managers import PythonExecutor
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
+
# Load environment variables
|
| 30 |
+
load_dotenv("./.env")
|
|
|
|
|
|
|
|
|
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
+
class GradioAgentUI:
|
| 34 |
+
"""
|
| 35 |
+
Gradio interface for interacting with the LangGraph ReAct Agent.
|
| 36 |
+
Styled similar to the smolagents GradioUI with sidebar layout.
|
| 37 |
+
"""
|
| 38 |
+
|
| 39 |
+
def __init__(self, model=None, config=None):
|
| 40 |
+
"""Initialize the Gradio Agent UI."""
|
| 41 |
+
|
| 42 |
+
# Default model
|
| 43 |
+
if model is None:
|
| 44 |
+
api_key = os.environ.get("OPENROUTER_API_KEY")
|
| 45 |
+
if not api_key:
|
| 46 |
+
raise ValueError(
|
| 47 |
+
"OPENROUTER_API_KEY environment variable is not set. "
|
| 48 |
+
"Please set it or provide a model instance."
|
| 49 |
+
)
|
| 50 |
+
# model = ChatOpenAI(
|
| 51 |
+
# model="google/gemini-2.5-flash",
|
| 52 |
+
# base_url="https://openrouter.ai/api/v1",
|
| 53 |
+
# temperature=0.7,
|
| 54 |
+
# api_key=api_key,
|
| 55 |
+
# )
|
| 56 |
+
|
| 57 |
+
model = ChatAnthropic(
|
| 58 |
+
model='claude-sonnet-4-5-20250929',
|
| 59 |
+
temperature=0.7,
|
| 60 |
+
api_key='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'
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
# Default config
|
| 67 |
+
if config is None:
|
| 68 |
+
config = AgentConfig(
|
| 69 |
+
max_steps=15,
|
| 70 |
+
max_conversation_length=30,
|
| 71 |
+
retry_attempts=3,
|
| 72 |
+
timeout_seconds=1200,
|
| 73 |
+
verbose=True
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
self.model = model
|
| 77 |
+
self.config = config
|
| 78 |
+
self.name = "AlphaGenome Agent Interface"
|
| 79 |
+
self.description = "AlphaGenome Agent is automatically created by [Paper2Agent](https://github.com/jmiao24/Paper2Agent) to use [AlphaGenome](https://github.com/google-deepmind/alphagenome) and interpret DNA variants."
|
| 80 |
+
|
| 81 |
+
def get_step_footnote(self, step_num: int, duration: float) -> str:
|
| 82 |
+
"""Create a footnote for a step with timing information."""
|
| 83 |
+
return f'<span style="color: #888; font-size: 0.9em;">Step {step_num} | Duration: {duration:.2f}s</span>'
|
| 84 |
+
|
| 85 |
+
def format_code_block(self, code: str) -> str:
|
| 86 |
+
"""Format code as a Python code block."""
|
| 87 |
+
code = code.strip()
|
| 88 |
+
if not code.startswith("```"):
|
| 89 |
+
code = f"```python\n{code}\n```"
|
| 90 |
+
return code
|
| 91 |
+
|
| 92 |
+
def extract_content_parts(self, message_content: str) -> dict:
|
| 93 |
+
"""Extract different parts from the message content."""
|
| 94 |
+
parts = {
|
| 95 |
+
'thinking': '',
|
| 96 |
+
'plan': None,
|
| 97 |
+
'code': None,
|
| 98 |
+
'solution': None,
|
| 99 |
+
'error': None,
|
| 100 |
+
'observation': None
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
# Extract thinking (content before any tags)
|
| 104 |
+
thinking_pattern = r'^(.*?)(?=<(?:execute|solution|error|observation)|$)'
|
| 105 |
+
thinking_match = re.match(thinking_pattern, message_content, re.DOTALL)
|
| 106 |
+
if thinking_match:
|
| 107 |
+
thinking = thinking_match.group(1).strip()
|
| 108 |
+
# Remove plan from thinking
|
| 109 |
+
plan_pattern = r'\d+\.\s*\[[^\]]*\]\s*[^\n]+(?:\n\d+\.\s*\[[^\]]*\]\s*[^\n]+)*'
|
| 110 |
+
thinking = re.sub(plan_pattern, '', thinking).strip()
|
| 111 |
+
# Remove any existing "Thinking:" or "Plan:" labels (including duplicates)
|
| 112 |
+
thinking = re.sub(r'(^|\n)(Thinking:|Plan:)\s*', '\n', thinking).strip()
|
| 113 |
+
# Remove any duplicate Thinking: that might remain
|
| 114 |
+
thinking = re.sub(r'Thinking:\s*', '', thinking).strip()
|
| 115 |
+
if thinking:
|
| 116 |
+
parts['thinking'] = thinking
|
| 117 |
+
|
| 118 |
+
# Extract plan
|
| 119 |
+
plan_pattern = r'\d+\.\s*\[[^\]]*\]\s*[^\n]+(?:\n\d+\.\s*\[[^\]]*\]\s*[^\n]+)*'
|
| 120 |
+
plan_match = re.search(plan_pattern, message_content)
|
| 121 |
+
if plan_match:
|
| 122 |
+
parts['plan'] = plan_match.group(0)
|
| 123 |
+
|
| 124 |
+
# Extract code
|
| 125 |
+
code_match = re.search(r'<execute>(.*?)</execute>', message_content, re.DOTALL)
|
| 126 |
+
if code_match:
|
| 127 |
+
parts['code'] = code_match.group(1).strip()
|
| 128 |
+
|
| 129 |
+
# Extract solution
|
| 130 |
+
solution_match = re.search(r'<solution>(.*?)</solution>', message_content, re.DOTALL)
|
| 131 |
+
if solution_match:
|
| 132 |
+
parts['solution'] = solution_match.group(1).strip()
|
| 133 |
+
|
| 134 |
+
# Extract error
|
| 135 |
+
error_match = re.search(r'<error>(.*?)</error>', message_content, re.DOTALL)
|
| 136 |
+
if error_match:
|
| 137 |
+
parts['error'] = error_match.group(1).strip()
|
| 138 |
+
|
| 139 |
+
# Extract observation
|
| 140 |
+
obs_match = re.search(r'<observation>(.*?)</observation>', message_content, re.DOTALL)
|
| 141 |
+
if obs_match:
|
| 142 |
+
parts['observation'] = obs_match.group(1).strip()
|
| 143 |
+
|
| 144 |
+
return parts
|
| 145 |
+
|
| 146 |
+
def truncate_output(self, text: str, max_lines: int = 20) -> str:
|
| 147 |
+
"""Truncate output to specified number of lines."""
|
| 148 |
+
lines = text.split('\n')
|
| 149 |
+
if len(lines) > max_lines:
|
| 150 |
+
truncated = '\n'.join(lines[:max_lines])
|
| 151 |
+
truncated += f"\n... (truncated {len(lines) - max_lines} lines)"
|
| 152 |
+
return truncated
|
| 153 |
+
return text
|
| 154 |
+
|
| 155 |
+
def stream_agent_response(self, agent: CodeAgent, query: str) -> Generator:
|
| 156 |
+
"""Stream agent responses as Gradio ChatMessages with structured blocks."""
|
| 157 |
+
|
| 158 |
+
# Get all tool functions using the agent's method
|
| 159 |
+
tools_dict = agent.get_all_tool_functions()
|
| 160 |
+
agent.python_executor.send_functions(tools_dict)
|
| 161 |
+
agent.python_executor.send_variables({})
|
| 162 |
+
|
| 163 |
+
# Create initial state
|
| 164 |
+
input_state = {
|
| 165 |
+
"messages": [HumanMessage(content=query)],
|
| 166 |
+
"step_count": 0,
|
| 167 |
+
"error_count": 0,
|
| 168 |
+
"start_time": time.time(),
|
| 169 |
+
"current_plan": None
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
# Track state
|
| 173 |
+
displayed_reasoning = set()
|
| 174 |
+
previous_plan = None
|
| 175 |
+
step_timings = {}
|
| 176 |
+
|
| 177 |
+
try:
|
| 178 |
+
# Stream through the workflow execution
|
| 179 |
+
for state in agent.workflow_engine.graph.stream(input_state, stream_mode="values"):
|
| 180 |
+
step_count = state.get("step_count", 0)
|
| 181 |
+
error_count = state.get("error_count", 0)
|
| 182 |
+
current_plan = state.get("current_plan")
|
| 183 |
+
|
| 184 |
+
# Track timing
|
| 185 |
+
if step_count not in step_timings:
|
| 186 |
+
step_timings[step_count] = time.time()
|
| 187 |
+
|
| 188 |
+
message = state["messages"][-1]
|
| 189 |
+
|
| 190 |
+
if isinstance(message, AIMessage):
|
| 191 |
+
content = message.content
|
| 192 |
+
parts = self.extract_content_parts(content)
|
| 193 |
+
|
| 194 |
+
# First yield step header only
|
| 195 |
+
if step_count > 0:
|
| 196 |
+
step_header = f"## Step {step_count}\n"
|
| 197 |
+
yield gr.ChatMessage(
|
| 198 |
+
role="assistant",
|
| 199 |
+
content=step_header,
|
| 200 |
+
metadata={"status": "done"}
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
# Display Thinking block (styled like Current Plan)
|
| 204 |
+
if parts['thinking'] and len(parts['thinking']) > 20:
|
| 205 |
+
thinking_text = parts['thinking']
|
| 206 |
+
# Clean up any remaining labels
|
| 207 |
+
thinking_text = re.sub(r'(Thinking:|Plan:)\s*', '', thinking_text).strip()
|
| 208 |
+
# Clean up excessive whitespace - replace multiple newlines with max 2
|
| 209 |
+
thinking_text = re.sub(r'\n\s*\n\s*\n+', '\n\n', thinking_text).strip()
|
| 210 |
+
# Also clean up any leading/trailing whitespace on each line
|
| 211 |
+
thinking_text = '\n'.join(line.strip() for line in thinking_text.split('\n') if line.strip())
|
| 212 |
+
|
| 213 |
+
content_hash = hash(thinking_text)
|
| 214 |
+
if content_hash not in displayed_reasoning and thinking_text:
|
| 215 |
+
thinking_block = f"""<div style="background-color: #f8f9fa; border-left: 4px solid #6b7280; padding: 12px; margin: 10px 0; border-radius: 4px;">
|
| 216 |
+
<div style="font-weight: bold; color: #374151; margin-bottom: 8px;">🤔 Thinking</div>
|
| 217 |
+
<div style="margin: 0; white-space: pre-line; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #000000; background-color: #ffffff; padding: 10px; border-radius: 4px; font-size: 14px; line-height: 1.6;">{thinking_text}</div>
|
| 218 |
+
</div>"""
|
| 219 |
+
yield gr.ChatMessage(
|
| 220 |
+
role="assistant",
|
| 221 |
+
content=thinking_block,
|
| 222 |
+
metadata={"status": "done"}
|
| 223 |
+
)
|
| 224 |
+
displayed_reasoning.add(content_hash)
|
| 225 |
+
|
| 226 |
+
# Then display Current Plan after thinking
|
| 227 |
+
if current_plan and current_plan != previous_plan:
|
| 228 |
+
formatted_plan = current_plan.replace('[ ]', '☐').replace('[✓]', '✅').replace('[✗]', '❌')
|
| 229 |
+
|
| 230 |
+
plan_block = f"""<div style="background-color: #f8f9fa; border-left: 4px solid #000000; padding: 12px; margin: 10px 0; border-radius: 4px;">
|
| 231 |
+
<div style="font-weight: bold; color: #000000; margin-bottom: 8px;">📋 Current Plan</div>
|
| 232 |
+
<pre style="margin: 0; white-space: pre-wrap; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; color: #000000; background-color: #ffffff; padding: 10px; border-radius: 4px; font-size: 14px; line-height: 1.6;">{formatted_plan}</pre>
|
| 233 |
+
</div>"""
|
| 234 |
+
yield gr.ChatMessage(
|
| 235 |
+
role="assistant",
|
| 236 |
+
content=plan_block,
|
| 237 |
+
metadata={"status": "done"}
|
| 238 |
+
)
|
| 239 |
+
previous_plan = current_plan
|
| 240 |
+
|
| 241 |
+
# Display Executing Code as independent block with proper syntax highlighting
|
| 242 |
+
if parts['code']:
|
| 243 |
+
# Format as markdown code block for proper rendering
|
| 244 |
+
code_block = f"""<div style="background-color: #f7fafc; border-left: 4px solid #48bb78; padding: 12px; margin: 10px 0; border-radius: 4px;">
|
| 245 |
+
<div style="font-weight: bold; color: #22543d; margin-bottom: 8px;">⚡ Executing Code</div>
|
| 246 |
+
</div>
|
| 247 |
+
|
| 248 |
+
```python
|
| 249 |
+
{parts['code']}
|
| 250 |
+
```"""
|
| 251 |
+
yield gr.ChatMessage(
|
| 252 |
+
role="assistant",
|
| 253 |
+
content=code_block,
|
| 254 |
+
metadata={"status": "done"}
|
| 255 |
+
)
|
| 256 |
+
|
| 257 |
+
# Display Execution Result as independent block
|
| 258 |
+
if parts['observation']:
|
| 259 |
+
truncated = self.truncate_output(parts['observation'])
|
| 260 |
+
|
| 261 |
+
result_block = f"""<div style="background-color: #fef5e7; border-left: 4px solid #f6ad55; padding: 12px; margin: 10px 0; border-radius: 4px;">
|
| 262 |
+
<div style="font-weight: bold; color: #744210; margin-bottom: 8px;">📊 Execution Result</div>
|
| 263 |
+
</div>
|
| 264 |
+
|
| 265 |
```
|
| 266 |
+
{truncated}
|
| 267 |
+
```"""
|
| 268 |
+
yield gr.ChatMessage(
|
| 269 |
+
role="assistant",
|
| 270 |
+
content=result_block,
|
| 271 |
+
metadata={"status": "done"}
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
# Display solution with special formatting as independent block
|
| 275 |
+
if parts['solution']:
|
| 276 |
+
solution_block = f"""<div style="background-color: #d1fae5; border-left: 4px solid #10b981; padding: 16px; margin: 10px 0; border-radius: 6px;">
|
| 277 |
+
<div style="font-weight: bold; color: #065f46; margin-bottom: 12px; font-size: 16px;">✅ Final Solution</div>
|
| 278 |
+
<div class="solution-content" style="color: #1f2937 !important; background-color: #ffffff; padding: 16px; border-radius: 4px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; font-size: 15px; line-height: 1.7; border: 1px solid #e5e7eb;">
|
| 279 |
+
|
| 280 |
+
{parts['solution']}
|
| 281 |
+
|
| 282 |
+
</div>
|
| 283 |
+
<style>
|
| 284 |
+
.solution-content, .solution-content * {{
|
| 285 |
+
color: #1f2937 !important;
|
| 286 |
+
background-color: transparent !important;
|
| 287 |
+
}}
|
| 288 |
+
.solution-content code, .solution-content pre {{
|
| 289 |
+
background-color: #f3f4f6 !important;
|
| 290 |
+
color: #1f2937 !important;
|
| 291 |
+
padding: 2px 6px;
|
| 292 |
+
border-radius: 3px;
|
| 293 |
+
font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
|
| 294 |
+
}}
|
| 295 |
+
.solution-content pre {{
|
| 296 |
+
padding: 12px;
|
| 297 |
+
overflow-x: auto;
|
| 298 |
+
}}
|
| 299 |
+
.solution-content table {{
|
| 300 |
+
border-collapse: collapse;
|
| 301 |
+
width: 100%;
|
| 302 |
+
margin: 10px 0;
|
| 303 |
+
background-color: #ffffff !important;
|
| 304 |
+
}}
|
| 305 |
+
.solution-content td, .solution-content th {{
|
| 306 |
+
border: 1px solid #d1d5db;
|
| 307 |
+
padding: 8px;
|
| 308 |
+
color: #1f2937 !important;
|
| 309 |
+
background-color: #ffffff !important;
|
| 310 |
+
}}
|
| 311 |
+
.solution-content th {{
|
| 312 |
+
background-color: #f3f4f6 !important;
|
| 313 |
+
font-weight: 600;
|
| 314 |
+
}}
|
| 315 |
+
.solution-content p, .solution-content div, .solution-content span {{
|
| 316 |
+
background-color: transparent !important;
|
| 317 |
+
}}
|
| 318 |
+
</style>
|
| 319 |
+
</div>"""
|
| 320 |
+
yield gr.ChatMessage(
|
| 321 |
+
role="assistant",
|
| 322 |
+
content=solution_block,
|
| 323 |
+
metadata={"status": "done"}
|
| 324 |
+
)
|
| 325 |
+
|
| 326 |
+
# Display error with special formatting
|
| 327 |
+
if parts['error']:
|
| 328 |
+
error_block = f"""<div style="background-color: #fed7d7; border-left: 4px solid #fc8181; padding: 12px; margin: 10px 0; border-radius: 4px;">
|
| 329 |
+
<div style="font-weight: bold; color: #742a2a; margin-bottom: 8px;">⚠️ Error</div>
|
| 330 |
+
<div style="color: #742a2a;">{parts['error']}</div>
|
| 331 |
+
</div>"""
|
| 332 |
+
yield gr.ChatMessage(
|
| 333 |
+
role="assistant",
|
| 334 |
+
content=error_block,
|
| 335 |
+
metadata={"status": "done"}
|
| 336 |
+
)
|
| 337 |
+
|
| 338 |
+
# Add step footnote with timing ONLY after Execution Result
|
| 339 |
+
if parts['observation'] and step_count in step_timings:
|
| 340 |
+
duration = time.time() - step_timings[step_count]
|
| 341 |
+
footnote = f'<div style="color: #718096; font-size: 0.875em; margin-top: 8px;">Step {step_count} | Duration: {duration:.2f}s</div>'
|
| 342 |
+
yield gr.ChatMessage(
|
| 343 |
+
role="assistant",
|
| 344 |
+
content=footnote,
|
| 345 |
+
metadata={"status": "done"}
|
| 346 |
+
)
|
| 347 |
+
|
| 348 |
+
# Add separator between steps
|
| 349 |
+
if step_count > 0:
|
| 350 |
+
yield gr.ChatMessage(
|
| 351 |
+
role="assistant",
|
| 352 |
+
content='<hr style="border: none; border-top: 1px solid #e2e8f0; margin: 20px 0;">',
|
| 353 |
+
metadata={"status": "done"}
|
| 354 |
+
)
|
| 355 |
+
|
| 356 |
+
except Exception as e:
|
| 357 |
+
error_block = f"""<div style="background-color: #fed7d7; border-left: 4px solid #fc8181; padding: 12px; margin: 10px 0; border-radius: 4px;">
|
| 358 |
+
<div style="font-weight: bold; color: #742a2a; margin-bottom: 8px;">💥 Critical Error</div>
|
| 359 |
+
<div style="color: #742a2a;">Error during agent execution: {str(e)}</div>
|
| 360 |
+
</div>"""
|
| 361 |
+
yield gr.ChatMessage(
|
| 362 |
+
role="assistant",
|
| 363 |
+
content=error_block,
|
| 364 |
+
metadata={"status": "done"}
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
+
def interact_with_agent(self, prompt: str, api_key: str, messages: list, session_state: dict) -> Generator:
|
| 368 |
+
"""Handle interaction with the agent."""
|
| 369 |
+
|
| 370 |
+
# Store original user input for display
|
| 371 |
+
original_prompt = prompt
|
| 372 |
+
|
| 373 |
+
# Add API key to the prompt if provided (for agent processing)
|
| 374 |
+
if api_key and api_key.strip():
|
| 375 |
+
prompt = f"{prompt} My API key is: {api_key.strip()}."
|
| 376 |
+
|
| 377 |
+
# Initialize agent in session if needed
|
| 378 |
+
if "agent" not in session_state:
|
| 379 |
+
session_state["agent"] = CodeAgent(
|
| 380 |
+
model=self.model,
|
| 381 |
+
config=self.config,
|
| 382 |
+
use_tool_manager=True,
|
| 383 |
+
use_tool_selection=False # Disable for Gradio UI by default
|
| 384 |
+
)
|
| 385 |
+
|
| 386 |
+
# Try to load MCP config if available
|
| 387 |
+
mcp_config_path = "./mcp_config.yaml"
|
| 388 |
+
if os.path.exists(mcp_config_path):
|
| 389 |
+
try:
|
| 390 |
+
session_state["agent"].add_mcp(mcp_config_path)
|
| 391 |
+
print(f"✅ Loaded MCP tools from {mcp_config_path}")
|
| 392 |
+
except Exception as e:
|
| 393 |
+
print(f"⚠️ Could not load MCP tools: {e}")
|
| 394 |
+
|
| 395 |
+
agent = session_state["agent"]
|
| 396 |
+
|
| 397 |
+
try:
|
| 398 |
+
# Add user message immediately (use original prompt for display)
|
| 399 |
+
messages.append(gr.ChatMessage(role="user", content=original_prompt, metadata={"status": "done"}))
|
| 400 |
+
yield messages
|
| 401 |
+
|
| 402 |
+
# Add a "thinking" indicator immediately
|
| 403 |
+
messages.append(gr.ChatMessage(role="assistant", content="🤔 Processing...", metadata={"status": "pending"}))
|
| 404 |
+
yield messages
|
| 405 |
+
|
| 406 |
+
# Remove the thinking indicator before adding real content
|
| 407 |
+
messages.pop()
|
| 408 |
+
|
| 409 |
+
# Stream agent responses with real-time updates
|
| 410 |
+
for msg in self.stream_agent_response(agent, prompt):
|
| 411 |
+
messages.append(msg)
|
| 412 |
+
yield messages
|
| 413 |
+
|
| 414 |
+
except Exception as e:
|
| 415 |
+
messages.append(
|
| 416 |
+
gr.ChatMessage(
|
| 417 |
+
role="assistant",
|
| 418 |
+
content=f"Error: {str(e)}",
|
| 419 |
+
metadata={"title": "💥 Error", "status": "done"}
|
| 420 |
+
)
|
| 421 |
+
)
|
| 422 |
+
yield messages
|
| 423 |
+
|
| 424 |
+
def create_app(self):
|
| 425 |
+
"""Create the Gradio app with sidebar layout."""
|
| 426 |
+
|
| 427 |
+
# Create custom theme with modern fonts
|
| 428 |
+
modern_theme = gr.themes.Monochrome(
|
| 429 |
+
font=fonts.GoogleFont("Inter"),
|
| 430 |
+
font_mono=fonts.GoogleFont("JetBrains Mono")
|
| 431 |
+
)
|
| 432 |
+
|
| 433 |
+
with gr.Blocks(theme=modern_theme, fill_height=True, title=self.name, css="""
|
| 434 |
+
/* Hide Gradio footer */
|
| 435 |
+
.footer {display: none !important;}
|
| 436 |
+
footer {display: none !important;}
|
| 437 |
+
.gradio-footer {display: none !important;}
|
| 438 |
+
#footer {display: none !important;}
|
| 439 |
+
[class*="footer"] {display: none !important;}
|
| 440 |
+
[id*="footer"] {display: none !important;}
|
| 441 |
+
.block.svelte-1scc9gv {display: none !important;}
|
| 442 |
+
.built-with-gradio {display: none !important;}
|
| 443 |
+
.gradio-container footer {display: none !important;}
|
| 444 |
+
""") as demo:
|
| 445 |
+
# Force light theme and hide footer
|
| 446 |
+
demo.load(js="""
|
| 447 |
+
() => {
|
| 448 |
+
document.body.classList.remove('dark');
|
| 449 |
+
document.querySelector('gradio-app').classList.remove('dark');
|
| 450 |
+
const url = new URL(window.location);
|
| 451 |
+
url.searchParams.set('__theme', 'light');
|
| 452 |
+
window.history.replaceState({}, '', url);
|
| 453 |
+
|
| 454 |
+
// Hide footer elements
|
| 455 |
+
setTimeout(() => {
|
| 456 |
+
const footers = document.querySelectorAll('footer, .footer, .gradio-footer, #footer, [class*="footer"], [id*="footer"], .built-with-gradio');
|
| 457 |
+
footers.forEach(footer => footer.style.display = 'none');
|
| 458 |
+
|
| 459 |
+
// Also hide any Gradio logo/branding
|
| 460 |
+
const brandingElements = document.querySelectorAll('a[href*="gradio"], .gradio-logo, [alt*="gradio"]');
|
| 461 |
+
brandingElements.forEach(el => el.style.display = 'none');
|
| 462 |
+
}, 100);
|
| 463 |
+
}
|
| 464 |
+
""")
|
| 465 |
+
# Session state
|
| 466 |
+
session_state = gr.State({})
|
| 467 |
+
stored_messages = gr.State([])
|
| 468 |
+
|
| 469 |
+
with gr.Row():
|
| 470 |
+
# Sidebar
|
| 471 |
+
with gr.Column(scale=1):
|
| 472 |
+
gr.Markdown(f"# {self.name}")
|
| 473 |
+
gr.Markdown(f"> {self.description}")
|
| 474 |
+
|
| 475 |
+
gr.Markdown("---")
|
| 476 |
+
|
| 477 |
+
# API Key section
|
| 478 |
+
with gr.Group():
|
| 479 |
+
gr.Markdown("**AlphaGenome API Key**")
|
| 480 |
+
api_key_input = gr.Textbox(
|
| 481 |
+
label="API Key",
|
| 482 |
+
type="password",
|
| 483 |
+
placeholder="Enter your AlphaGenome API key",
|
| 484 |
+
value="AIzaSyD1USDNy9WqfIROICB3FWI1wJHmkO2z21U"
|
| 485 |
+
)
|
| 486 |
+
|
| 487 |
+
# Input section
|
| 488 |
+
with gr.Group():
|
| 489 |
+
gr.Markdown("**Your Request**")
|
| 490 |
+
text_input = gr.Textbox(
|
| 491 |
+
lines=4,
|
| 492 |
+
label="Query",
|
| 493 |
+
placeholder="Enter your query here and press Enter or click Submit",
|
| 494 |
+
value="""Use AlphaGenome MCP to analyze heart gene expression data to identify the causal gene
|
| 495 |
+
for the variant chr11:116837649:T>G, associated with Hypoalphalipoproteinemia."""
|
| 496 |
+
)
|
| 497 |
+
submit_btn = gr.Button("🚀 Submit", variant="primary", size="lg")
|
| 498 |
+
|
| 499 |
+
# Configuration section
|
| 500 |
+
with gr.Accordion("⚙️ Configuration", open=False):
|
| 501 |
+
max_steps_input = gr.Slider(
|
| 502 |
+
label="Max Steps",
|
| 503 |
+
minimum=5,
|
| 504 |
+
maximum=50,
|
| 505 |
+
value=self.config.max_steps,
|
| 506 |
+
step=1
|
| 507 |
+
)
|
| 508 |
+
|
| 509 |
+
temperature_input = gr.Slider(
|
| 510 |
+
label="Temperature",
|
| 511 |
+
minimum=0.0,
|
| 512 |
+
maximum=1.0,
|
| 513 |
+
value=0.7,
|
| 514 |
+
step=0.1
|
| 515 |
+
)
|
| 516 |
+
|
| 517 |
+
apply_config_btn = gr.Button("Apply Configuration", size="sm")
|
| 518 |
+
|
| 519 |
+
# Example queries
|
| 520 |
+
with gr.Accordion("📚 Example Queries", open=False):
|
| 521 |
+
gr.Examples(
|
| 522 |
+
examples=[
|
| 523 |
+
["What's the quantile score of chr3:197081044:TACTC>T on splice junction in Artery (tibial)?"],
|
| 524 |
+
["What are the raw and quantile scores of the variant chr3:120280774:G>T for chromatin accessibility in the GM12878 cell line?"],
|
| 525 |
+
],
|
| 526 |
+
inputs=text_input,
|
| 527 |
+
)
|
| 528 |
+
|
| 529 |
+
gr.Markdown("---")
|
| 530 |
+
# gr.HTML(
|
| 531 |
+
# "<center><small>Powered by LangGraph & OpenRouter</small></center>"
|
| 532 |
+
# )
|
| 533 |
+
|
| 534 |
+
# Main chat area
|
| 535 |
+
with gr.Column(scale=3):
|
| 536 |
+
chatbot = gr.Chatbot(
|
| 537 |
+
label="Agent Conversation",
|
| 538 |
+
type="messages",
|
| 539 |
+
height=700,
|
| 540 |
+
show_copy_button=True,
|
| 541 |
+
avatar_images=(
|
| 542 |
+
None, # Default user avatar
|
| 543 |
+
"images.png" # Assistant avatar
|
| 544 |
+
),
|
| 545 |
+
latex_delimiters=[
|
| 546 |
+
{"left": r"$$", "right": r"$$", "display": True},
|
| 547 |
+
{"left": r"$", "right": r"$", "display": False},
|
| 548 |
+
],
|
| 549 |
+
render_markdown=True,
|
| 550 |
+
sanitize_html=False, # Allow custom HTML for better formatting
|
| 551 |
+
)
|
| 552 |
+
|
| 553 |
+
with gr.Row():
|
| 554 |
+
clear_btn = gr.Button("🗑️ Clear", size="sm")
|
| 555 |
+
# Note: stop_btn is created but not wired up yet
|
| 556 |
+
# This could be used for future cancellation functionality
|
| 557 |
+
stop_btn = gr.Button("⏹️ Stop", size="sm", variant="stop")
|
| 558 |
+
|
| 559 |
+
# Event handlers
|
| 560 |
+
def update_config(max_steps, temperature, session_state):
|
| 561 |
+
"""Update agent configuration."""
|
| 562 |
+
if "agent" in session_state:
|
| 563 |
+
agent = session_state["agent"]
|
| 564 |
+
agent.config.max_steps = max_steps
|
| 565 |
+
if hasattr(agent.model, 'temperature'):
|
| 566 |
+
agent.model.temperature = temperature
|
| 567 |
+
return "Configuration updated!"
|
| 568 |
+
|
| 569 |
+
def clear_chat():
|
| 570 |
+
"""Clear the chat."""
|
| 571 |
+
return [], []
|
| 572 |
+
|
| 573 |
+
# Wire up events
|
| 574 |
+
text_input.submit(
|
| 575 |
+
lambda x: ("", gr.Button(interactive=False)),
|
| 576 |
+
[text_input],
|
| 577 |
+
[text_input, submit_btn]
|
| 578 |
+
).then(
|
| 579 |
+
self.interact_with_agent,
|
| 580 |
+
[stored_messages, api_key_input, chatbot, session_state],
|
| 581 |
+
[chatbot]
|
| 582 |
+
).then(
|
| 583 |
+
lambda: gr.Button(interactive=True),
|
| 584 |
+
None,
|
| 585 |
+
[submit_btn]
|
| 586 |
+
)
|
| 587 |
+
|
| 588 |
+
submit_btn.click(
|
| 589 |
+
lambda x: (x, "", gr.Button(interactive=False)),
|
| 590 |
+
[text_input],
|
| 591 |
+
[stored_messages, text_input, submit_btn]
|
| 592 |
+
).then(
|
| 593 |
+
self.interact_with_agent,
|
| 594 |
+
[stored_messages, api_key_input, chatbot, session_state],
|
| 595 |
+
[chatbot]
|
| 596 |
+
).then(
|
| 597 |
+
lambda: gr.Button(interactive=True),
|
| 598 |
+
None,
|
| 599 |
+
[submit_btn]
|
| 600 |
+
)
|
| 601 |
+
|
| 602 |
+
apply_config_btn.click(
|
| 603 |
+
update_config,
|
| 604 |
+
[max_steps_input, temperature_input, session_state],
|
| 605 |
+
None
|
| 606 |
+
)
|
| 607 |
+
|
| 608 |
+
clear_btn.click(
|
| 609 |
+
clear_chat,
|
| 610 |
+
None,
|
| 611 |
+
[chatbot, stored_messages]
|
| 612 |
+
)
|
| 613 |
+
|
| 614 |
+
return demo
|
| 615 |
+
|
| 616 |
+
def launch(self, share: bool = False, **kwargs):
|
| 617 |
+
"""Launch the Gradio app."""
|
| 618 |
+
app = self.create_app()
|
| 619 |
+
# Set defaults if not provided in kwargs
|
| 620 |
+
kwargs.setdefault('server_name', '0.0.0.0')
|
| 621 |
+
kwargs.setdefault('server_port', 7860)
|
| 622 |
+
kwargs.setdefault('show_error', True)
|
| 623 |
+
|
| 624 |
+
app.queue(max_size=10).launch(
|
| 625 |
+
share=share,
|
| 626 |
+
show_api=False, # 隐藏 "Use via API" 链接
|
| 627 |
+
favicon_path=None, # 移除默认favicon
|
| 628 |
+
**kwargs
|
| 629 |
+
)
|
| 630 |
+
|
| 631 |
+
|
| 632 |
+
if __name__ == "__main__":
|
| 633 |
+
# Check for API key before starting
|
| 634 |
+
if not os.environ.get("OPENROUTER_API_KEY"):
|
| 635 |
+
print("\n⚠️ Error: OPENROUTER_API_KEY environment variable is not set!")
|
| 636 |
+
print("\nPlease set it using one of these methods:")
|
| 637 |
+
print("1. Export it in your shell:")
|
| 638 |
+
print(" export OPENROUTER_API_KEY='your-api-key-here'")
|
| 639 |
+
print("\n2. Create a .env file in the current directory with:")
|
| 640 |
+
print(" OPENROUTER_API_KEY=your-api-key-here")
|
| 641 |
+
print("\n3. Or provide your own model instance when initializing GradioAgentUI")
|
| 642 |
+
sys.exit(1)
|
| 643 |
+
|
| 644 |
+
try:
|
| 645 |
+
# Create and launch the interface with optional MCP config
|
| 646 |
+
ui = GradioAgentUI()
|
| 647 |
+
|
| 648 |
+
# Optional: Load MCP tools if config exists
|
| 649 |
+
mcp_config_path = "./mcp_config.yaml"
|
| 650 |
+
if os.path.exists(mcp_config_path):
|
| 651 |
+
print(f"Found MCP config at {mcp_config_path}")
|
| 652 |
+
|
| 653 |
+
ui.launch(share=False, quiet=False)
|
| 654 |
+
except Exception as e:
|
| 655 |
+
print(f"\n❌ Error starting Gradio interface: {e}")
|
| 656 |
+
sys.exit(1)
|
core/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Core module for CodeAct agent - contains shared types and utilities.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from .types import AgentState, AgentConfig
|
| 6 |
+
from .constants import LIBRARY_CONTENT_DICT, SYSTEM_PROMPT_TEMPLATE
|
| 7 |
+
|
| 8 |
+
__all__ = ['AgentState', 'AgentConfig', 'LIBRARY_CONTENT_DICT', 'SYSTEM_PROMPT_TEMPLATE']
|
core/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (508 Bytes). View file
|
|
|
core/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (449 Bytes). View file
|
|
|
core/__pycache__/constants.cpython-311.pyc
ADDED
|
Binary file (7.6 kB). View file
|
|
|
core/__pycache__/constants.cpython-312.pyc
ADDED
|
Binary file (7.58 kB). View file
|
|
|
core/__pycache__/types.cpython-311.pyc
ADDED
|
Binary file (1.78 kB). View file
|
|
|
core/__pycache__/types.cpython-312.pyc
ADDED
|
Binary file (1.49 kB). View file
|
|
|
core/constants.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Constants and templates for CodeAct agent.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
# Available packages with descriptions
|
| 6 |
+
LIBRARY_CONTENT_DICT = {
|
| 7 |
+
"numpy": "[Python Package] The fundamental package for scientific computing with Python, providing support for arrays, matrices, and mathematical functions.",
|
| 8 |
+
"scipy": "[Python Package] A Python library for scientific and technical computing, including modules for optimization, linear algebra, integration, and statistics.",
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
# System prompt template
|
| 12 |
+
SYSTEM_PROMPT_TEMPLATE = """You are an expert assistant who can solve any task using Python code execution. You will be given a task to solve as best you can.
|
| 13 |
+
|
| 14 |
+
## Planning & Execution Workflow
|
| 15 |
+
|
| 16 |
+
### Step 1: Make a Plan
|
| 17 |
+
For every task, start by creating a clear, numbered plan with checkboxes:
|
| 18 |
+
```
|
| 19 |
+
1. [ ] First step
|
| 20 |
+
2. [ ] Second step
|
| 21 |
+
3. [ ] Third step
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
### Step 2: Execute & Update
|
| 25 |
+
After completing each step, update the checklist with [✓]:
|
| 26 |
+
```
|
| 27 |
+
1. [✓] First step (completed)
|
| 28 |
+
2. [ ] Second step
|
| 29 |
+
3. [ ] Third step
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
If a step fails, mark it with [✗] and add a corrective step:
|
| 33 |
+
```
|
| 34 |
+
1. [✓] First step (completed)
|
| 35 |
+
2. [✗] Second step (failed because...)
|
| 36 |
+
3. [ ] Corrected second step
|
| 37 |
+
4. [ ] Third step
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
**Always show the updated plan after each step** so progress can be tracked.
|
| 41 |
+
|
| 42 |
+
## Response Format
|
| 43 |
+
|
| 44 |
+
At each turn, first provide your **thinking and reasoning** based on the conversation history.
|
| 45 |
+
|
| 46 |
+
Then choose ONE of these two options:
|
| 47 |
+
|
| 48 |
+
### Option 1: Execute Code (<execute> tag)
|
| 49 |
+
Use this to run Python code and get results. The output will appear in <observation></observation> tags.
|
| 50 |
+
|
| 51 |
+
**Format:**
|
| 52 |
+
```
|
| 53 |
+
<execute>
|
| 54 |
+
print("Hello World!")
|
| 55 |
+
</execute>
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
**CRITICAL**: Always end with `</execute>` tag.
|
| 59 |
+
|
| 60 |
+
### Option 2: Provide Solution (<solution> tag)
|
| 61 |
+
Use this when you have the final answer ready to present to the user.
|
| 62 |
+
|
| 63 |
+
**Format:**
|
| 64 |
+
```
|
| 65 |
+
The answer is <solution> Your final answer </solution>
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
**CRITICAL**:
|
| 69 |
+
- Always end with `</solution>` tag.
|
| 70 |
+
- **Always provide a comprehensive summary** in your final solution that includes:
|
| 71 |
+
- A clear statement of what was accomplished
|
| 72 |
+
- Key results, findings, or outputs
|
| 73 |
+
- Any important data, numbers, or visualizations generated
|
| 74 |
+
- Steps taken to reach the solution (brief overview)
|
| 75 |
+
- Any files created or modified
|
| 76 |
+
- Next steps or recommendations (if applicable)
|
| 77 |
+
|
| 78 |
+
## Code Execution Best Practices
|
| 79 |
+
|
| 80 |
+
### Printing Results
|
| 81 |
+
**YOU MUST print outputs** - the system cannot see unpublished results:
|
| 82 |
+
```python
|
| 83 |
+
# ✓ CORRECT
|
| 84 |
+
result = some_function(param1, param2)
|
| 85 |
+
print(result)
|
| 86 |
+
|
| 87 |
+
# ✗ WRONG - output is invisible
|
| 88 |
+
some_function(param1, param2)
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
### Working with Data
|
| 92 |
+
When working with data structures, always inspect them **but limit output to avoid context overflow**:
|
| 93 |
+
```python
|
| 94 |
+
# For DataFrames - ALWAYS use head() for large tables
|
| 95 |
+
print(f"Shape: {df.shape}")
|
| 96 |
+
print(f"Columns: {list(df.columns)}")
|
| 97 |
+
print(df.head(10)) # Show first 10 rows max
|
| 98 |
+
|
| 99 |
+
# For large DataFrames, also show basic info
|
| 100 |
+
if len(df) > 20:
|
| 101 |
+
print(f"DataFrame has {len(df)} rows. Showing first 10 rows only.")
|
| 102 |
+
|
| 103 |
+
# For lists/dicts
|
| 104 |
+
print(f"Length: {len(data)}")
|
| 105 |
+
print(data[:5]) # Show first few items
|
| 106 |
+
|
| 107 |
+
# For any large output, use head() or limit display
|
| 108 |
+
if hasattr(data, 'head'):
|
| 109 |
+
print(data.head(10)) # For pandas objects
|
| 110 |
+
else:
|
| 111 |
+
print(str(data)[:1000] + "..." if len(str(data)) > 1000 else data)
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
### Function Calls
|
| 115 |
+
When calling available functions, **capture and print the output**:
|
| 116 |
+
```python
|
| 117 |
+
result = function_name(param1="value1", param2="value2")
|
| 118 |
+
print(f"Function result: {result}")
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
### Creating Input Files for Functions
|
| 122 |
+
If a function requires a file as input (e.g., CSV, Excel, TSV), **you must create the file first** with the exact columns specified in the function's parameter description:
|
| 123 |
+
|
| 124 |
+
```python
|
| 125 |
+
# Example: If function requires a CSV with specific columns
|
| 126 |
+
import pandas as pd
|
| 127 |
+
|
| 128 |
+
# Create DataFrame with required columns (as specified in function parameters)
|
| 129 |
+
df = pd.DataFrame({
|
| 130 |
+
'column1': [...], # Fill with appropriate data
|
| 131 |
+
'column2': [...], # Fill with appropriate data
|
| 132 |
+
'column3': [...] # Fill with appropriate data
|
| 133 |
+
})
|
| 134 |
+
|
| 135 |
+
# Save to file
|
| 136 |
+
df.to_csv('input_file.csv', index=False)
|
| 137 |
+
print(f"Created input file with shape: {df.shape}")
|
| 138 |
+
|
| 139 |
+
# Then call the function
|
| 140 |
+
result = function_name(file_path='input_file.csv')
|
| 141 |
+
print(f"Function result: {result}")
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
**Important**: Always check the function's parameter descriptions to understand:
|
| 145 |
+
- What columns are required in the input file
|
| 146 |
+
- The expected format of each column
|
| 147 |
+
- Any specific data types or constraints
|
| 148 |
+
|
| 149 |
+
### Code Quality
|
| 150 |
+
- Keep code simple and readable
|
| 151 |
+
- Add comments for clarity
|
| 152 |
+
- Print intermediate results to create a clear execution log
|
| 153 |
+
- Handle errors gracefully
|
| 154 |
+
|
| 155 |
+
## Important Rules
|
| 156 |
+
|
| 157 |
+
1. **Every response must include EXACTLY ONE tag**: Either `<execute>` or `<solution>`
|
| 158 |
+
2. **Never use both tags in the same response**
|
| 159 |
+
3. **Never respond without any tags**
|
| 160 |
+
4. **Always close tags properly** with `</execute>` or `</solution>`
|
| 161 |
+
5. **Always print outputs** when executing code - unpublished results are invisible to the system
|
| 162 |
+
|
| 163 |
+
## Examples
|
| 164 |
+
|
| 165 |
+
### Example 1: Executing Code
|
| 166 |
+
|
| 167 |
+
**User Query**: "Calculate the factorial of 10"
|
| 168 |
+
|
| 169 |
+
**Correct Response**:
|
| 170 |
+
```
|
| 171 |
+
I'll calculate the factorial of 10 using Python.
|
| 172 |
+
|
| 173 |
+
<execute>
|
| 174 |
+
import math
|
| 175 |
+
result = math.factorial(10)
|
| 176 |
+
print(f"The factorial of 10 is: {result}")
|
| 177 |
+
</execute>
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
**WRONG Response** (missing tags):
|
| 181 |
+
```
|
| 182 |
+
I'll calculate the factorial of 10.
|
| 183 |
+
import math
|
| 184 |
+
result = math.factorial(10)
|
| 185 |
+
print(result)
|
| 186 |
+
```
|
| 187 |
+
|
| 188 |
+
### Example 2: Using Available Functions
|
| 189 |
+
|
| 190 |
+
**User Query**: "Add 5 and 3"
|
| 191 |
+
|
| 192 |
+
**Correct Response**:
|
| 193 |
+
```
|
| 194 |
+
I'll use the add_numbers function to add 5 and 3.
|
| 195 |
+
|
| 196 |
+
<execute>
|
| 197 |
+
result = add_numbers(5, 3)
|
| 198 |
+
print(f"Result: {result}")
|
| 199 |
+
</execute>
|
| 200 |
+
```
|
| 201 |
+
|
| 202 |
+
### Example 3: Providing Final Solution
|
| 203 |
+
|
| 204 |
+
**User Query**: "What is 2+2?"
|
| 205 |
+
|
| 206 |
+
**Correct Response**:
|
| 207 |
+
```
|
| 208 |
+
<execute>
|
| 209 |
+
result = 2 + 2
|
| 210 |
+
print(f"2 + 2 = {result}")
|
| 211 |
+
</execute>
|
| 212 |
+
```
|
| 213 |
+
|
| 214 |
+
Then after seeing the output:
|
| 215 |
+
```
|
| 216 |
+
<solution>
|
| 217 |
+
The answer is 4. I calculated 2 + 2 and got the result of 4.
|
| 218 |
+
</solution>
|
| 219 |
+
```
|
| 220 |
+
|
| 221 |
+
**CRITICAL REMINDER**:
|
| 222 |
+
- ALWAYS wrap code in `<execute>` tags
|
| 223 |
+
- ALWAYS wrap final answers in `<solution>` tags
|
| 224 |
+
- NEVER output code without `<execute>` tags
|
| 225 |
+
|
| 226 |
+
## Available Functions
|
| 227 |
+
|
| 228 |
+
You have access to the following functions. These functions are already available in your Python environment and can be called directly:
|
| 229 |
+
|
| 230 |
+
{% for func_name, schema in functions.items() %}
|
| 231 |
+
**{{ schema.function.name }}({% for param_name in schema.function.parameters.properties.keys() %}{{ param_name }}{{ ", " if not loop.last }}{% endfor %})**
|
| 232 |
+
- Description: {{ schema.function.description }}
|
| 233 |
+
- Parameters:
|
| 234 |
+
{% for param_name, param_info in schema.function.parameters.properties.items() %}
|
| 235 |
+
- {{ param_name }} ({{ param_info.type }}{% if param_info.enum %}, choices: {{ param_info.enum }}{% endif %}): {{ param_info.description }}{% if 'default' in param_info %} [default: {{ param_info.default }}]{% endif %}{% if param_name in schema.function.parameters.required %} [REQUIRED]{% endif %}
|
| 236 |
+
{% endfor %}
|
| 237 |
+
- Example: result = {{ schema.function.name }}({% for param_name in schema.function.parameters.properties.keys() %}{{ param_name }}=example_value{{ ", " if not loop.last }}{% endfor %})
|
| 238 |
+
{% if schema.function.parameters.required %}
|
| 239 |
+
- **Important**: This function requires the following parameters: {{ schema.function.parameters.required|join(', ') }}
|
| 240 |
+
{% endif %}
|
| 241 |
+
|
| 242 |
+
{% endfor %}
|
| 243 |
+
|
| 244 |
+
Important: These functions are pre-loaded in your Python environment. Call them directly like regular Python functions.
|
| 245 |
+
**CRITICAL**: Always provide ALL required parameters when calling functions. Check the [REQUIRED] markers above.
|
| 246 |
+
|
| 247 |
+
## Available Python Packages
|
| 248 |
+
|
| 249 |
+
The following Python packages are available in your environment and can be imported directly:
|
| 250 |
+
|
| 251 |
+
{% for package_name, description in packages.items() %}
|
| 252 |
+
**{{ package_name }}**
|
| 253 |
+
- {{ description }}
|
| 254 |
+
- Import: `import {{ package_name }}`
|
| 255 |
+
|
| 256 |
+
{% endfor %}
|
| 257 |
+
|
| 258 |
+
Remember to import these packages before using them in your code.
|
| 259 |
+
"""
|
core/types.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Shared type definitions for CodeAct agent.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from typing import Annotated, List, Optional, TypedDict
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
from langchain_core.messages import BaseMessage
|
| 8 |
+
from langgraph.graph.message import add_messages
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@dataclass
|
| 12 |
+
class AgentConfig:
|
| 13 |
+
"""Configuration for the Code Agent."""
|
| 14 |
+
max_steps: int = 20
|
| 15 |
+
max_conversation_length: int = 50
|
| 16 |
+
retry_attempts: int = 3
|
| 17 |
+
timeout_seconds: int = 900
|
| 18 |
+
verbose: bool = True
|
| 19 |
+
memory_window: int = 15 # Keep last N messages for context
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class AgentState(TypedDict):
|
| 23 |
+
"""State type for the agent workflow."""
|
| 24 |
+
# reducer tells LangGraph to append new messages
|
| 25 |
+
messages: Annotated[List[BaseMessage], add_messages]
|
| 26 |
+
step_count: int
|
| 27 |
+
error_count: int
|
| 28 |
+
start_time: float
|
| 29 |
+
current_plan: Optional[str]
|
images.png
ADDED
|
managers/.DS_Store
ADDED
|
Binary file (8.2 kB). View file
|
|
|
managers/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Manager modules for CodeAct agent - organized by subsystem.
|
| 3 |
+
|
| 4 |
+
Subsystems:
|
| 5 |
+
- execution: Python execution and monitoring
|
| 6 |
+
- tools: Tool management, registry, and integrations
|
| 7 |
+
- workflow: Workflow execution, state, and plan management
|
| 8 |
+
- support: Console display and package management
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
# Execution subsystem
|
| 12 |
+
from .execution import PythonExecutor, Timing, TokenUsage
|
| 13 |
+
|
| 14 |
+
# Tools subsystem
|
| 15 |
+
from .tools import ToolManager, ToolSource, ToolRegistry, ToolSelector, MCPManager
|
| 16 |
+
|
| 17 |
+
# Workflow subsystem
|
| 18 |
+
from .workflow import WorkflowEngine, StateManager, PlanManager
|
| 19 |
+
|
| 20 |
+
# Support subsystem
|
| 21 |
+
from .support import ConsoleDisplay, PackageManager
|
| 22 |
+
|
| 23 |
+
__all__ = [
|
| 24 |
+
# Execution
|
| 25 |
+
'PythonExecutor',
|
| 26 |
+
'Timing',
|
| 27 |
+
'TokenUsage',
|
| 28 |
+
# Tools
|
| 29 |
+
'ToolManager',
|
| 30 |
+
'ToolSource',
|
| 31 |
+
'ToolRegistry',
|
| 32 |
+
'ToolSelector',
|
| 33 |
+
'MCPManager',
|
| 34 |
+
# Workflow
|
| 35 |
+
'WorkflowEngine',
|
| 36 |
+
'StateManager',
|
| 37 |
+
'PlanManager',
|
| 38 |
+
# Support
|
| 39 |
+
'ConsoleDisplay',
|
| 40 |
+
'PackageManager'
|
| 41 |
+
]
|
managers/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (1.09 kB). View file
|
|
|
managers/execution/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Execution subsystem - Python execution and monitoring.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from .python_executor import PythonExecutor
|
| 6 |
+
from .monitoring import Timing, TokenUsage
|
| 7 |
+
|
| 8 |
+
__all__ = ['PythonExecutor', 'Timing', 'TokenUsage']
|
managers/execution/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (464 Bytes). View file
|
|
|
managers/execution/__pycache__/monitoring.cpython-311.pyc
ADDED
|
Binary file (2.54 kB). View file
|
|
|
managers/execution/__pycache__/python_executor.cpython-311.pyc
ADDED
|
Binary file (7.6 kB). View file
|
|
|
managers/execution/monitoring.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Monitoring utilities for tracking execution timing and token usage.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from dataclasses import dataclass, field
|
| 6 |
+
|
| 7 |
+
__all__ = ["Timing", "TokenUsage"]
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@dataclass
|
| 11 |
+
class TokenUsage:
|
| 12 |
+
"""
|
| 13 |
+
Contains the token usage information for a given step or run.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
input_tokens: int
|
| 17 |
+
output_tokens: int
|
| 18 |
+
total_tokens: int = field(init=False)
|
| 19 |
+
|
| 20 |
+
def __post_init__(self):
|
| 21 |
+
self.total_tokens = self.input_tokens + self.output_tokens
|
| 22 |
+
|
| 23 |
+
def dict(self):
|
| 24 |
+
return {
|
| 25 |
+
"input_tokens": self.input_tokens,
|
| 26 |
+
"output_tokens": self.output_tokens,
|
| 27 |
+
"total_tokens": self.total_tokens,
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@dataclass
|
| 32 |
+
class Timing:
|
| 33 |
+
"""
|
| 34 |
+
Contains the timing information for a given step or run.
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
start_time: float
|
| 38 |
+
end_time: float | None = None
|
| 39 |
+
|
| 40 |
+
@property
|
| 41 |
+
def duration(self):
|
| 42 |
+
return None if self.end_time is None else self.end_time - self.start_time
|
| 43 |
+
|
| 44 |
+
def dict(self):
|
| 45 |
+
return {
|
| 46 |
+
"start_time": self.start_time,
|
| 47 |
+
"end_time": self.end_time,
|
| 48 |
+
"duration": self.duration,
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
def __repr__(self) -> str:
|
| 52 |
+
return f"Timing(start_time={self.start_time}, end_time={self.end_time}, duration={self.duration})"
|
managers/execution/python_executor.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Python Executor - Persistent Python execution environment.
|
| 3 |
+
Similar to Jupyter kernel or smolagents LocalPythonExecutor.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import io
|
| 7 |
+
import sys
|
| 8 |
+
from typing import Any, Dict
|
| 9 |
+
|
| 10 |
+
__all__ = ["PythonExecutor"]
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class PythonExecutor:
|
| 14 |
+
"""
|
| 15 |
+
Python execution environment similar to smolagents LocalPythonExecutor.
|
| 16 |
+
Maintains persistent state and tool namespace across executions.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
def __init__(self):
|
| 20 |
+
self.namespace = {}
|
| 21 |
+
self.reset_environment()
|
| 22 |
+
|
| 23 |
+
def reset_environment(self):
|
| 24 |
+
"""Reset the execution environment."""
|
| 25 |
+
# Start with basic Python builtins
|
| 26 |
+
self.namespace = {
|
| 27 |
+
"__builtins__": __builtins__,
|
| 28 |
+
}
|
| 29 |
+
# Add print output capture
|
| 30 |
+
self.namespace["_print_outputs"] = []
|
| 31 |
+
|
| 32 |
+
# Override print to capture outputs
|
| 33 |
+
def captured_print(*args, **kwargs):
|
| 34 |
+
output = " ".join(str(arg) for arg in args)
|
| 35 |
+
self.namespace["_print_outputs"].append(output)
|
| 36 |
+
print(*args, **kwargs) # Also print to console
|
| 37 |
+
|
| 38 |
+
self.namespace["print"] = captured_print
|
| 39 |
+
|
| 40 |
+
# Pre-import commonly used libraries
|
| 41 |
+
try:
|
| 42 |
+
import pandas as pd
|
| 43 |
+
import numpy as np
|
| 44 |
+
import os
|
| 45 |
+
from pathlib import Path
|
| 46 |
+
self.namespace["pd"] = pd
|
| 47 |
+
self.namespace["np"] = np
|
| 48 |
+
self.namespace["os"] = os
|
| 49 |
+
self.namespace["Path"] = Path
|
| 50 |
+
|
| 51 |
+
# Add file I/O helper functions
|
| 52 |
+
def write_text_file(filepath, content):
|
| 53 |
+
"""Write text content to a file."""
|
| 54 |
+
with open(filepath, 'w') as f:
|
| 55 |
+
f.write(content)
|
| 56 |
+
return f"File written to {filepath}"
|
| 57 |
+
|
| 58 |
+
def write_dataframe_to_csv(df, filepath, **kwargs):
|
| 59 |
+
"""Write pandas DataFrame to CSV file."""
|
| 60 |
+
df.to_csv(filepath, **kwargs)
|
| 61 |
+
return f"DataFrame written to {filepath}"
|
| 62 |
+
|
| 63 |
+
def write_dataframe_to_tsv(df, filepath, **kwargs):
|
| 64 |
+
"""Write pandas DataFrame to TSV file."""
|
| 65 |
+
df.to_csv(filepath, sep='\t', **kwargs)
|
| 66 |
+
return f"DataFrame written to {filepath}"
|
| 67 |
+
|
| 68 |
+
def create_directory(dirpath):
|
| 69 |
+
"""Create directory if it doesn't exist."""
|
| 70 |
+
Path(dirpath).mkdir(parents=True, exist_ok=True)
|
| 71 |
+
return f"Directory created: {dirpath}"
|
| 72 |
+
|
| 73 |
+
self.namespace["write_text_file"] = write_text_file
|
| 74 |
+
self.namespace["write_dataframe_to_csv"] = write_dataframe_to_csv
|
| 75 |
+
self.namespace["write_dataframe_to_tsv"] = write_dataframe_to_tsv
|
| 76 |
+
self.namespace["create_directory"] = create_directory
|
| 77 |
+
|
| 78 |
+
except ImportError as e:
|
| 79 |
+
print(f"Warning: Could not import library: {e}")
|
| 80 |
+
|
| 81 |
+
def send_functions(self, functions: Dict[str, Any]):
|
| 82 |
+
"""Inject functions into the execution namespace."""
|
| 83 |
+
for func_name, func in functions.items():
|
| 84 |
+
self.namespace[func_name] = func
|
| 85 |
+
|
| 86 |
+
def send_tools(self, tools: Dict[str, Any]):
|
| 87 |
+
"""Legacy method for backward compatibility - redirects to send_functions."""
|
| 88 |
+
self.send_functions(tools)
|
| 89 |
+
|
| 90 |
+
def send_variables(self, variables: Dict[str, Any]):
|
| 91 |
+
"""Inject variables into the execution namespace."""
|
| 92 |
+
if variables:
|
| 93 |
+
print(f"📝 Injecting {len(variables)} variables into Python executor...")
|
| 94 |
+
self.namespace.update(variables)
|
| 95 |
+
|
| 96 |
+
def __call__(self, code: str) -> Any:
|
| 97 |
+
"""Execute code in the persistent namespace."""
|
| 98 |
+
return self.execute(code)
|
| 99 |
+
|
| 100 |
+
def execute(self, code: str) -> Any:
|
| 101 |
+
"""
|
| 102 |
+
Execute Python code in the persistent environment.
|
| 103 |
+
Similar to smolagents python execution.
|
| 104 |
+
"""
|
| 105 |
+
try:
|
| 106 |
+
# Clear previous print outputs
|
| 107 |
+
self.namespace["_print_outputs"] = []
|
| 108 |
+
|
| 109 |
+
# Capture stdout
|
| 110 |
+
old_stdout = sys.stdout
|
| 111 |
+
sys.stdout = captured_output = io.StringIO()
|
| 112 |
+
|
| 113 |
+
try:
|
| 114 |
+
# Execute the code in our persistent namespace
|
| 115 |
+
exec(code, self.namespace)
|
| 116 |
+
|
| 117 |
+
# Get any stdout output
|
| 118 |
+
stdout_output = captured_output.getvalue()
|
| 119 |
+
|
| 120 |
+
# Get print outputs from our custom print function
|
| 121 |
+
print_outputs = self.namespace.get("_print_outputs", [])
|
| 122 |
+
|
| 123 |
+
# Combine outputs
|
| 124 |
+
all_outputs = []
|
| 125 |
+
if stdout_output.strip():
|
| 126 |
+
all_outputs.append(stdout_output.strip())
|
| 127 |
+
if print_outputs:
|
| 128 |
+
all_outputs.extend(print_outputs)
|
| 129 |
+
|
| 130 |
+
result = "\n".join(all_outputs) if all_outputs else "Code executed successfully"
|
| 131 |
+
|
| 132 |
+
return result
|
| 133 |
+
|
| 134 |
+
finally:
|
| 135 |
+
sys.stdout = old_stdout
|
| 136 |
+
|
| 137 |
+
except Exception as e:
|
| 138 |
+
return f"Error: {str(e)}"
|
managers/support/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Support subsystem - Console display and package management.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from .console_display import ConsoleDisplay
|
| 6 |
+
from .package_manager import PackageManager
|
| 7 |
+
|
| 8 |
+
__all__ = ['ConsoleDisplay', 'PackageManager']
|
managers/support/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (446 Bytes). View file
|
|
|
managers/support/__pycache__/console_display.cpython-311.pyc
ADDED
|
Binary file (9.14 kB). View file
|
|
|
managers/support/__pycache__/package_manager.cpython-311.pyc
ADDED
|
Binary file (3.76 kB). View file
|
|
|
managers/support/console_display.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Console Display Manager for CodeAct Agent.
|
| 3 |
+
Handles all console output and display formatting.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from typing import List, Dict
|
| 7 |
+
from rich import box
|
| 8 |
+
from rich.console import Console
|
| 9 |
+
from rich.panel import Panel
|
| 10 |
+
from rich.rule import Rule
|
| 11 |
+
from rich.syntax import Syntax
|
| 12 |
+
from rich.table import Table
|
| 13 |
+
from rich.text import Text
|
| 14 |
+
from ..workflow.plan_manager import PlanManager
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class ConsoleDisplay:
|
| 18 |
+
"""Handles all console output and display formatting."""
|
| 19 |
+
|
| 20 |
+
def __init__(self):
|
| 21 |
+
self.console = Console()
|
| 22 |
+
|
| 23 |
+
def print_task_header(self, query: str):
|
| 24 |
+
"""Print the task header panel."""
|
| 25 |
+
self.console.print(
|
| 26 |
+
Panel(
|
| 27 |
+
f"\n[bold]{query}\n",
|
| 28 |
+
title="[bold]Code Agent - New Run",
|
| 29 |
+
subtitle="Starting agent execution...",
|
| 30 |
+
border_style="yellow",
|
| 31 |
+
subtitle_align="left",
|
| 32 |
+
)
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
def print_tools_info(self, total_tools: int, regular_tools: List[str], mcp_tools: Dict):
|
| 36 |
+
"""Print information about loaded tools."""
|
| 37 |
+
self.console.print(f"🛠️ Loaded {total_tools} total functions:")
|
| 38 |
+
if regular_tools:
|
| 39 |
+
self.console.print(f" 📋 Regular tools ({len(regular_tools)}): {regular_tools}")
|
| 40 |
+
|
| 41 |
+
if mcp_tools:
|
| 42 |
+
self.console.print(f" 🔗 MCP tools ({len(mcp_tools)}):")
|
| 43 |
+
for tool_name, tool_info in mcp_tools.items():
|
| 44 |
+
server_name = tool_info.get('server', 'unknown')
|
| 45 |
+
self.console.print(f" • {tool_name} (from {server_name} server)")
|
| 46 |
+
|
| 47 |
+
def print_packages_info(self, imported_packages: List[str], failed_packages: List[tuple]):
|
| 48 |
+
"""Print information about imported packages."""
|
| 49 |
+
if imported_packages:
|
| 50 |
+
self.console.print(f"📦 Imported {len(imported_packages)} packages: {imported_packages}")
|
| 51 |
+
|
| 52 |
+
for package_name, error in failed_packages:
|
| 53 |
+
self.console.print(f"⚠️ Failed to import {package_name}: {error}")
|
| 54 |
+
|
| 55 |
+
def print_reasoning(self, content: str, step_count: int):
|
| 56 |
+
"""Print agent reasoning panel."""
|
| 57 |
+
self.console.print(
|
| 58 |
+
Panel(
|
| 59 |
+
Text(content, style="italic"),
|
| 60 |
+
title=f"[bold]🧠 Agent Reasoning - Step {step_count}",
|
| 61 |
+
border_style="yellow",
|
| 62 |
+
box=box.SIMPLE,
|
| 63 |
+
)
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
def print_plan(self, plan: str):
|
| 67 |
+
"""Print current plan panel with progress statistics."""
|
| 68 |
+
# Get plan progress statistics
|
| 69 |
+
progress = PlanManager.get_plan_progress(plan)
|
| 70 |
+
progress_text = f"({progress['completed']}/{progress['total']} completed)"
|
| 71 |
+
|
| 72 |
+
self.console.print(
|
| 73 |
+
Panel(
|
| 74 |
+
Text(plan, style="cyan"),
|
| 75 |
+
title=f"[bold]📋 Current Plan {progress_text}",
|
| 76 |
+
border_style="cyan",
|
| 77 |
+
box=box.SIMPLE,
|
| 78 |
+
)
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
def print_code_execution(self, code: str, step_count: int):
|
| 82 |
+
"""Print code execution panel."""
|
| 83 |
+
self.console.print(f"⚡ Step {step_count}: Executing code...")
|
| 84 |
+
self.console.print(
|
| 85 |
+
Panel(
|
| 86 |
+
Syntax(code, "python", theme="monokai", word_wrap=True),
|
| 87 |
+
title=f"[bold]💻 Code Execution - Step {step_count}",
|
| 88 |
+
title_align="left",
|
| 89 |
+
box=box.HORIZONTALS,
|
| 90 |
+
)
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
def print_solution(self, solution: str, step_count: int):
|
| 94 |
+
"""Print final solution panel."""
|
| 95 |
+
self.console.print(f"🎯 Step {step_count}: Providing final solution...")
|
| 96 |
+
self.console.print(
|
| 97 |
+
Panel(
|
| 98 |
+
Text(solution, style="bold green"),
|
| 99 |
+
title=f"[bold]✅ Final Solution - Step {step_count}",
|
| 100 |
+
border_style="green",
|
| 101 |
+
)
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
def print_error(self, error: str, step_count: int):
|
| 105 |
+
"""Print error panel."""
|
| 106 |
+
self.console.print(f"❌ Step {step_count}: Error encountered...")
|
| 107 |
+
self.console.print(
|
| 108 |
+
Panel(
|
| 109 |
+
Text(error, style="bold red"),
|
| 110 |
+
title=f"[bold]⚠️ Error - Step {step_count}",
|
| 111 |
+
border_style="red",
|
| 112 |
+
)
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
def print_execution_result(self, result: str, step_count: int):
|
| 116 |
+
"""Print execution result panel."""
|
| 117 |
+
self.console.print(
|
| 118 |
+
Panel(
|
| 119 |
+
Syntax(result, "text", theme="github-dark", word_wrap=True),
|
| 120 |
+
title=f"[bold]📊 Execution Result - Step {step_count}",
|
| 121 |
+
border_style="cyan",
|
| 122 |
+
title_align="left",
|
| 123 |
+
)
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
def print_execution_summary(self, step_count: int, error_count: int, duration: float):
|
| 127 |
+
"""Print execution summary table."""
|
| 128 |
+
self.console.print(Rule(title="Execution Summary", style="yellow"))
|
| 129 |
+
|
| 130 |
+
summary_table = Table(show_header=False, box=box.SIMPLE)
|
| 131 |
+
summary_table.add_column("Metric", style="cyan")
|
| 132 |
+
summary_table.add_column("Value", style="bold")
|
| 133 |
+
|
| 134 |
+
summary_table.add_row("✅ Total steps", str(step_count))
|
| 135 |
+
summary_table.add_row("⚠️ Total errors", str(error_count))
|
| 136 |
+
summary_table.add_row("⏱️ Total execution time", f"{duration:.2f} seconds")
|
| 137 |
+
if step_count > 0:
|
| 138 |
+
summary_table.add_row("📊 Average time per step", f"{duration/step_count:.2f}s")
|
| 139 |
+
|
| 140 |
+
self.console.print(summary_table)
|
managers/support/package_manager.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Package Manager for CodeAct Agent.
|
| 3 |
+
Handles Python package management and imports.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from typing import Dict
|
| 7 |
+
from core.constants import LIBRARY_CONTENT_DICT
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class PackageManager:
|
| 11 |
+
"""Manages Python packages available to the agent."""
|
| 12 |
+
|
| 13 |
+
def __init__(self, default_packages: Dict[str, str] = None):
|
| 14 |
+
self.default_packages = default_packages or LIBRARY_CONTENT_DICT.copy()
|
| 15 |
+
self.custom_packages = {}
|
| 16 |
+
|
| 17 |
+
def add_packages(self, packages: Dict[str, str]) -> bool:
|
| 18 |
+
"""Add new packages to the available packages."""
|
| 19 |
+
try:
|
| 20 |
+
if not isinstance(packages, dict):
|
| 21 |
+
raise ValueError("Packages must be a dictionary with package name as key and description as value")
|
| 22 |
+
|
| 23 |
+
for package_name, description in packages.items():
|
| 24 |
+
if not isinstance(package_name, str) or not isinstance(description, str):
|
| 25 |
+
print("Warning: Skipping invalid package entry - package_name and description must be strings")
|
| 26 |
+
continue
|
| 27 |
+
|
| 28 |
+
self.custom_packages[package_name] = description
|
| 29 |
+
print(f"Added package '{package_name}': {description}")
|
| 30 |
+
|
| 31 |
+
print(f"Successfully added {len(packages)} package(s) to the library")
|
| 32 |
+
return True
|
| 33 |
+
|
| 34 |
+
except Exception as e:
|
| 35 |
+
print(f"Error adding packages: {e}")
|
| 36 |
+
return False
|
| 37 |
+
|
| 38 |
+
def get_all_packages(self) -> Dict[str, str]:
|
| 39 |
+
"""Get all available packages (default + custom)."""
|
| 40 |
+
all_packages = self.default_packages.copy()
|
| 41 |
+
all_packages.update(self.custom_packages)
|
| 42 |
+
return all_packages
|
| 43 |
+
|
| 44 |
+
def import_packages(self, python_executor) -> tuple:
|
| 45 |
+
"""Import all packages into the executor environment."""
|
| 46 |
+
all_packages = self.get_all_packages()
|
| 47 |
+
imported_packages = []
|
| 48 |
+
failed_packages = []
|
| 49 |
+
|
| 50 |
+
for package_name in all_packages.keys():
|
| 51 |
+
try:
|
| 52 |
+
import_code = f"import {package_name}"
|
| 53 |
+
python_executor(import_code)
|
| 54 |
+
imported_packages.append(package_name)
|
| 55 |
+
except Exception as e:
|
| 56 |
+
failed_packages.append((package_name, str(e)))
|
| 57 |
+
|
| 58 |
+
return imported_packages, failed_packages
|
managers/tools/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tools subsystem - Tool management, registry, and integrations.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from .tool_manager import ToolManager, ToolSource, ToolInfo
|
| 6 |
+
from .tool_registry import ToolRegistry, tool, discover_tools_in_module, create_module2api_from_functions
|
| 7 |
+
from .tool_selector import ToolSelector
|
| 8 |
+
from .mcp_manager import MCPManager
|
| 9 |
+
from .builtin_tools import FUNCTION_REGISTRY, get_all_tool_functions
|
| 10 |
+
|
| 11 |
+
__all__ = [
|
| 12 |
+
'ToolManager',
|
| 13 |
+
'ToolSource',
|
| 14 |
+
'ToolInfo',
|
| 15 |
+
'ToolRegistry',
|
| 16 |
+
'tool',
|
| 17 |
+
'discover_tools_in_module',
|
| 18 |
+
'create_module2api_from_functions',
|
| 19 |
+
'ToolSelector',
|
| 20 |
+
'MCPManager',
|
| 21 |
+
'FUNCTION_REGISTRY',
|
| 22 |
+
'get_all_tool_functions'
|
| 23 |
+
]
|
managers/tools/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (891 Bytes). View file
|
|
|
managers/tools/__pycache__/builtin_tools.cpython-311.pyc
ADDED
|
Binary file (1.24 kB). View file
|
|
|
managers/tools/__pycache__/mcp_manager.cpython-311.pyc
ADDED
|
Binary file (31 kB). View file
|
|
|
managers/tools/__pycache__/tool_manager.cpython-311.pyc
ADDED
|
Binary file (22.1 kB). View file
|
|
|
managers/tools/__pycache__/tool_registry.cpython-311.pyc
ADDED
|
Binary file (16.3 kB). View file
|
|
|
managers/tools/__pycache__/tool_selector.cpython-311.pyc
ADDED
|
Binary file (5.89 kB). View file
|
|
|
managers/tools/builtin_tools.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Built-in tool functions for CodeAct agent.
|
| 3 |
+
Example tools that demonstrate the tool system.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from typing import Any, List
|
| 7 |
+
|
| 8 |
+
__all__ = ["FUNCTION_REGISTRY", "get_all_tool_functions",
|
| 9 |
+
"add_numbers", "multiply_numbers"]
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def add_numbers(a: int, b: int) -> int:
|
| 13 |
+
"""Add two numbers together."""
|
| 14 |
+
return a + b
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def multiply_numbers(a: int, b: int) -> int:
|
| 18 |
+
"""Multiply two numbers together."""
|
| 19 |
+
return a * b
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# ====================
|
| 23 |
+
# FUNCTION REGISTRY
|
| 24 |
+
# ====================
|
| 25 |
+
|
| 26 |
+
FUNCTION_REGISTRY = {
|
| 27 |
+
"add_numbers": add_numbers,
|
| 28 |
+
"multiply_numbers": multiply_numbers,
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def get_all_tool_functions() -> List[Any]:
|
| 33 |
+
"""Get all functions from FUNCTION_REGISTRY."""
|
| 34 |
+
return list(FUNCTION_REGISTRY.values())
|
managers/tools/mcp_manager.py
ADDED
|
@@ -0,0 +1,588 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MCP Manager for CodeAct Agent.
|
| 3 |
+
Manages MCP (Model Context Protocol) tools and servers.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
import types
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from typing import Dict, List, Optional, Any
|
| 11 |
+
from rich.console import Console
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class MCPManager:
|
| 15 |
+
"""Manages MCP (Model Context Protocol) tools and servers."""
|
| 16 |
+
|
| 17 |
+
def __init__(self, console_display=None):
|
| 18 |
+
self.mcp_functions = {}
|
| 19 |
+
self.console = console_display.console if console_display else Console()
|
| 20 |
+
|
| 21 |
+
def has_mcp_functions(self) -> bool:
|
| 22 |
+
"""Check if MCP functions are available."""
|
| 23 |
+
return bool(self.mcp_functions)
|
| 24 |
+
|
| 25 |
+
def group_tools_by_server(self, mcp_tools: Dict[str, dict]) -> Dict[str, List[Dict]]:
|
| 26 |
+
"""Group MCP tools by server name."""
|
| 27 |
+
servers = {}
|
| 28 |
+
for tool_name, tool_info in mcp_tools.items():
|
| 29 |
+
server_name = tool_info.get('server', 'unknown')
|
| 30 |
+
if server_name not in servers:
|
| 31 |
+
servers[server_name] = []
|
| 32 |
+
servers[server_name].append({
|
| 33 |
+
'name': tool_name,
|
| 34 |
+
'description': tool_info.get('description', 'No description')
|
| 35 |
+
})
|
| 36 |
+
return servers
|
| 37 |
+
|
| 38 |
+
def add_mcp(self, config_path: str = "./mcp_config.yaml", tool_registry=None) -> None:
|
| 39 |
+
"""Add MCP tools from configuration file."""
|
| 40 |
+
try:
|
| 41 |
+
import asyncio
|
| 42 |
+
import yaml
|
| 43 |
+
except ImportError as e:
|
| 44 |
+
raise ImportError(f"Required packages not available: {e}. Install with: pip install pyyaml") from e
|
| 45 |
+
|
| 46 |
+
try:
|
| 47 |
+
import nest_asyncio
|
| 48 |
+
from mcp import ClientSession
|
| 49 |
+
from mcp.client.stdio import StdioServerParameters, stdio_client
|
| 50 |
+
from mcp.client.streamable_http import streamablehttp_client
|
| 51 |
+
from langchain_mcp_adapters.tools import _list_all_tools
|
| 52 |
+
nest_asyncio.apply()
|
| 53 |
+
except ImportError as e:
|
| 54 |
+
raise ImportError(f"MCP packages not available: {e}. Install with: pip install mcp langchain-mcp-adapters") from e
|
| 55 |
+
|
| 56 |
+
def discover_mcp_tools_sync(server_params: StdioServerParameters) -> List[dict]:
|
| 57 |
+
"""Discover available tools from MCP server synchronously."""
|
| 58 |
+
try:
|
| 59 |
+
async def _discover_async():
|
| 60 |
+
async with stdio_client(server_params) as (reader, writer):
|
| 61 |
+
async with ClientSession(reader, writer) as session:
|
| 62 |
+
await session.initialize()
|
| 63 |
+
|
| 64 |
+
tools_result = await session.list_tools()
|
| 65 |
+
tools = tools_result.tools if hasattr(tools_result, "tools") else tools_result
|
| 66 |
+
print(tools)
|
| 67 |
+
|
| 68 |
+
discovered_tools = []
|
| 69 |
+
for tool in tools:
|
| 70 |
+
if hasattr(tool, "name"):
|
| 71 |
+
# Ensure description is never empty or None
|
| 72 |
+
description = getattr(tool, 'description', None)
|
| 73 |
+
if not description or description.strip() == "":
|
| 74 |
+
# Generate description from tool name
|
| 75 |
+
formatted_name = tool.name.replace('_', ' ').title()
|
| 76 |
+
description = f"MCP tool: {formatted_name}"
|
| 77 |
+
|
| 78 |
+
discovered_tools.append({
|
| 79 |
+
"name": tool.name,
|
| 80 |
+
"description": description,
|
| 81 |
+
"inputSchema": tool.inputSchema,
|
| 82 |
+
})
|
| 83 |
+
else:
|
| 84 |
+
print(f"Warning: Skipping tool with no name attribute: {tool}")
|
| 85 |
+
|
| 86 |
+
return discovered_tools
|
| 87 |
+
|
| 88 |
+
return asyncio.run(_discover_async())
|
| 89 |
+
except Exception as e:
|
| 90 |
+
print(f"Failed to discover tools: {e}")
|
| 91 |
+
return []
|
| 92 |
+
|
| 93 |
+
def discover_remote_mcp_tools_sync(url: str) -> List[dict]:
|
| 94 |
+
"""Discover available tools from remote MCP server synchronously."""
|
| 95 |
+
try:
|
| 96 |
+
async def _discover_remote_async():
|
| 97 |
+
async with streamablehttp_client(url) as (read, write, _):
|
| 98 |
+
async with ClientSession(read, write) as session:
|
| 99 |
+
await session.initialize()
|
| 100 |
+
tools = await _list_all_tools(session)
|
| 101 |
+
|
| 102 |
+
discovered_tools = []
|
| 103 |
+
for tool in tools:
|
| 104 |
+
if hasattr(tool, "name"):
|
| 105 |
+
# Ensure description is never empty or None
|
| 106 |
+
description = getattr(tool, 'description', None)
|
| 107 |
+
if not description or description.strip() == "":
|
| 108 |
+
# Generate description from tool name
|
| 109 |
+
formatted_name = tool.name.replace('_', ' ').title()
|
| 110 |
+
description = f"MCP tool: {formatted_name}"
|
| 111 |
+
|
| 112 |
+
discovered_tools.append({
|
| 113 |
+
"name": tool.name,
|
| 114 |
+
"description": description,
|
| 115 |
+
"inputSchema": tool.inputSchema,
|
| 116 |
+
})
|
| 117 |
+
else:
|
| 118 |
+
print(f"Warning: Skipping tool with no name attribute: {tool}")
|
| 119 |
+
|
| 120 |
+
return discovered_tools
|
| 121 |
+
|
| 122 |
+
return asyncio.run(_discover_remote_async())
|
| 123 |
+
except Exception as e:
|
| 124 |
+
print(f"Failed to discover remote tools from {url}: {e}")
|
| 125 |
+
return []
|
| 126 |
+
|
| 127 |
+
def make_mcp_wrapper(cmd: str, args: List[str], tool_name: str, doc: str, env_vars: dict = None):
|
| 128 |
+
"""Create a synchronous wrapper for an async MCP tool call."""
|
| 129 |
+
|
| 130 |
+
def sync_tool_wrapper(**kwargs):
|
| 131 |
+
"""Synchronous wrapper for MCP tool execution."""
|
| 132 |
+
try:
|
| 133 |
+
server_params = StdioServerParameters(command=cmd, args=args, env=env_vars)
|
| 134 |
+
|
| 135 |
+
async def async_tool_call():
|
| 136 |
+
async with stdio_client(server_params) as (reader, writer):
|
| 137 |
+
async with ClientSession(reader, writer) as session:
|
| 138 |
+
await session.initialize()
|
| 139 |
+
result = await session.call_tool(tool_name, kwargs)
|
| 140 |
+
content = result.content[0]
|
| 141 |
+
if hasattr(content, "model_dump_json"):
|
| 142 |
+
return content.model_dump_json()
|
| 143 |
+
elif hasattr(content, "json"):
|
| 144 |
+
return content.json()
|
| 145 |
+
return content.text
|
| 146 |
+
|
| 147 |
+
try:
|
| 148 |
+
loop = asyncio.get_running_loop()
|
| 149 |
+
return loop.create_task(async_tool_call())
|
| 150 |
+
except RuntimeError:
|
| 151 |
+
return asyncio.run(async_tool_call())
|
| 152 |
+
|
| 153 |
+
except Exception as e:
|
| 154 |
+
raise RuntimeError(f"MCP tool execution failed for '{tool_name}': {e}") from e
|
| 155 |
+
|
| 156 |
+
sync_tool_wrapper.__name__ = tool_name
|
| 157 |
+
sync_tool_wrapper.__doc__ = doc
|
| 158 |
+
return sync_tool_wrapper
|
| 159 |
+
|
| 160 |
+
def make_remote_mcp_wrapper(url: str, tool_name: str, doc: str):
|
| 161 |
+
"""Create a synchronous wrapper for an async remote MCP tool call."""
|
| 162 |
+
|
| 163 |
+
def sync_tool_wrapper(**kwargs):
|
| 164 |
+
"""Synchronous wrapper for remote MCP tool execution."""
|
| 165 |
+
try:
|
| 166 |
+
async def async_tool_call():
|
| 167 |
+
async with streamablehttp_client(url) as (read, write, _):
|
| 168 |
+
async with ClientSession(read, write) as session:
|
| 169 |
+
await session.initialize()
|
| 170 |
+
result = await session.call_tool(tool_name, kwargs)
|
| 171 |
+
content = result.content[0]
|
| 172 |
+
if hasattr(content, "model_dump_json"):
|
| 173 |
+
return content.model_dump_json()
|
| 174 |
+
elif hasattr(content, "json"):
|
| 175 |
+
return content.json()
|
| 176 |
+
return content.text
|
| 177 |
+
|
| 178 |
+
try:
|
| 179 |
+
loop = asyncio.get_running_loop()
|
| 180 |
+
return loop.create_task(async_tool_call())
|
| 181 |
+
except RuntimeError:
|
| 182 |
+
return asyncio.run(async_tool_call())
|
| 183 |
+
|
| 184 |
+
except Exception as e:
|
| 185 |
+
raise RuntimeError(f"Remote MCP tool execution failed for '{tool_name}': {e}") from e
|
| 186 |
+
|
| 187 |
+
sync_tool_wrapper.__name__ = tool_name
|
| 188 |
+
sync_tool_wrapper.__doc__ = doc
|
| 189 |
+
return sync_tool_wrapper
|
| 190 |
+
|
| 191 |
+
# Load and validate configuration
|
| 192 |
+
try:
|
| 193 |
+
config_content = Path(config_path).read_text(encoding="utf-8")
|
| 194 |
+
cfg = yaml.safe_load(config_content) or {}
|
| 195 |
+
except FileNotFoundError:
|
| 196 |
+
raise FileNotFoundError(f"MCP config file not found: {config_path}") from None
|
| 197 |
+
except yaml.YAMLError as e:
|
| 198 |
+
raise yaml.YAMLError(f"Invalid YAML in MCP config: {e}") from e
|
| 199 |
+
|
| 200 |
+
mcp_servers = cfg.get("mcp_servers", {})
|
| 201 |
+
if not mcp_servers:
|
| 202 |
+
print("Warning: No MCP servers found in configuration")
|
| 203 |
+
return
|
| 204 |
+
|
| 205 |
+
# Process each MCP server configuration
|
| 206 |
+
for server_name, server_meta in mcp_servers.items():
|
| 207 |
+
if not server_meta.get("enabled", True):
|
| 208 |
+
continue
|
| 209 |
+
|
| 210 |
+
# Check if this is a remote server configuration
|
| 211 |
+
remote_url = server_meta.get("url")
|
| 212 |
+
if remote_url:
|
| 213 |
+
# Handle remote MCP server
|
| 214 |
+
self._process_remote_server(server_name, server_meta, remote_url, tool_registry, discover_remote_mcp_tools_sync, make_remote_mcp_wrapper)
|
| 215 |
+
continue
|
| 216 |
+
|
| 217 |
+
# Handle local MCP server (existing logic)
|
| 218 |
+
# Validate command configuration
|
| 219 |
+
cmd_list = server_meta.get("command", [])
|
| 220 |
+
if not cmd_list or not isinstance(cmd_list, list):
|
| 221 |
+
print(f"Warning: Invalid command configuration for server '{server_name}'")
|
| 222 |
+
continue
|
| 223 |
+
|
| 224 |
+
cmd, *args = cmd_list
|
| 225 |
+
|
| 226 |
+
# Process environment variables
|
| 227 |
+
env_vars = server_meta.get("env", {})
|
| 228 |
+
if env_vars:
|
| 229 |
+
processed_env = {}
|
| 230 |
+
for key, value in env_vars.items():
|
| 231 |
+
if isinstance(value, str) and value.startswith("${") and value.endswith("}"):
|
| 232 |
+
var_name = value[2:-1]
|
| 233 |
+
processed_env[key] = os.getenv(var_name, "")
|
| 234 |
+
else:
|
| 235 |
+
processed_env[key] = value
|
| 236 |
+
env_vars = processed_env
|
| 237 |
+
|
| 238 |
+
# Create module namespace for this MCP server
|
| 239 |
+
mcp_module_name = f"mcp_servers.{server_name}"
|
| 240 |
+
if mcp_module_name not in sys.modules:
|
| 241 |
+
sys.modules[mcp_module_name] = types.ModuleType(mcp_module_name)
|
| 242 |
+
server_module = sys.modules[mcp_module_name]
|
| 243 |
+
|
| 244 |
+
tools_config = server_meta.get("tools", [])
|
| 245 |
+
|
| 246 |
+
# Auto-discover tools if not manually configured
|
| 247 |
+
if not tools_config:
|
| 248 |
+
try:
|
| 249 |
+
server_params = StdioServerParameters(command=cmd, args=args, env=env_vars)
|
| 250 |
+
tools_config = discover_mcp_tools_sync(server_params)
|
| 251 |
+
|
| 252 |
+
if tools_config:
|
| 253 |
+
print(f"🔍 Discovered {len(tools_config)} tools from {server_name} MCP server")
|
| 254 |
+
else:
|
| 255 |
+
print(f"Warning: No tools discovered from {server_name} MCP server")
|
| 256 |
+
continue
|
| 257 |
+
|
| 258 |
+
except Exception as e:
|
| 259 |
+
print(f"Failed to discover tools for {server_name}: {e}")
|
| 260 |
+
continue
|
| 261 |
+
|
| 262 |
+
# Register each tool
|
| 263 |
+
tools_added = 0
|
| 264 |
+
for tool_meta in tools_config:
|
| 265 |
+
if isinstance(tool_meta, dict) and "biomni_name" in tool_meta:
|
| 266 |
+
# Manual tool definition (Biomni-style)
|
| 267 |
+
tool_name = tool_meta.get("biomni_name")
|
| 268 |
+
description = tool_meta.get("description", f"MCP tool: {tool_name}")
|
| 269 |
+
parameters = tool_meta.get("parameters", {})
|
| 270 |
+
required_param_names = []
|
| 271 |
+
for param_name, param_spec in parameters.items():
|
| 272 |
+
if param_spec.get("required", False):
|
| 273 |
+
required_param_names.append(param_name)
|
| 274 |
+
else:
|
| 275 |
+
# Auto-discovered tool
|
| 276 |
+
tool_name = tool_meta.get("name")
|
| 277 |
+
description = tool_meta.get("description", "")
|
| 278 |
+
|
| 279 |
+
# Ensure description is never empty
|
| 280 |
+
if not description or description.strip() == "":
|
| 281 |
+
formatted_name = tool_name.replace('_', ' ').title()
|
| 282 |
+
description = f"MCP tool: {formatted_name}"
|
| 283 |
+
|
| 284 |
+
input_schema = tool_meta.get("inputSchema", {})
|
| 285 |
+
parameters = input_schema.get("properties", {})
|
| 286 |
+
required_param_names = input_schema.get("required", [])
|
| 287 |
+
|
| 288 |
+
if not tool_name:
|
| 289 |
+
print(f"Warning: Skipping tool with no name in {server_name}")
|
| 290 |
+
continue
|
| 291 |
+
|
| 292 |
+
# Create wrapper function
|
| 293 |
+
wrapper_function = make_mcp_wrapper(cmd, args, tool_name, description, env_vars)
|
| 294 |
+
|
| 295 |
+
# Add to module namespace
|
| 296 |
+
setattr(server_module, tool_name, wrapper_function)
|
| 297 |
+
|
| 298 |
+
# Store in MCP functions registry with parameter information
|
| 299 |
+
self.mcp_functions[tool_name] = {
|
| 300 |
+
"function": wrapper_function,
|
| 301 |
+
"server": server_name,
|
| 302 |
+
"module": mcp_module_name,
|
| 303 |
+
"description": description,
|
| 304 |
+
"required_parameters": [], # Will be populated below
|
| 305 |
+
"optional_parameters": [] # Will be populated below
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
# Register with tool registry if available
|
| 309 |
+
if tool_registry:
|
| 310 |
+
from .tool_registry import ToolRegistry
|
| 311 |
+
# Create tool schema with proper parameter information
|
| 312 |
+
required_params = []
|
| 313 |
+
optional_params = []
|
| 314 |
+
|
| 315 |
+
for param_name, param_spec in parameters.items():
|
| 316 |
+
param_info = {
|
| 317 |
+
"name": param_name,
|
| 318 |
+
"type": param_spec.get("type", "string"),
|
| 319 |
+
"description": param_spec.get("description", f"Parameter {param_name}"),
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
# Extract enum/literal values if present
|
| 323 |
+
if "enum" in param_spec:
|
| 324 |
+
param_info["enum"] = param_spec["enum"]
|
| 325 |
+
|
| 326 |
+
# Handle anyOf schemas (common for optional literal types)
|
| 327 |
+
if "anyOf" in param_spec:
|
| 328 |
+
# Look for enum in anyOf schemas
|
| 329 |
+
for schema_option in param_spec["anyOf"]:
|
| 330 |
+
if "enum" in schema_option:
|
| 331 |
+
param_info["enum"] = schema_option["enum"]
|
| 332 |
+
# Update type if specified
|
| 333 |
+
if "type" in schema_option:
|
| 334 |
+
param_info["type"] = schema_option["type"]
|
| 335 |
+
break
|
| 336 |
+
|
| 337 |
+
# Handle oneOf schemas (alternative union syntax)
|
| 338 |
+
if "oneOf" in param_spec:
|
| 339 |
+
# Look for enum in oneOf schemas
|
| 340 |
+
for schema_option in param_spec["oneOf"]:
|
| 341 |
+
if "enum" in schema_option:
|
| 342 |
+
param_info["enum"] = schema_option["enum"]
|
| 343 |
+
if "type" in schema_option:
|
| 344 |
+
param_info["type"] = schema_option["type"]
|
| 345 |
+
break
|
| 346 |
+
|
| 347 |
+
# Determine if parameter is required based on:
|
| 348 |
+
# 1. Explicit required list (if provided)
|
| 349 |
+
# 2. If no default value is present in the schema
|
| 350 |
+
is_required = (param_name in required_param_names) or ("default" not in param_spec)
|
| 351 |
+
|
| 352 |
+
if is_required:
|
| 353 |
+
required_params.append(param_info)
|
| 354 |
+
else:
|
| 355 |
+
param_info["default"] = param_spec.get("default")
|
| 356 |
+
optional_params.append(param_info)
|
| 357 |
+
|
| 358 |
+
# Create complete tool schema
|
| 359 |
+
tool_schema = {
|
| 360 |
+
"name": tool_name,
|
| 361 |
+
"description": description,
|
| 362 |
+
"required_parameters": required_params,
|
| 363 |
+
"optional_parameters": optional_params,
|
| 364 |
+
"module": mcp_module_name,
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
success = tool_registry.register_tool(tool_schema, mcp_module_name)
|
| 368 |
+
if success:
|
| 369 |
+
tool_registry._name_to_function[tool_name] = wrapper_function
|
| 370 |
+
tools_added += 1
|
| 371 |
+
|
| 372 |
+
# Update MCP functions registry with parameter information
|
| 373 |
+
self.mcp_functions[tool_name]["required_parameters"] = required_params
|
| 374 |
+
self.mcp_functions[tool_name]["optional_parameters"] = optional_params
|
| 375 |
+
|
| 376 |
+
if tools_added > 0:
|
| 377 |
+
print(f"✅ Added {tools_added} MCP tools from {server_name} server")
|
| 378 |
+
|
| 379 |
+
print(f"🛠️ Total MCP tools loaded: {len(self.mcp_functions)}")
|
| 380 |
+
|
| 381 |
+
def _process_remote_server(self, server_name: str, server_meta: dict, remote_url: str, tool_registry, discover_remote_mcp_tools_sync, make_remote_mcp_wrapper):
|
| 382 |
+
"""Process a remote MCP server configuration."""
|
| 383 |
+
import sys
|
| 384 |
+
import types
|
| 385 |
+
|
| 386 |
+
# Create module namespace for this remote MCP server
|
| 387 |
+
mcp_module_name = f"mcp_servers.{server_name}"
|
| 388 |
+
if mcp_module_name not in sys.modules:
|
| 389 |
+
sys.modules[mcp_module_name] = types.ModuleType(mcp_module_name)
|
| 390 |
+
server_module = sys.modules[mcp_module_name]
|
| 391 |
+
|
| 392 |
+
tools_config = server_meta.get("tools", [])
|
| 393 |
+
|
| 394 |
+
# Auto-discover tools if not manually configured
|
| 395 |
+
if not tools_config:
|
| 396 |
+
try:
|
| 397 |
+
tools_config = discover_remote_mcp_tools_sync(remote_url)
|
| 398 |
+
|
| 399 |
+
if tools_config:
|
| 400 |
+
print(f"🔍 Discovered {len(tools_config)} tools from {server_name} remote MCP server")
|
| 401 |
+
else:
|
| 402 |
+
print(f"Warning: No tools discovered from {server_name} remote MCP server")
|
| 403 |
+
return
|
| 404 |
+
|
| 405 |
+
except Exception as e:
|
| 406 |
+
print(f"Failed to discover tools for remote {server_name}: {e}")
|
| 407 |
+
return
|
| 408 |
+
|
| 409 |
+
# Register each tool
|
| 410 |
+
tools_added = 0
|
| 411 |
+
for tool_meta in tools_config:
|
| 412 |
+
if isinstance(tool_meta, dict) and "biomni_name" in tool_meta:
|
| 413 |
+
# Manual tool definition (Biomni-style)
|
| 414 |
+
tool_name = tool_meta.get("biomni_name")
|
| 415 |
+
description = tool_meta.get("description", f"Remote MCP tool: {tool_name}")
|
| 416 |
+
parameters = tool_meta.get("parameters", {})
|
| 417 |
+
required_param_names = []
|
| 418 |
+
for param_name, param_spec in parameters.items():
|
| 419 |
+
if param_spec.get("required", False):
|
| 420 |
+
required_param_names.append(param_name)
|
| 421 |
+
else:
|
| 422 |
+
# Auto-discovered tool
|
| 423 |
+
tool_name = tool_meta.get("name")
|
| 424 |
+
description = tool_meta.get("description", "")
|
| 425 |
+
|
| 426 |
+
# Ensure description is never empty
|
| 427 |
+
if not description or description.strip() == "":
|
| 428 |
+
formatted_name = tool_name.replace('_', ' ').title()
|
| 429 |
+
description = f"Remote MCP tool: {formatted_name}"
|
| 430 |
+
|
| 431 |
+
input_schema = tool_meta.get("inputSchema", {})
|
| 432 |
+
parameters = input_schema.get("properties", {})
|
| 433 |
+
required_param_names = input_schema.get("required", [])
|
| 434 |
+
|
| 435 |
+
if not tool_name:
|
| 436 |
+
print(f"Warning: Skipping tool with no name in remote {server_name}")
|
| 437 |
+
continue
|
| 438 |
+
|
| 439 |
+
# Create wrapper function for remote tool
|
| 440 |
+
wrapper_function = make_remote_mcp_wrapper(remote_url, tool_name, description)
|
| 441 |
+
|
| 442 |
+
# Add to module namespace
|
| 443 |
+
setattr(server_module, tool_name, wrapper_function)
|
| 444 |
+
|
| 445 |
+
# Store in MCP functions registry with parameter information
|
| 446 |
+
self.mcp_functions[tool_name] = {
|
| 447 |
+
"function": wrapper_function,
|
| 448 |
+
"server": server_name,
|
| 449 |
+
"module": mcp_module_name,
|
| 450 |
+
"description": description,
|
| 451 |
+
"required_parameters": [], # Will be populated below
|
| 452 |
+
"optional_parameters": [], # Will be populated below
|
| 453 |
+
"remote_url": remote_url
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
# Register with tool registry if available
|
| 457 |
+
if tool_registry:
|
| 458 |
+
from .tool_registry import ToolRegistry
|
| 459 |
+
# Create tool schema with proper parameter information
|
| 460 |
+
required_params = []
|
| 461 |
+
optional_params = []
|
| 462 |
+
|
| 463 |
+
for param_name, param_spec in parameters.items():
|
| 464 |
+
param_info = {
|
| 465 |
+
"name": param_name,
|
| 466 |
+
"type": param_spec.get("type", "string"),
|
| 467 |
+
"description": param_spec.get("description", f"Parameter {param_name}"),
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
# Extract enum/literal values if present
|
| 471 |
+
if "enum" in param_spec:
|
| 472 |
+
param_info["enum"] = param_spec["enum"]
|
| 473 |
+
|
| 474 |
+
# Handle anyOf schemas (common for optional literal types)
|
| 475 |
+
if "anyOf" in param_spec:
|
| 476 |
+
# Look for enum in anyOf schemas
|
| 477 |
+
for schema_option in param_spec["anyOf"]:
|
| 478 |
+
if "enum" in schema_option:
|
| 479 |
+
param_info["enum"] = schema_option["enum"]
|
| 480 |
+
# Update type if specified
|
| 481 |
+
if "type" in schema_option:
|
| 482 |
+
param_info["type"] = schema_option["type"]
|
| 483 |
+
break
|
| 484 |
+
|
| 485 |
+
# Handle oneOf schemas (alternative union syntax)
|
| 486 |
+
if "oneOf" in param_spec:
|
| 487 |
+
# Look for enum in oneOf schemas
|
| 488 |
+
for schema_option in param_spec["oneOf"]:
|
| 489 |
+
if "enum" in schema_option:
|
| 490 |
+
param_info["enum"] = schema_option["enum"]
|
| 491 |
+
if "type" in schema_option:
|
| 492 |
+
param_info["type"] = schema_option["type"]
|
| 493 |
+
break
|
| 494 |
+
|
| 495 |
+
# Determine if parameter is required based on:
|
| 496 |
+
# 1. Explicit required list (if provided)
|
| 497 |
+
# 2. If no default value is present in the schema
|
| 498 |
+
is_required = (param_name in required_param_names) or ("default" not in param_spec)
|
| 499 |
+
|
| 500 |
+
if is_required:
|
| 501 |
+
required_params.append(param_info)
|
| 502 |
+
else:
|
| 503 |
+
param_info["default"] = param_spec.get("default")
|
| 504 |
+
optional_params.append(param_info)
|
| 505 |
+
|
| 506 |
+
# Create complete tool schema
|
| 507 |
+
tool_schema = {
|
| 508 |
+
"name": tool_name,
|
| 509 |
+
"description": description,
|
| 510 |
+
"required_parameters": required_params,
|
| 511 |
+
"optional_parameters": optional_params,
|
| 512 |
+
"module": mcp_module_name,
|
| 513 |
+
}
|
| 514 |
+
|
| 515 |
+
success = tool_registry.register_tool(tool_schema, mcp_module_name)
|
| 516 |
+
if success:
|
| 517 |
+
tool_registry._name_to_function[tool_name] = wrapper_function
|
| 518 |
+
tools_added += 1
|
| 519 |
+
|
| 520 |
+
# Update MCP functions registry with parameter information
|
| 521 |
+
self.mcp_functions[tool_name]["required_parameters"] = required_params
|
| 522 |
+
self.mcp_functions[tool_name]["optional_parameters"] = optional_params
|
| 523 |
+
|
| 524 |
+
if tools_added > 0:
|
| 525 |
+
print(f"✅ Added {tools_added} remote MCP tools from {server_name} server")
|
| 526 |
+
|
| 527 |
+
def list_mcp_tools(self) -> Dict[str, dict]:
|
| 528 |
+
"""List all loaded MCP tools."""
|
| 529 |
+
return self.mcp_functions.copy()
|
| 530 |
+
|
| 531 |
+
def remove_mcp_tool(self, tool_name: str, tool_registry=None) -> bool:
|
| 532 |
+
"""Remove an MCP tool by name."""
|
| 533 |
+
if not self.has_mcp_functions() or tool_name not in self.mcp_functions:
|
| 534 |
+
return False
|
| 535 |
+
|
| 536 |
+
# Remove from tool registry
|
| 537 |
+
if tool_registry:
|
| 538 |
+
tool_registry.remove_tool_by_name(tool_name)
|
| 539 |
+
|
| 540 |
+
# Remove from MCP functions
|
| 541 |
+
del self.mcp_functions[tool_name]
|
| 542 |
+
return True
|
| 543 |
+
|
| 544 |
+
def show_mcp_status(self) -> None:
|
| 545 |
+
"""Display detailed MCP status information to the user."""
|
| 546 |
+
if not self.has_mcp_functions():
|
| 547 |
+
self.console.print("🔗 No MCP tools loaded")
|
| 548 |
+
return
|
| 549 |
+
|
| 550 |
+
mcp_tools = self.mcp_functions
|
| 551 |
+
if not mcp_tools:
|
| 552 |
+
self.console.print("🔗 MCP system initialized but no tools loaded")
|
| 553 |
+
return
|
| 554 |
+
|
| 555 |
+
# Group tools by server
|
| 556 |
+
servers = self.group_tools_by_server(mcp_tools)
|
| 557 |
+
|
| 558 |
+
# Display server information
|
| 559 |
+
self.console.print(f"\n🔗 MCP Status Report:")
|
| 560 |
+
self.console.print(f" 📊 Total servers: {len(servers)}")
|
| 561 |
+
self.console.print(f" 🛠️ Total MCP tools: {len(mcp_tools)}")
|
| 562 |
+
|
| 563 |
+
for server_name, tools in servers.items():
|
| 564 |
+
self.console.print(f"\n 📡 Server: {server_name}")
|
| 565 |
+
self.console.print(f" Status: ✅ Active ({len(tools)} tools)")
|
| 566 |
+
for tool in tools:
|
| 567 |
+
self.console.print(f" • {tool['name']}: {tool['description']}")
|
| 568 |
+
|
| 569 |
+
def get_mcp_summary(self) -> Dict[str, any]:
|
| 570 |
+
"""Get a summary of MCP tools for programmatic access."""
|
| 571 |
+
if not self.has_mcp_functions():
|
| 572 |
+
return {"total_tools": 0, "servers": {}, "tools": {}}
|
| 573 |
+
|
| 574 |
+
mcp_tools = self.mcp_functions
|
| 575 |
+
# Group tools by server but only get tool names
|
| 576 |
+
servers = {}
|
| 577 |
+
for tool_name, tool_info in mcp_tools.items():
|
| 578 |
+
server_name = tool_info.get('server', 'unknown')
|
| 579 |
+
if server_name not in servers:
|
| 580 |
+
servers[server_name] = []
|
| 581 |
+
servers[server_name].append(tool_name)
|
| 582 |
+
|
| 583 |
+
return {
|
| 584 |
+
"total_tools": len(mcp_tools),
|
| 585 |
+
"total_servers": len(servers),
|
| 586 |
+
"servers": servers,
|
| 587 |
+
"tools": {name: info.get('description', '') for name, info in mcp_tools.items()}
|
| 588 |
+
}
|
managers/tools/tool_manager.py
ADDED
|
@@ -0,0 +1,521 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tool Manager for CodeAct Agent.
|
| 3 |
+
Unified management system for all types of tools: local functions, decorated tools, and MCP tools.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from typing import Dict, List, Optional, Callable, Any, Union
|
| 7 |
+
from enum import Enum
|
| 8 |
+
from dataclasses import dataclass
|
| 9 |
+
import re
|
| 10 |
+
|
| 11 |
+
# Import components
|
| 12 |
+
from .tool_registry import ToolRegistry, create_module2api_from_functions
|
| 13 |
+
from .mcp_manager import MCPManager
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class ToolSource(Enum):
|
| 17 |
+
"""Enumeration of tool sources."""
|
| 18 |
+
LOCAL = "local"
|
| 19 |
+
DECORATED = "decorated"
|
| 20 |
+
MCP = "mcp"
|
| 21 |
+
ALL = "all"
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@dataclass
|
| 25 |
+
class ToolInfo:
|
| 26 |
+
"""Comprehensive tool information."""
|
| 27 |
+
name: str
|
| 28 |
+
description: str
|
| 29 |
+
source: ToolSource
|
| 30 |
+
function: Optional[Callable] = None
|
| 31 |
+
schema: Optional[Dict] = None
|
| 32 |
+
server: Optional[str] = None # For MCP tools
|
| 33 |
+
module: Optional[str] = None
|
| 34 |
+
required_parameters: List[Dict] = None
|
| 35 |
+
optional_parameters: List[Dict] = None
|
| 36 |
+
|
| 37 |
+
def __post_init__(self):
|
| 38 |
+
if self.required_parameters is None:
|
| 39 |
+
self.required_parameters = []
|
| 40 |
+
if self.optional_parameters is None:
|
| 41 |
+
self.optional_parameters = []
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class ToolManager:
|
| 45 |
+
"""
|
| 46 |
+
Unified tool management system for CodeAct Agent.
|
| 47 |
+
|
| 48 |
+
Manages all types of tools:
|
| 49 |
+
- Local functions (legacy function registry)
|
| 50 |
+
- Decorated tools (@tool decorator)
|
| 51 |
+
- MCP tools (Model Context Protocol)
|
| 52 |
+
|
| 53 |
+
Provides a single, consistent interface for tool operations.
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
def __init__(self, console_display=None):
|
| 57 |
+
"""
|
| 58 |
+
Initialize the ToolManager.
|
| 59 |
+
|
| 60 |
+
Args:
|
| 61 |
+
console_display: Optional console display for MCP status output
|
| 62 |
+
"""
|
| 63 |
+
# Core components
|
| 64 |
+
self.tool_registry = ToolRegistry()
|
| 65 |
+
self.mcp_manager = MCPManager(console_display)
|
| 66 |
+
|
| 67 |
+
# Unified tool catalog
|
| 68 |
+
self._tool_catalog: Dict[str, ToolInfo] = {}
|
| 69 |
+
|
| 70 |
+
# Legacy function registry (for backward compatibility)
|
| 71 |
+
self._legacy_functions: Dict[str, Callable] = {}
|
| 72 |
+
|
| 73 |
+
# Initialize with decorated tools from function_tools.py
|
| 74 |
+
self._discover_decorated_tools()
|
| 75 |
+
|
| 76 |
+
# ====================
|
| 77 |
+
# CORE TOOL MANAGEMENT
|
| 78 |
+
# ====================
|
| 79 |
+
|
| 80 |
+
def add_tool(self, tool: Union[Callable, Dict], name: str = None,
|
| 81 |
+
description: str = None, source: ToolSource = ToolSource.LOCAL) -> bool:
|
| 82 |
+
"""
|
| 83 |
+
Add a tool to the manager.
|
| 84 |
+
|
| 85 |
+
Args:
|
| 86 |
+
tool: Either a callable function or a tool schema dict
|
| 87 |
+
name: Optional custom name (defaults to function.__name__)
|
| 88 |
+
description: Optional description (defaults to function.__doc__)
|
| 89 |
+
source: Source type of the tool
|
| 90 |
+
|
| 91 |
+
Returns:
|
| 92 |
+
True if successfully added
|
| 93 |
+
"""
|
| 94 |
+
try:
|
| 95 |
+
if callable(tool):
|
| 96 |
+
# Handle callable functions
|
| 97 |
+
tool_name = name or tool.__name__
|
| 98 |
+
tool_desc = description or tool.__doc__ or f"Function {tool.__name__}"
|
| 99 |
+
|
| 100 |
+
# Add to tool registry
|
| 101 |
+
success = self.tool_registry.add_function_directly(tool_name, tool, tool_desc)
|
| 102 |
+
|
| 103 |
+
if success:
|
| 104 |
+
# Create ToolInfo and add to catalog
|
| 105 |
+
tool_info = ToolInfo(
|
| 106 |
+
name=tool_name,
|
| 107 |
+
description=tool_desc,
|
| 108 |
+
source=source,
|
| 109 |
+
function=tool,
|
| 110 |
+
schema=self._create_schema_from_function(tool_name, tool, tool_desc)
|
| 111 |
+
)
|
| 112 |
+
self._tool_catalog[tool_name] = tool_info
|
| 113 |
+
|
| 114 |
+
if source == ToolSource.LOCAL:
|
| 115 |
+
self._legacy_functions[tool_name] = tool
|
| 116 |
+
|
| 117 |
+
return True
|
| 118 |
+
|
| 119 |
+
elif isinstance(tool, dict):
|
| 120 |
+
# Handle tool schema dictionaries
|
| 121 |
+
tool_name = tool.get("name") or name
|
| 122 |
+
tool_desc = tool.get("description") or description
|
| 123 |
+
|
| 124 |
+
if not tool_name:
|
| 125 |
+
print("Warning: Tool schema must have a name")
|
| 126 |
+
return False
|
| 127 |
+
|
| 128 |
+
# Register with tool registry
|
| 129 |
+
success = self.tool_registry.register_tool(tool)
|
| 130 |
+
|
| 131 |
+
if success:
|
| 132 |
+
tool_info = ToolInfo(
|
| 133 |
+
name=tool_name,
|
| 134 |
+
description=tool_desc,
|
| 135 |
+
source=source,
|
| 136 |
+
schema=tool,
|
| 137 |
+
required_parameters=tool.get("required_parameters", []),
|
| 138 |
+
optional_parameters=tool.get("optional_parameters", [])
|
| 139 |
+
)
|
| 140 |
+
self._tool_catalog[tool_name] = tool_info
|
| 141 |
+
return True
|
| 142 |
+
|
| 143 |
+
return False
|
| 144 |
+
|
| 145 |
+
except Exception as e:
|
| 146 |
+
print(f"Error adding tool {name}: {e}")
|
| 147 |
+
return False
|
| 148 |
+
|
| 149 |
+
def remove_tool(self, name: str) -> bool:
|
| 150 |
+
"""Remove a tool by name from all registries."""
|
| 151 |
+
try:
|
| 152 |
+
success = False
|
| 153 |
+
|
| 154 |
+
# Remove from tool registry
|
| 155 |
+
if self.tool_registry.remove_tool_by_name(name):
|
| 156 |
+
success = True
|
| 157 |
+
|
| 158 |
+
# Remove from MCP if it's an MCP tool
|
| 159 |
+
if name in self.mcp_manager.mcp_functions:
|
| 160 |
+
if self.mcp_manager.remove_mcp_tool(name, self.tool_registry):
|
| 161 |
+
success = True
|
| 162 |
+
|
| 163 |
+
# Remove from legacy functions
|
| 164 |
+
if name in self._legacy_functions:
|
| 165 |
+
del self._legacy_functions[name]
|
| 166 |
+
success = True
|
| 167 |
+
|
| 168 |
+
# Remove from catalog
|
| 169 |
+
if name in self._tool_catalog:
|
| 170 |
+
del self._tool_catalog[name]
|
| 171 |
+
success = True
|
| 172 |
+
|
| 173 |
+
return success
|
| 174 |
+
|
| 175 |
+
except Exception as e:
|
| 176 |
+
print(f"Error removing tool {name}: {e}")
|
| 177 |
+
return False
|
| 178 |
+
|
| 179 |
+
def get_tool(self, name: str) -> Optional[ToolInfo]:
|
| 180 |
+
"""Get comprehensive tool information by name."""
|
| 181 |
+
return self._tool_catalog.get(name)
|
| 182 |
+
|
| 183 |
+
def get_tool_function(self, name: str) -> Optional[Callable]:
|
| 184 |
+
"""Get the actual function object by name."""
|
| 185 |
+
tool_info = self.get_tool(name)
|
| 186 |
+
if tool_info and tool_info.function:
|
| 187 |
+
return tool_info.function
|
| 188 |
+
|
| 189 |
+
# Check tool registry
|
| 190 |
+
return self.tool_registry.get_function_by_name(name)
|
| 191 |
+
|
| 192 |
+
# ====================
|
| 193 |
+
# TOOL DISCOVERY AND LISTING
|
| 194 |
+
# ====================
|
| 195 |
+
|
| 196 |
+
def list_tools(self, source: ToolSource = ToolSource.ALL,
|
| 197 |
+
include_details: bool = False) -> List[Dict]:
|
| 198 |
+
"""
|
| 199 |
+
List tools with optional filtering by source.
|
| 200 |
+
|
| 201 |
+
Args:
|
| 202 |
+
source: Filter by tool source (LOCAL, DECORATED, MCP, ALL)
|
| 203 |
+
include_details: Whether to include detailed information
|
| 204 |
+
|
| 205 |
+
Returns:
|
| 206 |
+
List of tool dictionaries
|
| 207 |
+
"""
|
| 208 |
+
tools = []
|
| 209 |
+
|
| 210 |
+
for tool_name, tool_info in self._tool_catalog.items():
|
| 211 |
+
if source == ToolSource.ALL or tool_info.source == source:
|
| 212 |
+
if include_details:
|
| 213 |
+
tools.append({
|
| 214 |
+
"name": tool_info.name,
|
| 215 |
+
"description": tool_info.description,
|
| 216 |
+
"source": tool_info.source.value,
|
| 217 |
+
"server": tool_info.server,
|
| 218 |
+
"module": tool_info.module,
|
| 219 |
+
"has_function": tool_info.function is not None,
|
| 220 |
+
"required_params": len(tool_info.required_parameters),
|
| 221 |
+
"optional_params": len(tool_info.optional_parameters)
|
| 222 |
+
})
|
| 223 |
+
else:
|
| 224 |
+
tools.append({
|
| 225 |
+
"name": tool_info.name,
|
| 226 |
+
"description": tool_info.description,
|
| 227 |
+
"source": tool_info.source.value
|
| 228 |
+
})
|
| 229 |
+
|
| 230 |
+
return sorted(tools, key=lambda x: x["name"])
|
| 231 |
+
|
| 232 |
+
def search_tools(self, query: str, search_descriptions: bool = True) -> List[Dict]:
|
| 233 |
+
"""
|
| 234 |
+
Search tools by name and optionally description.
|
| 235 |
+
|
| 236 |
+
Args:
|
| 237 |
+
query: Search query (supports regex)
|
| 238 |
+
search_descriptions: Whether to also search in descriptions
|
| 239 |
+
|
| 240 |
+
Returns:
|
| 241 |
+
List of matching tools
|
| 242 |
+
"""
|
| 243 |
+
pattern = re.compile(query, re.IGNORECASE)
|
| 244 |
+
matching_tools = []
|
| 245 |
+
|
| 246 |
+
for tool_name, tool_info in self._tool_catalog.items():
|
| 247 |
+
match = False
|
| 248 |
+
|
| 249 |
+
# Search in name
|
| 250 |
+
if pattern.search(tool_name):
|
| 251 |
+
match = True
|
| 252 |
+
|
| 253 |
+
# Search in description if enabled
|
| 254 |
+
elif search_descriptions and pattern.search(tool_info.description or ""):
|
| 255 |
+
match = True
|
| 256 |
+
|
| 257 |
+
if match:
|
| 258 |
+
matching_tools.append({
|
| 259 |
+
"name": tool_info.name,
|
| 260 |
+
"description": tool_info.description,
|
| 261 |
+
"source": tool_info.source.value,
|
| 262 |
+
"server": tool_info.server
|
| 263 |
+
})
|
| 264 |
+
|
| 265 |
+
return sorted(matching_tools, key=lambda x: x["name"])
|
| 266 |
+
|
| 267 |
+
def get_tools_by_source(self, source: ToolSource) -> Dict[str, ToolInfo]:
|
| 268 |
+
"""Get all tools from a specific source."""
|
| 269 |
+
return {
|
| 270 |
+
name: tool_info
|
| 271 |
+
for name, tool_info in self._tool_catalog.items()
|
| 272 |
+
if tool_info.source == source
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
# ====================
|
| 276 |
+
# MCP INTEGRATION
|
| 277 |
+
# ====================
|
| 278 |
+
|
| 279 |
+
def add_mcp_server(self, config_path: str = "./mcp_config.yaml") -> None:
|
| 280 |
+
"""Add MCP tools from configuration file."""
|
| 281 |
+
try:
|
| 282 |
+
# Use MCP manager to load tools
|
| 283 |
+
self.mcp_manager.add_mcp(config_path, self.tool_registry)
|
| 284 |
+
|
| 285 |
+
# Update our catalog with MCP tools
|
| 286 |
+
mcp_tools = self.mcp_manager.list_mcp_tools()
|
| 287 |
+
for tool_name, tool_data in mcp_tools.items():
|
| 288 |
+
tool_info = ToolInfo(
|
| 289 |
+
name=tool_name,
|
| 290 |
+
description=tool_data.get("description", "MCP tool"),
|
| 291 |
+
source=ToolSource.MCP,
|
| 292 |
+
function=tool_data.get("function"),
|
| 293 |
+
server=tool_data.get("server"),
|
| 294 |
+
module=tool_data.get("module"),
|
| 295 |
+
required_parameters=tool_data.get("required_parameters", []),
|
| 296 |
+
optional_parameters=tool_data.get("optional_parameters", [])
|
| 297 |
+
)
|
| 298 |
+
self._tool_catalog[tool_name] = tool_info
|
| 299 |
+
|
| 300 |
+
except Exception as e:
|
| 301 |
+
print(f"Error adding MCP server: {e}")
|
| 302 |
+
|
| 303 |
+
def list_mcp_servers(self) -> Dict[str, List[str]]:
|
| 304 |
+
"""List all MCP servers and their tools."""
|
| 305 |
+
mcp_tools = self.get_tools_by_source(ToolSource.MCP)
|
| 306 |
+
servers = {}
|
| 307 |
+
|
| 308 |
+
for tool_name, tool_info in mcp_tools.items():
|
| 309 |
+
server_name = tool_info.server or "unknown"
|
| 310 |
+
if server_name not in servers:
|
| 311 |
+
servers[server_name] = []
|
| 312 |
+
servers[server_name].append(tool_name)
|
| 313 |
+
|
| 314 |
+
return servers
|
| 315 |
+
|
| 316 |
+
def show_mcp_status(self) -> None:
|
| 317 |
+
"""Display detailed MCP status."""
|
| 318 |
+
self.mcp_manager.show_mcp_status()
|
| 319 |
+
|
| 320 |
+
def get_mcp_summary(self) -> Dict[str, Any]:
|
| 321 |
+
"""Get MCP tools summary."""
|
| 322 |
+
return self.mcp_manager.get_mcp_summary()
|
| 323 |
+
|
| 324 |
+
# ====================
|
| 325 |
+
# TOOL EXECUTION SUPPORT
|
| 326 |
+
# ====================
|
| 327 |
+
|
| 328 |
+
def get_all_functions(self) -> Dict[str, Callable]:
|
| 329 |
+
"""Get all available functions as a dictionary."""
|
| 330 |
+
functions = {}
|
| 331 |
+
|
| 332 |
+
# Add from tool registry
|
| 333 |
+
functions.update(self.tool_registry.get_all_functions())
|
| 334 |
+
|
| 335 |
+
# Add from legacy functions
|
| 336 |
+
functions.update(self._legacy_functions)
|
| 337 |
+
|
| 338 |
+
# Add MCP functions
|
| 339 |
+
mcp_tools = self.mcp_manager.list_mcp_tools()
|
| 340 |
+
for tool_name, tool_data in mcp_tools.items():
|
| 341 |
+
if tool_data.get("function"):
|
| 342 |
+
functions[tool_name] = tool_data["function"]
|
| 343 |
+
|
| 344 |
+
return functions
|
| 345 |
+
|
| 346 |
+
def get_tool_schemas(self, openai_format: bool = True) -> List[Dict]:
|
| 347 |
+
"""
|
| 348 |
+
Get tool schemas for all tools.
|
| 349 |
+
|
| 350 |
+
Args:
|
| 351 |
+
openai_format: Whether to format as OpenAI function schemas
|
| 352 |
+
|
| 353 |
+
Returns:
|
| 354 |
+
List of tool schemas
|
| 355 |
+
"""
|
| 356 |
+
schemas = []
|
| 357 |
+
|
| 358 |
+
for tool_name, tool_info in self._tool_catalog.items():
|
| 359 |
+
if openai_format:
|
| 360 |
+
# Convert to OpenAI function schema format
|
| 361 |
+
schema = {
|
| 362 |
+
"type": "function",
|
| 363 |
+
"function": {
|
| 364 |
+
"name": tool_info.name,
|
| 365 |
+
"description": tool_info.description,
|
| 366 |
+
"parameters": {
|
| 367 |
+
"type": "object",
|
| 368 |
+
"properties": {},
|
| 369 |
+
"required": []
|
| 370 |
+
}
|
| 371 |
+
}
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
# Add required parameters
|
| 375 |
+
for param in tool_info.required_parameters:
|
| 376 |
+
param_schema = {
|
| 377 |
+
"type": param.get("type", "string"),
|
| 378 |
+
"description": param.get("description", "")
|
| 379 |
+
}
|
| 380 |
+
# Add enum values if present
|
| 381 |
+
if "enum" in param:
|
| 382 |
+
param_schema["enum"] = param["enum"]
|
| 383 |
+
schema["function"]["parameters"]["properties"][param["name"]] = param_schema
|
| 384 |
+
schema["function"]["parameters"]["required"].append(param["name"])
|
| 385 |
+
|
| 386 |
+
# Add optional parameters
|
| 387 |
+
for param in tool_info.optional_parameters:
|
| 388 |
+
param_schema = {
|
| 389 |
+
"type": param.get("type", "string"),
|
| 390 |
+
"description": param.get("description", "")
|
| 391 |
+
}
|
| 392 |
+
# Add enum values if present
|
| 393 |
+
if "enum" in param:
|
| 394 |
+
param_schema["enum"] = param["enum"]
|
| 395 |
+
if "default" in param:
|
| 396 |
+
param_schema["default"] = param["default"]
|
| 397 |
+
schema["function"]["parameters"]["properties"][param["name"]] = param_schema
|
| 398 |
+
|
| 399 |
+
schemas.append(schema)
|
| 400 |
+
else:
|
| 401 |
+
# Return raw schema
|
| 402 |
+
if tool_info.schema:
|
| 403 |
+
schemas.append(tool_info.schema)
|
| 404 |
+
|
| 405 |
+
return schemas
|
| 406 |
+
|
| 407 |
+
# ====================
|
| 408 |
+
# STATISTICS AND REPORTING
|
| 409 |
+
# ====================
|
| 410 |
+
|
| 411 |
+
def get_tool_statistics(self) -> Dict[str, Any]:
|
| 412 |
+
"""Get comprehensive tool statistics."""
|
| 413 |
+
stats = {
|
| 414 |
+
"total_tools": len(self._tool_catalog),
|
| 415 |
+
"by_source": {source.value: 0 for source in ToolSource if source != ToolSource.ALL},
|
| 416 |
+
"with_functions": 0,
|
| 417 |
+
"mcp_servers": len(self.list_mcp_servers()),
|
| 418 |
+
"tool_registry_size": len(self.tool_registry.tools),
|
| 419 |
+
"legacy_functions": len(self._legacy_functions)
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
for tool_info in self._tool_catalog.values():
|
| 423 |
+
stats["by_source"][tool_info.source.value] += 1
|
| 424 |
+
if tool_info.function:
|
| 425 |
+
stats["with_functions"] += 1
|
| 426 |
+
|
| 427 |
+
return stats
|
| 428 |
+
|
| 429 |
+
def validate_tools(self) -> Dict[str, List[str]]:
|
| 430 |
+
"""Validate all tools and return any issues found."""
|
| 431 |
+
issues = {
|
| 432 |
+
"missing_functions": [],
|
| 433 |
+
"missing_descriptions": [],
|
| 434 |
+
"duplicate_names": [],
|
| 435 |
+
"invalid_schemas": []
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
seen_names = set()
|
| 439 |
+
for tool_name, tool_info in self._tool_catalog.items():
|
| 440 |
+
# Check for duplicates
|
| 441 |
+
if tool_name in seen_names:
|
| 442 |
+
issues["duplicate_names"].append(tool_name)
|
| 443 |
+
seen_names.add(tool_name)
|
| 444 |
+
|
| 445 |
+
# Check for missing functions (except MCP tools which may not have direct functions)
|
| 446 |
+
if not tool_info.function and tool_info.source != ToolSource.MCP:
|
| 447 |
+
issues["missing_functions"].append(tool_name)
|
| 448 |
+
|
| 449 |
+
# Check for missing descriptions
|
| 450 |
+
if not tool_info.description or tool_info.description.strip() == "":
|
| 451 |
+
issues["missing_descriptions"].append(tool_name)
|
| 452 |
+
|
| 453 |
+
return issues
|
| 454 |
+
|
| 455 |
+
# ====================
|
| 456 |
+
# PRIVATE METHODS
|
| 457 |
+
# ====================
|
| 458 |
+
|
| 459 |
+
def _discover_decorated_tools(self) -> None:
|
| 460 |
+
"""Discover and register tools marked with @tool decorator."""
|
| 461 |
+
try:
|
| 462 |
+
from .builtin_tools import get_all_tool_functions
|
| 463 |
+
|
| 464 |
+
tool_functions = get_all_tool_functions()
|
| 465 |
+
for func in tool_functions:
|
| 466 |
+
name = getattr(func, '_tool_name', func.__name__)
|
| 467 |
+
description = getattr(func, '_tool_description', func.__doc__ or f"Function {func.__name__}")
|
| 468 |
+
|
| 469 |
+
tool_info = ToolInfo(
|
| 470 |
+
name=name,
|
| 471 |
+
description=description,
|
| 472 |
+
source=ToolSource.DECORATED,
|
| 473 |
+
function=func,
|
| 474 |
+
schema=self._create_schema_from_function(name, func, description)
|
| 475 |
+
)
|
| 476 |
+
self._tool_catalog[name] = tool_info
|
| 477 |
+
|
| 478 |
+
# Also add to tool registry for consistency
|
| 479 |
+
self.tool_registry.add_function_directly(name, func, description)
|
| 480 |
+
|
| 481 |
+
except ImportError:
|
| 482 |
+
print("Warning: Could not import builtin_tools module for decorated tool discovery")
|
| 483 |
+
|
| 484 |
+
def _create_schema_from_function(self, name: str, function: Callable, description: str) -> Dict:
|
| 485 |
+
"""Create a tool schema from a function object."""
|
| 486 |
+
return self.tool_registry._create_schema_from_function(name, function, description)
|
| 487 |
+
|
| 488 |
+
def _refresh_catalog(self) -> None:
|
| 489 |
+
"""Refresh the tool catalog from all sources."""
|
| 490 |
+
# Clear current catalog
|
| 491 |
+
self._tool_catalog.clear()
|
| 492 |
+
|
| 493 |
+
# Re-discover decorated tools
|
| 494 |
+
self._discover_decorated_tools()
|
| 495 |
+
|
| 496 |
+
# Re-add MCP tools
|
| 497 |
+
mcp_tools = self.mcp_manager.list_mcp_tools()
|
| 498 |
+
for tool_name, tool_data in mcp_tools.items():
|
| 499 |
+
tool_info = ToolInfo(
|
| 500 |
+
name=tool_name,
|
| 501 |
+
description=tool_data.get("description", "MCP tool"),
|
| 502 |
+
source=ToolSource.MCP,
|
| 503 |
+
function=tool_data.get("function"),
|
| 504 |
+
server=tool_data.get("server"),
|
| 505 |
+
module=tool_data.get("module"),
|
| 506 |
+
required_parameters=tool_data.get("required_parameters", []),
|
| 507 |
+
optional_parameters=tool_data.get("optional_parameters", [])
|
| 508 |
+
)
|
| 509 |
+
self._tool_catalog[tool_name] = tool_info
|
| 510 |
+
|
| 511 |
+
# ====================
|
| 512 |
+
# LEGACY COMPATIBILITY
|
| 513 |
+
# ====================
|
| 514 |
+
|
| 515 |
+
def add_legacy_functions(self, functions: Dict[str, Callable]) -> int:
|
| 516 |
+
"""Add legacy functions for backward compatibility."""
|
| 517 |
+
added_count = 0
|
| 518 |
+
for name, func in functions.items():
|
| 519 |
+
if self.add_tool(func, name, source=ToolSource.LOCAL):
|
| 520 |
+
added_count += 1
|
| 521 |
+
return added_count
|
managers/tools/tool_registry.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tool Registry System for CodeAct - Based on biomni's approach.
|
| 3 |
+
Provides centralized tool management with schema validation and discovery.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import ast
|
| 7 |
+
import importlib
|
| 8 |
+
import importlib.util
|
| 9 |
+
import inspect
|
| 10 |
+
import os
|
| 11 |
+
import pickle
|
| 12 |
+
from typing import Any, Dict, List, Optional, Callable
|
| 13 |
+
import pandas as pd
|
| 14 |
+
|
| 15 |
+
__all__ = ["ToolRegistry", "tool", "discover_tools_in_module", "create_module2api_from_functions"]
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class ToolRegistry:
|
| 19 |
+
"""
|
| 20 |
+
Central registry for managing tools, similar to biomni's ToolRegistry.
|
| 21 |
+
Handles tool registration, validation, and lookup by name/ID.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
def __init__(self, module2api: Optional[Dict[str, List[Dict]]] = None):
|
| 25 |
+
"""
|
| 26 |
+
Initialize the tool registry.
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
module2api: Dictionary mapping module names to lists of tool schemas
|
| 30 |
+
e.g., {"module.tools": [{"name": "func1", "description": "...", ...}]}
|
| 31 |
+
"""
|
| 32 |
+
self.tools = []
|
| 33 |
+
self.next_id = 0
|
| 34 |
+
self._name_to_function = {} # Map tool names to actual functions
|
| 35 |
+
|
| 36 |
+
# Register tools from module2api if provided
|
| 37 |
+
if module2api:
|
| 38 |
+
for module_name, tool_list in module2api.items():
|
| 39 |
+
for tool_schema in tool_list:
|
| 40 |
+
self.register_tool(tool_schema, module_name)
|
| 41 |
+
|
| 42 |
+
# Create document dataframe for retrieval (similar to biomni)
|
| 43 |
+
self._create_document_df()
|
| 44 |
+
|
| 45 |
+
def register_tool(self, tool_schema: Dict, module_name: Optional[str] = None) -> bool:
|
| 46 |
+
"""
|
| 47 |
+
Register a tool with the registry.
|
| 48 |
+
|
| 49 |
+
Args:
|
| 50 |
+
tool_schema: Tool schema dictionary with name, description, parameters
|
| 51 |
+
module_name: Optional module name where the tool function is located
|
| 52 |
+
|
| 53 |
+
Returns:
|
| 54 |
+
True if registration successful, False otherwise
|
| 55 |
+
"""
|
| 56 |
+
if not self.validate_tool(tool_schema):
|
| 57 |
+
raise ValueError(f"Invalid tool format for {tool_schema.get('name', 'unknown')}")
|
| 58 |
+
|
| 59 |
+
# Add unique ID
|
| 60 |
+
tool_schema = tool_schema.copy()
|
| 61 |
+
tool_schema["id"] = self.next_id
|
| 62 |
+
tool_schema["module_name"] = module_name
|
| 63 |
+
|
| 64 |
+
# Try to load the actual function if module_name provided
|
| 65 |
+
if module_name and "name" in tool_schema:
|
| 66 |
+
try:
|
| 67 |
+
function = self._load_function_from_module(module_name, tool_schema["name"])
|
| 68 |
+
if function:
|
| 69 |
+
self._name_to_function[tool_schema["name"]] = function
|
| 70 |
+
except Exception as e:
|
| 71 |
+
print(f"Warning: Could not load function {tool_schema['name']} from {module_name}: {e}")
|
| 72 |
+
|
| 73 |
+
self.tools.append(tool_schema)
|
| 74 |
+
self.next_id += 1
|
| 75 |
+
return True
|
| 76 |
+
|
| 77 |
+
def validate_tool(self, tool_schema: Dict) -> bool:
|
| 78 |
+
"""Validate that a tool schema has required fields."""
|
| 79 |
+
required_keys = ["name", "description"]
|
| 80 |
+
return all(key in tool_schema for key in required_keys)
|
| 81 |
+
|
| 82 |
+
def get_tool_by_name(self, name: str) -> Optional[Dict]:
|
| 83 |
+
"""Get tool schema by name."""
|
| 84 |
+
for tool in self.tools:
|
| 85 |
+
if tool["name"] == name:
|
| 86 |
+
return tool
|
| 87 |
+
return None
|
| 88 |
+
|
| 89 |
+
def get_tool_by_id(self, tool_id: int) -> Optional[Dict]:
|
| 90 |
+
"""Get tool schema by ID."""
|
| 91 |
+
for tool in self.tools:
|
| 92 |
+
if tool["id"] == tool_id:
|
| 93 |
+
return tool
|
| 94 |
+
return None
|
| 95 |
+
|
| 96 |
+
def get_function_by_name(self, name: str) -> Optional[Callable]:
|
| 97 |
+
"""Get the actual function object by name."""
|
| 98 |
+
return self._name_to_function.get(name)
|
| 99 |
+
|
| 100 |
+
def get_all_functions(self) -> Dict[str, Callable]:
|
| 101 |
+
"""Get all registered functions as a dictionary."""
|
| 102 |
+
return self._name_to_function.copy()
|
| 103 |
+
|
| 104 |
+
def list_tools(self) -> List[Dict]:
|
| 105 |
+
"""List all registered tools with basic info."""
|
| 106 |
+
return [{"name": tool["name"], "id": tool["id"], "description": tool["description"]}
|
| 107 |
+
for tool in self.tools]
|
| 108 |
+
|
| 109 |
+
def list_tool_names(self) -> List[str]:
|
| 110 |
+
"""Get list of all tool names."""
|
| 111 |
+
return [tool["name"] for tool in self.tools]
|
| 112 |
+
|
| 113 |
+
def add_function_directly(self, name: str, function: Callable, description: str = None) -> bool:
|
| 114 |
+
"""
|
| 115 |
+
Add a function directly to the registry.
|
| 116 |
+
|
| 117 |
+
Args:
|
| 118 |
+
name: Function name
|
| 119 |
+
function: The callable function
|
| 120 |
+
description: Optional description, will be extracted from docstring if not provided
|
| 121 |
+
|
| 122 |
+
Returns:
|
| 123 |
+
True if added successfully
|
| 124 |
+
"""
|
| 125 |
+
if description is None:
|
| 126 |
+
description = function.__doc__ or f"Function {name}"
|
| 127 |
+
|
| 128 |
+
# Create schema from function signature
|
| 129 |
+
schema = self._create_schema_from_function(name, function, description)
|
| 130 |
+
|
| 131 |
+
# Register the tool
|
| 132 |
+
self.register_tool(schema)
|
| 133 |
+
self._name_to_function[name] = function
|
| 134 |
+
return True
|
| 135 |
+
|
| 136 |
+
def _create_schema_from_function(self, name: str, function: Callable, description: str) -> Dict:
|
| 137 |
+
"""Create a tool schema from a function object."""
|
| 138 |
+
sig = inspect.signature(function)
|
| 139 |
+
|
| 140 |
+
schema = {
|
| 141 |
+
"name": name,
|
| 142 |
+
"description": description,
|
| 143 |
+
"required_parameters": [],
|
| 144 |
+
"optional_parameters": []
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
for param_name, param in sig.parameters.items():
|
| 148 |
+
param_info = {
|
| 149 |
+
"name": param_name,
|
| 150 |
+
"type": self._get_param_type(param),
|
| 151 |
+
"description": f"Parameter {param_name}"
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
if param.default == inspect.Parameter.empty:
|
| 155 |
+
schema["required_parameters"].append(param_info)
|
| 156 |
+
else:
|
| 157 |
+
param_info["default"] = param.default
|
| 158 |
+
schema["optional_parameters"].append(param_info)
|
| 159 |
+
|
| 160 |
+
return schema
|
| 161 |
+
|
| 162 |
+
def _get_param_type(self, param: inspect.Parameter) -> str:
|
| 163 |
+
"""Extract parameter type as string."""
|
| 164 |
+
if param.annotation != inspect.Parameter.empty:
|
| 165 |
+
if hasattr(param.annotation, '__name__'):
|
| 166 |
+
return param.annotation.__name__
|
| 167 |
+
else:
|
| 168 |
+
return str(param.annotation)
|
| 169 |
+
return "Any"
|
| 170 |
+
|
| 171 |
+
def _load_function_from_module(self, module_name: str, function_name: str) -> Optional[Callable]:
|
| 172 |
+
"""Load a function from a module."""
|
| 173 |
+
try:
|
| 174 |
+
module = importlib.import_module(module_name)
|
| 175 |
+
return getattr(module, function_name, None)
|
| 176 |
+
except (ImportError, AttributeError):
|
| 177 |
+
return None
|
| 178 |
+
|
| 179 |
+
def _create_document_df(self):
|
| 180 |
+
"""Create a pandas DataFrame for tool retrieval (similar to biomni)."""
|
| 181 |
+
docs = []
|
| 182 |
+
for tool in self.tools:
|
| 183 |
+
doc_content = {
|
| 184 |
+
"name": tool["name"],
|
| 185 |
+
"description": tool["description"],
|
| 186 |
+
"required_parameters": tool.get("required_parameters", []),
|
| 187 |
+
"optional_parameters": tool.get("optional_parameters", []),
|
| 188 |
+
"module_name": tool.get("module_name", "")
|
| 189 |
+
}
|
| 190 |
+
docs.append([tool["id"], doc_content])
|
| 191 |
+
|
| 192 |
+
self.document_df = pd.DataFrame(docs, columns=["docid", "document_content"])
|
| 193 |
+
|
| 194 |
+
def remove_tool_by_name(self, name: str) -> bool:
|
| 195 |
+
"""Remove a tool by name."""
|
| 196 |
+
tool = self.get_tool_by_name(name)
|
| 197 |
+
if tool:
|
| 198 |
+
self.tools = [t for t in self.tools if t["name"] != name]
|
| 199 |
+
self._name_to_function.pop(name, None)
|
| 200 |
+
self._create_document_df() # Refresh document df
|
| 201 |
+
return True
|
| 202 |
+
return False
|
| 203 |
+
|
| 204 |
+
def save_registry(self, filename: str):
|
| 205 |
+
"""Save registry to file."""
|
| 206 |
+
with open(filename, "wb") as file:
|
| 207 |
+
pickle.dump(self, file)
|
| 208 |
+
|
| 209 |
+
@staticmethod
|
| 210 |
+
def load_registry(filename: str) -> 'ToolRegistry':
|
| 211 |
+
"""Load registry from file."""
|
| 212 |
+
with open(filename, "rb") as file:
|
| 213 |
+
return pickle.load(file)
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
# ====================
|
| 217 |
+
# TOOL DECORATOR
|
| 218 |
+
# ====================
|
| 219 |
+
|
| 220 |
+
def tool(func: Callable = None, *, name: str = None, description: str = None):
|
| 221 |
+
"""
|
| 222 |
+
Decorator to mark functions as tools, similar to biomni's @tool decorator.
|
| 223 |
+
|
| 224 |
+
Usage:
|
| 225 |
+
@tool
|
| 226 |
+
def my_function(x: int) -> int:
|
| 227 |
+
'''This function does something'''
|
| 228 |
+
return x * 2
|
| 229 |
+
|
| 230 |
+
@tool(name="custom_name", description="Custom description")
|
| 231 |
+
def another_function():
|
| 232 |
+
pass
|
| 233 |
+
"""
|
| 234 |
+
def decorator(f):
|
| 235 |
+
# Store metadata on the function
|
| 236 |
+
f._tool_name = name or f.__name__
|
| 237 |
+
f._tool_description = description or f.__doc__ or f"Function {f.__name__}"
|
| 238 |
+
f._is_tool = True
|
| 239 |
+
return f
|
| 240 |
+
|
| 241 |
+
if func is None:
|
| 242 |
+
# Called with arguments: @tool(name="...", description="...")
|
| 243 |
+
return decorator
|
| 244 |
+
else:
|
| 245 |
+
# Called without arguments: @tool
|
| 246 |
+
return decorator(func)
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
# ====================
|
| 250 |
+
# TOOL DISCOVERY UTILITIES
|
| 251 |
+
# ====================
|
| 252 |
+
|
| 253 |
+
def discover_tools_in_module(module_path: str) -> List[Callable]:
|
| 254 |
+
"""
|
| 255 |
+
Discover all functions marked with @tool decorator in a module.
|
| 256 |
+
|
| 257 |
+
Args:
|
| 258 |
+
module_path: Path to the Python module file
|
| 259 |
+
|
| 260 |
+
Returns:
|
| 261 |
+
List of function objects marked as tools
|
| 262 |
+
"""
|
| 263 |
+
with open(module_path, 'r') as file:
|
| 264 |
+
tree = ast.parse(file.read(), filename=module_path)
|
| 265 |
+
|
| 266 |
+
tool_function_names = []
|
| 267 |
+
|
| 268 |
+
# Find functions with @tool decorator
|
| 269 |
+
for node in ast.walk(tree):
|
| 270 |
+
if isinstance(node, ast.FunctionDef):
|
| 271 |
+
for decorator in node.decorator_list:
|
| 272 |
+
if (isinstance(decorator, ast.Name) and decorator.id == "tool") or \
|
| 273 |
+
(isinstance(decorator, ast.Call) and
|
| 274 |
+
isinstance(decorator.func, ast.Name) and decorator.func.id == "tool"):
|
| 275 |
+
tool_function_names.append(node.name)
|
| 276 |
+
break
|
| 277 |
+
|
| 278 |
+
# Import the module and get function objects
|
| 279 |
+
spec = importlib.util.spec_from_file_location("temp_module", module_path)
|
| 280 |
+
module = importlib.util.module_from_spec(spec)
|
| 281 |
+
spec.loader.exec_module(module)
|
| 282 |
+
|
| 283 |
+
tool_functions = []
|
| 284 |
+
for name in tool_function_names:
|
| 285 |
+
func = getattr(module, name, None)
|
| 286 |
+
if func and hasattr(func, '_is_tool'):
|
| 287 |
+
tool_functions.append(func)
|
| 288 |
+
|
| 289 |
+
return tool_functions
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
def create_module2api_from_functions(functions: List[Callable], module_name: str = "custom_tools") -> Dict[str, List[Dict]]:
|
| 293 |
+
"""
|
| 294 |
+
Create a module2api dictionary from a list of functions.
|
| 295 |
+
|
| 296 |
+
Args:
|
| 297 |
+
functions: List of function objects
|
| 298 |
+
module_name: Name to assign to the module
|
| 299 |
+
|
| 300 |
+
Returns:
|
| 301 |
+
Dictionary in module2api format
|
| 302 |
+
"""
|
| 303 |
+
tool_schemas = []
|
| 304 |
+
|
| 305 |
+
for func in functions:
|
| 306 |
+
name = getattr(func, '_tool_name', func.__name__)
|
| 307 |
+
description = getattr(func, '_tool_description', func.__doc__ or f"Function {func.__name__}")
|
| 308 |
+
|
| 309 |
+
# Create schema from function signature
|
| 310 |
+
registry = ToolRegistry()
|
| 311 |
+
schema = registry._create_schema_from_function(name, func, description)
|
| 312 |
+
tool_schemas.append(schema)
|
| 313 |
+
|
| 314 |
+
return {module_name: tool_schemas}
|
managers/tools/tool_selector.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tool Selector for CodeAct Agent.
|
| 3 |
+
Pure LLM-based tool selection mechanism similar to Biomni's prompt_based_retrieval.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import re
|
| 7 |
+
from typing import Dict, List, Optional
|
| 8 |
+
from langchain_core.messages import HumanMessage
|
| 9 |
+
from langchain_core.language_models.chat_models import BaseChatModel
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class ToolSelector:
|
| 13 |
+
"""
|
| 14 |
+
LLM-based tool selection system inspired by Biomni's approach.
|
| 15 |
+
Uses an LLM to intelligently select the most relevant tools for a given task.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
def __init__(self, model: BaseChatModel):
|
| 19 |
+
"""
|
| 20 |
+
Initialize the ToolSelector.
|
| 21 |
+
|
| 22 |
+
Args:
|
| 23 |
+
model: The language model to use for tool selection
|
| 24 |
+
"""
|
| 25 |
+
self.model = model
|
| 26 |
+
|
| 27 |
+
def select_tools_for_task(self, query: str, available_tools: Dict[str, Dict], max_tools: int = 15) -> List[str]:
|
| 28 |
+
"""
|
| 29 |
+
Use LLM-based selection to choose the most relevant tools for a query.
|
| 30 |
+
Inspired by Biomni's prompt_based_retrieval mechanism.
|
| 31 |
+
|
| 32 |
+
Args:
|
| 33 |
+
query: The user's query/task description
|
| 34 |
+
available_tools: Dictionary of {tool_name: tool_info} available
|
| 35 |
+
max_tools: Maximum number of tools to select
|
| 36 |
+
|
| 37 |
+
Returns:
|
| 38 |
+
List of selected tool names
|
| 39 |
+
"""
|
| 40 |
+
if not available_tools:
|
| 41 |
+
return []
|
| 42 |
+
|
| 43 |
+
# Format tools for LLM prompt
|
| 44 |
+
tools_list = self._format_tools_for_prompt(available_tools)
|
| 45 |
+
|
| 46 |
+
# Create selection prompt (similar to Biomni's approach)
|
| 47 |
+
selection_prompt = f"""You are an expert biomedical research assistant. Your task is to select the most relevant tools to help answer a user's query.
|
| 48 |
+
|
| 49 |
+
USER QUERY: {query}
|
| 50 |
+
|
| 51 |
+
Below are the available tools. Select items that are directly or indirectly relevant to answering the query.
|
| 52 |
+
Be generous in your selection - include tools that might be useful for the task, even if they're not explicitly mentioned in the query.
|
| 53 |
+
It's better to include slightly more tools than to miss potentially useful ones.
|
| 54 |
+
|
| 55 |
+
AVAILABLE TOOLS:
|
| 56 |
+
{tools_list}
|
| 57 |
+
|
| 58 |
+
Select up to {max_tools} tools that would be most helpful for this task.
|
| 59 |
+
|
| 60 |
+
Respond with ONLY a comma-separated list of the exact tool names, like this:
|
| 61 |
+
tool_name_1, tool_name_2, tool_name_3
|
| 62 |
+
|
| 63 |
+
Selected tools:"""
|
| 64 |
+
|
| 65 |
+
try:
|
| 66 |
+
# Get LLM response
|
| 67 |
+
response = self.model.invoke([HumanMessage(content=selection_prompt)])
|
| 68 |
+
response_content = response.content.strip()
|
| 69 |
+
|
| 70 |
+
# Parse the response to extract tool names
|
| 71 |
+
selected_tools = self._parse_tool_selection_response(response_content, available_tools)
|
| 72 |
+
|
| 73 |
+
# Ensure we don't exceed max_tools
|
| 74 |
+
return selected_tools[:max_tools]
|
| 75 |
+
|
| 76 |
+
except Exception as e:
|
| 77 |
+
print(f"Error in LLM-based tool selection: {e}")
|
| 78 |
+
# Return all tools if LLM fails (no keyword fallback)
|
| 79 |
+
return list(available_tools.keys())[:max_tools]
|
| 80 |
+
|
| 81 |
+
def _format_tools_for_prompt(self, tools: Dict[str, Dict]) -> str:
|
| 82 |
+
"""Format tools for the LLM prompt."""
|
| 83 |
+
formatted = []
|
| 84 |
+
for i, (tool_name, tool_info) in enumerate(tools.items(), 1):
|
| 85 |
+
description = tool_info.get('description', 'No description available')
|
| 86 |
+
source = tool_info.get('source', 'unknown')
|
| 87 |
+
formatted.append(f"{i}. {tool_name} ({source}): {description}")
|
| 88 |
+
return "\n".join(formatted)
|
| 89 |
+
|
| 90 |
+
def _parse_tool_selection_response(self, response: str, available_tools: Dict[str, Dict]) -> List[str]:
|
| 91 |
+
"""Parse the LLM response to extract valid tool names."""
|
| 92 |
+
selected_tools = []
|
| 93 |
+
|
| 94 |
+
# Split by commas and clean up
|
| 95 |
+
tool_candidates = [name.strip() for name in response.split(',')]
|
| 96 |
+
|
| 97 |
+
for candidate in tool_candidates:
|
| 98 |
+
# Remove any extra characters, numbers, or formatting
|
| 99 |
+
clean_candidate = re.sub(r'^\d+\.\s*', '', candidate) # Remove "1. " prefixes
|
| 100 |
+
clean_candidate = clean_candidate.strip()
|
| 101 |
+
|
| 102 |
+
# Check if this matches any available tool (case-insensitive)
|
| 103 |
+
for tool_name in available_tools.keys():
|
| 104 |
+
if clean_candidate.lower() == tool_name.lower():
|
| 105 |
+
if tool_name not in selected_tools: # Avoid duplicates
|
| 106 |
+
selected_tools.append(tool_name)
|
| 107 |
+
break
|
| 108 |
+
|
| 109 |
+
return selected_tools
|
managers/workflow/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Workflow subsystem - Workflow execution, state, and plan management.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from .workflow_engine import WorkflowEngine
|
| 6 |
+
from .state_manager import StateManager
|
| 7 |
+
from .plan_manager import PlanManager
|
| 8 |
+
|
| 9 |
+
__all__ = ['WorkflowEngine', 'StateManager', 'PlanManager']
|
managers/workflow/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (523 Bytes). View file
|
|
|
managers/workflow/__pycache__/plan_manager.cpython-311.pyc
ADDED
|
Binary file (3.91 kB). View file
|
|
|
managers/workflow/__pycache__/state_manager.cpython-311.pyc
ADDED
|
Binary file (1.29 kB). View file
|
|
|
managers/workflow/__pycache__/workflow_engine.cpython-311.pyc
ADDED
|
Binary file (16.3 kB). View file
|
|
|
managers/workflow/plan_manager.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Plan Manager for CodeAct Agent.
|
| 3 |
+
Handles plan creation, updates, and progress tracking.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import re
|
| 7 |
+
from typing import Optional, Dict
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class PlanManager:
|
| 11 |
+
"""Manages plan creation, updates, and progress tracking."""
|
| 12 |
+
|
| 13 |
+
@staticmethod
|
| 14 |
+
def extract_plan_from_content(content: str) -> Optional[str]:
|
| 15 |
+
"""Extract plan from agent content."""
|
| 16 |
+
plan_pattern = r'\d+\.\s*\[[^\]]*\]\s*[^\n]+(?:\n\d+\.\s*\[[^\]]*\]\s*[^\n]+)*'
|
| 17 |
+
matches = re.findall(plan_pattern, content)
|
| 18 |
+
# Return the last (most recent) plan if multiple plans exist
|
| 19 |
+
return matches[-1] if matches else None
|
| 20 |
+
|
| 21 |
+
@staticmethod
|
| 22 |
+
def update_plan_for_solution(plan_text: str) -> str:
|
| 23 |
+
"""Update plan to mark all remaining steps as completed when providing final solution."""
|
| 24 |
+
if not plan_text:
|
| 25 |
+
return plan_text
|
| 26 |
+
|
| 27 |
+
lines = plan_text.split('\n')
|
| 28 |
+
updated_lines = []
|
| 29 |
+
|
| 30 |
+
for line in lines:
|
| 31 |
+
# Mark any unchecked or failed steps as completed since we're providing final solution
|
| 32 |
+
if '[ ]' in line or '[✗]' in line:
|
| 33 |
+
updated_line = re.sub(r'\[\s*[^\]]*\]', '[✓]', line)
|
| 34 |
+
updated_lines.append(updated_line)
|
| 35 |
+
else:
|
| 36 |
+
updated_lines.append(line)
|
| 37 |
+
|
| 38 |
+
return '\n'.join(updated_lines)
|
| 39 |
+
|
| 40 |
+
@staticmethod
|
| 41 |
+
def mark_step_completed(plan_text: str, step_description: str) -> str:
|
| 42 |
+
"""Mark a specific step as completed based on its description."""
|
| 43 |
+
if not plan_text or not step_description:
|
| 44 |
+
return plan_text
|
| 45 |
+
|
| 46 |
+
lines = plan_text.split('\n')
|
| 47 |
+
updated_lines = []
|
| 48 |
+
|
| 49 |
+
for line in lines:
|
| 50 |
+
# Check if this line contains the step description and is unchecked
|
| 51 |
+
if step_description.lower() in line.lower() and ('[ ]' in line or '[✗]' in line):
|
| 52 |
+
updated_line = re.sub(r'\[\s*[^\]]*\]', '[✓]', line)
|
| 53 |
+
updated_lines.append(updated_line)
|
| 54 |
+
else:
|
| 55 |
+
updated_lines.append(line)
|
| 56 |
+
|
| 57 |
+
return '\n'.join(updated_lines)
|
| 58 |
+
|
| 59 |
+
@staticmethod
|
| 60 |
+
def get_plan_progress(plan_text: str) -> Dict[str, int]:
|
| 61 |
+
"""Get plan progress statistics."""
|
| 62 |
+
if not plan_text:
|
| 63 |
+
return {"total": 0, "completed": 0, "pending": 0, "failed": 0}
|
| 64 |
+
|
| 65 |
+
lines = plan_text.split('\n')
|
| 66 |
+
stats = {"total": 0, "completed": 0, "pending": 0, "failed": 0}
|
| 67 |
+
|
| 68 |
+
for line in lines:
|
| 69 |
+
if re.search(r'\d+\.\s*\[', line):
|
| 70 |
+
stats["total"] += 1
|
| 71 |
+
if '[✓]' in line:
|
| 72 |
+
stats["completed"] += 1
|
| 73 |
+
elif '[ ]' in line:
|
| 74 |
+
stats["pending"] += 1
|
| 75 |
+
elif '[✗]' in line:
|
| 76 |
+
stats["failed"] += 1
|
| 77 |
+
|
| 78 |
+
return stats
|
managers/workflow/state_manager.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
State Manager for CodeAct Agent.
|
| 3 |
+
Manages agent state creation and manipulation.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import time
|
| 7 |
+
from typing import List, Dict, Optional
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class StateManager:
|
| 11 |
+
"""Manages agent state creation and manipulation."""
|
| 12 |
+
|
| 13 |
+
@staticmethod
|
| 14 |
+
def create_state_dict(messages: List = None, step_count: int = 0,
|
| 15 |
+
error_count: int = 0, start_time: float = None,
|
| 16 |
+
current_plan: str = None) -> Dict:
|
| 17 |
+
"""Create a standardized state dictionary."""
|
| 18 |
+
return {
|
| 19 |
+
"messages": messages or [],
|
| 20 |
+
"step_count": step_count,
|
| 21 |
+
"error_count": error_count,
|
| 22 |
+
"start_time": start_time or time.time(),
|
| 23 |
+
"current_plan": current_plan
|
| 24 |
+
}
|
managers/workflow/workflow_engine.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Workflow Engine Manager for CodeAct Agent.
|
| 3 |
+
Manages the LangGraph workflow execution.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import re
|
| 7 |
+
import json
|
| 8 |
+
import datetime
|
| 9 |
+
from typing import Dict, Tuple, List, Any
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
| 12 |
+
from rich.rule import Rule
|
| 13 |
+
from langchain_core.messages import AIMessage, HumanMessage, BaseMessage
|
| 14 |
+
from langgraph.graph import StateGraph, START, END
|
| 15 |
+
from core.types import AgentState, AgentConfig
|
| 16 |
+
from .plan_manager import PlanManager
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class WorkflowEngine:
|
| 20 |
+
"""Manages the LangGraph workflow execution."""
|
| 21 |
+
|
| 22 |
+
def __init__(self, model, config: AgentConfig, console_display, state_manager):
|
| 23 |
+
self.model = model
|
| 24 |
+
self.config = config
|
| 25 |
+
self.console = console_display
|
| 26 |
+
self.state_manager = state_manager
|
| 27 |
+
self.plan_manager = PlanManager()
|
| 28 |
+
self.graph = None
|
| 29 |
+
self.trace_logs = [] # Store all trace logs
|
| 30 |
+
self.message_history = [] # Store all messages
|
| 31 |
+
|
| 32 |
+
def setup_workflow(self, generate_func, execute_func, should_continue_func):
|
| 33 |
+
"""Setup the LangGraph workflow with provided functions."""
|
| 34 |
+
workflow = StateGraph(AgentState)
|
| 35 |
+
|
| 36 |
+
workflow.add_node("generate", generate_func)
|
| 37 |
+
workflow.add_node("execute", execute_func)
|
| 38 |
+
|
| 39 |
+
workflow.add_edge(START, "generate")
|
| 40 |
+
workflow.add_edge("execute", "generate")
|
| 41 |
+
|
| 42 |
+
workflow.add_conditional_edges("generate", should_continue_func, {
|
| 43 |
+
"end": END,
|
| 44 |
+
"execute": "execute"
|
| 45 |
+
})
|
| 46 |
+
|
| 47 |
+
self.graph = workflow.compile()
|
| 48 |
+
|
| 49 |
+
def run_workflow(self, initial_state: Dict) -> Tuple:
|
| 50 |
+
"""Execute the workflow and handle display.
|
| 51 |
+
|
| 52 |
+
Returns:
|
| 53 |
+
tuple: (result_content, final_state)
|
| 54 |
+
"""
|
| 55 |
+
# Clear previous traces for new run
|
| 56 |
+
self.trace_logs = []
|
| 57 |
+
self.message_history = []
|
| 58 |
+
|
| 59 |
+
# Track if final solution has been provided
|
| 60 |
+
final_solution_provided = False
|
| 61 |
+
previous_plan = None
|
| 62 |
+
displayed_reasoning = set()
|
| 63 |
+
|
| 64 |
+
# Stream the workflow execution with monitoring
|
| 65 |
+
self.console.console.print(Rule(title="Execution Steps", style="yellow"))
|
| 66 |
+
|
| 67 |
+
with Progress(
|
| 68 |
+
SpinnerColumn(),
|
| 69 |
+
TextColumn("[progress.description]{task.description}"),
|
| 70 |
+
console=self.console.console,
|
| 71 |
+
transient=True
|
| 72 |
+
) as progress:
|
| 73 |
+
task = progress.add_task("Executing agent...", total=None)
|
| 74 |
+
|
| 75 |
+
final_state = None
|
| 76 |
+
for s in self.graph.stream(initial_state, stream_mode="values"):
|
| 77 |
+
step_count = s.get("step_count", 0)
|
| 78 |
+
current_plan = s.get("current_plan")
|
| 79 |
+
final_state = s
|
| 80 |
+
|
| 81 |
+
progress.update(task, description=f"Step {step_count}")
|
| 82 |
+
|
| 83 |
+
message = s["messages"][-1]
|
| 84 |
+
|
| 85 |
+
# Serialize and store the message
|
| 86 |
+
serialized_msg = self._serialize_message(message)
|
| 87 |
+
self.message_history.append(serialized_msg)
|
| 88 |
+
|
| 89 |
+
# Process different types of messages
|
| 90 |
+
if isinstance(message, AIMessage):
|
| 91 |
+
self._process_ai_message(
|
| 92 |
+
message, step_count, current_plan, previous_plan,
|
| 93 |
+
displayed_reasoning, final_solution_provided
|
| 94 |
+
)
|
| 95 |
+
if current_plan != previous_plan:
|
| 96 |
+
previous_plan = current_plan
|
| 97 |
+
|
| 98 |
+
elif "<observation>" in message.content:
|
| 99 |
+
self._process_observation_message(message, step_count)
|
| 100 |
+
|
| 101 |
+
result_content = final_state["messages"][-1].content if final_state else ""
|
| 102 |
+
return result_content, final_state
|
| 103 |
+
|
| 104 |
+
def _process_ai_message(self, message, step_count, current_plan, previous_plan,
|
| 105 |
+
displayed_reasoning, final_solution_provided):
|
| 106 |
+
"""Process AI message and display appropriate panels."""
|
| 107 |
+
full_content = message.content
|
| 108 |
+
|
| 109 |
+
# 1. REASONING: Extract and display agent's thinking
|
| 110 |
+
thinking_content = self._extract_thinking_content(full_content)
|
| 111 |
+
if thinking_content and len(thinking_content) > 20:
|
| 112 |
+
content_hash = hash(thinking_content.strip())
|
| 113 |
+
if content_hash not in displayed_reasoning:
|
| 114 |
+
self.console.print_reasoning(thinking_content, step_count)
|
| 115 |
+
displayed_reasoning.add(content_hash)
|
| 116 |
+
# Add trace entry for reasoning
|
| 117 |
+
self._add_trace_entry("reasoning", step_count, thinking_content)
|
| 118 |
+
|
| 119 |
+
# 2. PLAN: Show plan only when it has changed
|
| 120 |
+
if (current_plan and current_plan != previous_plan and
|
| 121 |
+
self.config.verbose and not final_solution_provided):
|
| 122 |
+
self.console.print_plan(current_plan)
|
| 123 |
+
# Add trace entry for plan
|
| 124 |
+
self._add_trace_entry("plan", step_count, current_plan)
|
| 125 |
+
|
| 126 |
+
# 3. ACTION & CODE: Handle different action types
|
| 127 |
+
if "<execute>" in full_content and "</execute>" in full_content:
|
| 128 |
+
execute_match = re.search(r"<execute>(.*?)</execute>", full_content, re.DOTALL)
|
| 129 |
+
if execute_match:
|
| 130 |
+
code = execute_match.group(1).strip()
|
| 131 |
+
self.console.print_code_execution(code, step_count)
|
| 132 |
+
# Add trace entry for code execution
|
| 133 |
+
self._add_trace_entry("code_execution", step_count, code)
|
| 134 |
+
|
| 135 |
+
elif "<solution>" in full_content and "</solution>" in full_content:
|
| 136 |
+
solution_match = re.search(r"<solution>(.*?)</solution>", full_content, re.DOTALL)
|
| 137 |
+
if solution_match:
|
| 138 |
+
# Update plan to mark all remaining steps as completed
|
| 139 |
+
if current_plan:
|
| 140 |
+
updated_plan = self.plan_manager.update_plan_for_solution(current_plan)
|
| 141 |
+
if updated_plan != current_plan:
|
| 142 |
+
self.console.print_plan(updated_plan)
|
| 143 |
+
|
| 144 |
+
solution = solution_match.group(1).strip()
|
| 145 |
+
self.console.print_solution(solution, step_count)
|
| 146 |
+
final_solution_provided = True
|
| 147 |
+
# Add trace entry for solution
|
| 148 |
+
self._add_trace_entry("solution", step_count, solution)
|
| 149 |
+
|
| 150 |
+
elif "<error>" in full_content:
|
| 151 |
+
error_match = re.search(r"<error>(.*?)</error>", full_content, re.DOTALL)
|
| 152 |
+
if error_match:
|
| 153 |
+
error_content = error_match.group(1).strip()
|
| 154 |
+
self.console.print_error(error_content, step_count)
|
| 155 |
+
# Add trace entry for error
|
| 156 |
+
self._add_trace_entry("error", step_count, error_content)
|
| 157 |
+
|
| 158 |
+
def _process_observation_message(self, message, step_count):
|
| 159 |
+
"""Process observation message and display results."""
|
| 160 |
+
obs_match = re.search(r"<observation>(.*?)</observation>", message.content, re.DOTALL)
|
| 161 |
+
if obs_match:
|
| 162 |
+
observation = obs_match.group(1).strip()
|
| 163 |
+
formatted_output = self._truncate_to_20_rows(observation)
|
| 164 |
+
self.console.print_execution_result(formatted_output, step_count)
|
| 165 |
+
# Add trace entry for observation
|
| 166 |
+
self._add_trace_entry("observation", step_count, observation)
|
| 167 |
+
|
| 168 |
+
def _extract_thinking_content(self, content: str) -> str:
|
| 169 |
+
"""Extract thinking content from the message, removing tags and plan information."""
|
| 170 |
+
# Remove specific tags but keep observation content for separate handling
|
| 171 |
+
content = re.sub(r'</?(execute|solution|error)>', '', content)
|
| 172 |
+
|
| 173 |
+
# Remove plan content (numbered lists with checkboxes)
|
| 174 |
+
plan_pattern = r'\d+\.\s*\[[^\]]*\]\s*[^\n]+(?:\n\d+\.\s*\[[^\]]*\]\s*[^\n]+)*'
|
| 175 |
+
content = re.sub(plan_pattern, '', content).strip()
|
| 176 |
+
|
| 177 |
+
# Remove observation blocks entirely
|
| 178 |
+
content = re.sub(r'<observation>.*?</observation>', '', content, flags=re.DOTALL)
|
| 179 |
+
|
| 180 |
+
# Clean up extra whitespace and empty lines
|
| 181 |
+
lines = [line.strip() for line in content.split('\n') if line.strip()]
|
| 182 |
+
return '\n'.join(lines)
|
| 183 |
+
|
| 184 |
+
def _truncate_to_20_rows(self, text: str) -> str:
|
| 185 |
+
"""Truncate any text output to show only the first 20 rows."""
|
| 186 |
+
lines = text.split('\n')
|
| 187 |
+
|
| 188 |
+
if len(lines) > 20:
|
| 189 |
+
truncated = '\n'.join(lines[:20])
|
| 190 |
+
total_lines = len(lines)
|
| 191 |
+
truncated += f"\n\n⚠️ Output truncated to 20 rows. Full output contains {total_lines} rows."
|
| 192 |
+
return truncated
|
| 193 |
+
|
| 194 |
+
return text
|
| 195 |
+
|
| 196 |
+
def _add_trace_entry(self, step_type: str, step_count: int, content: Any, metadata: Dict = None):
|
| 197 |
+
"""Add an entry to the trace log."""
|
| 198 |
+
entry = {
|
| 199 |
+
"timestamp": datetime.datetime.now().isoformat(),
|
| 200 |
+
"step_count": step_count,
|
| 201 |
+
"step_type": step_type,
|
| 202 |
+
"content": content,
|
| 203 |
+
"metadata": metadata or {}
|
| 204 |
+
}
|
| 205 |
+
self.trace_logs.append(entry)
|
| 206 |
+
|
| 207 |
+
def _serialize_message(self, message: BaseMessage) -> Dict:
|
| 208 |
+
"""Serialize a message for saving."""
|
| 209 |
+
if isinstance(message, HumanMessage):
|
| 210 |
+
msg_type = "human"
|
| 211 |
+
elif isinstance(message, AIMessage):
|
| 212 |
+
msg_type = "ai"
|
| 213 |
+
else:
|
| 214 |
+
msg_type = "system"
|
| 215 |
+
|
| 216 |
+
return {
|
| 217 |
+
"type": msg_type,
|
| 218 |
+
"content": message.content,
|
| 219 |
+
"timestamp": datetime.datetime.now().isoformat()
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
def save_trace_to_file(self, filepath: str = None) -> str:
|
| 223 |
+
"""Save the complete trace log to a JSON file."""
|
| 224 |
+
if filepath is None:
|
| 225 |
+
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 226 |
+
filepath = f"agent_trace_{timestamp}.json"
|
| 227 |
+
|
| 228 |
+
trace_data = {
|
| 229 |
+
"execution_time": datetime.datetime.now().isoformat(),
|
| 230 |
+
"config": {
|
| 231 |
+
"max_steps": self.config.max_steps,
|
| 232 |
+
"timeout_seconds": self.config.timeout_seconds,
|
| 233 |
+
"verbose": self.config.verbose
|
| 234 |
+
},
|
| 235 |
+
"messages": self.message_history,
|
| 236 |
+
"trace_logs": self.trace_logs
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
with open(filepath, 'w', encoding='utf-8') as f:
|
| 240 |
+
json.dump(trace_data, f, indent=2, ensure_ascii=False)
|
| 241 |
+
|
| 242 |
+
return filepath
|
| 243 |
+
|
| 244 |
+
def generate_summary(self) -> Dict:
|
| 245 |
+
"""Generate a summary of the agent execution."""
|
| 246 |
+
summary = {
|
| 247 |
+
"total_steps": len(self.trace_logs),
|
| 248 |
+
"message_count": len(self.message_history),
|
| 249 |
+
"execution_flow": [],
|
| 250 |
+
"code_executions": [],
|
| 251 |
+
"observations": [],
|
| 252 |
+
"errors": [],
|
| 253 |
+
"final_solution": None
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
for entry in self.trace_logs:
|
| 257 |
+
step_info = {
|
| 258 |
+
"step": entry["step_count"],
|
| 259 |
+
"type": entry["step_type"],
|
| 260 |
+
"timestamp": entry["timestamp"]
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
if entry["step_type"] == "reasoning":
|
| 264 |
+
summary["execution_flow"].append({
|
| 265 |
+
**step_info,
|
| 266 |
+
"reasoning": entry["content"][:200] + "..." if len(entry["content"]) > 200 else entry["content"]
|
| 267 |
+
})
|
| 268 |
+
elif entry["step_type"] == "code_execution":
|
| 269 |
+
summary["code_executions"].append({
|
| 270 |
+
**step_info,
|
| 271 |
+
"code": entry["content"]
|
| 272 |
+
})
|
| 273 |
+
elif entry["step_type"] == "observation":
|
| 274 |
+
summary["observations"].append({
|
| 275 |
+
**step_info,
|
| 276 |
+
"output": entry["content"][:500] + "..." if len(entry["content"]) > 500 else entry["content"]
|
| 277 |
+
})
|
| 278 |
+
elif entry["step_type"] == "error":
|
| 279 |
+
summary["errors"].append({
|
| 280 |
+
**step_info,
|
| 281 |
+
"error": entry["content"]
|
| 282 |
+
})
|
| 283 |
+
elif entry["step_type"] == "solution":
|
| 284 |
+
summary["final_solution"] = {
|
| 285 |
+
**step_info,
|
| 286 |
+
"solution": entry["content"]
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
return summary
|
| 290 |
+
|
| 291 |
+
def save_summary_to_file(self, filepath: str = None) -> str:
|
| 292 |
+
"""Save the execution summary to a JSON file."""
|
| 293 |
+
if filepath is None:
|
| 294 |
+
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 295 |
+
filepath = f"agent_summary_{timestamp}.json"
|
| 296 |
+
|
| 297 |
+
summary = self.generate_summary()
|
| 298 |
+
summary["timestamp"] = datetime.datetime.now().isoformat()
|
| 299 |
+
|
| 300 |
+
with open(filepath, 'w', encoding='utf-8') as f:
|
| 301 |
+
json.dump(summary, f, indent=2, ensure_ascii=False)
|
| 302 |
+
|
| 303 |
+
return filepath
|