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.
@@ -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