clap-agents 0.2.2__py3-none-any.whl → 0.3.1__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.
@@ -36,7 +36,7 @@ class FastEmbedEmbeddings(EmbeddingFunctionInterface):
36
36
  if not _FASTEMBED_LIB_AVAILABLE:
37
37
  raise ImportError(
38
38
  "The 'fastembed' library is required to use FastEmbedEmbeddings. "
39
- "Install with 'pip install fastembed' or 'pip install \"clap-agents[qdrant]\"' (if qdrant includes it as an extra)."
39
+ "Install with 'pip install fastembed' "
40
40
  )
41
41
 
42
42
  self.model_name = model_name
@@ -57,7 +57,7 @@ class FastEmbedEmbeddings(EmbeddingFunctionInterface):
57
57
  except Exception as e:
58
58
  raise RuntimeError(f"Failed to initialize fastembed model '{self.model_name}': {e}")
59
59
 
60
- async def __call__(self, input: List[str]) -> List[List[float]]: # Changed to 'input'
60
+ async def __call__(self, input: List[str]) -> List[List[float]]:
61
61
  if not input: return []
62
62
  if not _FASTEMBED_LIB_AVAILABLE: raise RuntimeError("FastEmbed library not available.")
63
63
 
@@ -1,4 +1,3 @@
1
- # src/clap/llm_services/__init__.py
2
1
  from .base import LLMServiceInterface, StandardizedLLMResponse, LLMToolCall
3
2
  from .groq_service import GroqService
4
3
  from .google_openai_compat_service import GoogleOpenAICompatService
clap/llm_services/base.py CHANGED
@@ -32,7 +32,6 @@ class LLMServiceInterface(abc.ABC):
32
32
  messages: List[Dict[str, Any]],
33
33
  tools: Optional[List[Dict[str, Any]]] = None,
34
34
  tool_choice: str = "auto",
35
- # Optional:
36
35
  # temperature: Optional[float] = None,
37
36
  # max_tokens: Optional[int] = None,
38
37
  ) -> StandardizedLLMResponse:
@@ -1,4 +1,3 @@
1
- # --- START OF agentic_patterns/llm_services/google_openai_compat_service.py ---
2
1
 
3
2
  import os
4
3
  import json
@@ -56,7 +55,7 @@ class GoogleOpenAICompatService(LLMServiceInterface):
56
55
  Sends messages via the OpenAI SDK (to Google's endpoint) and returns a standardized response.
57
56
 
58
57
  Args:
59
- model: The Google model identifier (e.g., "gemini-1.5-flash").
58
+ model: The Google model identifier (e.g., "gemini-2.0-flash").
60
59
  messages: Chat history in the OpenAI dictionary format.
61
60
  tools: Tool schemas in the OpenAI function format.
62
61
  tool_choice: Tool choice setting ("auto", "none", etc.).
@@ -92,6 +91,7 @@ class GoogleOpenAICompatService(LLMServiceInterface):
92
91
  for tc in message.tool_calls:
93
92
  tool_call_id = getattr(tc, 'id', None)
94
93
  if not tool_call_id:
94
+ #raise ValueError("Received a tool call from the Gemini API without a required 'id'.")
95
95
  tool_call_id = f"compat_call_{uuid.uuid4().hex[:6]}"
96
96
  print(f"{Fore.YELLOW}Warning: Tool call from Google compat layer missing ID. Generated fallback: {tool_call_id}{Fore.RESET}")
97
97
 
@@ -18,7 +18,6 @@ class GroqService(LLMServiceInterface):
18
18
  If None, a new client will be created using environment variables.
19
19
  """
20
20
  self.client = client or AsyncGroq()
21
- # Add any other Groq-specific initialization here if needed
22
21
 
23
22
  async def get_llm_response(
24
23
  self,
@@ -26,7 +25,6 @@ class GroqService(LLMServiceInterface):
26
25
  messages: List[Dict[str, Any]],
27
26
  tools: Optional[List[Dict[str, Any]]] = None,
28
27
  tool_choice: str = "auto",
29
- # Add other relevant Groq parameters if desired, e.g., temperature, max_tokens
30
28
  # temperature: Optional[float] = None,
31
29
  # max_tokens: Optional[int] = None,
32
30
  ) -> StandardizedLLMResponse:
@@ -64,27 +64,35 @@ class OllamaOpenAICompatService(LLMServiceInterface):
64
64
  if not request_model: raise ValueError("Ollama model name not specified.")
65
65
  try:
66
66
  api_kwargs: Dict[str, Any] = {"messages": messages, "model": request_model}
67
+
67
68
  if tools and tool_choice != "none":
68
69
  api_kwargs["tools"] = tools
69
70
  if isinstance(tool_choice, dict) or tool_choice in ["auto", "required", "none"]: api_kwargs["tool_choice"] = tool_choice
70
71
  else: api_kwargs["tools"] = None; api_kwargs["tool_choice"] = None
72
+
71
73
  if temperature is not None: api_kwargs["temperature"] = temperature
72
74
  if max_tokens is not None: api_kwargs["max_tokens"] = max_tokens
73
75
  api_kwargs = {k: v for k, v in api_kwargs.items() if v is not None}
74
76
  # print(f"OllamaService: Sending request to model '{request_model}'")
75
77
  response = await self._client.chat.completions.create(**api_kwargs)
78
+
76
79
  message = response.choices[0].message
80
+
77
81
  text_content = message.content
78
82
  tool_calls_std: List[LLMToolCall] = []
83
+
79
84
  if message.tool_calls:
80
85
  for tc in message.tool_calls:
81
86
  if tc.id and tc.function and tc.function.name and tc.function.arguments is not None:
82
87
  tool_calls_std.append(LLMToolCall(id=tc.id, function_name=tc.function.name, function_arguments_json_str=tc.function.arguments))
83
88
  else: print(f"{Fore.YELLOW}Warning: Incomplete tool_call from Ollama: {tc}{Fore.RESET}")
89
+
84
90
  return StandardizedLLMResponse(text_content=text_content, tool_calls=tool_calls_std)
91
+
85
92
  except _OpenAIError_Placeholder_Type as e: # Use placeholder
86
93
  err_msg = f"Ollama (OpenAI Compat) API Error: {e}"
87
94
  if hasattr(e, 'response') and e.response and hasattr(e.response, 'text'): err_msg += f" - Details: {e.response.text}"
95
+
88
96
  print(f"{Fore.RED}{err_msg}{Fore.RESET}")
89
97
  return StandardizedLLMResponse(text_content=err_msg)
90
98
  except Exception as e:
@@ -98,4 +106,3 @@ class OllamaOpenAICompatService(LLMServiceInterface):
98
106
  elif hasattr(self._client, "_client") and hasattr(self._client._client, "is_closed"): # For httpx client in openai <1.0
99
107
  if not self._client._client.is_closed: await self._client._client.aclose() # type: ignore
100
108
  # print("OllamaService: Client closed.")
101
- # --- END OF FILE ---
clap/mcp_client/client.py CHANGED
@@ -39,7 +39,7 @@ class MCPClientManager:
39
39
  self._connect_locks: Dict[str, asyncio.Lock] = {
40
40
  name: asyncio.Lock() for name in server_configs
41
41
  }
42
- self._manager_lock = asyncio.Lock() # General lock for manager state
42
+ self._manager_lock = asyncio.Lock()
43
43
 
44
44
  async def _ensure_connected(self, server_name: str):
45
45
  """
@@ -56,7 +56,8 @@ class Agent:
56
56
  llm_service (Optional[LLMServiceInterface]): Service for LLM calls (defaults to GroqService).
57
57
  mcp_manager (Optional[MCPClientManager]): Shared MCP client manager.
58
58
  mcp_server_names (Optional[List[str]]): MCP servers this agent uses.
59
- vector_store (Optional[VectorStoreInterface]): Vector store instance for RAG.
59
+ vector_store (Optional[VectorStoreInterface]): Vector store instance for RAG.
60
+ parallel_tool_calls : Determine parallel or sequential execution of agent's tools.
60
61
  # embedding_function(Optional[EmbeddingFunction]): EF if needed by agent.
61
62
 
62
63
  """
@@ -71,7 +72,9 @@ class Agent:
71
72
  llm_service: Optional[LLMServiceInterface] = None,
72
73
  mcp_manager: Optional[MCPClientManager] = None,
73
74
  mcp_server_names: Optional[List[str]] = None,
74
- vector_store: Optional[VectorStoreInterface] = None,
75
+ vector_store: Optional[VectorStoreInterface] = None,
76
+ parallel_tool_calls: bool = True ,
77
+ **kwargs
75
78
  # embedding_function: Optional[EmbeddingFunction] = None,
76
79
 
77
80
  ):
@@ -84,6 +87,7 @@ class Agent:
84
87
  self.local_tools = tools or []
85
88
 
86
89
  self.vector_store = vector_store
90
+ self.react_agent_kwargs = kwargs
87
91
  # self.embedding_function = embedding_function
88
92
 
89
93
  llm_service_instance = llm_service or GroqService()
@@ -98,7 +102,8 @@ class Agent:
98
102
  tools=self.local_tools,
99
103
  mcp_manager=self.mcp_manager,
100
104
  mcp_server_names=self.mcp_server_names,
101
- vector_store=self.vector_store
105
+ vector_store=self.vector_store,
106
+ parallel_tool_calls=parallel_tool_calls
102
107
  )
103
108
 
104
109
  self.dependencies: List['Agent'] = []
@@ -190,7 +195,7 @@ class Agent:
190
195
  self.task_description = original_task_description
191
196
 
192
197
  print(f"Agent {self.name}: Running ReactAgent...")
193
- raw_output = await self.react_agent.run(user_msg=msg)
198
+ raw_output = await self.react_agent.run(user_msg=msg,**self.react_agent_kwargs)
194
199
  output_data = {"output": raw_output}
195
200
 
196
201
  print(f"Agent {self.name}: Passing context to {len(self.dependents)} dependents...")
@@ -54,10 +54,12 @@ class ReactAgent:
54
54
  mcp_server_names: Optional[List[str]] = None,
55
55
  vector_store: Optional[VectorStoreInterface] = None,
56
56
  system_prompt: str = "",
57
+ parallel_tool_calls: bool = True,
57
58
  ) -> None:
58
59
  self.llm_service = llm_service
59
60
  self.model = model
60
61
  self.agent_name = agent_name
62
+ self.parallel_tool_calls = parallel_tool_calls
61
63
  self.system_prompt = (system_prompt + "\n\n" + CORE_SYSTEM_PROMPT).strip()
62
64
 
63
65
 
@@ -275,30 +277,45 @@ class ReactAgent:
275
277
  return {tool_call_id: result_str}
276
278
 
277
279
 
278
-
279
-
280
280
 
281
281
  async def process_tool_calls(self, tool_calls: List[LLMToolCall]) -> Dict[str, Any]:
282
- """Processes multiple tool calls concurrently."""
283
- observations = {}
282
+ """
283
+ Processes tool calls using the configured strategy (parallel or sequential).
284
+ """
284
285
  if not isinstance(tool_calls, list):
285
286
  print(f"{Fore.RED}Error: Expected a list of LLMToolCall, got {type(tool_calls)}{Fore.RESET}")
286
- return observations
287
+ return {}
287
288
 
288
- tasks = [self._execute_single_tool_call(tc) for tc in tool_calls]
289
- results = await asyncio.gather(*tasks, return_exceptions=True)
289
+ observations = {}
290
290
 
291
- for result in results:
292
- if isinstance(result, dict) and len(result) == 1:
293
- observations.update(result)
294
- elif isinstance(result, Exception):
295
-
296
- print(f"{Fore.RED}Error during concurrent tool execution gather: {result}{Fore.RESET}")
297
-
298
- # observations[f"error_{len(observations)}"] = f"Tool execution failed: {result}"
299
- else:
300
- print(f"{Fore.RED}Error: Unexpected item in tool execution results: {result}{Fore.RESET}")
291
+ if self.parallel_tool_calls:
292
+ # PARALLEL EXECUTION (FASTER, BUT CAN CAUSE RACE CONDITIONS)
293
+ print(f"{Fore.BLUE}[{self.agent_name}] Executing {len(tool_calls)} tool calls in PARALLEL...{Fore.RESET}")
294
+ tasks = [self._execute_single_tool_call(tc) for tc in tool_calls]
295
+ results = await asyncio.gather(*tasks, return_exceptions=True)
296
+
297
+ for result in results:
298
+ if isinstance(result, dict) and len(result) == 1:
299
+ observations.update(result)
300
+ elif isinstance(result, Exception):
301
+ print(f"{Fore.RED}Error during parallel tool execution gather: {result}{Fore.RESET}")
302
+ else:
303
+ print(f"{Fore.RED}Error: Unexpected item in parallel tool execution results: {result}{Fore.RESET}")
301
304
 
305
+ else:
306
+ # SEQUENTIAL EXECUTION
307
+ print(f"{Fore.YELLOW}[{self.agent_name}] Executing {len(tool_calls)} tool calls SEQUENTIALLY...{Fore.RESET}")
308
+ for tool_call in tool_calls:
309
+ try:
310
+ result = await self._execute_single_tool_call(tool_call)
311
+ if isinstance(result, dict) and len(result) == 1:
312
+ observations.update(result)
313
+ else:
314
+ print(f"{Fore.RED}Error: Unexpected item in sequential tool execution result: {result}{Fore.RESET}")
315
+ except Exception as e:
316
+ print(f"{Fore.RED}Error during sequential execution of {tool_call.function_name}: {e}{Fore.RESET}")
317
+ observations[tool_call.id] = f"An unexpected error occurred: {e}"
318
+
302
319
  return observations
303
320
 
304
321
 
clap/tool_pattern/tool.py CHANGED
@@ -16,9 +16,11 @@ def get_fn_signature(fn: Callable) -> dict:
16
16
  sig = inspect.signature(fn)
17
17
  for name, type_hint in fn.__annotations__.items():
18
18
  if name == "return": continue
19
+
19
20
  param_type_name = getattr(type_hint, "__name__", str(type_hint))
20
21
  schema_type = type_mapping.get(param_type_name.lower(), "string")
21
22
  parameters["properties"][name] = {"type": schema_type}
23
+
22
24
  if sig.parameters[name].default is inspect.Parameter.empty:
23
25
  parameters["required"].append(name)
24
26
  if not parameters.get("required"):
clap/tools/__init__.py CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  from .web_search import duckduckgo_search
3
2
  from .web_crawler import scrape_url, extract_text_by_query
4
3
  from .email_tools import send_email, fetch_recent_emails
@@ -9,5 +8,20 @@ __all__ = [
9
8
  "extract_text_by_query",
10
9
  "send_email",
11
10
  "fetch_recent_emails",
11
+ "get_wallet_balance",
12
+ "send_eth",
13
+ "interact_with_contract",
14
+ 'get_erc20_balance'
15
+ 'swap_tokens_for_tokens',
16
+ 'wrap_eth',
17
+ 'get_token_price'
12
18
  ]
13
19
 
20
+ try:
21
+ from .web3_tools import get_wallet_balance, send_eth, interact_with_contract, get_erc20_balance, swap_tokens_for_tokens, wrap_eth, get_token_price
22
+ __all__.extend([
23
+ "get_wallet_balance", "send_eth", "interact_with_contract",
24
+ "get_erc20_balance", "swap_tokens_for_tokens", "wrap_eth", "get_token_price"
25
+ ])
26
+ except ImportError:
27
+ pass
@@ -0,0 +1,301 @@
1
+ import os
2
+ from web3 import Web3
3
+ from clap.tool_pattern.tool import tool
4
+ import json
5
+
6
+ # Environment variables are loaded by the main application script.
7
+
8
+ WEB3_PROVIDER_URL = os.getenv("WEB3_PROVIDER_URL")
9
+ AGENT_PRIVATE_KEY = os.getenv("AGENT_PRIVATE_KEY")
10
+
11
+ # --- Official Testnet Addresses ---
12
+ WETH_CONTRACT_ADDRESS = "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14"
13
+ UNISWAP_ROUTER_ADDRESS = "0x3bFA4769FB09eefC5a399D6D47036A5d3fA67B54"
14
+ CHAINLINK_ETH_USD_PRICE_FEED_ADDRESS = "0x694AA1769357215DE4FAC081bf1f309aDC325306"
15
+
16
+
17
+ # --- Correct, Centralized ABIs ---
18
+ CHAINLINK_PRICE_FEED_ABI = """
19
+ [
20
+ {
21
+ "inputs": [],
22
+ "name": "latestRoundData",
23
+ "outputs": [
24
+ { "internalType": "uint80", "name": "roundId", "type": "uint80" },
25
+ { "internalType": "int256", "name": "answer", "type": "int256" },
26
+ { "internalType": "uint256", "name": "startedAt", "type": "uint256" },
27
+ { "internalType": "uint256", "name": "updatedAt", "type": "uint256" },
28
+ { "internalType": "uint80", "name": "answeredInRound", "type": "uint80" }
29
+ ],
30
+ "stateMutability": "view",
31
+ "type": "function"
32
+ },
33
+ {
34
+ "inputs": [],
35
+ "name": "decimals",
36
+ "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }],
37
+ "stateMutability": "view",
38
+ "type": "function"
39
+ }
40
+ ]
41
+ """
42
+ ERC20_STANDARD_ABI = """
43
+ [
44
+ {"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"type":"function"},
45
+ {"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"type":"function"},
46
+ {"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"type":"function"}
47
+ ]
48
+ """
49
+ WETH_ABI = """
50
+ [
51
+ {"constant":false,"inputs":[],"name":"deposit","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},
52
+ {"constant":true,"inputs":[{"name":"","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}
53
+ ]
54
+ """
55
+ UNISWAP_ABI = """
56
+ [{"inputs":[{"components":[{"type":"address","name":"tokenIn"},{"type":"address","name":"tokenOut"},{"type":"uint24","name":"fee"},{"type":"address","name":"recipient"},{"type":"uint256","name":"amountIn"},{"type":"uint256","name":"amountOutMinimum"},{"type":"uint160","name":"sqrtPriceLimitX96"}],"type":"tuple","name":"params"}],"name":"exactInputSingle","outputs":[{"type":"uint256","name":"amountOut"}],"stateMutability":"payable","type":"function"}]
57
+ """
58
+
59
+
60
+ w3 = None
61
+ agent_account = None
62
+
63
+ def _initialize_web3():
64
+ """Initializes Web3 instance and account if they don't exist."""
65
+ global w3, agent_account, WEB3_PROVIDER_URL, AGENT_PRIVATE_KEY
66
+ if w3 is None:
67
+ WEB3_PROVIDER_URL = os.getenv("WEB3_PROVIDER_URL")
68
+ AGENT_PRIVATE_KEY = os.getenv("AGENT_PRIVATE_KEY")
69
+
70
+ if not WEB3_PROVIDER_URL or not AGENT_PRIVATE_KEY:
71
+ raise ConnectionError("Web3 provider URL or Agent private key not found.")
72
+ w3 = Web3(Web3.HTTPProvider(WEB3_PROVIDER_URL))
73
+ agent_account = w3.eth.account.from_key(AGENT_PRIVATE_KEY)
74
+
75
+ @tool
76
+ def get_wallet_balance(address: str) -> str:
77
+ """
78
+ Gets the native token balance (Sepolia ETH) of a given wallet address.
79
+ """
80
+ try:
81
+ _initialize_web3()
82
+ balance_wei = w3.eth.get_balance(address)
83
+ balance_eth = w3.from_wei(balance_wei, 'ether')
84
+ return f"The balance of address {address} is {balance_eth} ETH."
85
+ except Exception as e:
86
+ return f"Error getting balance for address {address}: {e}"
87
+
88
+ @tool
89
+ def get_token_price(token_pair: str) -> str:
90
+ """
91
+ Gets the latest price of a token pair (e.g., 'ETH/USD') from a Chainlink Price Feed.
92
+
93
+ Args:
94
+ token_pair: The token pair to get the price for. Currently supports 'ETH/USD'.
95
+
96
+ Returns:
97
+ A string indicating the latest price of the token pair.
98
+ """
99
+ try:
100
+ _initialize_web3()
101
+ if token_pair.upper() != "ETH/USD":
102
+ return "Error: This tool currently only supports the 'ETH/USD' token pair."
103
+
104
+ price_feed_contract = w3.eth.contract(
105
+ address=CHAINLINK_ETH_USD_PRICE_FEED_ADDRESS,
106
+ abi=CHAINLINK_PRICE_FEED_ABI
107
+ )
108
+
109
+ # The latestRoundData function returns a tuple of values. The price is the second element.
110
+ latest_data = price_feed_contract.functions.latestRoundData().call()
111
+ price_raw = latest_data[1]
112
+
113
+ # The price feed also has a 'decimals' function to tell us where to put the decimal point.
114
+ price_decimals = price_feed_contract.functions.decimals().call()
115
+
116
+ # Convert the raw price to a human-readable format
117
+ price = price_raw / (10 ** price_decimals)
118
+
119
+ return f"The latest price for {token_pair} is ${price:.2f}"
120
+
121
+ except Exception as e:
122
+ return f"Error getting token price: {type(e).__name__} - {e}"
123
+
124
+ @tool
125
+ def send_eth(to_address: str, amount_eth: float) -> str:
126
+ """
127
+ Creates, signs, and sends a transaction to transfer Sepolia ETH.
128
+ """
129
+ try:
130
+ _initialize_web3()
131
+ nonce = w3.eth.get_transaction_count(agent_account.address)
132
+
133
+ tx = {
134
+ 'from': agent_account.address,
135
+ 'to': to_address,
136
+ 'value': w3.to_wei(amount_eth, 'ether'),
137
+ 'gas': 21000,
138
+ 'gasPrice': w3.eth.gas_price,
139
+ 'nonce': nonce,
140
+ }
141
+
142
+ signed_tx = w3.eth.account.sign_transaction(tx,AGENT_PRIVATE_KEY)
143
+
144
+ tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
145
+
146
+ return f"Transaction sent successfully. Transaction hash: {w3.to_hex(tx_hash)}"
147
+ except Exception as e:
148
+ return f"Error sending transaction: {type(e).__name__} - {e}"
149
+
150
+
151
+
152
+ @tool
153
+ def interact_with_contract(
154
+ contract_address: str,
155
+ abi: str,
156
+ function_name: str,
157
+ function_args: list,
158
+ is_write_transaction: bool = False
159
+ ) -> str:
160
+ """
161
+ Interacts with a smart contract by calling one of its functions.
162
+ Can perform read-only calls (e.g., getting data) or write transactions (e.g., sending tokens).
163
+
164
+ Args:
165
+ contract_address: The address of the smart contract (e.g., "0x...").
166
+ abi: The contract's Application Binary Interface (ABI) as a JSON string.
167
+ function_name: The exact name of the function to call.
168
+ function_args: A list of arguments to pass to the function, in order.
169
+ is_write_transaction: Set to True if this is a state-changing transaction that requires gas. Defaults to False (read-only call).
170
+
171
+ Returns:
172
+ A string containing the result of the call or a transaction hash if it's a write transaction.
173
+ """
174
+ try:
175
+ _initialize_web3()
176
+
177
+ # 1. Validate inputs
178
+ if not w3.is_address(contract_address):
179
+ return "Error: Invalid 'contract_address'."
180
+ try:
181
+ abi_json = json.loads(abi)
182
+ except json.JSONDecodeError:
183
+ return "Error: The provided 'abi' is not a valid JSON string."
184
+
185
+ # 2. Create the contract object
186
+ contract = w3.eth.contract(address=contract_address, abi=abi_json)
187
+
188
+ # 3. Get the function object from the contract
189
+ func_to_call = getattr(contract.functions, function_name)
190
+ if not func_to_call:
191
+ return f"Error: Function '{function_name}' not found in the contract's ABI."
192
+
193
+ # 4. Prepare the function call with its arguments
194
+ prepared_func = func_to_call(*function_args)
195
+
196
+ # 5. Execute as either a read-only 'call' or a write 'transaction'
197
+ if is_write_transaction:
198
+ # This is a state-changing transaction that costs gas
199
+ nonce = w3.eth.get_transaction_count(agent_account.address)
200
+ tx = prepared_func.build_transaction({
201
+ 'from': agent_account.address,
202
+ 'nonce': nonce,
203
+ 'gasPrice': w3.eth.gas_price
204
+ })
205
+ signed_tx = w3.eth.account.sign_transaction(tx, private_key=AGENT_PRIVATE_KEY)
206
+ tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
207
+ return f"Write transaction sent successfully. Transaction hash: {w3.to_hex(tx_hash)}"
208
+ else:
209
+ # This is a read-only call that is free and instant
210
+ result = prepared_func.call()
211
+ # Convert the result to a JSON string to ensure it's readable by the LLM
212
+ return json.dumps(result)
213
+
214
+ except Exception as e:
215
+ return f"Error interacting with contract: {type(e).__name__} - {e}"
216
+
217
+ @tool
218
+ def get_erc20_balance(token_address: str, wallet_address: str) -> str:
219
+ """Gets the balance of a specific ERC-20 token for a given wallet."""
220
+ try:
221
+ _initialize_web3()
222
+ chk_token_address = Web3.to_checksum_address(token_address)
223
+ chk_wallet_address = Web3.to_checksum_address(wallet_address)
224
+
225
+ token_contract = w3.eth.contract(address=chk_token_address, abi=ERC20_STANDARD_ABI)
226
+
227
+ # This will now work correctly with a real contract address
228
+ decimals = token_contract.functions.decimals().call()
229
+ raw_balance = token_contract.functions.balanceOf(chk_wallet_address).call()
230
+
231
+ balance = raw_balance / (10 ** decimals)
232
+ return str(balance)
233
+
234
+ except Exception as e:
235
+ # A simple, honest error handler for genuine problems.
236
+ return f"Error in get_erc20_balance for token {token_address}: {e}"
237
+
238
+ @tool
239
+ def wrap_eth(amount_eth: float) -> str:
240
+ """Converts native ETH into WETH (Wrapped ETH) by depositing it into the WETH contract."""
241
+ try:
242
+ _initialize_web3()
243
+ weth_contract = w3.eth.contract(address=WETH_CONTRACT_ADDRESS, abi=WETH_ABI)
244
+ tx = weth_contract.functions.deposit().build_transaction({
245
+ 'from': agent_account.address,
246
+ 'value': w3.to_wei(amount_eth, 'ether'),
247
+ 'nonce': w3.eth.get_transaction_count(agent_account.address),
248
+ 'gasPrice': w3.eth.gas_price
249
+ })
250
+ signed_tx = w3.eth.account.sign_transaction(tx, private_key=AGENT_PRIVATE_KEY)
251
+ tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
252
+ print(f"Waiting for wrap transaction {w3.to_hex(tx_hash)} to be confirmed...")
253
+ w3.eth.wait_for_transaction_receipt(tx_hash)
254
+
255
+ return f"ETH wrapping successful and confirmed. Hash: {w3.to_hex(tx_hash)}"
256
+ except Exception as e:
257
+ return f"Error wrapping ETH: {e}"
258
+
259
+ @tool
260
+ def swap_tokens_for_tokens(token_in_address: str, token_out_address: str, amount_in: float, fee: int = 3000) -> str:
261
+ """Swaps an exact amount of an input token for another on Uniswap V3, waiting for confirmations."""
262
+ try:
263
+ _initialize_web3()
264
+ chk_token_in = Web3.to_checksum_address(token_in_address)
265
+ chk_token_out = Web3.to_checksum_address(token_out_address)
266
+ chk_router_address = Web3.to_checksum_address(UNISWAP_ROUTER_ADDRESS)
267
+
268
+ token_in_contract = w3.eth.contract(address=chk_token_in, abi=ERC20_STANDARD_ABI)
269
+ decimals = token_in_contract.functions.decimals().call()
270
+ amount_in_wei = int(amount_in * (10**decimals))
271
+
272
+ current_nonce = w3.eth.get_transaction_count(agent_account.address)
273
+
274
+ # Step 1: Approve
275
+ approve_tx = token_in_contract.functions.approve(chk_router_address, amount_in_wei).build_transaction({
276
+ 'from': agent_account.address, 'nonce': current_nonce
277
+ })
278
+ signed_approve_tx = w3.eth.account.sign_transaction(approve_tx, private_key=os.getenv("AGENT_PRIVATE_KEY"))
279
+ approve_tx_hash = w3.eth.send_raw_transaction(signed_approve_tx.raw_transaction)
280
+
281
+ # --- THE FIX ---
282
+ # Wait for the approval transaction to be confirmed.
283
+ print(f"Waiting for approval transaction {w3.to_hex(approve_tx_hash)} to be confirmed...")
284
+ w3.eth.wait_for_transaction_receipt(approve_tx_hash)
285
+
286
+ # Step 2: Swap
287
+ uniswap_router = w3.eth.contract(address=chk_router_address, abi=UNISWAP_ABI)
288
+ swap_params = (chk_token_in, chk_token_out, fee, agent_account.address, amount_in_wei, 0, 0)
289
+
290
+ swap_tx = uniswap_router.functions.exactInputSingle(swap_params).build_transaction({
291
+ 'from': agent_account.address, 'nonce': current_nonce + 1
292
+ })
293
+ signed_swap_tx = w3.eth.account.sign_transaction(swap_tx, private_key=os.getenv("AGENT_PRIVATE_KEY"))
294
+ swap_tx_hash = w3.eth.send_raw_transaction(signed_swap_tx.raw_transaction)
295
+
296
+ print(f"Waiting for swap transaction {w3.to_hex(swap_tx_hash)} to be confirmed...")
297
+ w3.eth.wait_for_transaction_receipt(swap_tx_hash)
298
+
299
+ return f"Swap successful and confirmed. Swap hash: {w3.to_hex(swap_tx_hash)}"
300
+ except Exception as e:
301
+ return f"Error during swap: {e}"
@@ -139,4 +139,4 @@ class ChromaStore(VectorStoreInterface):
139
139
  documents=results.get("documents"), metadatas=results.get("metadatas"), distances=results.get("distances") )
140
140
 
141
141
  async def adelete(self, ids: Optional[List[ID]] = None, where: Optional[Dict[str, Any]] = None, where_document: Optional[Dict[str, Any]] = None) -> None:
142
- await self._run_sync(self._collection.delete, ids=ids, where=where, where_document=where_document)
142
+ await self._run_sync(self._collection.delete, ids=ids, where=where, where_document=where_document)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clap-agents
3
- Version: 0.2.2
3
+ Version: 0.3.1
4
4
  Summary: A Python framework for building cognitive agentic patterns including ReAct agents, Multi-Agent Teams, native tool calling, and MCP client integration.
5
5
  Project-URL: Homepage, https://github.com/MaitreyaM/CLAP-AGENTS.git
6
6
  Project-URL: Repository, https://github.com/MaitreyaM/CLAP-AGENTS.git
@@ -242,6 +242,8 @@ Requires-Dist: pandas; extra == 'all'
242
242
  Requires-Dist: pypdf; extra == 'all'
243
243
  Requires-Dist: qdrant-client[fastembed]>=1.7.0; extra == 'all'
244
244
  Requires-Dist: sentence-transformers; extra == 'all'
245
+ Requires-Dist: tf-keras; extra == 'all'
246
+ Requires-Dist: web3<8.0.0,>=7.12.0; extra == 'all'
245
247
  Provides-Extra: chromadb
246
248
  Requires-Dist: chromadb>=0.5.0; extra == 'chromadb'
247
249
  Provides-Extra: fastembed
@@ -261,12 +263,16 @@ Requires-Dist: ollama>=0.2.0; extra == 'rag'
261
263
  Requires-Dist: pypdf; extra == 'rag'
262
264
  Requires-Dist: qdrant-client[fastembed]>=1.7.0; extra == 'rag'
263
265
  Requires-Dist: sentence-transformers; extra == 'rag'
266
+ Requires-Dist: tf-keras; extra == 'rag'
264
267
  Provides-Extra: sentence-transformers
265
268
  Requires-Dist: sentence-transformers; extra == 'sentence-transformers'
269
+ Requires-Dist: tf-keras; extra == 'sentence-transformers'
266
270
  Provides-Extra: standard-tools
267
271
  Requires-Dist: crawl4ai; extra == 'standard-tools'
268
272
  Provides-Extra: viz
269
273
  Requires-Dist: graphviz; extra == 'viz'
274
+ Provides-Extra: web3
275
+ Requires-Dist: web3<8.0.0,>=7.12.0; extra == 'web3'
270
276
  Description-Content-Type: text/markdown
271
277
 
272
278
  <p align="center">
@@ -506,6 +512,83 @@ async def main():
506
512
  asyncio.run(main())
507
513
  ```
508
514
 
515
+
516
+ New in v0.3.0: Web3 & On-Chain Agent Capabilities
517
+ CLAP now includes a powerful toolkit for building autonomous agents that can interact directly with EVM-compatible blockchains like Ethereum. Your agents can now hold assets, execute transactions, and interact with smart contracts, opening up a new world of possibilities in DeFi, DAOs, and on-chain automation.
518
+ Setup
519
+ To enable Web3 capabilities, install the web3 extra:
520
+ pip install "clap-agents[web3]"
521
+ Use code with caution.
522
+ Bash
523
+ You will also need to set the following variables in your .env file:
524
+ # Your connection to the blockchain (e.g., from Alchemy or Infura)
525
+ WEB3_PROVIDER_URL="https://sepolia.infura.io/v3/YOUR_API_KEY"
526
+
527
+ # The private key for your agent's wallet.
528
+ # WARNING: For testing only. Do not use a key with real funds.
529
+ AGENT_PRIVATE_KEY="0xYourTestnetPrivateKeyHere"
530
+
531
+
532
+
533
+ # Core Web3 Tools
534
+ The framework now includes a suite of pre-built, robust tools for on-chain interaction:
535
+
536
+ get_erc20_balance: Checks the balance of any standard ERC-20 token in a wallet.
537
+
538
+ wrap_eth: Converts native ETH into WETH (Wrapped Ether), a necessary step for interacting with many DeFi protocols.
539
+
540
+ swap_exact_tokens_for_tokens: Executes trades on Uniswap V3, allowing your agent to autonomously rebalance its portfolio.
541
+
542
+ get_token_price: Fetches real-time asset prices from on-chain Chainlink oracles, enabling data-driven decision-making.
543
+
544
+ interact_with_contract: A powerful, generic tool to call any function on any smart contract, given its address and ABI.
545
+
546
+
547
+ # Quick Start: A Simple DeFi Agent
548
+ This example demonstrates an agent that can wrap ETH and then swap it for another token, a common DeFi task.
549
+ # examples/simple_defi_agent.py
550
+ import os
551
+ import asyncio
552
+ from dotenv import load_dotenv
553
+ from clap import ReactAgent, GroqService
554
+ from clap.tools import wrap_eth, swap_exact_tokens_for_tokens
555
+
556
+ load_dotenv()
557
+
558
+ # --- Configuration ---
559
+ WETH_ADDRESS = "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14" # WETH on Sepolia
560
+ USDC_ADDRESS = "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7a98" # USDC on Sepolia
561
+
562
+ async def main():
563
+ # We use a ReactAgent for multi-step reasoning
564
+ agent = ReactAgent(
565
+ llm_service=GroqService(),
566
+ tools=[wrap_eth, swap_exact_tokens_for_tokens],
567
+ model="llama-3.3-70b-versatile",
568
+ system_prompt="You are a DeFi agent. You execute financial transactions precisely as instructed.",
569
+ # For on-chain tasks, sequential execution is safer to avoid race conditions
570
+ parallel_tool_calls=False
571
+ )
572
+
573
+ # A clear, two-step task for the agent
574
+ user_query = f"""
575
+ First, wrap 0.01 ETH.
576
+ Second, after the wrap is successful, swap that 0.01 WETH for USDC.
577
+ The WETH address is {WETH_ADDRESS} and the USDC address is {USDC_ADDRESS}.
578
+ """
579
+
580
+ print("--- Running Simple DeFi Agent ---")
581
+ response = await agent.run(user_msg=user_query, max_rounds=5)
582
+
583
+ print("\n--- Agent Final Response ---")
584
+ print(response)
585
+
586
+ if __name__ == "__main__":
587
+ asyncio.run(main())
588
+
589
+
590
+ This new capability transforms your CLAP agents from simple observers into active participants in the decentralized economy.
591
+
509
592
  ## Exploring Further
510
593
 
511
594
 
@@ -1,26 +1,27 @@
1
1
  clap/__init__.py,sha256=rxxESl-xpSZpM4ZIh-GvHYF74CkQdbe-dSLvhMC_2dQ,1069
2
2
  clap/embedding/__init__.py,sha256=PqnqcSiA_JwEvn69g2DCQHdsffL2l4GNEKo0fAtCqbs,520
3
3
  clap/embedding/base_embedding.py,sha256=0SYicQ-A-rSDqHoFK0IOrRQe0cisOl8OBnis6V43Chs,696
4
- clap/embedding/fastembed_embedding.py,sha256=Xo2H7r45wNqN89WzI8InXtjahOXAcKDyHS0fYt_jtsc,2959
4
+ clap/embedding/fastembed_embedding.py,sha256=fUXCRyctPxwinAG2JCkdmlARU945z7dEsXScIkpqwb0,2862
5
5
  clap/embedding/ollama_embedding.py,sha256=s7IYFs4BuM114Md1cqxim5WzCwCjbEJ48wAZZOgR7KQ,3702
6
6
  clap/embedding/sentence_transformer_embedding.py,sha256=0RAqGxDpjZVwerOLmVirqqnCwC07kHdfAPiy2fgOSCk,1798
7
- clap/llm_services/__init__.py,sha256=js__xIlkbXBLd2q1trYKjATYWa4xLNgPwwJz2Flwi0s,532
8
- clap/llm_services/base.py,sha256=BjSBVOtPMvPZJjI3HwSSI3OvVFyxd5HeOKLI9bN6P7M,2204
9
- clap/llm_services/google_openai_compat_service.py,sha256=X5ehwQEv4DobXCBjhtuo2v365DUl-thDa9Gdc16L67k,4522
10
- clap/llm_services/groq_service.py,sha256=HQoY3IpNHiOe0h6Gy0EMa1YsCUex0CvZ_MxApNMo8Ig,3206
11
- clap/llm_services/ollama_service.py,sha256=LsoJhCw8TZVbfuJeHATo2uPpkXsOWOuuE78HAxTQ9RY,5326
7
+ clap/llm_services/__init__.py,sha256=IBvWmE99PGxHq5Dt4u0G1erZSV80QEC981UULnrD6Tk,496
8
+ clap/llm_services/base.py,sha256=-XKWd6gLAXedIhUUqM_f7sqkVxdfifP2j-BwmF0hUkI,2183
9
+ clap/llm_services/google_openai_compat_service.py,sha256=vN0osfCS6DIFHsCCiB03mKUp4n7SkIJd2PAypBAnC30,4552
10
+ clap/llm_services/groq_service.py,sha256=pcTp24_NgLfp3bGaABzli_Sey7wZsXvFI74VjZ1GvkQ,3051
11
+ clap/llm_services/ollama_service.py,sha256=Qh3W2fb-NDMVB8DS9o3q4jisZvK9U6s-r4ATNbAwVLE,5333
12
12
  clap/mcp_client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- clap/mcp_client/client.py,sha256=skIWhwkHnRXjlHwBvKXNZD5yQDe1iRIjrikn7WwczGM,8713
13
+ clap/mcp_client/client.py,sha256=IVwtkOAEN7LRaFqw14HQDC7n6OTZ3ciVYWj-lWQddE0,8681
14
14
  clap/multiagent_pattern/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- clap/multiagent_pattern/agent.py,sha256=6Og1ZujhAIzQ_JLzuhc4oh9TwJ_mTXl2q2ca302PE8g,8345
15
+ clap/multiagent_pattern/agent.py,sha256=SDebwxaUquFUN_MMCGSYKLIIA3a6tVIls-IGrnEKqJI,8617
16
16
  clap/multiagent_pattern/team.py,sha256=t8Xru3fVPblw75pyuPT1wmI3jlsrZHD_GKAW5APbpFg,7966
17
17
  clap/react_pattern/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
- clap/react_pattern/react_agent.py,sha256=Ron5zz-yn5EHmnXiL5G1sDYF0-3DCiTVujZ2RJI27JQ,23478
18
+ clap/react_pattern/react_agent.py,sha256=v9JYyIwv0vzkOl6kq8Aua7u50rJyU02NomCHTjt24vo,24596
19
19
  clap/tool_pattern/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
- clap/tool_pattern/tool.py,sha256=7fHkLTPafsAWr_ECXZKpQz0zzqEAxSrQ56ns5oWcBSU,5964
20
+ clap/tool_pattern/tool.py,sha256=Y4Uvu5FsCA3S3BZfjM3OOKfhA-o5Q9SfhCzGWfGIQ6o,5974
21
21
  clap/tool_pattern/tool_agent.py,sha256=VTQv9DNU16zgINZKVcX5oDw1lPfw5Y_8bUnW6wad2vE,14439
22
- clap/tools/__init__.py,sha256=veMtEmjaY6aj_aUXNnPT_ijI2XM1DJU-y8dLqwx_t2s,293
22
+ clap/tools/__init__.py,sha256=8UMtxaPkq-pEOD2C0Qm4WZoyJpMxEOEQSDhWNLwAAiI,822
23
23
  clap/tools/email_tools.py,sha256=18aAlbjcSaOzpf9R3H-EGeRsqL5gdzmcJJcW619xOHU,9729
24
+ clap/tools/web3_tools.py,sha256=N5enOoEVMx0T7TjB15NtEYt-_KcAdkm3C-UN0_QLVoo,12894
24
25
  clap/tools/web_crawler.py,sha256=WdFbAKhUsUVveJimmLbzQ6k1BhOdMsg87FjL628HEKM,3542
25
26
  clap/tools/web_search.py,sha256=YT0I1kPrdxMUst-dpsGqqF6aqxMgj3ACwiW_jN9Pu9s,985
26
27
  clap/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -30,9 +31,9 @@ clap/utils/logging.py,sha256=FfrOHXnVJYCgc3mepIMKcIcGSdSWkMpV7ccKPGFxFiM,727
30
31
  clap/utils/rag_utils.py,sha256=Zy-C9l39tHrxYSluaDWcEl1g__uozu4jx5hEGC7h370,6455
31
32
  clap/vector_stores/__init__.py,sha256=H3w5jLdQFbXArVgiidy4RlAalM8a6LAiMlAX0Z-2v7U,404
32
33
  clap/vector_stores/base.py,sha256=nvk8J1oNG3OKFhJfxBGFyVeh9YxoDs9RkB_iOzPBm1w,2853
33
- clap/vector_stores/chroma_store.py,sha256=GWshlx0ZhiCLAhuZOCY_NjJT69r1wM8GSUVCY4CgLgo,7243
34
+ clap/vector_stores/chroma_store.py,sha256=vwkWWGxPwuW45T1PS6D44dXhDG9U_KZWjrMZCOkEXsA,7242
34
35
  clap/vector_stores/qdrant_store.py,sha256=-SwMTb0yaGngpQ9AddDzDIt3x8GZevlFT-0FMkWD28I,9923
35
- clap_agents-0.2.2.dist-info/METADATA,sha256=9qa7O_D0uNxkMTb9oqR5hsjDEmet-g_RIPPgaWCvsqc,27199
36
- clap_agents-0.2.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
37
- clap_agents-0.2.2.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
38
- clap_agents-0.2.2.dist-info/RECORD,,
36
+ clap_agents-0.3.1.dist-info/METADATA,sha256=4HKAihZchNLte3fWinmWBZAKjDtkpIMYJYLyD6jgfUs,30573
37
+ clap_agents-0.3.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
38
+ clap_agents-0.3.1.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
39
+ clap_agents-0.3.1.dist-info/RECORD,,