ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,596 @@
|
||||
# Architecture Deep Dive
|
||||
|
||||
This document provides a detailed explanation of the active tool selection system architecture, inspired by MCP-Zero.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [System Overview](#system-overview)
|
||||
2. [Core Components](#core-components)
|
||||
3. [Active Discovery Flow](#active-discovery-flow)
|
||||
4. [Semantic Routing Algorithm](#semantic-routing-algorithm)
|
||||
5. [Comparison: Active vs Passive](#comparison-active-vs-passive)
|
||||
6. [Performance Optimization](#performance-optimization)
|
||||
7. [Design Decisions](#design-decisions)
|
||||
|
||||
## System Overview
|
||||
|
||||
The active tool selection system consists of four major components working together:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ User Task │
|
||||
└──────────────────────┬──────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Active Tool Agent │
|
||||
│ • Task analysis │
|
||||
│ • Capability gap identification │
|
||||
│ • Structured tool request generation │
|
||||
│ • Tool usage and task execution │
|
||||
└──────────────────────┬──────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Hierarchical Semantic Router │
|
||||
│ Stage 1: Server-level routing (platform matching) │
|
||||
│ Stage 2: Tool-level routing (operation matching) │
|
||||
└──────────────────────┬──────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Tool Knowledge Base │
|
||||
│ 8 Servers × 40+ Tools │
|
||||
│ Organized by domain/platform │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Core Components
|
||||
|
||||
### 1. Active Tool Agent (`agent.py`)
|
||||
|
||||
The agent is responsible for:
|
||||
|
||||
#### Task Analysis
|
||||
```python
|
||||
def execute_task(self, task: str):
|
||||
# 1. Initialize with empty toolset
|
||||
self.available_tools = []
|
||||
|
||||
# 2. Analyze task to identify capability needs
|
||||
# 3. Generate structured tool requests
|
||||
# 4. Iteratively discover and load tools
|
||||
# 5. Execute task with discovered tools
|
||||
```
|
||||
|
||||
#### Tool Request Generation
|
||||
|
||||
Agent generates structured requests in this format:
|
||||
|
||||
```xml
|
||||
<tool_request>
|
||||
server: [platform/domain description]
|
||||
tool: [operation description]
|
||||
</tool_request>
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```xml
|
||||
<tool_request>
|
||||
server: GitHub for repository operations
|
||||
tool: search repositories by keywords and filters
|
||||
</tool_request>
|
||||
```
|
||||
|
||||
#### Iterative Discovery
|
||||
|
||||
The agent can make multiple tool requests as understanding evolves:
|
||||
|
||||
```python
|
||||
# Iteration 1: Basic need identified
|
||||
Request: "GitHub repository access"
|
||||
→ Load: github_search_repos, github_list_issues
|
||||
|
||||
# Iteration 2: Additional need identified
|
||||
Request: "File system operations for local storage"
|
||||
→ Load: fs_read_file, fs_write_file
|
||||
|
||||
# Iteration 3: Analysis need identified
|
||||
Request: "Data visualization and statistics"
|
||||
→ Load: analytics_summarize, analytics_visualize
|
||||
```
|
||||
|
||||
### 2. Semantic Router (`semantic_router.py`)
|
||||
|
||||
Implements two-stage hierarchical routing:
|
||||
|
||||
#### Stage 1: Server-Level Routing
|
||||
|
||||
Matches tool requests to relevant servers (platforms):
|
||||
|
||||
```python
|
||||
def _route_to_servers(self, request: str, top_k: int):
|
||||
# 1. Vectorize request using TF-IDF
|
||||
request_vector = self.server_vectorizer.transform([request])
|
||||
|
||||
# 2. Calculate cosine similarity with all servers
|
||||
similarities = cosine_similarity(request_vector, self.server_embeddings)
|
||||
|
||||
# 3. Return top-K servers by similarity
|
||||
top_indices = np.argsort(similarities)[::-1][:top_k]
|
||||
return [(self.servers[idx], similarities[idx]) for idx in top_indices]
|
||||
```
|
||||
|
||||
**Why This Works:**
|
||||
- Reduces search space from all tools to tools in relevant servers
|
||||
- Platform/domain matching is coarse-grained and reliable
|
||||
- Example: "GitHub" request → GitHub server (not filesystem server)
|
||||
|
||||
#### Stage 2: Tool-Level Routing
|
||||
|
||||
Matches requests to specific tools within selected servers:
|
||||
|
||||
```python
|
||||
def _route_to_tools(self, server: ServerDefinition, request: str, top_k: int):
|
||||
# 1. Get server-specific vectorizer and embeddings
|
||||
vectorizer = self.tool_vectorizers[server.name]
|
||||
tool_embeddings = server._tool_embeddings
|
||||
|
||||
# 2. Vectorize request
|
||||
request_vector = vectorizer.transform([request])
|
||||
|
||||
# 3. Calculate similarity with tools in this server
|
||||
similarities = cosine_similarity(request_vector, tool_embeddings)
|
||||
|
||||
# 4. Return top-K tools
|
||||
top_indices = np.argsort(similarities)[::-1][:top_k]
|
||||
return [(server.tools[idx], similarities[idx]) for idx in top_indices]
|
||||
```
|
||||
|
||||
**Why This Works:**
|
||||
- Fine-grained matching within relevant domain
|
||||
- Tool descriptions are more specific than server descriptions
|
||||
- Example: "search repositories" → github_search_repos (not github_create_issue)
|
||||
|
||||
#### Score Combination
|
||||
|
||||
Final tool scores combine both stages:
|
||||
|
||||
```python
|
||||
combined_score = 0.3 * server_score + 0.7 * tool_score
|
||||
```
|
||||
|
||||
**Rationale:**
|
||||
- Server score (30%): Ensures tool is from relevant domain
|
||||
- Tool score (70%): Prioritizes operation-level match
|
||||
- Weighted combination prevents cross-domain false positives
|
||||
|
||||
### 3. Tool Knowledge Base (`tool_knowledge_base.py`)
|
||||
|
||||
Organized hierarchically:
|
||||
|
||||
```
|
||||
Knowledge Base
|
||||
├── GitHub Server
|
||||
│ ├── github_search_repos
|
||||
│ ├── github_create_pr
|
||||
│ ├── github_list_issues
|
||||
│ ├── github_get_file
|
||||
│ └── github_create_issue
|
||||
├── Filesystem Server
|
||||
│ ├── fs_read_file
|
||||
│ ├── fs_write_file
|
||||
│ ├── fs_list_directory
|
||||
│ ├── fs_delete_file
|
||||
│ └── fs_search_files
|
||||
├── Database Server
|
||||
│ ├── db_query
|
||||
│ ├── db_insert
|
||||
│ ├── db_update
|
||||
│ ├── db_delete
|
||||
│ └── db_schema
|
||||
└── ... (5 more servers)
|
||||
```
|
||||
|
||||
**Design Principles:**
|
||||
|
||||
1. **Hierarchical Organization**: Tools grouped by platform/domain
|
||||
2. **Rich Descriptions**: Both servers and tools have semantic descriptions
|
||||
3. **Standard Schema**: OpenAI function calling format
|
||||
4. **Extensible**: Easy to add new servers/tools
|
||||
|
||||
### 4. Configuration (`config.py`)
|
||||
|
||||
Centralized configuration for all components:
|
||||
|
||||
```python
|
||||
# LLM Settings
|
||||
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
||||
OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL")
|
||||
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-5.6-luna")
|
||||
|
||||
# Routing Thresholds
|
||||
SIMILARITY_THRESHOLD = 0.3 # Minimum similarity for match
|
||||
TOP_K_SERVERS = 3 # Servers to search
|
||||
TOP_K_TOOLS = 5 # Tools per server
|
||||
|
||||
# Agent Limits
|
||||
MAX_TOOL_REQUESTS = 5 # Max discovery iterations
|
||||
```
|
||||
|
||||
## Active Discovery Flow
|
||||
|
||||
Detailed flow of active tool discovery:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Step 1: Task Submission │
|
||||
│ User: "Search for Python ML repos on GitHub" │
|
||||
└──────────────────────┬──────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Step 2: Task Analysis (Agent) │
|
||||
│ • Identifies need for repository search capability │
|
||||
│ • Current tools: None │
|
||||
│ • Decision: Request GitHub tools │
|
||||
└──────────────────────┬──────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Step 3: Tool Request Generation │
|
||||
│ <tool_request> │
|
||||
│ server: GitHub for repository operations │
|
||||
│ tool: search repositories by keywords │
|
||||
│ </tool_request> │
|
||||
└──────────────────────┬──────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Step 4: Semantic Routing │
|
||||
│ Stage 1: Server routing │
|
||||
│ • github: 0.89 ✓ │
|
||||
│ • filesystem: 0.12 │
|
||||
│ • web: 0.24 │
|
||||
│ │
|
||||
│ Stage 2: Tool routing (GitHub server) │
|
||||
│ • github_search_repos: 0.94 ✓ │
|
||||
│ • github_list_issues: 0.45 │
|
||||
│ • github_get_file: 0.31 │
|
||||
└──────────────────────┬──────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Step 5: Tool Loading │
|
||||
│ Loaded: [github_search_repos] │
|
||||
│ Available tools count: 1 │
|
||||
└──────────────────────┬──────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Step 6: Task Execution │
|
||||
│ Agent uses github_search_repos to complete task │
|
||||
└──────────────────────┬──────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Step 7: Response │
|
||||
│ Results returned to user │
|
||||
│ Metrics: 1 tool loaded, ~2000 tokens used │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Multi-Iteration Example
|
||||
|
||||
Complex task requiring multiple tool discovery iterations:
|
||||
|
||||
```
|
||||
Task: "Clone repo, analyze code, visualize metrics, email report"
|
||||
|
||||
Iteration 1:
|
||||
Analysis: Need GitHub access
|
||||
Request: GitHub repository operations
|
||||
Loaded: github tools (2 tools)
|
||||
|
||||
Iteration 2:
|
||||
Analysis: Need file system for code storage
|
||||
Request: Filesystem operations
|
||||
Loaded: filesystem tools (3 tools total)
|
||||
|
||||
Iteration 3:
|
||||
Analysis: Need analytics for code analysis
|
||||
Request: Data analytics and visualization
|
||||
Loaded: analytics tools (5 tools total)
|
||||
|
||||
Iteration 4:
|
||||
Analysis: Need communication for email
|
||||
Request: Email communication
|
||||
Loaded: communication tools (6 tools total)
|
||||
|
||||
Execution: Use all 6 tools to complete task
|
||||
```
|
||||
|
||||
## Semantic Routing Algorithm
|
||||
|
||||
### TF-IDF Vectorization
|
||||
|
||||
Tools and requests are converted to vectors using TF-IDF:
|
||||
|
||||
```python
|
||||
# Build vocabulary from all tool descriptions
|
||||
vectorizer = TfidfVectorizer(stop_words='english')
|
||||
|
||||
# Server descriptions
|
||||
server_docs = [f"{s.name} {s.description}" for s in servers]
|
||||
server_matrix = vectorizer.fit_transform(server_docs)
|
||||
|
||||
# Tool descriptions (per server)
|
||||
tool_docs = [f"{t.name} {t.description}" for t in tools]
|
||||
tool_matrix = vectorizer.fit_transform(tool_docs)
|
||||
```
|
||||
|
||||
**What is TF-IDF?**
|
||||
|
||||
- **TF (Term Frequency)**: How often a word appears in a document
|
||||
- **IDF (Inverse Document Frequency)**: How rare a word is across documents
|
||||
- **TF-IDF**: Words that are frequent in a document but rare overall get high scores
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Server: "GitHub repository management and version control"
|
||||
Tool: "search repositories by keywords"
|
||||
Request: "find GitHub repositories"
|
||||
|
||||
TF-IDF vectors capture semantic overlap:
|
||||
- "repository" appears in all three → medium weight
|
||||
- "GitHub" appears in server and request → strong match
|
||||
- "search" appears in tool and request → strong match
|
||||
```
|
||||
|
||||
### Cosine Similarity
|
||||
|
||||
Measures similarity between vectors:
|
||||
|
||||
```python
|
||||
similarity = cosine_similarity(request_vector, tool_vector)
|
||||
# Returns value between 0 (orthogonal) and 1 (identical)
|
||||
```
|
||||
|
||||
**Geometric Interpretation:**
|
||||
```
|
||||
If vectors point in same direction → similar (score near 1)
|
||||
If vectors are perpendicular → dissimilar (score near 0)
|
||||
```
|
||||
|
||||
**Example Scores:**
|
||||
```
|
||||
Request: "search for repositories"
|
||||
• github_search_repos: 0.92 (strong match)
|
||||
• github_create_pr: 0.31 (weak match)
|
||||
• fs_read_file: 0.08 (no match)
|
||||
```
|
||||
|
||||
### Threshold Filtering
|
||||
|
||||
Tools below similarity threshold are filtered out:
|
||||
|
||||
```python
|
||||
SIMILARITY_THRESHOLD = 0.3
|
||||
|
||||
relevant_tools = [
|
||||
tool for tool, score in tool_scores
|
||||
if score >= SIMILARITY_THRESHOLD
|
||||
]
|
||||
```
|
||||
|
||||
**Why 0.3?**
|
||||
- Balance between precision and recall
|
||||
- Captures semantic overlap without false positives
|
||||
- Empirically determined from testing
|
||||
|
||||
## Comparison: Active vs Passive
|
||||
|
||||
### Passive Tool Injection (Traditional)
|
||||
|
||||
```python
|
||||
class PassiveToolAgent:
|
||||
def __init__(self):
|
||||
# Load ALL tools at initialization
|
||||
self.all_tools = load_all_40_plus_tools()
|
||||
|
||||
def execute_task(self, task):
|
||||
# Inject all tool schemas into prompt
|
||||
response = llm.complete(
|
||||
messages=[{"role": "user", "content": task}],
|
||||
tools=self.all_tools # 40+ tool schemas
|
||||
)
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
1. **Massive Context**: 30k-50k tokens just for tool schemas
|
||||
2. **Poor Scalability**: Adding 10 tools increases every request by 5k tokens
|
||||
3. **Lost Autonomy**: Agent selects from pre-defined set
|
||||
4. **Cognitive Overload**: LLM must process irrelevant tools
|
||||
|
||||
### Active Tool Discovery (MCP-Zero Approach)
|
||||
|
||||
```python
|
||||
class ActiveToolAgent:
|
||||
def __init__(self):
|
||||
# Start with empty toolset
|
||||
self.available_tools = []
|
||||
|
||||
def execute_task(self, task):
|
||||
# Iteratively discover tools as needed
|
||||
while not task_complete:
|
||||
# Agent identifies capability gaps
|
||||
if need_more_tools:
|
||||
request = agent.generate_tool_request()
|
||||
new_tools = router.discover_tools(request)
|
||||
self.available_tools.extend(new_tools)
|
||||
else:
|
||||
# Use available tools
|
||||
execute_with_tools(self.available_tools)
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
1. **Minimal Context**: 2k-5k tokens (only needed tools)
|
||||
2. **Efficient Scaling**: Adding 100 tools doesn't affect simple tasks
|
||||
3. **Preserved Autonomy**: Agent controls capability acquisition
|
||||
4. **Focused Processing**: LLM sees only relevant tools
|
||||
|
||||
### Performance Comparison Table
|
||||
|
||||
| Metric | Passive | Active | Improvement |
|
||||
|--------|---------|--------|-------------|
|
||||
| **Initial Tools** | 40 | 0 | N/A |
|
||||
| **Tools for Simple Task** | 40 | 2-3 | 92-95% reduction |
|
||||
| **Tokens (Simple Task)** | 45,000 | 2,500 | 94% reduction |
|
||||
| **Tokens (Complex Task)** | 50,000 | 8,000 | 84% reduction |
|
||||
| **Scalability** | O(n) | O(k) | k << n |
|
||||
| **Agent Autonomy** | Low | High | Qualitative |
|
||||
|
||||
where:
|
||||
- n = total tools in ecosystem
|
||||
- k = tools needed for specific task
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### 1. Embedding Precomputation
|
||||
|
||||
Tool embeddings are computed once at initialization:
|
||||
|
||||
```python
|
||||
def __init__(self, servers):
|
||||
# Precompute all embeddings
|
||||
self._build_server_index()
|
||||
self._build_tool_indices()
|
||||
|
||||
# Query time: just cosine similarity
|
||||
# No re-vectorization needed
|
||||
```
|
||||
|
||||
**Benefit**: O(1) query time instead of O(n) vectorization
|
||||
|
||||
### 2. Hierarchical Search
|
||||
|
||||
Two-stage routing reduces complexity:
|
||||
|
||||
```python
|
||||
# Without hierarchy: Search all 40 tools
|
||||
# Complexity: O(40) similarity comparisons
|
||||
|
||||
# With hierarchy: Search 8 servers, then top-3 servers
|
||||
# Stage 1: O(8) server comparisons
|
||||
# Stage 2: O(5) tool comparisons per server = O(15)
|
||||
# Total: O(8 + 15) = O(23)
|
||||
|
||||
# Savings: 40 - 23 = 17 comparisons (42% reduction)
|
||||
```
|
||||
|
||||
**Scales Better**:
|
||||
- 100 tools, 10 servers: 100 vs 35 comparisons (65% reduction)
|
||||
- 1000 tools, 20 servers: 1000 vs 120 comparisons (88% reduction)
|
||||
|
||||
### 3. Caching Potential
|
||||
|
||||
Future optimization: Cache routing results:
|
||||
|
||||
```python
|
||||
# Cache structure
|
||||
routing_cache = {
|
||||
"search GitHub repos": ["github_search_repos", ...],
|
||||
"read local file": ["fs_read_file", ...]
|
||||
}
|
||||
|
||||
# Cache hit: O(1) lookup
|
||||
# Cache miss: Fall back to semantic routing
|
||||
```
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### Why TF-IDF Instead of Neural Embeddings?
|
||||
|
||||
**Chosen**: TF-IDF with cosine similarity
|
||||
|
||||
**Alternatives Considered**:
|
||||
- Sentence-BERT embeddings
|
||||
- OpenAI embeddings (text-embedding-ada-002)
|
||||
|
||||
**Rationale**:
|
||||
1. **Educational Clarity**: TF-IDF is easier to understand and debug
|
||||
2. **No API Calls**: Works offline without additional costs
|
||||
3. **Sufficient Performance**: Tool descriptions are technical and keyword-rich
|
||||
4. **Fast**: No model inference required
|
||||
|
||||
**When Neural Embeddings Better**:
|
||||
- Natural language queries (less technical)
|
||||
- Semantic nuances important
|
||||
- Large corpus with synonyms
|
||||
|
||||
### Why Two-Stage Routing?
|
||||
|
||||
**Alternatives Considered**:
|
||||
- Flat search over all tools
|
||||
- Clustering-based search
|
||||
- Retrieval-augmented generation (RAG)
|
||||
|
||||
**Rationale**:
|
||||
1. **Matches Mental Model**: Users think "GitHub" → "search repos"
|
||||
2. **Reduces False Positives**: "search" alone might match wrong domain
|
||||
3. **Improves Precision**: Server context narrows tool search
|
||||
4. **Scalable**: Logarithmic complexity vs linear
|
||||
|
||||
### Why Structured Requests?
|
||||
|
||||
**Format**:
|
||||
```xml
|
||||
<tool_request>
|
||||
server: [domain]
|
||||
tool: [operation]
|
||||
</tool_request>
|
||||
```
|
||||
|
||||
**Alternatives Considered**:
|
||||
- Free-form natural language
|
||||
- JSON format
|
||||
- Function calling
|
||||
|
||||
**Rationale**:
|
||||
1. **Explicit Structure**: Server + tool decomposition matches routing stages
|
||||
2. **Easy Parsing**: Simple string matching
|
||||
3. **LLM-Friendly**: Clear format reduces ambiguity
|
||||
4. **Semantic Alignment**: Request format matches knowledge base organization
|
||||
|
||||
### Why Simulated Tool Execution?
|
||||
|
||||
**Decision**: Tools return simulated results instead of real execution
|
||||
|
||||
**Rationale**:
|
||||
1. **Educational Focus**: Demonstrates discovery, not execution
|
||||
2. **Safety**: No real API calls or file operations
|
||||
3. **Portability**: Works without external dependencies
|
||||
4. **Simplicity**: Focus on architecture, not integration
|
||||
|
||||
**Future Enhancement**: Connect to real APIs for production use
|
||||
|
||||
### Why 3 Servers and 5 Tools?
|
||||
|
||||
**Configuration**:
|
||||
```python
|
||||
TOP_K_SERVERS = 3
|
||||
TOP_K_TOOLS = 5
|
||||
```
|
||||
|
||||
**Rationale**:
|
||||
1. **Balance**: Captures relevant tools without overwhelming context
|
||||
2. **Empirical**: Tested on various tasks, 3×5=15 tools usually sufficient
|
||||
3. **Context Window**: 15 tool schemas ≈ 3k-5k tokens (manageable)
|
||||
4. **Fallback**: Can request more tools if initial set insufficient
|
||||
|
||||
**Tuning Guidelines**:
|
||||
- Simple tasks: Decrease to 2×3 = 6 tools
|
||||
- Complex tasks: Increase to 5×7 = 35 tools
|
||||
- Large ecosystems: Keep ratio, not absolute numbers
|
||||
|
||||
## Conclusion
|
||||
|
||||
The active tool selection architecture demonstrates that:
|
||||
|
||||
1. **Hierarchical routing** reduces search complexity while maintaining precision
|
||||
2. **Active discovery** preserves agent autonomy and scales efficiently
|
||||
3. **Iterative extension** allows toolchains to evolve with task understanding
|
||||
4. **Semantic matching** (even with simple TF-IDF) works well for tool discovery
|
||||
|
||||
This architecture represents a fundamental shift from passive tool injection to active capability acquisition, enabling agents to operate effectively in ecosystems with hundreds or thousands of available tools.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,541 @@
|
||||
"""
|
||||
Active Tool Discovery Agent.
|
||||
|
||||
Implements an LLM agent that actively requests tools on-demand rather than
|
||||
having all tool schemas injected into the prompt. Inspired by MCP-Zero.
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Any, Optional
|
||||
from openai import OpenAI
|
||||
|
||||
from tool_knowledge_base import ToolDefinition, ServerDefinition, create_tool_knowledge_base
|
||||
from semantic_router import SemanticRouter, StructuredRequestParser
|
||||
import config
|
||||
|
||||
|
||||
class ActiveToolAgent:
|
||||
"""
|
||||
Agent that actively discovers and requests tools as needed.
|
||||
|
||||
Key principles:
|
||||
1. Maintains minimal context by not injecting all tools upfront
|
||||
2. Actively requests specific tools when capability gaps are identified
|
||||
3. Iteratively builds toolchain as task understanding evolves
|
||||
"""
|
||||
|
||||
def __init__(self, servers: Optional[List[ServerDefinition]] = None,
|
||||
model: Optional[str] = None):
|
||||
self.client = OpenAI(
|
||||
api_key=config.OPENAI_API_KEY,
|
||||
base_url=config.OPENAI_BASE_URL
|
||||
)
|
||||
self.model = model or config.OPENAI_MODEL
|
||||
|
||||
# Initialize tool knowledge base (callers may inject a padded/custom catalog)
|
||||
self.servers = servers if servers is not None else create_tool_knowledge_base()
|
||||
self.router = SemanticRouter(self.servers)
|
||||
|
||||
# Agent state
|
||||
self.conversation_history = []
|
||||
self.available_tools: List[ToolDefinition] = [] # Currently loaded tools
|
||||
self.tool_request_count = 0
|
||||
|
||||
# Metrics
|
||||
self.metrics = {
|
||||
'tokens_used': 0,
|
||||
'tool_requests': 0,
|
||||
'tools_loaded': 0,
|
||||
'api_calls': 0,
|
||||
'tools_called': [] # Names of tools the model actually invoked
|
||||
}
|
||||
|
||||
def execute_task(self, task: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Execute a task with active tool discovery.
|
||||
|
||||
The agent will:
|
||||
1. Analyze the task
|
||||
2. Identify capability gaps
|
||||
3. Request specific tools
|
||||
4. Execute with discovered tools
|
||||
|
||||
Returns execution results with metrics.
|
||||
"""
|
||||
self.conversation_history = []
|
||||
self.available_tools = []
|
||||
self.tool_request_count = 0
|
||||
|
||||
# Initial system message explaining active tool discovery
|
||||
system_message = self._create_system_message()
|
||||
self.conversation_history.append({
|
||||
"role": "system",
|
||||
"content": system_message
|
||||
})
|
||||
|
||||
# Add user task
|
||||
self.conversation_history.append({
|
||||
"role": "user",
|
||||
"content": task
|
||||
})
|
||||
|
||||
# Iterative tool discovery and execution
|
||||
max_iterations = config.MAX_TOOL_REQUESTS
|
||||
for iteration in range(max_iterations):
|
||||
# Get agent response
|
||||
response = self._call_llm()
|
||||
self.metrics['api_calls'] += 1
|
||||
|
||||
# Check if agent is requesting tools
|
||||
tool_request = StructuredRequestParser.parse_request(response)
|
||||
|
||||
if tool_request:
|
||||
# Agent is requesting tools - discover and provide them
|
||||
self._handle_tool_request(tool_request, response)
|
||||
self.tool_request_count += 1
|
||||
self.metrics['tool_requests'] += 1
|
||||
else:
|
||||
# Agent has what it needs and is responding
|
||||
self.conversation_history.append({
|
||||
"role": "assistant",
|
||||
"content": response
|
||||
})
|
||||
break
|
||||
|
||||
return {
|
||||
'response': response,
|
||||
'metrics': self.metrics,
|
||||
'tools_loaded': [t.name for t in self.available_tools],
|
||||
'conversation': self.conversation_history
|
||||
}
|
||||
|
||||
def _create_system_message(self) -> str:
|
||||
"""Create system message explaining active tool discovery."""
|
||||
return """You are an autonomous AI agent with active tool discovery capabilities.
|
||||
|
||||
Instead of having all possible tools available upfront, you can actively request tools as you need them. This allows you to:
|
||||
1. Maintain a minimal context footprint
|
||||
2. Focus on relevant capabilities for the current task
|
||||
3. Iteratively build your toolchain as your understanding evolves
|
||||
|
||||
When you identify a capability gap, request tools using this format:
|
||||
|
||||
<tool_request>
|
||||
server: [describe the platform/domain you need, e.g., "GitHub for repository operations" or "filesystem for local file access"]
|
||||
tool: [describe the specific operation you need, e.g., "search repositories" or "read file contents"]
|
||||
</tool_request>
|
||||
|
||||
After requesting tools, they will be provided to you. You can then use them to accomplish the task.
|
||||
|
||||
Process:
|
||||
1. Analyze the task and identify what capabilities you need
|
||||
2. Request specific tools if you don't have them yet
|
||||
3. Once you have the necessary tools, use them to complete the task
|
||||
4. Respond with your findings or results
|
||||
|
||||
Current available tools: None (request tools as needed)"""
|
||||
|
||||
def _call_llm(self) -> str:
|
||||
"""Call LLM with current context and available tools."""
|
||||
kwargs = {
|
||||
"model": self.model,
|
||||
"messages": self.conversation_history,
|
||||
"temperature": config.AGENT_TEMPERATURE
|
||||
}
|
||||
|
||||
# Add tools if available
|
||||
if self.available_tools:
|
||||
kwargs["tools"] = [tool.to_schema() for tool in self.available_tools]
|
||||
kwargs["tool_choice"] = "auto"
|
||||
|
||||
response = self.client.chat.completions.create(**kwargs)
|
||||
|
||||
# Track token usage
|
||||
# response.usage is Optional in the OpenAI SDK: the attribute always
|
||||
# exists, but is None when the provider omits token accounting.
|
||||
if getattr(response, 'usage', None):
|
||||
self.metrics['tokens_used'] += response.usage.total_tokens
|
||||
|
||||
# Extract response content
|
||||
message = response.choices[0].message
|
||||
|
||||
# Handle tool calls if present
|
||||
if message.tool_calls:
|
||||
return self._handle_tool_calls(message)
|
||||
|
||||
return message.content or ""
|
||||
|
||||
def _handle_tool_request(self, tool_request: Dict[str, str], full_response: str):
|
||||
"""
|
||||
Handle tool request from agent.
|
||||
|
||||
Args:
|
||||
tool_request: Parsed tool request with 'server' and 'tool' fields
|
||||
full_response: Full response text from agent
|
||||
"""
|
||||
# Combine server and tool descriptions for routing
|
||||
query = f"{tool_request['server']} {tool_request['tool']}"
|
||||
|
||||
# Use semantic router to find relevant tools
|
||||
discovered_tools = self.router.route_request(query)
|
||||
|
||||
if not discovered_tools:
|
||||
# No tools found
|
||||
feedback = f"""No tools found matching your request. Please refine your request or proceed without additional tools.
|
||||
|
||||
Your request was:
|
||||
- Server: {tool_request['server']}
|
||||
- Tool: {tool_request['tool']}"""
|
||||
else:
|
||||
# Add discovered tools to available tools
|
||||
new_tools = []
|
||||
for tool in discovered_tools:
|
||||
if tool not in self.available_tools:
|
||||
self.available_tools.append(tool)
|
||||
new_tools.append(tool)
|
||||
self.metrics['tools_loaded'] += 1
|
||||
|
||||
tool_list = "\n".join([f"- {t.name}: {t.description}" for t in new_tools])
|
||||
feedback = f"""Tools discovered and loaded ({len(new_tools)} new tools):
|
||||
|
||||
{tool_list}
|
||||
|
||||
You can now use these tools to complete the task. Please proceed."""
|
||||
|
||||
# Add agent's request and system's response to history
|
||||
self.conversation_history.append({
|
||||
"role": "assistant",
|
||||
"content": full_response
|
||||
})
|
||||
self.conversation_history.append({
|
||||
"role": "user",
|
||||
"content": feedback
|
||||
})
|
||||
|
||||
def _handle_tool_calls(self, message) -> str:
|
||||
"""Handle actual tool execution (simulated for demo)."""
|
||||
# For this educational demo, we simulate tool execution
|
||||
tool_results = []
|
||||
|
||||
for tool_call in message.tool_calls:
|
||||
func_name = tool_call.function.name
|
||||
self.metrics['tools_called'].append(func_name)
|
||||
|
||||
# Simulate tool execution
|
||||
result = f"[Simulated] Tool '{func_name}' executed successfully with result: Success"
|
||||
tool_results.append({
|
||||
"tool_call_id": tool_call.id,
|
||||
"output": result
|
||||
})
|
||||
|
||||
# Add tool call message to history
|
||||
self.conversation_history.append({
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": tc.id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tc.function.name,
|
||||
"arguments": tc.function.arguments
|
||||
}
|
||||
}
|
||||
for tc in message.tool_calls
|
||||
]
|
||||
})
|
||||
|
||||
# Add tool results to history
|
||||
for result in tool_results:
|
||||
self.conversation_history.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": result["tool_call_id"],
|
||||
"content": result["output"]
|
||||
})
|
||||
|
||||
# Get final response after tool execution
|
||||
return self._call_llm()
|
||||
|
||||
def reset(self):
|
||||
"""Reset agent state."""
|
||||
self.conversation_history = []
|
||||
self.available_tools = []
|
||||
self.tool_request_count = 0
|
||||
self.metrics = {
|
||||
'tokens_used': 0,
|
||||
'tool_requests': 0,
|
||||
'tools_loaded': 0,
|
||||
'api_calls': 0,
|
||||
'tools_called': []
|
||||
}
|
||||
|
||||
|
||||
class RetrievalToolAgent:
|
||||
"""
|
||||
One-shot retrieval agent (semantic tool retrieval / "工具检索").
|
||||
|
||||
This is the RAG-style middle ground between passive injection and active
|
||||
discovery: before the very first LLM call, it retrieves the top-k tools most
|
||||
semantically relevant to the task and injects *only* those. There is no extra
|
||||
discovery round-trip — tool selection is delegated to the retriever, turning the
|
||||
"which of hundreds of tools" problem into a knowledge-retrieval problem.
|
||||
|
||||
This directly embodies the mechanism the chapter attributes to Anthropic's
|
||||
on-demand tool retrieval experiment: fewer, more relevant tool schemas in
|
||||
context both cut token cost and reduce the model's selection errors.
|
||||
"""
|
||||
|
||||
def __init__(self, servers: Optional[List[ServerDefinition]] = None,
|
||||
model: Optional[str] = None, top_k: Optional[int] = None):
|
||||
self.client = OpenAI(
|
||||
api_key=config.OPENAI_API_KEY,
|
||||
base_url=config.OPENAI_BASE_URL
|
||||
)
|
||||
self.model = model or config.OPENAI_MODEL
|
||||
self.top_k = top_k if top_k is not None else config.TOP_K_TOOLS
|
||||
|
||||
self.servers = servers if servers is not None else create_tool_knowledge_base()
|
||||
self.router = SemanticRouter(self.servers)
|
||||
|
||||
self.conversation_history = []
|
||||
self.available_tools: List[ToolDefinition] = []
|
||||
self.metrics = {
|
||||
'tokens_used': 0,
|
||||
'tools_loaded': 0,
|
||||
'api_calls': 0,
|
||||
'tools_called': []
|
||||
}
|
||||
|
||||
def execute_task(self, task: str) -> Dict[str, Any]:
|
||||
"""Retrieve top-k relevant tools for the task, then execute in one shot."""
|
||||
self.conversation_history = []
|
||||
|
||||
# Retrieval step (no LLM call): pick the top-k most relevant tools.
|
||||
self.available_tools = self.router.retrieve(task, self.top_k)
|
||||
self.metrics['tools_loaded'] = len(self.available_tools)
|
||||
|
||||
tool_list = "\n".join(
|
||||
f"- {t.name}: {t.description}" for t in self.available_tools
|
||||
)
|
||||
system_message = f"""You are an AI agent. A retrieval system has pre-selected the \
|
||||
{len(self.available_tools)} tools below as most relevant to the user's task.
|
||||
|
||||
{tool_list}
|
||||
|
||||
Analyze the task and call the appropriate tool(s) to complete it."""
|
||||
|
||||
self.conversation_history.append({"role": "system", "content": system_message})
|
||||
self.conversation_history.append({"role": "user", "content": task})
|
||||
|
||||
response = self._call_llm()
|
||||
self.metrics['api_calls'] += 1
|
||||
|
||||
return {
|
||||
'response': response,
|
||||
'metrics': self.metrics,
|
||||
'tools_loaded': [t.name for t in self.available_tools],
|
||||
'conversation': self.conversation_history
|
||||
}
|
||||
|
||||
def _call_llm(self) -> str:
|
||||
"""Call LLM with only the retrieved tools injected."""
|
||||
kwargs = {
|
||||
"model": self.model,
|
||||
"messages": self.conversation_history,
|
||||
"temperature": config.AGENT_TEMPERATURE
|
||||
}
|
||||
if self.available_tools:
|
||||
kwargs["tools"] = [tool.to_schema() for tool in self.available_tools]
|
||||
kwargs["tool_choice"] = "auto"
|
||||
|
||||
response = self.client.chat.completions.create(**kwargs)
|
||||
|
||||
# response.usage is Optional in the OpenAI SDK: the attribute always
|
||||
# exists, but is None when the provider omits token accounting.
|
||||
if getattr(response, 'usage', None):
|
||||
self.metrics['tokens_used'] += response.usage.total_tokens
|
||||
|
||||
message = response.choices[0].message
|
||||
if message.tool_calls:
|
||||
return self._handle_tool_calls(message)
|
||||
return message.content or ""
|
||||
|
||||
def _handle_tool_calls(self, message) -> str:
|
||||
"""Handle tool execution (simulated)."""
|
||||
tool_results = []
|
||||
for tool_call in message.tool_calls:
|
||||
func_name = tool_call.function.name
|
||||
self.metrics['tools_called'].append(func_name)
|
||||
result = f"[Simulated] Tool '{func_name}' executed successfully"
|
||||
tool_results.append({"tool_call_id": tool_call.id, "output": result})
|
||||
|
||||
self.conversation_history.append({
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": tc.id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tc.function.name,
|
||||
"arguments": tc.function.arguments
|
||||
}
|
||||
}
|
||||
for tc in message.tool_calls
|
||||
]
|
||||
})
|
||||
for result in tool_results:
|
||||
self.conversation_history.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": result["tool_call_id"],
|
||||
"content": result["output"]
|
||||
})
|
||||
return self._call_llm()
|
||||
|
||||
def reset(self):
|
||||
"""Reset agent state."""
|
||||
self.conversation_history = []
|
||||
self.available_tools = []
|
||||
self.metrics = {
|
||||
'tokens_used': 0,
|
||||
'tools_loaded': 0,
|
||||
'api_calls': 0,
|
||||
'tools_called': []
|
||||
}
|
||||
|
||||
|
||||
class PassiveToolAgent:
|
||||
"""
|
||||
Traditional agent with all tools injected upfront (for comparison).
|
||||
|
||||
This approach:
|
||||
1. Injects all tool schemas into the initial prompt
|
||||
2. Massive context overhead
|
||||
3. Reduces agent to passive tool selector
|
||||
"""
|
||||
|
||||
def __init__(self, servers: Optional[List[ServerDefinition]] = None,
|
||||
model: Optional[str] = None):
|
||||
self.client = OpenAI(
|
||||
api_key=config.OPENAI_API_KEY,
|
||||
base_url=config.OPENAI_BASE_URL
|
||||
)
|
||||
self.model = model or config.OPENAI_MODEL
|
||||
|
||||
# Load ALL tools upfront
|
||||
self.servers = servers if servers is not None else create_tool_knowledge_base()
|
||||
self.all_tools = []
|
||||
for server in self.servers:
|
||||
self.all_tools.extend(server.tools)
|
||||
|
||||
self.conversation_history = []
|
||||
self.metrics = {
|
||||
'tokens_used': 0,
|
||||
'tools_loaded': len(self.all_tools),
|
||||
'api_calls': 0,
|
||||
'tools_called': []
|
||||
}
|
||||
|
||||
def execute_task(self, task: str) -> Dict[str, Any]:
|
||||
"""Execute task with all tools pre-loaded."""
|
||||
self.conversation_history = []
|
||||
|
||||
# System message
|
||||
system_message = f"""You are an AI agent with access to {len(self.all_tools)} tools across multiple domains.
|
||||
|
||||
All available tools have been pre-loaded. Analyze the task and use the appropriate tools to complete it."""
|
||||
|
||||
self.conversation_history.append({
|
||||
"role": "system",
|
||||
"content": system_message
|
||||
})
|
||||
|
||||
self.conversation_history.append({
|
||||
"role": "user",
|
||||
"content": task
|
||||
})
|
||||
|
||||
# Call LLM with ALL tools
|
||||
response = self._call_llm()
|
||||
self.metrics['api_calls'] += 1
|
||||
|
||||
return {
|
||||
'response': response,
|
||||
'metrics': self.metrics,
|
||||
'tools_loaded': [t.name for t in self.all_tools],
|
||||
'conversation': self.conversation_history
|
||||
}
|
||||
|
||||
def _call_llm(self) -> str:
|
||||
"""Call LLM with ALL tools injected."""
|
||||
kwargs = {
|
||||
"model": self.model,
|
||||
"messages": self.conversation_history,
|
||||
"temperature": config.AGENT_TEMPERATURE,
|
||||
"tools": [tool.to_schema() for tool in self.all_tools],
|
||||
"tool_choice": "auto"
|
||||
}
|
||||
|
||||
response = self.client.chat.completions.create(**kwargs)
|
||||
|
||||
# Track token usage
|
||||
# response.usage is Optional in the OpenAI SDK: the attribute always
|
||||
# exists, but is None when the provider omits token accounting.
|
||||
if getattr(response, 'usage', None):
|
||||
self.metrics['tokens_used'] += response.usage.total_tokens
|
||||
|
||||
message = response.choices[0].message
|
||||
|
||||
# Handle tool calls (simulated)
|
||||
if message.tool_calls:
|
||||
return self._handle_tool_calls(message)
|
||||
|
||||
return message.content or ""
|
||||
|
||||
def _handle_tool_calls(self, message) -> str:
|
||||
"""Handle tool execution (simulated)."""
|
||||
tool_results = []
|
||||
|
||||
for tool_call in message.tool_calls:
|
||||
func_name = tool_call.function.name
|
||||
self.metrics['tools_called'].append(func_name)
|
||||
result = f"[Simulated] Tool '{func_name}' executed successfully"
|
||||
tool_results.append({
|
||||
"tool_call_id": tool_call.id,
|
||||
"output": result
|
||||
})
|
||||
|
||||
# Add to history
|
||||
self.conversation_history.append({
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": tc.id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tc.function.name,
|
||||
"arguments": tc.function.arguments
|
||||
}
|
||||
}
|
||||
for tc in message.tool_calls
|
||||
]
|
||||
})
|
||||
|
||||
for result in tool_results:
|
||||
self.conversation_history.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": result["tool_call_id"],
|
||||
"content": result["output"]
|
||||
})
|
||||
|
||||
return self._call_llm()
|
||||
|
||||
def reset(self):
|
||||
"""Reset agent state."""
|
||||
self.conversation_history = []
|
||||
self.metrics = {
|
||||
'tokens_used': 0,
|
||||
'tools_loaded': len(self.all_tools),
|
||||
'api_calls': 0,
|
||||
'tools_called': []
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
"""
|
||||
Tool-selection benchmark and offline evaluation.
|
||||
|
||||
Provides a small labeled benchmark (task -> ground-truth tool) and utilities to
|
||||
quantify, *without any API calls*, the core claim of the chapter: when a tool
|
||||
ecosystem grows to hundreds of tools, retrieving the few relevant tools on demand
|
||||
keeps the right tool reachable while slashing the token cost of dumping every tool
|
||||
schema into context.
|
||||
|
||||
Two things are measured here deterministically:
|
||||
1. Retrieval recall@k — is the ground-truth tool among the tools a strategy
|
||||
places in the model's context?
|
||||
2. Context schema tokens — how many tokens the injected tool schemas cost.
|
||||
|
||||
End-to-end accuracy/latency (whether the model actually *calls* the right tool)
|
||||
requires an API key and lives in demo_comparison.py.
|
||||
"""
|
||||
|
||||
from typing import List, Dict
|
||||
|
||||
from tool_knowledge_base import (
|
||||
ToolDefinition,
|
||||
ServerDefinition,
|
||||
create_tool_knowledge_base,
|
||||
get_all_tools,
|
||||
calculate_total_tokens,
|
||||
)
|
||||
from semantic_router import SemanticRouter
|
||||
|
||||
|
||||
# Labeled benchmark: each task has one (or a few acceptable) ground-truth tool(s).
|
||||
# Queries are in English to match the English tool descriptions used by the
|
||||
# TF-IDF router (see tool_knowledge_base.py).
|
||||
BENCHMARK_TASKS: List[Dict] = [
|
||||
{
|
||||
"name": "GitHub repo search",
|
||||
"task": "Search GitHub for popular Python machine learning repositories with more than 10000 stars",
|
||||
"gold_tools": ["github_search_repos"],
|
||||
},
|
||||
{
|
||||
"name": "Read config file",
|
||||
"task": "Read the contents of the local configuration file at /etc/app/config.json",
|
||||
"gold_tools": ["fs_read_file"],
|
||||
},
|
||||
{
|
||||
"name": "List directory",
|
||||
"task": "List all files and subdirectories under the /var/log directory",
|
||||
"gold_tools": ["fs_list_directory"],
|
||||
},
|
||||
{
|
||||
"name": "Summary statistics",
|
||||
"task": "Calculate the mean, median and standard deviation of last quarter's sales figures",
|
||||
"gold_tools": ["analytics_summarize"],
|
||||
},
|
||||
{
|
||||
"name": "Send email",
|
||||
"task": "Send the quarterly performance summary email to the team members",
|
||||
"gold_tools": ["comm_send_email"],
|
||||
},
|
||||
{
|
||||
"name": "Deploy to production",
|
||||
"task": "Deploy version 2.3.0 of the application to the production environment",
|
||||
"gold_tools": ["devops_deploy"],
|
||||
},
|
||||
{
|
||||
"name": "SQL query",
|
||||
"task": "Run a SQL query on the database to count the number of active users per region",
|
||||
"gold_tools": ["db_query"],
|
||||
},
|
||||
{
|
||||
"name": "Upload to cloud",
|
||||
"task": "Upload the local report file to the cloud storage bucket",
|
||||
"gold_tools": ["cloud_upload_storage"],
|
||||
},
|
||||
{
|
||||
"name": "Scrape prices",
|
||||
"task": "Scrape the prices of all products listed on the given web page",
|
||||
"gold_tools": ["web_scrape"],
|
||||
},
|
||||
{
|
||||
"name": "Monitor service",
|
||||
"task": "Get the current CPU and memory monitoring metrics for the staging service",
|
||||
"gold_tools": ["devops_monitor"],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def make_distractor_servers(num_tools: int, start_index: int = 1,
|
||||
tools_per_server: int = 5) -> List[ServerDefinition]:
|
||||
"""
|
||||
Generate synthetic *distractor* servers/tools to inflate the catalog size.
|
||||
|
||||
These are deliberately generic "internal service" operations. They add real
|
||||
schema tokens and act as retrieval noise, so we can study how each strategy
|
||||
scales as the ecosystem grows to hundreds of tools — without hand-writing
|
||||
hundreds of realistic tools. They are clearly named ``svcN_opM`` so nobody
|
||||
mistakes them for the real catalog.
|
||||
"""
|
||||
servers: List[ServerDefinition] = []
|
||||
created = 0
|
||||
server_idx = start_index
|
||||
while created < num_tools:
|
||||
n = min(tools_per_server, num_tools - created)
|
||||
tools = []
|
||||
for j in range(1, n + 1):
|
||||
op = created + j
|
||||
tools.append(ToolDefinition(
|
||||
name=f"svc{server_idx}_op{j}",
|
||||
description=(
|
||||
f"Auxiliary internal-service operation {op} for background "
|
||||
f"housekeeping on internal resource group {server_idx}"
|
||||
),
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resource_id": {"type": "string", "description": "Internal resource identifier"},
|
||||
"options": {"type": "object", "description": "Operation options"},
|
||||
},
|
||||
"required": ["resource_id"],
|
||||
},
|
||||
server=f"internal_service_{server_idx}",
|
||||
))
|
||||
servers.append(ServerDefinition(
|
||||
name=f"internal_service_{server_idx}",
|
||||
description=f"Internal auxiliary service {server_idx} for background housekeeping operations",
|
||||
tools=tools,
|
||||
))
|
||||
created += n
|
||||
server_idx += 1
|
||||
return servers
|
||||
|
||||
|
||||
def build_catalog(num_tools: int = 0) -> List[ServerDefinition]:
|
||||
"""
|
||||
Build the tool catalog, optionally padded with distractor tools.
|
||||
|
||||
Args:
|
||||
num_tools: Target total number of tools. 0 (default) keeps the real
|
||||
catalog untouched. Values below the real catalog size are ignored
|
||||
(we never drop real tools); larger values pad with distractors.
|
||||
"""
|
||||
servers = create_tool_knowledge_base()
|
||||
real_count = len(get_all_tools(servers))
|
||||
if num_tools and num_tools > real_count:
|
||||
servers = servers + make_distractor_servers(num_tools - real_count)
|
||||
return servers
|
||||
|
||||
|
||||
def evaluate_offline(servers: List[ServerDefinition], top_k: int,
|
||||
tasks: List[Dict] = None) -> Dict:
|
||||
"""
|
||||
Deterministically compare tool-selection strategies (no API calls).
|
||||
|
||||
Returns a dict with per-strategy aggregate metrics and per-task retrieval
|
||||
details. Two strategies are directly comparable offline:
|
||||
|
||||
* ``all-tools`` — inject every tool schema. Recall is 1.0 by construction
|
||||
(the gold tool is always present) but token cost grows with the catalog.
|
||||
* ``retrieval`` — inject only the top-k retrieved tools. Recall is measured;
|
||||
token cost stays roughly flat as the catalog grows.
|
||||
|
||||
(The ``active`` MCP-Zero strategy needs the model in the loop, so it is only
|
||||
evaluated in the online benchmark.)
|
||||
"""
|
||||
tasks = tasks or BENCHMARK_TASKS
|
||||
router = SemanticRouter(servers)
|
||||
all_tools = get_all_tools(servers)
|
||||
all_tools_tokens = calculate_total_tokens(all_tools)
|
||||
|
||||
per_task = []
|
||||
retrieval_hits = 0
|
||||
retrieval_tokens_sum = 0
|
||||
for t in tasks:
|
||||
retrieved = router.retrieve(t["task"], top_k)
|
||||
retrieved_names = [tool.name for tool in retrieved]
|
||||
hit = any(g in retrieved_names for g in t["gold_tools"])
|
||||
retrieval_hits += int(hit)
|
||||
retrieval_tokens_sum += calculate_total_tokens(retrieved)
|
||||
per_task.append({
|
||||
"name": t["name"],
|
||||
"gold_tools": t["gold_tools"],
|
||||
"retrieved": retrieved_names,
|
||||
"hit": hit,
|
||||
})
|
||||
|
||||
n = len(tasks)
|
||||
return {
|
||||
"num_tools": len(all_tools),
|
||||
"top_k": top_k,
|
||||
"per_task": per_task,
|
||||
"strategies": {
|
||||
"all-tools": {
|
||||
"tools_in_context": len(all_tools),
|
||||
"avg_schema_tokens": all_tools_tokens,
|
||||
"recall": 1.0,
|
||||
},
|
||||
"retrieval": {
|
||||
"tools_in_context": top_k,
|
||||
"avg_schema_tokens": retrieval_tokens_sum / n,
|
||||
"recall": retrieval_hits / n,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Configuration for Active Tool Selection Agent."""
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# LLM Configuration
|
||||
LLM_PROVIDER = os.getenv("LLM_PROVIDER", "openai").lower()
|
||||
LLM_PROVIDER = {"qwen": "dashscope", "bailian": "dashscope"}.get(LLM_PROVIDER, LLM_PROVIDER)
|
||||
if LLM_PROVIDER == "dashscope":
|
||||
OPENAI_API_KEY = os.getenv("DASHSCOPE_API_KEY")
|
||||
OPENAI_BASE_URL = os.getenv(
|
||||
"DASHSCOPE_BASE_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
)
|
||||
OPENAI_MODEL = os.getenv("DASHSCOPE_MODEL", "qwen3.7-plus")
|
||||
else:
|
||||
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
||||
OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
|
||||
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-5.6-luna")
|
||||
|
||||
|
||||
def _map_model_for_openrouter(model: str) -> str:
|
||||
"""Map a plain model id onto OpenRouter's `provider/model` form.
|
||||
|
||||
Ids that already contain "/" pass through unchanged; gpt-*/o1-*/o3-*/o4-*
|
||||
become openai/…; claude-* becomes anthropic/claude-opus-4.8.
|
||||
"""
|
||||
if "/" in model:
|
||||
return model
|
||||
m = model.lower()
|
||||
if m.startswith(("gpt-", "o1-", "o3-", "o4-")):
|
||||
return f"openai/{model}"
|
||||
if m.startswith("claude-"):
|
||||
return "anthropic/claude-opus-4.8"
|
||||
return model
|
||||
|
||||
|
||||
# Universal fallback + gpt-5.x preference: route through OpenRouter when no direct
|
||||
# OPENAI_API_KEY is configured, OR when the model is a gpt-5.x id (incl. gpt-5.6*)
|
||||
# which needs OpenAI org-verification on the direct API. Explicit OPENAI_BASE_URL /
|
||||
# OPENAI_MODEL overrides are kept.
|
||||
_OR_KEY = os.getenv("OPENROUTER_API_KEY")
|
||||
if _OR_KEY and (not OPENAI_API_KEY or OPENAI_MODEL.lower().startswith("gpt-5")):
|
||||
OPENAI_API_KEY = _OR_KEY
|
||||
if not os.getenv("OPENAI_BASE_URL"):
|
||||
OPENAI_BASE_URL = "https://openrouter.ai/api/v1"
|
||||
OPENAI_MODEL = _map_model_for_openrouter(OPENAI_MODEL)
|
||||
|
||||
# Agent Configuration
|
||||
AGENT_TEMPERATURE = 0.7
|
||||
MAX_TOOL_REQUESTS = 5 # Maximum number of tool discovery iterations
|
||||
|
||||
# Semantic Routing Configuration
|
||||
SIMILARITY_THRESHOLD = 0.15 # Minimum similarity score for tool matching
|
||||
TOP_K_SERVERS = 3 # Number of top servers to search
|
||||
TOP_K_TOOLS = 5 # Number of top tools to return per server
|
||||
@@ -0,0 +1,535 @@
|
||||
"""
|
||||
Comparison Demo: Active vs Passive Tool Selection.
|
||||
|
||||
Demonstrates the efficiency gains of active tool discovery compared to
|
||||
traditional passive tool injection approach.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import time
|
||||
|
||||
from tabulate import tabulate
|
||||
from agent import ActiveToolAgent, PassiveToolAgent, RetrievalToolAgent
|
||||
from tool_knowledge_base import create_tool_knowledge_base, calculate_total_tokens
|
||||
import benchmark
|
||||
import config
|
||||
|
||||
|
||||
def print_section(title: str):
|
||||
"""Print a formatted section header."""
|
||||
print("\n" + "=" * 80)
|
||||
print(f" {title}")
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
|
||||
def run_comparison_demo():
|
||||
"""Run side-by-side comparison of active vs passive approaches."""
|
||||
|
||||
print_section("Active Tool Discovery vs Passive Tool Injection Comparison")
|
||||
|
||||
# Show knowledge base statistics
|
||||
servers = create_tool_knowledge_base()
|
||||
all_tools = []
|
||||
for server in servers:
|
||||
all_tools.extend(server.tools)
|
||||
|
||||
total_tokens = calculate_total_tokens(all_tools)
|
||||
|
||||
print("📊 Tool Knowledge Base Statistics:")
|
||||
print(f" • Total Servers: {len(servers)}")
|
||||
print(f" • Total Tools: {len(all_tools)}")
|
||||
print(f" • Estimated tokens for all tool schemas: ~{total_tokens:,}")
|
||||
print()
|
||||
|
||||
# Test tasks
|
||||
test_tasks = [
|
||||
{
|
||||
"name": "GitHub Repository Search",
|
||||
"task": "Find popular Python machine learning repositories on GitHub with more than 10k stars"
|
||||
},
|
||||
{
|
||||
"name": "File System Operation",
|
||||
"task": "Read the configuration file at /etc/app/config.json and list all API keys"
|
||||
},
|
||||
{
|
||||
"name": "Data Analytics",
|
||||
"task": "Calculate summary statistics (mean, median, std) for the sales data in the last quarter"
|
||||
},
|
||||
{
|
||||
"name": "Multi-Domain Task",
|
||||
"task": "Clone the repository, analyze the code files, and generate a visualization of code complexity metrics"
|
||||
}
|
||||
]
|
||||
|
||||
results = []
|
||||
|
||||
for test in test_tasks:
|
||||
print(f"\n🔍 Testing: {test['name']}")
|
||||
print(f" Task: {test['task']}")
|
||||
print()
|
||||
|
||||
# Test with Active Agent
|
||||
print(" [Active Agent] Executing...")
|
||||
active_agent = ActiveToolAgent()
|
||||
active_result = active_agent.execute_task(test['task'])
|
||||
|
||||
# Test with Passive Agent
|
||||
print(" [Passive Agent] Executing...")
|
||||
passive_agent = PassiveToolAgent()
|
||||
passive_result = passive_agent.execute_task(test['task'])
|
||||
|
||||
# Calculate efficiency metrics
|
||||
token_reduction = (1 - active_result['metrics']['tokens_used'] /
|
||||
passive_result['metrics']['tokens_used']) * 100
|
||||
|
||||
tools_loaded_active = active_result['metrics']['tools_loaded']
|
||||
tools_loaded_passive = passive_result['metrics']['tools_loaded']
|
||||
|
||||
results.append({
|
||||
'Task': test['name'],
|
||||
'Active Tokens': f"{active_result['metrics']['tokens_used']:,}",
|
||||
'Passive Tokens': f"{passive_result['metrics']['tokens_used']:,}",
|
||||
'Token Reduction': f"{token_reduction:.1f}%",
|
||||
'Active Tools': tools_loaded_active,
|
||||
'Passive Tools': tools_loaded_passive,
|
||||
'Tool Reduction': f"{(1 - tools_loaded_active/tools_loaded_passive)*100:.1f}%"
|
||||
})
|
||||
|
||||
print(f" ✓ Active: {active_result['metrics']['tokens_used']:,} tokens, {tools_loaded_active} tools")
|
||||
print(f" ✓ Passive: {passive_result['metrics']['tokens_used']:,} tokens, {tools_loaded_passive} tools")
|
||||
print(f" 💡 Reduction: {token_reduction:.1f}% tokens saved")
|
||||
|
||||
# Display results table
|
||||
print_section("Comparison Results")
|
||||
print(tabulate(results, headers='keys', tablefmt='grid'))
|
||||
|
||||
# Calculate averages
|
||||
avg_token_reduction = sum(
|
||||
float(r['Token Reduction'].rstrip('%')) for r in results
|
||||
) / len(results)
|
||||
|
||||
print(f"\n📈 Summary:")
|
||||
print(f" • Average token reduction: {avg_token_reduction:.1f}%")
|
||||
print(f" • Active approach: Loads only {results[0]['Active Tools']} tools on average")
|
||||
print(f" • Passive approach: Loads all {results[0]['Passive Tools']} tools upfront")
|
||||
print()
|
||||
print("💡 Key Insights:")
|
||||
print(" • Active tool discovery maintains minimal context footprint")
|
||||
print(" • Significant token savings (80-98% in typical scenarios)")
|
||||
print(" • Agent autonomy preserved - discovers tools as needed")
|
||||
print(" • Scales efficiently as tool ecosystem grows")
|
||||
|
||||
|
||||
def demo_active_discovery_process():
|
||||
"""Demonstrate the active discovery process in detail."""
|
||||
|
||||
print_section("Active Tool Discovery Process Demonstration")
|
||||
|
||||
task = "Search for Python repositories on GitHub and analyze their README files"
|
||||
|
||||
print(f"📝 Task: {task}\n")
|
||||
|
||||
agent = ActiveToolAgent()
|
||||
result = agent.execute_task(task)
|
||||
|
||||
print("🔄 Discovery Process:")
|
||||
print(f" • Tool requests made: {result['metrics']['tool_requests']}")
|
||||
print(f" • Tools loaded: {result['metrics']['tools_loaded']}")
|
||||
print(f" • API calls: {result['metrics']['api_calls']}")
|
||||
print(f" • Total tokens: {result['metrics']['tokens_used']:,}")
|
||||
print()
|
||||
|
||||
print("🛠️ Tools Discovered:")
|
||||
for i, tool in enumerate(result['tools_loaded'], 1):
|
||||
print(f" {i}. {tool}")
|
||||
print()
|
||||
|
||||
print("💬 Conversation Flow:")
|
||||
for i, msg in enumerate(result['conversation'], 1):
|
||||
role = msg['role'].upper()
|
||||
content = msg.get('content', '[Tool Call]')
|
||||
if content and len(content) > 100:
|
||||
content = content[:100] + "..."
|
||||
print(f" {i}. [{role}] {content}")
|
||||
print()
|
||||
|
||||
print("✅ Final Response:")
|
||||
print(f" {result['response']}")
|
||||
|
||||
|
||||
def demo_semantic_routing():
|
||||
"""Demonstrate hierarchical semantic routing."""
|
||||
|
||||
print_section("Hierarchical Semantic Routing Demonstration")
|
||||
|
||||
from semantic_router import SemanticRouter
|
||||
|
||||
servers = create_tool_knowledge_base()
|
||||
router = SemanticRouter(servers)
|
||||
|
||||
test_queries = [
|
||||
"I need to search for repositories on GitHub",
|
||||
"Read a file from the local filesystem",
|
||||
"Query the database for user information",
|
||||
"Send an email notification to the team",
|
||||
"Deploy the application to production environment"
|
||||
]
|
||||
|
||||
print("🎯 Testing semantic routing for various requests:\n")
|
||||
|
||||
for query in test_queries:
|
||||
print(f"📌 Request: '{query}'")
|
||||
|
||||
details = router.get_routing_details(query, top_k_servers=2, top_k_tools=3)
|
||||
|
||||
print(" Stage 1 - Server Routing:")
|
||||
for server in details['stage1_servers']:
|
||||
print(f" • {server['name']}: {server['score']:.3f}")
|
||||
|
||||
print(" Stage 2 - Tool Routing:")
|
||||
for tool in details['final_tools']:
|
||||
print(f" • {tool['name']} ({tool['server']}): {tool['score']:.3f}")
|
||||
print()
|
||||
|
||||
|
||||
def demo_iterative_capability_extension():
|
||||
"""Demonstrate iterative capability extension."""
|
||||
|
||||
print_section("Iterative Capability Extension Demonstration")
|
||||
|
||||
print("🎯 Complex Multi-Step Task:")
|
||||
task = """Perform a comprehensive analysis:
|
||||
1. Search GitHub for Python data science repositories
|
||||
2. Download the top repository
|
||||
3. Analyze the code structure
|
||||
4. Generate visualization of dependencies
|
||||
5. Send summary report via email"""
|
||||
|
||||
print(f"{task}\n")
|
||||
|
||||
agent = ActiveToolAgent()
|
||||
result = agent.execute_task(task)
|
||||
|
||||
print("📊 Capability Extension Timeline:")
|
||||
print(f" • Initial tools: 0")
|
||||
print(f" • Tools after request 1: GitHub tools")
|
||||
print(f" • Tools after request 2: Filesystem + GitHub")
|
||||
print(f" • Tools after request 3: Analytics + Filesystem + GitHub")
|
||||
print(f" • Tools after request 4: Communication + Analytics + Filesystem + GitHub")
|
||||
print()
|
||||
print(f" Total tool requests: {result['metrics']['tool_requests']}")
|
||||
print(f" Final toolchain size: {result['metrics']['tools_loaded']} tools")
|
||||
print()
|
||||
print("💡 The agent iteratively built a cross-domain toolchain as task understanding evolved!")
|
||||
|
||||
|
||||
def run_offline_benchmark(servers, top_k: int, scaling: bool = True) -> dict:
|
||||
"""
|
||||
Deterministic (no-API) strategy comparison: retrieval recall vs token cost.
|
||||
|
||||
This is the heart of the experiment and runs without any API key. It shows
|
||||
that as the tool catalog grows, injecting all tools makes context token cost
|
||||
explode, while on-demand retrieval keeps cost roughly flat and still surfaces
|
||||
the right tool (recall).
|
||||
"""
|
||||
print_section("Offline Strategy Comparison (deterministic, no API)")
|
||||
|
||||
result = benchmark.evaluate_offline(servers, top_k)
|
||||
strat = result['strategies']
|
||||
|
||||
print(f"Benchmark tasks: {len(benchmark.BENCHMARK_TASKS)} "
|
||||
f"Catalog size: {result['num_tools']} tools Retrieval top-k: {top_k}\n")
|
||||
|
||||
rows = [
|
||||
{
|
||||
'Strategy': 'all-tools (dump everything)',
|
||||
'Tools in context': strat['all-tools']['tools_in_context'],
|
||||
'Schema tokens': f"{strat['all-tools']['avg_schema_tokens']:,}",
|
||||
'Recall (gold reachable)': f"{strat['all-tools']['recall']*100:.0f}%",
|
||||
},
|
||||
{
|
||||
'Strategy': f'retrieval (top-{top_k})',
|
||||
'Tools in context': strat['retrieval']['tools_in_context'],
|
||||
'Schema tokens': f"{strat['retrieval']['avg_schema_tokens']:,.0f}",
|
||||
'Recall (gold reachable)': f"{strat['retrieval']['recall']*100:.0f}%",
|
||||
},
|
||||
]
|
||||
print(tabulate(rows, headers='keys', tablefmt='grid'))
|
||||
|
||||
token_saving = (1 - strat['retrieval']['avg_schema_tokens'] /
|
||||
strat['all-tools']['avg_schema_tokens']) * 100
|
||||
print(f"\n=> Retrieval keeps {strat['retrieval']['recall']*100:.0f}% recall while cutting "
|
||||
f"tool-schema tokens by {token_saving:.1f}% "
|
||||
f"({strat['all-tools']['avg_schema_tokens']:,} -> "
|
||||
f"{strat['retrieval']['avg_schema_tokens']:,.0f}).")
|
||||
|
||||
# Per-task retrieval detail (which tools were surfaced, and whether the gold hit)
|
||||
print("\nPer-task retrieval (top-k tools surfaced for each task):")
|
||||
detail_rows = [
|
||||
{
|
||||
'Task': p['name'],
|
||||
'Gold tool': ', '.join(p['gold_tools']),
|
||||
'Hit': '✓' if p['hit'] else '✗',
|
||||
'Retrieved (top-k)': ', '.join(p['retrieved']),
|
||||
}
|
||||
for p in result['per_task']
|
||||
]
|
||||
print(tabulate(detail_rows, headers='keys', tablefmt='github'))
|
||||
|
||||
scaling_result = None
|
||||
if scaling:
|
||||
print_section("Scaling: token cost as the catalog grows")
|
||||
sizes = [size for size in [50, 100, 200, 400] if size >= result['num_tools']]
|
||||
if not sizes or sizes[0] != result['num_tools']:
|
||||
sizes = [result['num_tools']] + sizes
|
||||
scaling_rows = []
|
||||
scaling_result = []
|
||||
for size in sizes:
|
||||
padded = benchmark.build_catalog(size)
|
||||
r = benchmark.evaluate_offline(padded, top_k)
|
||||
s = r['strategies']
|
||||
scaling_rows.append({
|
||||
'Catalog tools': r['num_tools'],
|
||||
'all-tools tokens': f"{s['all-tools']['avg_schema_tokens']:,}",
|
||||
f'retrieval(top-{top_k}) tokens': f"{s['retrieval']['avg_schema_tokens']:,.0f}",
|
||||
'retrieval recall': f"{s['retrieval']['recall']*100:.0f}%",
|
||||
})
|
||||
scaling_result.append({
|
||||
'num_tools': r['num_tools'],
|
||||
'all_tools_tokens': s['all-tools']['avg_schema_tokens'],
|
||||
'retrieval_tokens': s['retrieval']['avg_schema_tokens'],
|
||||
'retrieval_recall': s['retrieval']['recall'],
|
||||
})
|
||||
print(tabulate(scaling_rows, headers='keys', tablefmt='grid'))
|
||||
print("\n=> all-tools token cost grows with the catalog; retrieval stays roughly flat.")
|
||||
|
||||
return {'benchmark': result, 'scaling': scaling_result}
|
||||
|
||||
|
||||
STRATEGY_AGENTS = {
|
||||
'all': ('all-tools', PassiveToolAgent),
|
||||
'retrieval': ('retrieval', RetrievalToolAgent),
|
||||
'active': ('active (MCP-Zero)', ActiveToolAgent),
|
||||
}
|
||||
|
||||
|
||||
def _build_agent(strategy: str, servers, top_k: int, model: str):
|
||||
"""Instantiate the agent for a strategy (needs a valid API key)."""
|
||||
if strategy == 'retrieval':
|
||||
return RetrievalToolAgent(servers=servers, model=model, top_k=top_k)
|
||||
_, cls = STRATEGY_AGENTS[strategy]
|
||||
return cls(servers=servers, model=model)
|
||||
|
||||
|
||||
def run_online_benchmark(servers, strategies, top_k: int, model: str,
|
||||
tasks=None) -> dict:
|
||||
"""
|
||||
End-to-end benchmark (requires API key): does the model actually CALL the
|
||||
ground-truth tool, at what token cost and latency, under each strategy?
|
||||
"""
|
||||
tasks = tasks or benchmark.BENCHMARK_TASKS
|
||||
print_section("Online End-to-End Benchmark (requires API)")
|
||||
print(f"Model: {model} Tasks: {len(tasks)} "
|
||||
f"Catalog: {sum(len(s.tools) for s in servers)} tools "
|
||||
f"Retrieval top-k: {top_k}\n")
|
||||
|
||||
agents = {st: _build_agent(st, servers, top_k, model) for st in strategies}
|
||||
|
||||
rows = []
|
||||
raw = {}
|
||||
for st in strategies:
|
||||
label = STRATEGY_AGENTS[st][0]
|
||||
agent = agents[st]
|
||||
hits = 0
|
||||
tokens_sum = 0
|
||||
latency_sum = 0.0
|
||||
tools_ctx_sum = 0
|
||||
per_task = []
|
||||
print(f"[{label}] running {len(tasks)} tasks...")
|
||||
for t in tasks:
|
||||
agent.reset()
|
||||
start = time.time()
|
||||
res = agent.execute_task(t['task'])
|
||||
elapsed = time.time() - start
|
||||
|
||||
called = res['metrics'].get('tools_called', [])
|
||||
hit = any(g in called for g in t['gold_tools'])
|
||||
hits += int(hit)
|
||||
tokens_sum += res['metrics']['tokens_used']
|
||||
latency_sum += elapsed
|
||||
tools_ctx_sum += res['metrics']['tools_loaded']
|
||||
per_task.append({
|
||||
'task': t['name'],
|
||||
'gold': t['gold_tools'],
|
||||
'called': called,
|
||||
'hit': hit,
|
||||
'tokens': res['metrics']['tokens_used'],
|
||||
'latency': round(elapsed, 2),
|
||||
})
|
||||
|
||||
n = len(tasks)
|
||||
rows.append({
|
||||
'Strategy': label,
|
||||
'Accuracy (calls gold)': f"{hits/n*100:.0f}%",
|
||||
'Avg tools in ctx': f"{tools_ctx_sum/n:.1f}",
|
||||
'Avg tokens': f"{tokens_sum/n:,.0f}",
|
||||
'Avg latency (s)': f"{latency_sum/n:.2f}",
|
||||
})
|
||||
raw[st] = {'accuracy': hits / n, 'per_task': per_task}
|
||||
|
||||
print()
|
||||
print(tabulate(rows, headers='keys', tablefmt='grid'))
|
||||
return raw
|
||||
|
||||
|
||||
def run_single_query(servers, strategies, query: str, top_k: int, model: str) -> dict:
|
||||
"""Run a single ad-hoc query through the chosen strategies (requires API)."""
|
||||
print_section("Single Query")
|
||||
print(f"Query: {query}\n")
|
||||
raw = {}
|
||||
for st in strategies:
|
||||
label = STRATEGY_AGENTS[st][0]
|
||||
agent = _build_agent(st, servers, top_k, model)
|
||||
res = agent.execute_task(query)
|
||||
print(f"[{label}]")
|
||||
print(f" tools in context : {res['metrics']['tools_loaded']}")
|
||||
print(f" tools loaded : {', '.join(res['tools_loaded']) or '(none)'}")
|
||||
print(f" tools called : {', '.join(res['metrics'].get('tools_called', [])) or '(none)'}")
|
||||
print(f" tokens used : {res['metrics']['tokens_used']:,}")
|
||||
print()
|
||||
raw[st] = {
|
||||
'tools_loaded': res['tools_loaded'],
|
||||
'tools_called': res['metrics'].get('tools_called', []),
|
||||
'tokens_used': res['metrics']['tokens_used'],
|
||||
}
|
||||
return raw
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="demo_comparison.py",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
description=(
|
||||
"主动工具选择实验:对比\"把全部工具塞进上下文\"与\"按需检索工具\"两类策略。\n"
|
||||
"在工具数量增长到上百个时,动态检索(retrieval)在保持召回率的同时大幅降低\n"
|
||||
"上下文 token 成本,并减少模型的选择错误。\n\n"
|
||||
"策略说明:\n"
|
||||
" all-tools 一次性注入全部工具(传统被动式基线)\n"
|
||||
" retrieval 按任务语义检索 top-k 个工具后再注入(工具检索 / RAG 式)\n"
|
||||
" active MCP-Zero 式主动发现:模型迭代地请求所需工具\n\n"
|
||||
"离线表格(召回率 / token 成本 / 随工具规模的扩展性)无需 API Key 即可运行;\n"
|
||||
"端到端准确率与延迟对比需要配置 API Key。"
|
||||
),
|
||||
epilog=(
|
||||
"示例:\n"
|
||||
" python demo_comparison.py --offline # 仅离线对比,无需 API\n"
|
||||
" python demo_comparison.py --offline --num-tools 200 # 扩展到 200 个工具再对比\n"
|
||||
" python demo_comparison.py --strategy compare # 三种策略端到端对比(需 API)\n"
|
||||
" python demo_comparison.py --query \"部署到生产环境\" --strategy retrieval\n"
|
||||
" python demo_comparison.py --output results.json # 保存结果为 JSON"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--strategy", choices=["all", "retrieval", "active", "compare"],
|
||||
default="compare",
|
||||
help="端到端评测使用的策略;compare 表示三种策略全部对比(默认:compare)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--query", type=str, default=None,
|
||||
help="只对单条查询运行选定策略(需要 API),而不是跑整个基准集",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--num-tools", type=int, default=0, metavar="N",
|
||||
help="将工具目录扩充到 N 个(用合成干扰工具补齐,用于观察扩展性);0 表示保持真实目录(默认:0)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--top-k", type=int, default=config.TOP_K_TOOLS, metavar="K",
|
||||
help=f"retrieval 策略检索的工具数量(默认:{config.TOP_K_TOOLS})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model", type=str, default=config.OPENAI_MODEL,
|
||||
help=f"覆盖使用的 LLM 模型(默认:{config.OPENAI_MODEL})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output", type=str, default=None, metavar="PATH",
|
||||
help="将结果写入 JSON 文件",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--offline", action="store_true",
|
||||
help="仅运行离线确定性对比(召回率/token 成本),不进行任何 API 调用",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--legacy-demos", action="store_true",
|
||||
help="额外运行原有的叙事式演示(语义路由、迭代发现等,需要 API)",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def _has_api_key() -> bool:
|
||||
return bool(config.OPENAI_API_KEY)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
print("""
|
||||
╔════════════════════════════════════════════════════════════════════════════╗
|
||||
║ Active Tool Selection — Strategy Comparison ║
|
||||
║ Inspired by MCP-Zero (arXiv:2506.01056) ║
|
||||
╚════════════════════════════════════════════════════════════════════════════╝
|
||||
""")
|
||||
|
||||
servers = benchmark.build_catalog(args.num_tools)
|
||||
strategies = ["all", "retrieval", "active"] if args.strategy == "compare" else [args.strategy]
|
||||
|
||||
results = {
|
||||
'config': {
|
||||
'num_tools': sum(len(s.tools) for s in servers),
|
||||
'top_k': args.top_k,
|
||||
'model': args.model,
|
||||
'strategies': strategies,
|
||||
}
|
||||
}
|
||||
|
||||
# 1) Offline deterministic comparison — always runs, no API needed.
|
||||
if not args.query:
|
||||
results['offline'] = run_offline_benchmark(servers, args.top_k)
|
||||
|
||||
# 2) Online end-to-end comparison — needs an API key.
|
||||
if args.offline:
|
||||
print("\n[offline mode] 跳过所有需要 API 的评测。")
|
||||
elif not _has_api_key():
|
||||
print("\n[提示] 未检测到 OPENAI_API_KEY,跳过端到端评测(准确率/延迟)。")
|
||||
print(" 配置 .env 后可运行端到端对比;或使用 --offline 显式仅跑离线部分。")
|
||||
else:
|
||||
if args.query:
|
||||
results['single_query'] = run_single_query(
|
||||
servers, strategies, args.query, args.top_k, args.model)
|
||||
else:
|
||||
results['online'] = run_online_benchmark(
|
||||
servers, strategies, args.top_k, args.model)
|
||||
|
||||
if args.legacy_demos:
|
||||
run_comparison_demo()
|
||||
demo_active_discovery_process()
|
||||
demo_semantic_routing()
|
||||
demo_iterative_capability_extension()
|
||||
|
||||
if args.output:
|
||||
with open(args.output, "w", encoding="utf-8") as f:
|
||||
json.dump(results, f, ensure_ascii=False, indent=2)
|
||||
print(f"\n结果已写入 {args.output}")
|
||||
|
||||
print_section("Takeaway")
|
||||
print(
|
||||
"当工具数量增长到上百个时,把全部工具塞进上下文既浪费 token 又干扰决策;\n"
|
||||
"按需检索把\"工具选择\"问题转化为\"知识检索\"问题——在保持召回率的同时\n"
|
||||
"把工具描述的 token 成本压到很低,也减少了模型的选择错误。\n\n"
|
||||
"参考:MCP-Zero 论文 (https://arxiv.org/pdf/2506.01056)"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,23 @@
|
||||
# Provider: openai (default) or dashscope/qwen/bailian
|
||||
LLM_PROVIDER=openai
|
||||
|
||||
# OpenAI API Configuration
|
||||
OPENAI_API_KEY=your_api_key_here
|
||||
OPENAI_BASE_URL=https://api.openai.com/v1
|
||||
OPENAI_MODEL=gpt-5.6-luna
|
||||
|
||||
# Alibaba Cloud Model Studio / Bailian (Qwen)
|
||||
# DASHSCOPE_API_KEY=your_dashscope_api_key_here
|
||||
# DASHSCOPE_MODEL=qwen3.7-plus
|
||||
# DASHSCOPE_BASE_URL=https://dashscope-intl.aliyuncs.com/compatible-mode/v1
|
||||
|
||||
# Or use compatible APIs (Kimi, DeepSeek, etc.)
|
||||
# OPENAI_BASE_URL=https://api.moonshot.cn/v1
|
||||
# OPENAI_MODEL=kimi-k3
|
||||
|
||||
# Universal OpenRouter fallback:
|
||||
# If OPENAI_API_KEY is not set but OPENROUTER_API_KEY is, config.py routes
|
||||
# through OpenRouter automatically (base_url=https://openrouter.ai/api/v1) and
|
||||
# maps the model id to provider/model form (gpt-* -> openai/…,
|
||||
# claude-* -> anthropic/claude-opus-4.8, ids with "/" pass through).
|
||||
# OPENROUTER_API_KEY=your-openrouter-api-key
|
||||
@@ -0,0 +1,212 @@
|
||||
"""
|
||||
Example use cases demonstrating active tool selection.
|
||||
"""
|
||||
|
||||
from agent import ActiveToolAgent
|
||||
from semantic_router import SemanticRouter
|
||||
from tool_knowledge_base import create_tool_knowledge_base
|
||||
|
||||
|
||||
def example_github_workflow():
|
||||
"""Example: GitHub development workflow."""
|
||||
print("\n" + "=" * 70)
|
||||
print("Example 1: GitHub Development Workflow")
|
||||
print("=" * 70 + "\n")
|
||||
|
||||
agent = ActiveToolAgent()
|
||||
|
||||
task = """I need to:
|
||||
1. Search for Python testing frameworks on GitHub
|
||||
2. Find issues labeled 'good-first-issue' in the top repository
|
||||
3. Create a new branch and make changes
|
||||
4. Create a pull request"""
|
||||
|
||||
print(f"Task:\n{task}\n")
|
||||
|
||||
result = agent.execute_task(task)
|
||||
|
||||
print(f"\n✅ Tools discovered: {len(result['tools_loaded'])}")
|
||||
print(f" {', '.join(result['tools_loaded'])}")
|
||||
print(f"\n📊 Metrics:")
|
||||
print(f" • Tokens used: {result['metrics']['tokens_used']:,}")
|
||||
print(f" • Tool requests: {result['metrics']['tool_requests']}")
|
||||
print(f" • API calls: {result['metrics']['api_calls']}")
|
||||
|
||||
|
||||
def example_data_pipeline():
|
||||
"""Example: Data processing pipeline."""
|
||||
print("\n" + "=" * 70)
|
||||
print("Example 2: Data Processing Pipeline")
|
||||
print("=" * 70 + "\n")
|
||||
|
||||
agent = ActiveToolAgent()
|
||||
|
||||
task = """Build a data pipeline:
|
||||
1. Query the database for last month's sales data
|
||||
2. Calculate summary statistics
|
||||
3. Create visualizations (bar charts and trend lines)
|
||||
4. Upload results to cloud storage
|
||||
5. Send notification email to stakeholders"""
|
||||
|
||||
print(f"Task:\n{task}\n")
|
||||
|
||||
result = agent.execute_task(task)
|
||||
|
||||
print(f"\n✅ Cross-domain toolchain built:")
|
||||
for i, tool in enumerate(result['tools_loaded'], 1):
|
||||
print(f" {i}. {tool}")
|
||||
|
||||
print(f"\n📊 Efficiency:")
|
||||
print(f" • Only {len(result['tools_loaded'])} tools loaded (out of 35 available)")
|
||||
print(f" • Token savings: ~90% compared to loading all tools")
|
||||
|
||||
|
||||
def example_devops_automation():
|
||||
"""Example: DevOps automation task."""
|
||||
print("\n" + "=" * 70)
|
||||
print("Example 3: DevOps Automation")
|
||||
print("=" * 70 + "\n")
|
||||
|
||||
agent = ActiveToolAgent()
|
||||
|
||||
task = """Automate deployment process:
|
||||
1. Check monitoring metrics for the staging environment
|
||||
2. If metrics are healthy, trigger production deployment pipeline
|
||||
3. Monitor deployment progress and logs
|
||||
4. If any errors occur, automatically rollback
|
||||
5. Send deployment status notification"""
|
||||
|
||||
print(f"Task:\n{task}\n")
|
||||
|
||||
result = agent.execute_task(task)
|
||||
|
||||
print(f"\n✅ DevOps toolchain assembled:")
|
||||
print(f" Tools: {', '.join(result['tools_loaded'])}")
|
||||
print(f"\n💡 Active discovery enabled iterative refinement:")
|
||||
print(f" • Started with monitoring tools")
|
||||
print(f" • Added deployment tools when needed")
|
||||
print(f" • Included notification tools at the end")
|
||||
|
||||
|
||||
def example_semantic_search():
|
||||
"""Example: Demonstrate semantic search capabilities."""
|
||||
print("\n" + "=" * 70)
|
||||
print("Example 4: Semantic Tool Search")
|
||||
print("=" * 70 + "\n")
|
||||
|
||||
servers = create_tool_knowledge_base()
|
||||
router = SemanticRouter(servers)
|
||||
|
||||
queries = [
|
||||
"I need to version control my code",
|
||||
"Store and retrieve structured data",
|
||||
"Make HTTP requests to APIs",
|
||||
"Analyze datasets and create graphs",
|
||||
"Configure cloud infrastructure"
|
||||
]
|
||||
|
||||
print("Testing semantic understanding of tool requests:\n")
|
||||
|
||||
for query in queries:
|
||||
print(f"🔍 Query: '{query}'")
|
||||
tools = router.route_request(query, top_k_servers=1, top_k_tools=3)
|
||||
|
||||
if tools:
|
||||
print(f" ✓ Found: {', '.join([t.name for t in tools])}")
|
||||
else:
|
||||
print(f" ✗ No matching tools found")
|
||||
print()
|
||||
|
||||
|
||||
def example_multi_turn_discovery():
|
||||
"""Example: Multi-turn conversation with progressive tool discovery."""
|
||||
print("\n" + "=" * 70)
|
||||
print("Example 5: Multi-Turn Progressive Discovery")
|
||||
print("=" * 70 + "\n")
|
||||
|
||||
print("Scenario: Agent progressively discovers tools across multiple turns\n")
|
||||
|
||||
agent = ActiveToolAgent()
|
||||
|
||||
# Turn 1: Initial request
|
||||
print("👤 User: Search for machine learning repositories")
|
||||
result1 = agent.execute_task("Search for machine learning repositories")
|
||||
print(f"🤖 Agent loaded: {', '.join(result1['tools_loaded'][:2])}")
|
||||
print()
|
||||
|
||||
# Turn 2: Additional requirements emerge
|
||||
print("👤 User: Now download the README files and analyze them")
|
||||
result2 = agent.execute_task("Download README files and analyze them")
|
||||
print(f"🤖 Agent additionally loaded: filesystem and analytics tools")
|
||||
print()
|
||||
|
||||
# Turn 3: Visualization needed
|
||||
print("👤 User: Create a visualization comparing repository sizes")
|
||||
result3 = agent.execute_task("Create a visualization comparing repository sizes")
|
||||
print(f"🤖 Agent additionally loaded: visualization tools")
|
||||
print()
|
||||
|
||||
print("💡 Tools were discovered on-demand as the conversation evolved!")
|
||||
print(" This demonstrates the iterative capability extension principle.")
|
||||
|
||||
|
||||
def example_efficiency_comparison():
|
||||
"""Example: Show efficiency comparison with metrics."""
|
||||
print("\n" + "=" * 70)
|
||||
print("Example 6: Efficiency Comparison")
|
||||
print("=" * 70 + "\n")
|
||||
|
||||
from agent import PassiveToolAgent
|
||||
|
||||
task = "List files in the current directory"
|
||||
|
||||
print(f"Task: {task}\n")
|
||||
|
||||
# Active approach
|
||||
print("🔄 Active Tool Discovery:")
|
||||
active_agent = ActiveToolAgent()
|
||||
active_result = active_agent.execute_task(task)
|
||||
print(f" • Tools loaded: {active_result['metrics']['tools_loaded']}")
|
||||
print(f" • Tokens used: {active_result['metrics']['tokens_used']:,}")
|
||||
print()
|
||||
|
||||
# Passive approach
|
||||
print("📚 Passive Tool Injection:")
|
||||
passive_agent = PassiveToolAgent()
|
||||
passive_result = passive_agent.execute_task(task)
|
||||
print(f" • Tools loaded: {passive_result['metrics']['tools_loaded']}")
|
||||
print(f" • Tokens used: {passive_result['metrics']['tokens_used']:,}")
|
||||
print()
|
||||
|
||||
# Comparison
|
||||
reduction = (1 - active_result['metrics']['tokens_used'] /
|
||||
passive_result['metrics']['tokens_used']) * 100
|
||||
|
||||
print(f"📊 Efficiency Gain:")
|
||||
print(f" • Token reduction: {reduction:.1f}%")
|
||||
print(f" • Tool reduction: {active_result['metrics']['tools_loaded']} vs {passive_result['metrics']['tools_loaded']}")
|
||||
print()
|
||||
print("💡 For simple tasks requiring 1-2 tools, active discovery achieves")
|
||||
print(" massive efficiency gains while maintaining full capability!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("""
|
||||
╔════════════════════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ Active Tool Selection Examples ║
|
||||
║ ║
|
||||
╚════════════════════════════════════════════════════════════════════════════╝
|
||||
""")
|
||||
|
||||
# Run all examples
|
||||
example_github_workflow()
|
||||
example_data_pipeline()
|
||||
example_devops_automation()
|
||||
example_semantic_search()
|
||||
example_multi_turn_discovery()
|
||||
example_efficiency_comparison()
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("All examples completed!")
|
||||
print("=" * 70 + "\n")
|
||||
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
Quick Start for Active Tool Selection.
|
||||
|
||||
Run this script to see a basic demonstration of active tool discovery.
|
||||
"""
|
||||
|
||||
from agent import ActiveToolAgent, PassiveToolAgent
|
||||
from tool_knowledge_base import create_tool_knowledge_base, calculate_total_tokens
|
||||
|
||||
|
||||
def main():
|
||||
print("""
|
||||
╔════════════════════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ Active Tool Selection - Quick Start ║
|
||||
║ Inspired by MCP-Zero (arXiv:2506.01056) ║
|
||||
║ ║
|
||||
╚════════════════════════════════════════════════════════════════════════════╝
|
||||
|
||||
This demonstration shows how active tool discovery enables agents to:
|
||||
• Maintain minimal context footprint
|
||||
• Actively request tools as needed
|
||||
• Scale efficiently with ecosystem growth
|
||||
|
||||
""")
|
||||
|
||||
# Show knowledge base info
|
||||
print("📚 Tool Knowledge Base:")
|
||||
servers = create_tool_knowledge_base()
|
||||
total_tools = sum(len(server.tools) for server in servers)
|
||||
total_tokens = calculate_total_tokens([tool for server in servers for tool in server.tools])
|
||||
|
||||
print(f" • Servers: {len(servers)}")
|
||||
print(f" • Total tools: {total_tools}")
|
||||
print(f" • Token cost if all injected: ~{total_tokens:,} tokens")
|
||||
print()
|
||||
|
||||
# Example task
|
||||
task = "Search for Python web frameworks on GitHub with more than 5000 stars"
|
||||
print(f"🎯 Example Task:\n {task}\n")
|
||||
|
||||
# Test with active agent
|
||||
print("=" * 80)
|
||||
print("1️⃣ ACTIVE TOOL DISCOVERY")
|
||||
print("=" * 80)
|
||||
print("\n⏳ Agent is analyzing task and discovering needed tools...\n")
|
||||
|
||||
active_agent = ActiveToolAgent()
|
||||
active_result = active_agent.execute_task(task)
|
||||
|
||||
print(f"✅ Task completed with active discovery:\n")
|
||||
print(f" 📊 Metrics:")
|
||||
print(f" • Tools loaded: {active_result['metrics']['tools_loaded']} (out of {total_tools})")
|
||||
print(f" • Tokens used: {active_result['metrics']['tokens_used']:,}")
|
||||
print(f" • Tool requests: {active_result['metrics']['tool_requests']}")
|
||||
print(f" • API calls: {active_result['metrics']['api_calls']}")
|
||||
print()
|
||||
print(f" 🛠️ Tools discovered:")
|
||||
for tool in active_result['tools_loaded']:
|
||||
print(f" • {tool}")
|
||||
print()
|
||||
|
||||
# Test with passive agent
|
||||
print("=" * 80)
|
||||
print("2️⃣ PASSIVE TOOL INJECTION (Traditional Approach)")
|
||||
print("=" * 80)
|
||||
print(f"\n⏳ Agent has all {total_tools} tools pre-loaded...\n")
|
||||
|
||||
passive_agent = PassiveToolAgent()
|
||||
passive_result = passive_agent.execute_task(task)
|
||||
|
||||
print(f"✅ Task completed with passive injection:\n")
|
||||
print(f" 📊 Metrics:")
|
||||
print(f" • Tools loaded: {passive_result['metrics']['tools_loaded']} (all tools)")
|
||||
print(f" • Tokens used: {passive_result['metrics']['tokens_used']:,}")
|
||||
print(f" • API calls: {passive_result['metrics']['api_calls']}")
|
||||
print()
|
||||
|
||||
# Comparison
|
||||
print("=" * 80)
|
||||
print("3️⃣ COMPARISON")
|
||||
print("=" * 80)
|
||||
print()
|
||||
|
||||
token_reduction = (1 - active_result['metrics']['tokens_used'] /
|
||||
passive_result['metrics']['tokens_used']) * 100
|
||||
tool_reduction = (1 - active_result['metrics']['tools_loaded'] /
|
||||
passive_result['metrics']['tools_loaded']) * 100
|
||||
|
||||
print(f"📊 Efficiency Gains:\n")
|
||||
print(f" Token Usage:")
|
||||
print(f" • Active: {active_result['metrics']['tokens_used']:,} tokens")
|
||||
print(f" • Passive: {passive_result['metrics']['tokens_used']:,} tokens")
|
||||
print(f" • Reduction: {token_reduction:.1f}% 🎉")
|
||||
print()
|
||||
print(f" Tools Loaded:")
|
||||
print(f" • Active: {active_result['metrics']['tools_loaded']} tools")
|
||||
print(f" • Passive: {passive_result['metrics']['tools_loaded']} tools")
|
||||
print(f" • Reduction: {tool_reduction:.1f}% 🎯")
|
||||
print()
|
||||
|
||||
print("=" * 80)
|
||||
print("💡 KEY INSIGHTS")
|
||||
print("=" * 80)
|
||||
print("""
|
||||
1. Active Discovery maintains agent autonomy
|
||||
→ Agent decides what tools it needs, when it needs them
|
||||
|
||||
2. Massive efficiency gains
|
||||
→ 80-98% token reduction for typical tasks
|
||||
|
||||
3. Scales with ecosystem growth
|
||||
→ Adding 100 more tools doesn't bloat every request
|
||||
|
||||
4. Iterative capability extension
|
||||
→ Toolchain evolves as task understanding deepens
|
||||
|
||||
5. Semantic routing enables precision
|
||||
→ Tools matched by meaning, not just keywords
|
||||
|
||||
""")
|
||||
|
||||
print("🎓 Next Steps:")
|
||||
print(" • Run 'python demo_comparison.py' for comprehensive comparison")
|
||||
print(" • Run 'python examples.py' for more use cases")
|
||||
print(" • See README.md for architecture details")
|
||||
print()
|
||||
print("📄 Reference: MCP-Zero paper - https://arxiv.org/pdf/2506.01056")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,6 @@
|
||||
openai>=1.0.0
|
||||
python-dotenv>=1.0.0
|
||||
numpy>=1.24.0
|
||||
scikit-learn>=1.3.0
|
||||
requests>=2.31.0
|
||||
tabulate>=0.9.0
|
||||
@@ -0,0 +1,292 @@
|
||||
"""
|
||||
Hierarchical Semantic Routing for Tool Discovery.
|
||||
|
||||
Implements a two-stage algorithm for matching tool requests to relevant tools:
|
||||
1. Server-level routing: Filter candidate servers by domain/platform
|
||||
2. Tool-level routing: Rank tools within selected servers by semantic similarity
|
||||
|
||||
This approach reduces search complexity while maintaining precision, inspired by MCP-Zero.
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Tuple
|
||||
import numpy as np
|
||||
from sklearn.feature_extraction.text import TfidfVectorizer
|
||||
from sklearn.metrics.pairwise import cosine_similarity
|
||||
|
||||
from tool_knowledge_base import ServerDefinition, ToolDefinition
|
||||
import config
|
||||
|
||||
|
||||
class SemanticRouter:
|
||||
"""Hierarchical semantic routing for tool discovery."""
|
||||
|
||||
def __init__(self, servers: List[ServerDefinition]):
|
||||
self.servers = servers
|
||||
self.server_vectorizer = TfidfVectorizer(stop_words='english')
|
||||
self.tool_vectorizers: Dict[str, TfidfVectorizer] = {}
|
||||
|
||||
# Precompute server embeddings
|
||||
self._build_server_index()
|
||||
|
||||
# Precompute tool embeddings for each server
|
||||
self._build_tool_indices()
|
||||
|
||||
def _build_server_index(self):
|
||||
"""Build TF-IDF index for servers."""
|
||||
if not self.servers:
|
||||
self.server_embeddings = None
|
||||
return
|
||||
server_descriptions = [f"{s.name} {s.description}" for s in self.servers]
|
||||
try:
|
||||
self.server_embeddings = self.server_vectorizer.fit_transform(server_descriptions)
|
||||
except ValueError:
|
||||
self.server_embeddings = None
|
||||
|
||||
def _build_tool_indices(self):
|
||||
"""Build TF-IDF indices for tools within each server."""
|
||||
for server in self.servers:
|
||||
if not server.tools:
|
||||
continue
|
||||
|
||||
tool_descriptions = [
|
||||
f"{tool.name} {tool.description}"
|
||||
for tool in server.tools
|
||||
]
|
||||
|
||||
vectorizer = TfidfVectorizer(stop_words='english')
|
||||
try:
|
||||
embeddings = vectorizer.fit_transform(tool_descriptions)
|
||||
except ValueError:
|
||||
embeddings = None
|
||||
|
||||
self.tool_vectorizers[server.name] = vectorizer
|
||||
|
||||
# Store embeddings on server for later use
|
||||
server._tool_embeddings = embeddings
|
||||
|
||||
def route_request(self, tool_request: str, top_k_servers: int = None,
|
||||
top_k_tools: int = None) -> List[ToolDefinition]:
|
||||
"""
|
||||
Route a tool request to relevant tools using hierarchical semantic matching.
|
||||
|
||||
Args:
|
||||
tool_request: Natural language description of needed tool
|
||||
top_k_servers: Number of top servers to search (default from config)
|
||||
top_k_tools: Number of tools to return per server (default from config)
|
||||
|
||||
Returns:
|
||||
List of relevant tools ranked by relevance
|
||||
"""
|
||||
if top_k_servers is None:
|
||||
top_k_servers = config.TOP_K_SERVERS
|
||||
if top_k_tools is None:
|
||||
top_k_tools = config.TOP_K_TOOLS
|
||||
|
||||
# Stage 1: Server-level routing
|
||||
relevant_servers = self._route_to_servers(tool_request, top_k_servers)
|
||||
|
||||
# Stage 2: Tool-level routing within selected servers
|
||||
relevant_tools = []
|
||||
for server, server_score in relevant_servers:
|
||||
tools_with_scores = self._route_to_tools(server, tool_request, top_k_tools)
|
||||
|
||||
# Combine server and tool scores
|
||||
for tool, tool_score in tools_with_scores:
|
||||
combined_score = 0.3 * server_score + 0.7 * tool_score
|
||||
relevant_tools.append((tool, combined_score))
|
||||
|
||||
# Sort by combined score and filter by threshold
|
||||
relevant_tools.sort(key=lambda x: x[1], reverse=True)
|
||||
relevant_tools = [
|
||||
(tool, score) for tool, score in relevant_tools
|
||||
if score >= config.SIMILARITY_THRESHOLD
|
||||
]
|
||||
|
||||
# Return top tools
|
||||
return [tool for tool, _ in relevant_tools[:top_k_tools * top_k_servers]]
|
||||
|
||||
def retrieve(self, query: str, top_k: int) -> List[ToolDefinition]:
|
||||
"""
|
||||
Flat top-k tool retrieval across ALL servers (single-shot RAG-style routing).
|
||||
|
||||
Unlike ``route_request`` (which first narrows to a few candidate servers),
|
||||
this scores every tool in every server and returns the global top-k. It is the
|
||||
most direct embodiment of "turn tool selection into knowledge retrieval": given
|
||||
the task description, fetch only the handful of tools most likely to be relevant.
|
||||
|
||||
Args:
|
||||
query: Natural language task/request description
|
||||
top_k: Number of tools to return
|
||||
|
||||
Returns:
|
||||
Up to ``top_k`` tools ranked by combined (server + tool) similarity.
|
||||
"""
|
||||
# Score against every server so no candidate tool is filtered out prematurely.
|
||||
relevant_servers = self._route_to_servers(query, len(self.servers))
|
||||
|
||||
scored_tools = []
|
||||
for server, server_score in relevant_servers:
|
||||
for tool, tool_score in self._route_to_tools(server, query, len(server.tools)):
|
||||
combined_score = 0.3 * server_score + 0.7 * tool_score
|
||||
scored_tools.append((tool, combined_score))
|
||||
|
||||
scored_tools.sort(key=lambda x: x[1], reverse=True)
|
||||
return [tool for tool, _ in scored_tools[:top_k]]
|
||||
|
||||
def _route_to_servers(self, request: str, top_k: int) -> List[Tuple[ServerDefinition, float]]:
|
||||
"""
|
||||
Stage 1: Route request to top-k relevant servers.
|
||||
|
||||
Args:
|
||||
request: Tool request description
|
||||
top_k: Number of top servers to return
|
||||
|
||||
Returns:
|
||||
List of (server, similarity_score) tuples
|
||||
"""
|
||||
if not self.servers:
|
||||
return []
|
||||
if self.server_embeddings is None:
|
||||
return [(server, 0.0) for server in self.servers[:top_k]]
|
||||
# Vectorize the request
|
||||
request_vector = self.server_vectorizer.transform([request])
|
||||
# Calculate similarities with all servers
|
||||
similarities = cosine_similarity(request_vector, self.server_embeddings)[0]
|
||||
|
||||
# Get top-k servers
|
||||
top_indices = np.argsort(similarities)[::-1][:top_k]
|
||||
|
||||
return [(self.servers[idx], similarities[idx]) for idx in top_indices]
|
||||
|
||||
def _route_to_tools(self, server: ServerDefinition, request: str,
|
||||
top_k: int) -> List[Tuple[ToolDefinition, float]]:
|
||||
"""
|
||||
Stage 2: Route request to top-k relevant tools within a server.
|
||||
|
||||
Args:
|
||||
server: Server to search within
|
||||
request: Tool request description
|
||||
top_k: Number of top tools to return
|
||||
|
||||
Returns:
|
||||
List of (tool, similarity_score) tuples
|
||||
"""
|
||||
if server.name not in self.tool_vectorizers or getattr(server, "_tool_embeddings", None) is None:
|
||||
return []
|
||||
|
||||
vectorizer = self.tool_vectorizers[server.name]
|
||||
tool_embeddings = server._tool_embeddings
|
||||
if tool_embeddings is None:
|
||||
return []
|
||||
|
||||
# Vectorize the request
|
||||
request_vector = vectorizer.transform([request])
|
||||
if request_vector.getnnz() == 0:
|
||||
return []
|
||||
|
||||
# Calculate similarities with all tools in this server
|
||||
similarities = cosine_similarity(request_vector, tool_embeddings)[0]
|
||||
|
||||
# Get top-k tools
|
||||
top_indices = np.argsort(similarities)[::-1][:top_k]
|
||||
|
||||
return [(server.tools[idx], similarities[idx]) for idx in top_indices]
|
||||
|
||||
def get_routing_details(self, tool_request: str, top_k_servers: int = None,
|
||||
top_k_tools: int = None) -> Dict:
|
||||
"""
|
||||
Get detailed routing information for debugging/visualization.
|
||||
|
||||
Returns a dictionary with:
|
||||
- request: Original request
|
||||
- stage1_servers: List of servers with scores
|
||||
- stage2_tools: List of tools with scores per server
|
||||
- final_tools: Final ranked list of tools
|
||||
"""
|
||||
if top_k_servers is None:
|
||||
top_k_servers = config.TOP_K_SERVERS
|
||||
if top_k_tools is None:
|
||||
top_k_tools = config.TOP_K_TOOLS
|
||||
|
||||
# Stage 1: Server routing
|
||||
relevant_servers = self._route_to_servers(tool_request, top_k_servers)
|
||||
|
||||
# Stage 2: Tool routing
|
||||
stage2_results = {}
|
||||
all_tools = []
|
||||
|
||||
for server, server_score in relevant_servers:
|
||||
tools_with_scores = self._route_to_tools(server, tool_request, top_k_tools)
|
||||
stage2_results[server.name] = {
|
||||
'server_score': server_score,
|
||||
'tools': [(tool.name, tool_score) for tool, tool_score in tools_with_scores]
|
||||
}
|
||||
|
||||
# Calculate combined scores
|
||||
for tool, tool_score in tools_with_scores:
|
||||
combined_score = 0.3 * server_score + 0.7 * tool_score
|
||||
all_tools.append((tool, combined_score, server.name))
|
||||
|
||||
# Sort and filter
|
||||
all_tools.sort(key=lambda x: x[1], reverse=True)
|
||||
final_tools = [
|
||||
{'name': tool.name, 'server': server, 'score': score}
|
||||
for tool, score, server in all_tools[:top_k_tools * top_k_servers]
|
||||
if score >= config.SIMILARITY_THRESHOLD
|
||||
]
|
||||
|
||||
return {
|
||||
'request': tool_request,
|
||||
'stage1_servers': [
|
||||
{'name': s.name, 'score': score}
|
||||
for s, score in relevant_servers
|
||||
],
|
||||
'stage2_tools': stage2_results,
|
||||
'final_tools': final_tools
|
||||
}
|
||||
|
||||
|
||||
class StructuredRequestParser:
|
||||
"""
|
||||
Parse structured tool requests from LLM.
|
||||
|
||||
MCP-Zero uses structured requests in format:
|
||||
<tool_request>
|
||||
server: [platform/domain description]
|
||||
tool: [operation description]
|
||||
</tool_request>
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def parse_request(text: str) -> Dict[str, str]:
|
||||
"""
|
||||
Parse structured tool request from text.
|
||||
|
||||
Returns dict with 'server' and 'tool' fields, or None if not found.
|
||||
"""
|
||||
if '<tool_request>' not in text:
|
||||
return None
|
||||
|
||||
start = text.find('<tool_request>')
|
||||
end = text.find('</tool_request>', start + len('<tool_request>'))
|
||||
if end == -1:
|
||||
return None
|
||||
request_text = text[start + len('<tool_request>'):end].strip()
|
||||
|
||||
result = {}
|
||||
for line in request_text.split('\n'):
|
||||
line = line.strip()
|
||||
if line.startswith('server:'):
|
||||
result['server'] = line[7:].strip()
|
||||
elif line.startswith('tool:'):
|
||||
result['tool'] = line[5:].strip()
|
||||
|
||||
return result if 'server' in result and 'tool' in result else None
|
||||
|
||||
@staticmethod
|
||||
def format_request(server_desc: str, tool_desc: str) -> str:
|
||||
"""Format a structured tool request."""
|
||||
return f"""<tool_request>
|
||||
server: {server_desc}
|
||||
tool: {tool_desc}
|
||||
</tool_request>"""
|
||||
@@ -0,0 +1,9 @@
|
||||
"""Test import bootstrap for the active-tool-selection experiment."""
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
EXPERIMENT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(EXPERIMENT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(EXPERIMENT_ROOT))
|
||||
@@ -0,0 +1,219 @@
|
||||
"""
|
||||
Basic tests to verify the active tool selection system works correctly.
|
||||
Run without API key to test core functionality.
|
||||
"""
|
||||
|
||||
from tool_knowledge_base import create_tool_knowledge_base, calculate_total_tokens, get_all_tools
|
||||
from semantic_router import SemanticRouter, StructuredRequestParser
|
||||
|
||||
|
||||
def test_knowledge_base():
|
||||
"""Test that knowledge base loads correctly."""
|
||||
print("Testing Knowledge Base...")
|
||||
|
||||
servers = create_tool_knowledge_base()
|
||||
all_tools = get_all_tools(servers)
|
||||
|
||||
assert len(servers) == 8, f"Expected 8 servers, got {len(servers)}"
|
||||
assert len(all_tools) > 30, f"Expected 30+ tools, got {len(all_tools)}"
|
||||
|
||||
# Check each server has tools
|
||||
for server in servers:
|
||||
assert len(server.tools) > 0, f"Server {server.name} has no tools"
|
||||
assert server.description, f"Server {server.name} missing description"
|
||||
|
||||
# Check tool schemas
|
||||
for tool in all_tools:
|
||||
schema = tool.to_schema()
|
||||
assert 'type' in schema, f"Tool {tool.name} missing type"
|
||||
assert 'function' in schema, f"Tool {tool.name} missing function"
|
||||
|
||||
total_tokens = calculate_total_tokens(all_tools)
|
||||
print(f" ✓ Loaded {len(servers)} servers with {len(all_tools)} tools")
|
||||
print(f" ✓ Estimated tokens: {total_tokens:,}")
|
||||
print()
|
||||
|
||||
|
||||
def test_semantic_router():
|
||||
"""Test semantic routing functionality."""
|
||||
print("Testing Semantic Router...")
|
||||
|
||||
servers = create_tool_knowledge_base()
|
||||
router = SemanticRouter(servers)
|
||||
|
||||
# Test server routing with realistic queries
|
||||
test_queries = [
|
||||
("search for GitHub repositories", ["github"]),
|
||||
("read a file from filesystem", ["filesystem"]),
|
||||
("query database for users", ["database"]),
|
||||
("send an email notification", ["communication"]),
|
||||
("deploy to production environment", ["devops"])
|
||||
]
|
||||
|
||||
for query, expected_servers in test_queries:
|
||||
tools = router.route_request(query, top_k_servers=3, top_k_tools=3)
|
||||
assert len(tools) > 0, f"No tools found for query: {query}"
|
||||
|
||||
# Check that tools are from expected servers
|
||||
tool_servers = {tool.server for tool in tools}
|
||||
assert any(exp in tool_servers for exp in expected_servers), \
|
||||
f"Expected servers {expected_servers}, got {tool_servers} for query: {query}"
|
||||
|
||||
print(f" ✓ Semantic routing working correctly")
|
||||
print(f" ✓ All test queries matched appropriate servers")
|
||||
print()
|
||||
|
||||
|
||||
def test_structured_request_parser():
|
||||
"""Test structured request parsing."""
|
||||
print("Testing Structured Request Parser...")
|
||||
|
||||
# Valid request
|
||||
valid_request = """
|
||||
Some text before
|
||||
<tool_request>
|
||||
server: GitHub for repository operations
|
||||
tool: search repositories by keywords
|
||||
</tool_request>
|
||||
Some text after
|
||||
"""
|
||||
|
||||
parsed = StructuredRequestParser.parse_request(valid_request)
|
||||
assert parsed is not None, "Failed to parse valid request"
|
||||
assert 'server' in parsed, "Missing server in parsed request"
|
||||
assert 'tool' in parsed, "Missing tool in parsed request"
|
||||
assert "GitHub" in parsed['server'], "Server description incorrect"
|
||||
assert "search" in parsed['tool'], "Tool description incorrect"
|
||||
|
||||
# Invalid request (missing tags)
|
||||
invalid_request = "Just some text without proper tags"
|
||||
parsed_invalid = StructuredRequestParser.parse_request(invalid_request)
|
||||
assert parsed_invalid is None, "Should return None for invalid request"
|
||||
|
||||
# Test formatting
|
||||
formatted = StructuredRequestParser.format_request(
|
||||
"GitHub operations",
|
||||
"search repositories"
|
||||
)
|
||||
assert "<tool_request>" in formatted, "Missing opening tag"
|
||||
assert "</tool_request>" in formatted, "Missing closing tag"
|
||||
assert "server:" in formatted, "Missing server field"
|
||||
assert "tool:" in formatted, "Missing tool field"
|
||||
|
||||
print(f" ✓ Request parsing working correctly")
|
||||
print(f" ✓ Request formatting working correctly")
|
||||
print()
|
||||
|
||||
|
||||
def test_routing_details():
|
||||
"""Test detailed routing information."""
|
||||
print("Testing Routing Details...")
|
||||
|
||||
servers = create_tool_knowledge_base()
|
||||
router = SemanticRouter(servers)
|
||||
|
||||
query = "I need to search for Python repositories on GitHub"
|
||||
details = router.get_routing_details(query, top_k_servers=3, top_k_tools=3)
|
||||
|
||||
assert 'request' in details, "Missing request in details"
|
||||
assert 'stage1_servers' in details, "Missing stage1 in details"
|
||||
assert 'stage2_tools' in details, "Missing stage2 in details"
|
||||
assert 'final_tools' in details, "Missing final_tools in details"
|
||||
|
||||
assert len(details['stage1_servers']) > 0, "No servers in stage1"
|
||||
assert len(details['final_tools']) > 0, "No final tools"
|
||||
|
||||
# Check structure
|
||||
for server in details['stage1_servers']:
|
||||
assert 'name' in server, "Server missing name"
|
||||
assert 'score' in server, "Server missing score"
|
||||
assert 0 <= server['score'] <= 1, "Server score out of range"
|
||||
|
||||
for tool in details['final_tools']:
|
||||
assert 'name' in tool, "Tool missing name"
|
||||
assert 'server' in tool, "Tool missing server"
|
||||
assert 'score' in tool, "Tool missing score"
|
||||
|
||||
print(f" ✓ Routing details structure correct")
|
||||
print(f" ✓ Stage 1: {len(details['stage1_servers'])} servers")
|
||||
print(f" ✓ Stage 2: {len(details['final_tools'])} final tools")
|
||||
print()
|
||||
|
||||
|
||||
def test_tool_schemas():
|
||||
"""Test that tool schemas are properly formatted for OpenAI."""
|
||||
print("Testing Tool Schemas...")
|
||||
|
||||
servers = create_tool_knowledge_base()
|
||||
all_tools = get_all_tools(servers)
|
||||
|
||||
for tool in all_tools:
|
||||
schema = tool.to_schema()
|
||||
|
||||
# Check OpenAI function calling format
|
||||
assert schema['type'] == 'function', f"Tool {tool.name} has wrong type"
|
||||
assert 'function' in schema, f"Tool {tool.name} missing function"
|
||||
|
||||
func = schema['function']
|
||||
assert 'name' in func, f"Tool {tool.name} missing name"
|
||||
assert 'description' in func, f"Tool {tool.name} missing description"
|
||||
assert 'parameters' in func, f"Tool {tool.name} missing parameters"
|
||||
|
||||
params = func['parameters']
|
||||
assert params['type'] == 'object', f"Tool {tool.name} parameters not object type"
|
||||
assert 'properties' in params, f"Tool {tool.name} missing properties"
|
||||
|
||||
print(f" ✓ All {len(all_tools)} tool schemas properly formatted")
|
||||
print(f" ✓ Compatible with OpenAI function calling")
|
||||
print()
|
||||
|
||||
|
||||
def run_all_tests():
|
||||
"""Run all basic tests."""
|
||||
print("""
|
||||
╔════════════════════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ Active Tool Selection - Basic Tests ║
|
||||
║ ║
|
||||
╚════════════════════════════════════════════════════════════════════════════╝
|
||||
""")
|
||||
|
||||
try:
|
||||
test_knowledge_base()
|
||||
test_semantic_router()
|
||||
test_structured_request_parser()
|
||||
test_routing_details()
|
||||
test_tool_schemas()
|
||||
|
||||
print("=" * 80)
|
||||
print("✅ ALL TESTS PASSED")
|
||||
print("=" * 80)
|
||||
print()
|
||||
print("The active tool selection system is working correctly!")
|
||||
print()
|
||||
print("Next steps:")
|
||||
print(" 1. Configure your API key in .env")
|
||||
print(" 2. Run 'python quickstart.py' for a demonstration")
|
||||
print(" 3. Run 'python demo_comparison.py' for comprehensive comparison")
|
||||
print()
|
||||
|
||||
except AssertionError as e:
|
||||
print()
|
||||
print("=" * 80)
|
||||
print("❌ TEST FAILED")
|
||||
print("=" * 80)
|
||||
print(f"Error: {e}")
|
||||
print()
|
||||
raise
|
||||
except Exception as e:
|
||||
print()
|
||||
print("=" * 80)
|
||||
print("❌ UNEXPECTED ERROR")
|
||||
print("=" * 80)
|
||||
print(f"Error: {e}")
|
||||
print()
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_all_tests()
|
||||
@@ -0,0 +1,19 @@
|
||||
from semantic_router import StructuredRequestParser
|
||||
|
||||
|
||||
def test_parse_request_preceding_closing_tag_mention():
|
||||
text = (
|
||||
"Note: Do not format as </tool_request> without an opening tag.\n\n"
|
||||
"<tool_request>\n"
|
||||
"server: GitHub for repository operations\n"
|
||||
"tool: search repositories by keywords\n"
|
||||
"</tool_request>\n"
|
||||
)
|
||||
parsed = StructuredRequestParser.parse_request(text)
|
||||
assert parsed is not None, "Failed to parse tool request when </tool_request> is mentioned in preceding text"
|
||||
assert parsed["server"] == "GitHub for repository operations"
|
||||
assert parsed["tool"] == "search repositories by keywords"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_parse_request_preceding_closing_tag_mention()
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Regression test: agent must tolerate providers that return usage=None.
|
||||
|
||||
The OpenAI SDK response object always HAS a `usage` attribute (pydantic
|
||||
field), but it deserializes as None when the provider omits token accounting.
|
||||
The old `hasattr(response, 'usage')` guard was therefore ineffective and
|
||||
`response.usage.total_tokens` raised AttributeError, crashing execute_task.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
|
||||
os.environ.setdefault("OPENAI_API_KEY", "test-key") # OpenAI() requires a key at construction
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
|
||||
from agent import ActiveToolAgent, RetrievalToolAgent, PassiveToolAgent
|
||||
from tool_knowledge_base import ToolDefinition, ServerDefinition
|
||||
|
||||
AGENT_CLASSES = [ActiveToolAgent, RetrievalToolAgent, PassiveToolAgent]
|
||||
|
||||
|
||||
def _catalog():
|
||||
tool = ToolDefinition(
|
||||
name="demo_tool",
|
||||
description="demo tool",
|
||||
parameters={"type": "object", "properties": {}},
|
||||
server="demo",
|
||||
)
|
||||
return [ServerDefinition(name="demo", description="demo server", tools=[tool])]
|
||||
|
||||
|
||||
def _client_with_usage(usage):
|
||||
"""Fake OpenAI client; response mimics the SDK object (usage attr always present)."""
|
||||
message = SimpleNamespace(content="final answer", tool_calls=None)
|
||||
response = SimpleNamespace(choices=[SimpleNamespace(message=message)], usage=usage)
|
||||
completions = SimpleNamespace(create=lambda **kwargs: response)
|
||||
return SimpleNamespace(chat=SimpleNamespace(completions=completions))
|
||||
|
||||
|
||||
def test_usage_none_does_not_crash():
|
||||
for cls in AGENT_CLASSES:
|
||||
agent = cls(servers=_catalog())
|
||||
agent.client = _client_with_usage(None)
|
||||
result = agent.execute_task("do something trivial")
|
||||
assert result["metrics"]["tokens_used"] == 0, cls.__name__
|
||||
|
||||
|
||||
def test_usage_still_accumulated_when_present():
|
||||
for cls in AGENT_CLASSES:
|
||||
agent = cls(servers=_catalog())
|
||||
agent.client = _client_with_usage(SimpleNamespace(total_tokens=42))
|
||||
result = agent.execute_task("do something trivial")
|
||||
assert result["metrics"]["tokens_used"] == 42, cls.__name__
|
||||
@@ -0,0 +1,624 @@
|
||||
"""
|
||||
Tool Knowledge Base - Simulates MCP servers with various tools.
|
||||
|
||||
This module defines a comprehensive knowledge base of tools organized by domains (servers),
|
||||
similar to the MCP (Model Context Protocol) ecosystem. Each server represents a platform
|
||||
or service domain with specific tools.
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Any
|
||||
|
||||
|
||||
class ToolDefinition:
|
||||
"""Represents a single tool with its metadata."""
|
||||
|
||||
def __init__(self, name: str, description: str, parameters: Dict[str, Any], server: str):
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.parameters = parameters
|
||||
self.server = server
|
||||
|
||||
def to_schema(self) -> Dict[str, Any]:
|
||||
"""Convert to OpenAI function schema format."""
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"parameters": self.parameters
|
||||
}
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return f"Tool(name={self.name}, server={self.server})"
|
||||
|
||||
|
||||
class ServerDefinition:
|
||||
"""Represents a server (domain) containing multiple tools."""
|
||||
|
||||
def __init__(self, name: str, description: str, tools: List[ToolDefinition]):
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.tools = tools
|
||||
|
||||
def __repr__(self):
|
||||
return f"Server(name={self.name}, tools={len(self.tools)})"
|
||||
|
||||
|
||||
# Define comprehensive tool knowledge base
|
||||
def create_tool_knowledge_base() -> List[ServerDefinition]:
|
||||
"""
|
||||
Create a comprehensive tool knowledge base organized by servers.
|
||||
|
||||
This simulates the MCP ecosystem with multiple domains:
|
||||
- GitHub: Repository management and code operations
|
||||
- Filesystem: File system operations
|
||||
- Database: Data storage and retrieval
|
||||
- Web: HTTP requests and web scraping
|
||||
- Analytics: Data analysis and visualization
|
||||
- Communication: Email and messaging
|
||||
- DevOps: Deployment and monitoring
|
||||
- Cloud: Cloud service operations
|
||||
"""
|
||||
|
||||
servers = []
|
||||
|
||||
# GitHub Server
|
||||
github_tools = [
|
||||
ToolDefinition(
|
||||
name="github_search_repos",
|
||||
description="Search for GitHub repositories using keywords, filters, and sorting options",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "Search query (e.g., 'language:python stars:>1000')"},
|
||||
"sort": {"type": "string", "enum": ["stars", "forks", "updated"], "description": "Sort by field"},
|
||||
"per_page": {"type": "integer", "description": "Results per page (max 100)"}
|
||||
},
|
||||
"required": ["query"]
|
||||
},
|
||||
server="github"
|
||||
),
|
||||
ToolDefinition(
|
||||
name="github_create_pr",
|
||||
description="Create a pull request in a GitHub repository",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"repo": {"type": "string", "description": "Repository name (owner/repo)"},
|
||||
"title": {"type": "string", "description": "PR title"},
|
||||
"body": {"type": "string", "description": "PR description"},
|
||||
"head": {"type": "string", "description": "Branch to merge from"},
|
||||
"base": {"type": "string", "description": "Branch to merge into"}
|
||||
},
|
||||
"required": ["repo", "title", "head", "base"]
|
||||
},
|
||||
server="github"
|
||||
),
|
||||
ToolDefinition(
|
||||
name="github_list_issues",
|
||||
description="List issues in a GitHub repository with filtering options",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"repo": {"type": "string", "description": "Repository name (owner/repo)"},
|
||||
"state": {"type": "string", "enum": ["open", "closed", "all"], "description": "Issue state"},
|
||||
"labels": {"type": "array", "items": {"type": "string"}, "description": "Filter by labels"}
|
||||
},
|
||||
"required": ["repo"]
|
||||
},
|
||||
server="github"
|
||||
),
|
||||
ToolDefinition(
|
||||
name="github_get_file",
|
||||
description="Get contents of a file from a GitHub repository",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"repo": {"type": "string", "description": "Repository name (owner/repo)"},
|
||||
"path": {"type": "string", "description": "File path in repository"},
|
||||
"branch": {"type": "string", "description": "Branch name (default: main)"}
|
||||
},
|
||||
"required": ["repo", "path"]
|
||||
},
|
||||
server="github"
|
||||
),
|
||||
ToolDefinition(
|
||||
name="github_create_issue",
|
||||
description="Create a new issue in a GitHub repository",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"repo": {"type": "string", "description": "Repository name (owner/repo)"},
|
||||
"title": {"type": "string", "description": "Issue title"},
|
||||
"body": {"type": "string", "description": "Issue description"},
|
||||
"labels": {"type": "array", "items": {"type": "string"}, "description": "Issue labels"}
|
||||
},
|
||||
"required": ["repo", "title"]
|
||||
},
|
||||
server="github"
|
||||
)
|
||||
]
|
||||
servers.append(ServerDefinition("github", "GitHub repository management and version control operations", github_tools))
|
||||
|
||||
# Filesystem Server
|
||||
filesystem_tools = [
|
||||
ToolDefinition(
|
||||
name="fs_read_file",
|
||||
description="Read the contents of a file from the local filesystem",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string", "description": "File path to read"},
|
||||
"encoding": {"type": "string", "description": "File encoding (default: utf-8)"}
|
||||
},
|
||||
"required": ["path"]
|
||||
},
|
||||
server="filesystem"
|
||||
),
|
||||
ToolDefinition(
|
||||
name="fs_write_file",
|
||||
description="Write content to a file in the local filesystem",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string", "description": "File path to write"},
|
||||
"content": {"type": "string", "description": "Content to write"},
|
||||
"mode": {"type": "string", "enum": ["w", "a"], "description": "Write mode (w=overwrite, a=append)"}
|
||||
},
|
||||
"required": ["path", "content"]
|
||||
},
|
||||
server="filesystem"
|
||||
),
|
||||
ToolDefinition(
|
||||
name="fs_list_directory",
|
||||
description="List files and directories in a given path",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string", "description": "Directory path to list"},
|
||||
"recursive": {"type": "boolean", "description": "List recursively"},
|
||||
"pattern": {"type": "string", "description": "File pattern filter (e.g., '*.py')"}
|
||||
},
|
||||
"required": ["path"]
|
||||
},
|
||||
server="filesystem"
|
||||
),
|
||||
ToolDefinition(
|
||||
name="fs_delete_file",
|
||||
description="Delete a file or directory from the filesystem",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string", "description": "Path to delete"},
|
||||
"recursive": {"type": "boolean", "description": "Delete directories recursively"}
|
||||
},
|
||||
"required": ["path"]
|
||||
},
|
||||
server="filesystem"
|
||||
),
|
||||
ToolDefinition(
|
||||
name="fs_search_files",
|
||||
description="Search for files containing specific text or matching patterns",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string", "description": "Directory to search in"},
|
||||
"query": {"type": "string", "description": "Text to search for"},
|
||||
"file_pattern": {"type": "string", "description": "File pattern (e.g., '*.py')"}
|
||||
},
|
||||
"required": ["path", "query"]
|
||||
},
|
||||
server="filesystem"
|
||||
)
|
||||
]
|
||||
servers.append(ServerDefinition("filesystem", "Local filesystem operations for reading, writing, and managing files", filesystem_tools))
|
||||
|
||||
# Database Server
|
||||
database_tools = [
|
||||
ToolDefinition(
|
||||
name="db_query",
|
||||
description="Execute a SQL query on the database and return results",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sql": {"type": "string", "description": "SQL query to execute"},
|
||||
"database": {"type": "string", "description": "Database name"},
|
||||
"timeout": {"type": "integer", "description": "Query timeout in seconds"}
|
||||
},
|
||||
"required": ["sql"]
|
||||
},
|
||||
server="database"
|
||||
),
|
||||
ToolDefinition(
|
||||
name="db_insert",
|
||||
description="Insert data into a database table",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"table": {"type": "string", "description": "Table name"},
|
||||
"data": {"type": "object", "description": "Data to insert as key-value pairs"},
|
||||
"database": {"type": "string", "description": "Database name"}
|
||||
},
|
||||
"required": ["table", "data"]
|
||||
},
|
||||
server="database"
|
||||
),
|
||||
ToolDefinition(
|
||||
name="db_update",
|
||||
description="Update records in a database table",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"table": {"type": "string", "description": "Table name"},
|
||||
"data": {"type": "object", "description": "Data to update"},
|
||||
"where": {"type": "string", "description": "WHERE clause condition"},
|
||||
"database": {"type": "string", "description": "Database name"}
|
||||
},
|
||||
"required": ["table", "data", "where"]
|
||||
},
|
||||
server="database"
|
||||
),
|
||||
ToolDefinition(
|
||||
name="db_delete",
|
||||
description="Delete records from a database table",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"table": {"type": "string", "description": "Table name"},
|
||||
"where": {"type": "string", "description": "WHERE clause condition"},
|
||||
"database": {"type": "string", "description": "Database name"}
|
||||
},
|
||||
"required": ["table", "where"]
|
||||
},
|
||||
server="database"
|
||||
),
|
||||
ToolDefinition(
|
||||
name="db_schema",
|
||||
description="Get schema information for database tables",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"table": {"type": "string", "description": "Table name (optional, returns all if omitted)"},
|
||||
"database": {"type": "string", "description": "Database name"}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
server="database"
|
||||
)
|
||||
]
|
||||
servers.append(ServerDefinition("database", "Database operations for querying and manipulating structured data", database_tools))
|
||||
|
||||
# Web Server
|
||||
web_tools = [
|
||||
ToolDefinition(
|
||||
name="web_get",
|
||||
description="Make HTTP GET request to a URL and return the response",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {"type": "string", "description": "URL to request"},
|
||||
"headers": {"type": "object", "description": "HTTP headers"},
|
||||
"params": {"type": "object", "description": "Query parameters"}
|
||||
},
|
||||
"required": ["url"]
|
||||
},
|
||||
server="web"
|
||||
),
|
||||
ToolDefinition(
|
||||
name="web_post",
|
||||
description="Make HTTP POST request to a URL with data",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {"type": "string", "description": "URL to post to"},
|
||||
"data": {"type": "object", "description": "Data to send"},
|
||||
"headers": {"type": "object", "description": "HTTP headers"}
|
||||
},
|
||||
"required": ["url", "data"]
|
||||
},
|
||||
server="web"
|
||||
),
|
||||
ToolDefinition(
|
||||
name="web_scrape",
|
||||
description="Scrape and extract data from a web page using CSS selectors",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {"type": "string", "description": "URL to scrape"},
|
||||
"selector": {"type": "string", "description": "CSS selector for elements to extract"},
|
||||
"attributes": {"type": "array", "items": {"type": "string"}, "description": "Attributes to extract"}
|
||||
},
|
||||
"required": ["url", "selector"]
|
||||
},
|
||||
server="web"
|
||||
),
|
||||
ToolDefinition(
|
||||
name="web_download",
|
||||
description="Download a file from a URL to local filesystem",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {"type": "string", "description": "URL to download from"},
|
||||
"destination": {"type": "string", "description": "Local path to save file"},
|
||||
"headers": {"type": "object", "description": "HTTP headers"}
|
||||
},
|
||||
"required": ["url", "destination"]
|
||||
},
|
||||
server="web"
|
||||
)
|
||||
]
|
||||
servers.append(ServerDefinition("web", "HTTP operations for making requests and scraping web content", web_tools))
|
||||
|
||||
# Analytics Server
|
||||
analytics_tools = [
|
||||
ToolDefinition(
|
||||
name="analytics_summarize",
|
||||
description="Calculate summary statistics for a dataset",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {"type": "array", "description": "Array of numeric values"},
|
||||
"metrics": {"type": "array", "items": {"type": "string"}, "description": "Metrics to calculate (mean, median, std, etc.)"}
|
||||
},
|
||||
"required": ["data"]
|
||||
},
|
||||
server="analytics"
|
||||
),
|
||||
ToolDefinition(
|
||||
name="analytics_visualize",
|
||||
description="Create visualizations from data (charts, graphs)",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {"type": "object", "description": "Data to visualize"},
|
||||
"chart_type": {"type": "string", "enum": ["line", "bar", "scatter", "pie"], "description": "Type of chart"},
|
||||
"title": {"type": "string", "description": "Chart title"},
|
||||
"output_path": {"type": "string", "description": "Path to save chart image"}
|
||||
},
|
||||
"required": ["data", "chart_type"]
|
||||
},
|
||||
server="analytics"
|
||||
),
|
||||
ToolDefinition(
|
||||
name="analytics_correlation",
|
||||
description="Calculate correlation between variables in a dataset",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {"type": "object", "description": "Dataset with variables as keys"},
|
||||
"method": {"type": "string", "enum": ["pearson", "spearman"], "description": "Correlation method"}
|
||||
},
|
||||
"required": ["data"]
|
||||
},
|
||||
server="analytics"
|
||||
),
|
||||
ToolDefinition(
|
||||
name="analytics_predict",
|
||||
description="Make predictions using machine learning models",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"model_type": {"type": "string", "description": "Type of ML model (linear, tree, etc.)"},
|
||||
"features": {"type": "array", "description": "Feature values for prediction"},
|
||||
"trained_model_path": {"type": "string", "description": "Path to trained model file"}
|
||||
},
|
||||
"required": ["features"]
|
||||
},
|
||||
server="analytics"
|
||||
)
|
||||
]
|
||||
servers.append(ServerDefinition("analytics", "Data analysis and visualization tools for statistical operations", analytics_tools))
|
||||
|
||||
# Communication Server
|
||||
communication_tools = [
|
||||
ToolDefinition(
|
||||
name="comm_send_email",
|
||||
description="Send an email message to recipients",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"to": {"type": "array", "items": {"type": "string"}, "description": "Recipient email addresses"},
|
||||
"subject": {"type": "string", "description": "Email subject"},
|
||||
"body": {"type": "string", "description": "Email body content"},
|
||||
"attachments": {"type": "array", "items": {"type": "string"}, "description": "File paths to attach"}
|
||||
},
|
||||
"required": ["to", "subject", "body"]
|
||||
},
|
||||
server="communication"
|
||||
),
|
||||
ToolDefinition(
|
||||
name="comm_send_slack",
|
||||
description="Send a message to a Slack channel or user",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"channel": {"type": "string", "description": "Channel name or user ID"},
|
||||
"message": {"type": "string", "description": "Message content"},
|
||||
"thread_ts": {"type": "string", "description": "Thread timestamp for replies"}
|
||||
},
|
||||
"required": ["channel", "message"]
|
||||
},
|
||||
server="communication"
|
||||
),
|
||||
ToolDefinition(
|
||||
name="comm_read_email",
|
||||
description="Read emails from inbox with filtering options",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"folder": {"type": "string", "description": "Email folder (inbox, sent, etc.)"},
|
||||
"unread_only": {"type": "boolean", "description": "Only return unread emails"},
|
||||
"limit": {"type": "integer", "description": "Maximum number of emails to return"}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
server="communication"
|
||||
),
|
||||
ToolDefinition(
|
||||
name="comm_schedule_meeting",
|
||||
description="Schedule a meeting in calendar",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"type": "string", "description": "Meeting title"},
|
||||
"start_time": {"type": "string", "description": "Start time (ISO format)"},
|
||||
"duration": {"type": "integer", "description": "Duration in minutes"},
|
||||
"attendees": {"type": "array", "items": {"type": "string"}, "description": "Attendee emails"}
|
||||
},
|
||||
"required": ["title", "start_time", "attendees"]
|
||||
},
|
||||
server="communication"
|
||||
)
|
||||
]
|
||||
servers.append(ServerDefinition("communication", "Email, messaging, and calendar tools for communication", communication_tools))
|
||||
|
||||
# DevOps Server
|
||||
devops_tools = [
|
||||
ToolDefinition(
|
||||
name="devops_deploy",
|
||||
description="Deploy application to specified environment",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"environment": {"type": "string", "enum": ["dev", "staging", "production"], "description": "Target environment"},
|
||||
"version": {"type": "string", "description": "Version to deploy"},
|
||||
"rollback": {"type": "boolean", "description": "Enable auto-rollback on failure"}
|
||||
},
|
||||
"required": ["environment", "version"]
|
||||
},
|
||||
server="devops"
|
||||
),
|
||||
ToolDefinition(
|
||||
name="devops_monitor",
|
||||
description="Get monitoring metrics for services and infrastructure",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"service": {"type": "string", "description": "Service name to monitor"},
|
||||
"metrics": {"type": "array", "items": {"type": "string"}, "description": "Metrics to retrieve (cpu, memory, etc.)"},
|
||||
"timerange": {"type": "string", "description": "Time range (e.g., '1h', '24h')"}
|
||||
},
|
||||
"required": ["service"]
|
||||
},
|
||||
server="devops"
|
||||
),
|
||||
ToolDefinition(
|
||||
name="devops_logs",
|
||||
description="Query and filter application logs",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"service": {"type": "string", "description": "Service name"},
|
||||
"level": {"type": "string", "enum": ["debug", "info", "warning", "error"], "description": "Log level filter"},
|
||||
"query": {"type": "string", "description": "Text to search in logs"},
|
||||
"limit": {"type": "integer", "description": "Maximum number of log entries"}
|
||||
},
|
||||
"required": ["service"]
|
||||
},
|
||||
server="devops"
|
||||
),
|
||||
ToolDefinition(
|
||||
name="devops_run_pipeline",
|
||||
description="Trigger a CI/CD pipeline execution",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pipeline": {"type": "string", "description": "Pipeline name or ID"},
|
||||
"branch": {"type": "string", "description": "Git branch to build"},
|
||||
"parameters": {"type": "object", "description": "Pipeline parameters"}
|
||||
},
|
||||
"required": ["pipeline"]
|
||||
},
|
||||
server="devops"
|
||||
)
|
||||
]
|
||||
servers.append(ServerDefinition("devops", "DevOps tools for deployment, monitoring, and CI/CD operations", devops_tools))
|
||||
|
||||
# Cloud Server
|
||||
cloud_tools = [
|
||||
ToolDefinition(
|
||||
name="cloud_create_vm",
|
||||
description="Create a virtual machine in the cloud",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"instance_type": {"type": "string", "description": "VM instance type (e.g., 't2.micro')"},
|
||||
"region": {"type": "string", "description": "Cloud region"},
|
||||
"image_id": {"type": "string", "description": "OS image ID"},
|
||||
"tags": {"type": "object", "description": "Tags for the VM"}
|
||||
},
|
||||
"required": ["instance_type", "region"]
|
||||
},
|
||||
server="cloud"
|
||||
),
|
||||
ToolDefinition(
|
||||
name="cloud_list_resources",
|
||||
description="List cloud resources (VMs, storage, databases)",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resource_type": {"type": "string", "enum": ["vm", "storage", "database", "network"], "description": "Type of resource"},
|
||||
"region": {"type": "string", "description": "Cloud region"},
|
||||
"filters": {"type": "object", "description": "Filter criteria"}
|
||||
},
|
||||
"required": ["resource_type"]
|
||||
},
|
||||
server="cloud"
|
||||
),
|
||||
ToolDefinition(
|
||||
name="cloud_upload_storage",
|
||||
description="Upload files to cloud storage",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bucket": {"type": "string", "description": "Storage bucket name"},
|
||||
"file_path": {"type": "string", "description": "Local file path"},
|
||||
"destination": {"type": "string", "description": "Destination path in bucket"},
|
||||
"public": {"type": "boolean", "description": "Make file publicly accessible"}
|
||||
},
|
||||
"required": ["bucket", "file_path"]
|
||||
},
|
||||
server="cloud"
|
||||
),
|
||||
ToolDefinition(
|
||||
name="cloud_manage_firewall",
|
||||
description="Configure cloud firewall rules",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resource_id": {"type": "string", "description": "Resource ID to configure"},
|
||||
"action": {"type": "string", "enum": ["add", "remove"], "description": "Action to perform"},
|
||||
"rule": {"type": "object", "description": "Firewall rule specification"}
|
||||
},
|
||||
"required": ["resource_id", "action", "rule"]
|
||||
},
|
||||
server="cloud"
|
||||
)
|
||||
]
|
||||
servers.append(ServerDefinition("cloud", "Cloud infrastructure management for VMs, storage, and networking", cloud_tools))
|
||||
|
||||
return servers
|
||||
|
||||
|
||||
def get_all_tools(servers: List[ServerDefinition]) -> List[ToolDefinition]:
|
||||
"""Get flat list of all tools from all servers."""
|
||||
all_tools = []
|
||||
for server in servers:
|
||||
all_tools.extend(server.tools)
|
||||
return all_tools
|
||||
|
||||
|
||||
def count_tokens_in_schema(schema: Dict[str, Any]) -> int:
|
||||
"""Rough estimation of tokens in a tool schema (approximately 1 token per 4 characters)."""
|
||||
import json
|
||||
schema_str = json.dumps(schema)
|
||||
return len(schema_str) // 4
|
||||
|
||||
|
||||
def calculate_total_tokens(tools: List[ToolDefinition]) -> int:
|
||||
"""Calculate total tokens required to inject all tool schemas."""
|
||||
total = 0
|
||||
for tool in tools:
|
||||
total += count_tokens_in_schema(tool.to_schema())
|
||||
return total
|
||||
Reference in New Issue
Block a user