agentensemble 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentensemble/__init__.py +76 -0
- agentensemble/agents/__init__.py +32 -0
- agentensemble/agents/base.py +84 -0
- agentensemble/agents/hybrid_agent.py +147 -0
- agentensemble/agents/rag_agent.py +92 -0
- agentensemble/agents/react_agent.py +118 -0
- agentensemble/agents/stategraph_agent.py +97 -0
- agentensemble/agents/structured_agent.py +210 -0
- agentensemble/orchestration/__init__.py +22 -0
- agentensemble/orchestration/ensemble.py +145 -0
- agentensemble/orchestration/pipeline.py +27 -0
- agentensemble/orchestration/supervisor.py +27 -0
- agentensemble/orchestration/swarm.py +27 -0
- agentensemble/py.typed +3 -0
- agentensemble/testing/__init__.py +16 -0
- agentensemble/testing/benchmark.py +87 -0
- agentensemble/testing/comparison.py +82 -0
- agentensemble/testing/metrics.py +81 -0
- agentensemble/tools/__init__.py +20 -0
- agentensemble/tools/rag.py +226 -0
- agentensemble/tools/registry.py +83 -0
- agentensemble/tools/scraper.py +123 -0
- agentensemble/tools/search.py +155 -0
- agentensemble/tools/validator.py +51 -0
- agentensemble/utils/__init__.py +11 -0
- agentensemble/utils/llm.py +109 -0
- agentensemble-0.1.0.dist-info/METADATA +407 -0
- agentensemble-0.1.0.dist-info/RECORD +30 -0
- agentensemble-0.1.0.dist-info/WHEEL +5 -0
- agentensemble-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Metrics
|
|
3
|
+
|
|
4
|
+
Performance metrics for agent evaluation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, List
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Metrics:
|
|
11
|
+
"""
|
|
12
|
+
Metrics calculator for agent performance.
|
|
13
|
+
|
|
14
|
+
Tracks success rate, execution time, cost, etc.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def success_rate(results: List[Dict[str, Any]]) -> float:
|
|
19
|
+
"""
|
|
20
|
+
Calculate success rate.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
results: List of result dictionaries
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Success rate (0.0 to 1.0)
|
|
27
|
+
"""
|
|
28
|
+
if not results:
|
|
29
|
+
return 0.0
|
|
30
|
+
|
|
31
|
+
successful = sum(1 for r in results if r.get("success", False))
|
|
32
|
+
return successful / len(results)
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def average_execution_time(results: List[Dict[str, Any]]) -> float:
|
|
36
|
+
"""
|
|
37
|
+
Calculate average execution time.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
results: List of result dictionaries
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Average execution time in seconds
|
|
44
|
+
"""
|
|
45
|
+
if not results:
|
|
46
|
+
return 0.0
|
|
47
|
+
|
|
48
|
+
times = [r.get("execution_time", 0) for r in results]
|
|
49
|
+
return sum(times) / len(times)
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def total_cost(results: List[Dict[str, Any]]) -> float:
|
|
53
|
+
"""
|
|
54
|
+
Calculate total cost.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
results: List of result dictionaries
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Total cost in dollars
|
|
61
|
+
"""
|
|
62
|
+
return sum(r.get("cost", 0) for r in results)
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def calculate_all(results: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
66
|
+
"""
|
|
67
|
+
Calculate all metrics.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
results: List of result dictionaries
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Dictionary with all metrics
|
|
74
|
+
"""
|
|
75
|
+
return {
|
|
76
|
+
"success_rate": Metrics.success_rate(results),
|
|
77
|
+
"average_execution_time": Metrics.average_execution_time(results),
|
|
78
|
+
"total_cost": Metrics.total_cost(results),
|
|
79
|
+
"total_tests": len(results),
|
|
80
|
+
}
|
|
81
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool Ecosystem
|
|
3
|
+
|
|
4
|
+
Provides built-in tools and tool registry for agent use.
|
|
5
|
+
All tools integrate with LangChain community tools.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from agentensemble.tools.registry import ToolRegistry
|
|
9
|
+
from agentensemble.tools.search import SearchTool
|
|
10
|
+
from agentensemble.tools.scraper import ScraperTool
|
|
11
|
+
from agentensemble.tools.rag import RAGTool
|
|
12
|
+
from agentensemble.tools.validator import ValidationTool
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"ToolRegistry",
|
|
16
|
+
"SearchTool",
|
|
17
|
+
"ScraperTool",
|
|
18
|
+
"RAGTool",
|
|
19
|
+
"ValidationTool",
|
|
20
|
+
]
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RAG Tool
|
|
3
|
+
|
|
4
|
+
Retrieval-Augmented Generation tool following LangChain RAG patterns.
|
|
5
|
+
Based on: https://docs.langchain.com/oss/python/langchain/rag
|
|
6
|
+
|
|
7
|
+
Supports:
|
|
8
|
+
- Agentic RAG: Tool-based retrieval for iterative queries
|
|
9
|
+
- RAG Chain: Two-step chain with single LLM call
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Any, List, Optional, Tuple
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
from dotenv import load_dotenv
|
|
16
|
+
|
|
17
|
+
# Load environment variables
|
|
18
|
+
load_dotenv()
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from langchain_community.document_loaders import WebBaseLoader
|
|
22
|
+
from langchain_community.vectorstores import Chroma
|
|
23
|
+
from langchain_core.vectorstores import InMemoryVectorStore
|
|
24
|
+
from langchain_mistralai import MistralAIEmbeddings
|
|
25
|
+
from langchain_core.documents import Document
|
|
26
|
+
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
|
27
|
+
from langchain_core.tools import tool
|
|
28
|
+
|
|
29
|
+
LANGCHAIN_AVAILABLE = True
|
|
30
|
+
except ImportError:
|
|
31
|
+
LANGCHAIN_AVAILABLE = False
|
|
32
|
+
WebBaseLoader = None
|
|
33
|
+
Chroma = None
|
|
34
|
+
InMemoryVectorStore = None
|
|
35
|
+
MistralAIEmbeddings = None
|
|
36
|
+
Document = None
|
|
37
|
+
RecursiveCharacterTextSplitter = None
|
|
38
|
+
tool = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RAGTool:
|
|
42
|
+
"""
|
|
43
|
+
RAG tool following LangChain RAG patterns.
|
|
44
|
+
|
|
45
|
+
Implements both:
|
|
46
|
+
1. Agentic RAG: Tool-based retrieval (as per LangChain tutorial)
|
|
47
|
+
2. RAG Chain: Two-step chain with single LLM call
|
|
48
|
+
|
|
49
|
+
Based on: https://docs.langchain.com/oss/python/langchain/rag
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
vector_store: Optional[Any] = None,
|
|
55
|
+
embeddings: Optional[Any] = None,
|
|
56
|
+
chunk_size: int = 1000,
|
|
57
|
+
chunk_overlap: int = 200,
|
|
58
|
+
fallback_strategies: int = 3,
|
|
59
|
+
**kwargs,
|
|
60
|
+
):
|
|
61
|
+
"""
|
|
62
|
+
Initialize RAG tool.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
vector_store: Pre-initialized vector store (or None to create one)
|
|
66
|
+
embeddings: Pre-initialized embeddings model (or None to use Mistral)
|
|
67
|
+
chunk_size: Size of text chunks for splitting
|
|
68
|
+
chunk_overlap: Overlap between chunks
|
|
69
|
+
fallback_strategies: Number of fallback query variations
|
|
70
|
+
**kwargs: Additional configuration
|
|
71
|
+
"""
|
|
72
|
+
self.name = "rag"
|
|
73
|
+
self.vector_store = vector_store
|
|
74
|
+
self.embeddings = embeddings
|
|
75
|
+
self.chunk_size = chunk_size
|
|
76
|
+
self.chunk_overlap = chunk_overlap
|
|
77
|
+
self.fallback_strategies = fallback_strategies
|
|
78
|
+
self.config = kwargs
|
|
79
|
+
self._text_splitter: Optional[Any] = None
|
|
80
|
+
|
|
81
|
+
def _initialize_components(self) -> None:
|
|
82
|
+
"""Initialize text splitter and ensure embeddings/vector store exist"""
|
|
83
|
+
if not LANGCHAIN_AVAILABLE:
|
|
84
|
+
raise ImportError(
|
|
85
|
+
"langchain-community, langchain-mistralai, and langchain-text-splitters "
|
|
86
|
+
"are required for RAGTool. Install with: "
|
|
87
|
+
"pip install langchain-community langchain-mistralai langchain-text-splitters"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Initialize text splitter (as per LangChain RAG tutorial)
|
|
91
|
+
if not self._text_splitter and RecursiveCharacterTextSplitter:
|
|
92
|
+
self._text_splitter = RecursiveCharacterTextSplitter(
|
|
93
|
+
chunk_size=self.chunk_size, chunk_overlap=self.chunk_overlap
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Initialize embeddings if not provided (using Mistral)
|
|
97
|
+
if not self.embeddings and MistralAIEmbeddings:
|
|
98
|
+
from agentensemble.utils.llm import get_mistral_embeddings
|
|
99
|
+
|
|
100
|
+
self.embeddings = get_mistral_embeddings()
|
|
101
|
+
|
|
102
|
+
# Initialize vector store if not provided
|
|
103
|
+
if not self.vector_store:
|
|
104
|
+
if InMemoryVectorStore and self.embeddings:
|
|
105
|
+
self.vector_store = InMemoryVectorStore(self.embeddings)
|
|
106
|
+
|
|
107
|
+
def index_documents(self, urls: List[str]) -> None:
|
|
108
|
+
"""
|
|
109
|
+
Index documents from URLs (following LangChain RAG tutorial pattern).
|
|
110
|
+
|
|
111
|
+
This implements the indexing pipeline:
|
|
112
|
+
1. Load documents with WebBaseLoader
|
|
113
|
+
2. Split with RecursiveCharacterTextSplitter
|
|
114
|
+
3. Store in vector store
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
urls: List of URLs to load and index
|
|
118
|
+
"""
|
|
119
|
+
if not LANGCHAIN_AVAILABLE or not WebBaseLoader:
|
|
120
|
+
raise ImportError("langchain-community required for indexing")
|
|
121
|
+
|
|
122
|
+
self._initialize_components()
|
|
123
|
+
|
|
124
|
+
# Load documents (as per LangChain tutorial)
|
|
125
|
+
documents = []
|
|
126
|
+
for url in urls:
|
|
127
|
+
try:
|
|
128
|
+
loader = WebBaseLoader(web_paths=(url,))
|
|
129
|
+
docs = loader.load()
|
|
130
|
+
documents.extend(docs)
|
|
131
|
+
except Exception as e:
|
|
132
|
+
print(f"Warning: Failed to load {url}: {e}")
|
|
133
|
+
|
|
134
|
+
if not documents:
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
# Split documents (as per LangChain tutorial)
|
|
138
|
+
if self._text_splitter:
|
|
139
|
+
all_splits = self._text_splitter.split_documents(documents)
|
|
140
|
+
else:
|
|
141
|
+
all_splits = documents
|
|
142
|
+
|
|
143
|
+
# Store in vector store (as per LangChain tutorial)
|
|
144
|
+
if self.vector_store:
|
|
145
|
+
self.vector_store.add_documents(documents=all_splits)
|
|
146
|
+
|
|
147
|
+
def create_retrieval_tool(self):
|
|
148
|
+
"""
|
|
149
|
+
Create a LangChain tool for retrieval (following LangChain RAG tutorial).
|
|
150
|
+
|
|
151
|
+
Returns a tool decorated with @tool that can be used with create_agent.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
LangChain tool instance
|
|
155
|
+
"""
|
|
156
|
+
if not LANGCHAIN_AVAILABLE or not tool:
|
|
157
|
+
raise ImportError("langchain required for tool creation")
|
|
158
|
+
|
|
159
|
+
if not self.vector_store:
|
|
160
|
+
raise ValueError("Vector store must be initialized. Call index_documents() first.")
|
|
161
|
+
|
|
162
|
+
# Create retrieval tool following LangChain RAG tutorial pattern
|
|
163
|
+
@tool(response_format="content_and_artifact")
|
|
164
|
+
def retrieve_context(query: str):
|
|
165
|
+
"""Retrieve information to help answer a query."""
|
|
166
|
+
retrieved_docs = self.vector_store.similarity_search(query, k=2)
|
|
167
|
+
serialized = "\n\n".join(
|
|
168
|
+
(f"Source: {doc.metadata}\nContent: {doc.page_content}") for doc in retrieved_docs
|
|
169
|
+
)
|
|
170
|
+
return serialized, retrieved_docs
|
|
171
|
+
|
|
172
|
+
return retrieve_context
|
|
173
|
+
|
|
174
|
+
def retrieve(self, query: str, k: int = 2) -> Tuple[str, List[Document]]:
|
|
175
|
+
"""
|
|
176
|
+
Retrieve context for a query (following LangChain RAG tutorial pattern).
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
query: Search query
|
|
180
|
+
k: Number of documents to retrieve
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Tuple of (serialized context, retrieved documents)
|
|
184
|
+
"""
|
|
185
|
+
if not self.vector_store:
|
|
186
|
+
raise ValueError("Vector store not initialized. Call index_documents() first.")
|
|
187
|
+
|
|
188
|
+
retrieved_docs = self.vector_store.similarity_search(query, k=k)
|
|
189
|
+
serialized = "\n\n".join(
|
|
190
|
+
(f"Source: {doc.metadata}\nContent: {doc.page_content}") for doc in retrieved_docs
|
|
191
|
+
)
|
|
192
|
+
return serialized, retrieved_docs
|
|
193
|
+
|
|
194
|
+
def run(self, question: str, urls: Optional[List[str]] = None, **kwargs) -> str:
|
|
195
|
+
"""
|
|
196
|
+
Execute RAG query (simple interface for backward compatibility).
|
|
197
|
+
|
|
198
|
+
For full LangChain RAG patterns, use:
|
|
199
|
+
- create_retrieval_tool() for agentic RAG
|
|
200
|
+
- retrieve() for direct retrieval
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
question: Question to answer
|
|
204
|
+
urls: Optional URLs to index first
|
|
205
|
+
**kwargs: Additional parameters
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Retrieved context
|
|
209
|
+
"""
|
|
210
|
+
if not LANGCHAIN_AVAILABLE:
|
|
211
|
+
return f"RAG answer to '{question}' [langchain-community required]"
|
|
212
|
+
|
|
213
|
+
# If URLs provided, index them first
|
|
214
|
+
if urls:
|
|
215
|
+
self.index_documents(urls)
|
|
216
|
+
|
|
217
|
+
# Retrieve context
|
|
218
|
+
if self.vector_store:
|
|
219
|
+
context, docs = self.retrieve(question, k=3)
|
|
220
|
+
return context
|
|
221
|
+
|
|
222
|
+
return f"RAG answer to '{question}' [index documents first with index_documents()]"
|
|
223
|
+
|
|
224
|
+
def __call__(self, question: str, urls: Optional[List[str]] = None, **kwargs) -> str:
|
|
225
|
+
"""Make tool callable"""
|
|
226
|
+
return self.run(question, urls, **kwargs)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool Registry
|
|
3
|
+
|
|
4
|
+
Centralized tool management for agents.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ToolRegistry:
|
|
11
|
+
"""
|
|
12
|
+
Registry for managing and organizing tools.
|
|
13
|
+
|
|
14
|
+
Provides centralized tool discovery and management.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
"""Initialize tool registry"""
|
|
19
|
+
self._tools: Dict[str, Any] = {}
|
|
20
|
+
|
|
21
|
+
def register(self, tool: Any, name: Optional[str] = None) -> None:
|
|
22
|
+
"""
|
|
23
|
+
Register a tool.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
tool: Tool instance
|
|
27
|
+
name: Optional name (uses tool.name if available)
|
|
28
|
+
"""
|
|
29
|
+
tool_name = name or getattr(tool, "name", f"tool_{len(self._tools)}")
|
|
30
|
+
self._tools[tool_name] = tool
|
|
31
|
+
|
|
32
|
+
def register_many(self, tools: List[Any]) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Register multiple tools.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
tools: List of tool instances
|
|
38
|
+
"""
|
|
39
|
+
for tool in tools:
|
|
40
|
+
self.register(tool)
|
|
41
|
+
|
|
42
|
+
def get_tool(self, name: str) -> Any:
|
|
43
|
+
"""
|
|
44
|
+
Get a tool by name.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
name: Tool name
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Tool instance
|
|
51
|
+
"""
|
|
52
|
+
if name not in self._tools:
|
|
53
|
+
raise ValueError(f"Tool '{name}' not found in registry")
|
|
54
|
+
return self._tools[name]
|
|
55
|
+
|
|
56
|
+
def get_tools(self) -> List[Any]:
|
|
57
|
+
"""
|
|
58
|
+
Get all registered tools.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
List of all tools
|
|
62
|
+
"""
|
|
63
|
+
return list(self._tools.values())
|
|
64
|
+
|
|
65
|
+
def list_tools(self) -> List[str]:
|
|
66
|
+
"""
|
|
67
|
+
List all tool names.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
List of tool names
|
|
71
|
+
"""
|
|
72
|
+
return list(self._tools.keys())
|
|
73
|
+
|
|
74
|
+
def unregister(self, name: str) -> None:
|
|
75
|
+
"""
|
|
76
|
+
Unregister a tool.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
name: Tool name
|
|
80
|
+
"""
|
|
81
|
+
if name in self._tools:
|
|
82
|
+
del self._tools[name]
|
|
83
|
+
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Scraper Tool
|
|
3
|
+
|
|
4
|
+
Web scraping functionality for agents using LangChain community tools.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from langchain_community.tools import (
|
|
11
|
+
DuckDuckGoSearchRun,
|
|
12
|
+
)
|
|
13
|
+
from langchain_community.tools.playwright import (
|
|
14
|
+
ClickTool,
|
|
15
|
+
ExtractHyperlinksTool,
|
|
16
|
+
GetElementsTool,
|
|
17
|
+
NavigateTool,
|
|
18
|
+
NavigateBackTool,
|
|
19
|
+
)
|
|
20
|
+
from langchain_core.tools import BaseTool
|
|
21
|
+
LANGCHAIN_AVAILABLE = True
|
|
22
|
+
except ImportError:
|
|
23
|
+
LANGCHAIN_AVAILABLE = False
|
|
24
|
+
BaseTool = object
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ScraperTool:
|
|
28
|
+
"""
|
|
29
|
+
Web scraping tool for agents using LangChain community tools.
|
|
30
|
+
|
|
31
|
+
Supports:
|
|
32
|
+
- Playwright-based scraping (requires playwright)
|
|
33
|
+
- Simple URL fetching
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
provider: str = "simple",
|
|
39
|
+
api_key: Optional[str] = None,
|
|
40
|
+
**kwargs
|
|
41
|
+
):
|
|
42
|
+
"""
|
|
43
|
+
Initialize scraper tool.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
provider: Scraping provider ("simple", "playwright")
|
|
47
|
+
api_key: API key (not currently used, reserved for future)
|
|
48
|
+
**kwargs: Additional configuration
|
|
49
|
+
"""
|
|
50
|
+
self.name = "scraper"
|
|
51
|
+
self.provider = provider
|
|
52
|
+
self.api_key = api_key
|
|
53
|
+
self._navigate_tool: Optional[BaseTool] = None
|
|
54
|
+
self._initialize_tool()
|
|
55
|
+
|
|
56
|
+
def _initialize_tool(self) -> None:
|
|
57
|
+
"""Initialize the underlying LangChain tool"""
|
|
58
|
+
if not LANGCHAIN_AVAILABLE:
|
|
59
|
+
raise ImportError(
|
|
60
|
+
"langchain-community is required for ScraperTool. "
|
|
61
|
+
"Install with: pip install langchain-community"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if self.provider == "playwright":
|
|
65
|
+
try:
|
|
66
|
+
self._navigate_tool = NavigateTool()
|
|
67
|
+
except Exception as e:
|
|
68
|
+
raise ImportError(
|
|
69
|
+
"Playwright tools require playwright. "
|
|
70
|
+
"Install with: pip install playwright && playwright install"
|
|
71
|
+
) from e
|
|
72
|
+
elif self.provider == "simple":
|
|
73
|
+
# Simple HTTP-based scraping (placeholder for now)
|
|
74
|
+
# In production, integrate with requests/httpx
|
|
75
|
+
self._navigate_tool = None
|
|
76
|
+
else:
|
|
77
|
+
raise ValueError(
|
|
78
|
+
f"Unknown provider: {self.provider}. "
|
|
79
|
+
"Supported: 'simple', 'playwright'"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def run(self, url: str, question: Optional[str] = None, **kwargs) -> str:
|
|
83
|
+
"""
|
|
84
|
+
Scrape a URL and optionally answer a question.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
url: URL to scrape
|
|
88
|
+
question: Optional question to answer from scraped content
|
|
89
|
+
**kwargs: Additional parameters
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Scraped content or answer
|
|
93
|
+
"""
|
|
94
|
+
if self.provider == "playwright" and self._navigate_tool:
|
|
95
|
+
# Use Playwright navigation tool
|
|
96
|
+
try:
|
|
97
|
+
result = self._navigate_tool.invoke({"url": url})
|
|
98
|
+
content = str(result)
|
|
99
|
+
if question:
|
|
100
|
+
# In production, use LLM to answer question from content
|
|
101
|
+
return f"Answer to '{question}' from {url}: {content[:500]}..."
|
|
102
|
+
return content
|
|
103
|
+
except Exception as e:
|
|
104
|
+
return f"Error scraping {url}: {str(e)}"
|
|
105
|
+
else:
|
|
106
|
+
# Simple placeholder - in production, use httpx/requests
|
|
107
|
+
if question:
|
|
108
|
+
return f"Answer to '{question}' from {url}: [scraped content - install playwright for full functionality]"
|
|
109
|
+
return f"Scraped content from {url} [install playwright for full functionality]"
|
|
110
|
+
|
|
111
|
+
def __call__(self, url: str, question: Optional[str] = None, **kwargs) -> str:
|
|
112
|
+
"""Make tool callable"""
|
|
113
|
+
return self.run(url, question, **kwargs)
|
|
114
|
+
|
|
115
|
+
def as_langchain_tool(self) -> Optional[BaseTool]:
|
|
116
|
+
"""
|
|
117
|
+
Return the underlying LangChain tool for direct use.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
LangChain BaseTool instance or None
|
|
121
|
+
"""
|
|
122
|
+
return self._navigate_tool
|
|
123
|
+
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Search Tool
|
|
3
|
+
|
|
4
|
+
Web search functionality for agents using LangChain community tools.
|
|
5
|
+
Defaults to Serper API if API key is available, otherwise falls back to DuckDuckGo.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
from dotenv import load_dotenv
|
|
11
|
+
|
|
12
|
+
# Load environment variables
|
|
13
|
+
load_dotenv()
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from langchain_community.tools import (
|
|
17
|
+
DuckDuckGoSearchRun,
|
|
18
|
+
DuckDuckGoSearchResults,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Optional: Serper API (requires API key) - only import if needed
|
|
22
|
+
try:
|
|
23
|
+
from langchain_community.utilities import GoogleSerperAPIWrapper
|
|
24
|
+
from langchain_core.tools import Tool
|
|
25
|
+
|
|
26
|
+
SERPER_AVAILABLE = True
|
|
27
|
+
except ImportError:
|
|
28
|
+
SERPER_AVAILABLE = False
|
|
29
|
+
GoogleSerperAPIWrapper = None
|
|
30
|
+
Tool = None
|
|
31
|
+
from langchain_core.tools import BaseTool
|
|
32
|
+
|
|
33
|
+
LANGCHAIN_AVAILABLE = True
|
|
34
|
+
except ImportError:
|
|
35
|
+
LANGCHAIN_AVAILABLE = False
|
|
36
|
+
BaseTool = object
|
|
37
|
+
SERPER_AVAILABLE = False
|
|
38
|
+
GoogleSerperAPIWrapper = None
|
|
39
|
+
Tool = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SearchTool:
|
|
43
|
+
"""
|
|
44
|
+
Web search tool for agents using LangChain community tools.
|
|
45
|
+
|
|
46
|
+
**Default: Serper API** (if SERPER_API_KEY is in .env)
|
|
47
|
+
**Fallback: DuckDuckGo** (free, open-source, no API key required)
|
|
48
|
+
|
|
49
|
+
Supports multiple providers:
|
|
50
|
+
- Serper API (default if API key available, requires SERPER_API_KEY in .env)
|
|
51
|
+
- DuckDuckGo (fallback, free, open-source, no API key required) ⭐
|
|
52
|
+
- DuckDuckGo Results (structured results, free)
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, provider: Optional[str] = None, api_key: Optional[str] = None, **kwargs):
|
|
56
|
+
"""
|
|
57
|
+
Initialize search tool.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
provider: Search provider
|
|
61
|
+
- None (auto-detect: Serper if API key available, else DuckDuckGo)
|
|
62
|
+
- "serper" (requires SERPER_API_KEY in .env or api_key parameter)
|
|
63
|
+
- "duckduckgo" (free, open-source) ⭐
|
|
64
|
+
- "duckduckgo_results" (structured results, free)
|
|
65
|
+
api_key: API key for Serper (defaults to SERPER_API_KEY from .env)
|
|
66
|
+
**kwargs: Additional configuration
|
|
67
|
+
"""
|
|
68
|
+
self.name = "search"
|
|
69
|
+
|
|
70
|
+
# Auto-detect provider if not specified
|
|
71
|
+
if provider is None:
|
|
72
|
+
serper_key = api_key or os.getenv("SERPER_API_KEY")
|
|
73
|
+
if serper_key and SERPER_AVAILABLE:
|
|
74
|
+
provider = "serper"
|
|
75
|
+
else:
|
|
76
|
+
provider = "duckduckgo"
|
|
77
|
+
|
|
78
|
+
self.provider = provider
|
|
79
|
+
# Use provided api_key or get from environment
|
|
80
|
+
self.api_key = api_key or os.getenv("SERPER_API_KEY")
|
|
81
|
+
self._tool: Optional[BaseTool] = None
|
|
82
|
+
self._initialize_tool()
|
|
83
|
+
|
|
84
|
+
def _initialize_tool(self) -> None:
|
|
85
|
+
"""Initialize the underlying LangChain tool"""
|
|
86
|
+
if not LANGCHAIN_AVAILABLE:
|
|
87
|
+
raise ImportError(
|
|
88
|
+
"langchain-community is required for SearchTool. "
|
|
89
|
+
"Install with: pip install langchain-community"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if self.provider == "serper":
|
|
93
|
+
if not SERPER_AVAILABLE or not GoogleSerperAPIWrapper:
|
|
94
|
+
# Fallback to DuckDuckGo if Serper not available
|
|
95
|
+
print("Warning: Serper API not available, falling back to DuckDuckGo")
|
|
96
|
+
self.provider = "duckduckgo"
|
|
97
|
+
self._tool = DuckDuckGoSearchRun()
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
if not self.api_key:
|
|
101
|
+
# Fallback to DuckDuckGo if no API key
|
|
102
|
+
print("Warning: SERPER_API_KEY not found, falling back to DuckDuckGo")
|
|
103
|
+
self.provider = "duckduckgo"
|
|
104
|
+
self._tool = DuckDuckGoSearchRun()
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
# Create Serper API tool
|
|
108
|
+
serper = GoogleSerperAPIWrapper(serper_api_key=self.api_key)
|
|
109
|
+
self._tool = Tool(
|
|
110
|
+
name="serper_search", description="Search the web using Serper API", func=serper.run
|
|
111
|
+
)
|
|
112
|
+
elif self.provider == "duckduckgo":
|
|
113
|
+
self._tool = DuckDuckGoSearchRun()
|
|
114
|
+
elif self.provider == "duckduckgo_results":
|
|
115
|
+
self._tool = DuckDuckGoSearchResults()
|
|
116
|
+
else:
|
|
117
|
+
raise ValueError(
|
|
118
|
+
f"Unknown provider: {self.provider}. "
|
|
119
|
+
"Supported: 'serper' (default if API key available), 'duckduckgo' (free, fallback), 'duckduckgo_results' (free)"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def run(self, query: str, **kwargs) -> str:
|
|
123
|
+
"""
|
|
124
|
+
Execute search.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
query: Search query
|
|
128
|
+
**kwargs: Additional parameters
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Search results
|
|
132
|
+
"""
|
|
133
|
+
if not self._tool:
|
|
134
|
+
self._initialize_tool()
|
|
135
|
+
|
|
136
|
+
if self._tool:
|
|
137
|
+
# Use LangChain tool's invoke method
|
|
138
|
+
result = self._tool.invoke(query)
|
|
139
|
+
return str(result)
|
|
140
|
+
return f"Search results for: {query}"
|
|
141
|
+
|
|
142
|
+
def __call__(self, query: str, **kwargs) -> str:
|
|
143
|
+
"""Make tool callable"""
|
|
144
|
+
return self.run(query, **kwargs)
|
|
145
|
+
|
|
146
|
+
def as_langchain_tool(self) -> BaseTool:
|
|
147
|
+
"""
|
|
148
|
+
Return the underlying LangChain tool for direct use.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
LangChain BaseTool instance
|
|
152
|
+
"""
|
|
153
|
+
if not self._tool:
|
|
154
|
+
self._initialize_tool()
|
|
155
|
+
return self._tool
|