clap-agents 0.1.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.
- clap/__init__.py +57 -0
- clap/llm_services/__init__.py +0 -0
- clap/llm_services/base.py +68 -0
- clap/llm_services/google_openai_compat_service.py +122 -0
- clap/llm_services/groq_service.py +100 -0
- clap/mcp_client/__init__.py +0 -0
- clap/mcp_client/client.py +208 -0
- clap/multiagent_pattern/__init__.py +0 -0
- clap/multiagent_pattern/agent.py +128 -0
- clap/multiagent_pattern/team.py +154 -0
- clap/react_pattern/__init__.py +0 -0
- clap/react_pattern/react_agent.py +265 -0
- clap/tool_pattern/__init__.py +0 -0
- clap/tool_pattern/tool.py +229 -0
- clap/tool_pattern/tool_agent.py +241 -0
- clap/tools/__init__.py +13 -0
- clap/tools/email_tools.py +230 -0
- clap/tools/web_crawler.py +82 -0
- clap/tools/web_search.py +24 -0
- clap/utils/__init__.py +0 -0
- clap/utils/completions.py +173 -0
- clap/utils/extraction.py +42 -0
- clap/utils/logging.py +28 -0
- clap_agents-0.1.1.dist-info/METADATA +346 -0
- clap_agents-0.1.1.dist-info/RECORD +27 -0
- clap_agents-0.1.1.dist-info/WHEEL +4 -0
- clap_agents-0.1.1.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,229 @@
|
|
1
|
+
|
2
|
+
import json
|
3
|
+
import inspect
|
4
|
+
import functools # Import functools
|
5
|
+
from typing import Callable, Any
|
6
|
+
import anyio
|
7
|
+
import jsonschema
|
8
|
+
|
9
|
+
|
10
|
+
def get_fn_signature(fn: Callable) -> dict:
|
11
|
+
"""
|
12
|
+
Generates the signature (schema) for a given function in JSON Schema format.
|
13
|
+
|
14
|
+
Args:
|
15
|
+
fn (Callable): The function whose signature needs to be extracted.
|
16
|
+
|
17
|
+
Returns:
|
18
|
+
dict: A dictionary representing the function's schema.
|
19
|
+
"""
|
20
|
+
|
21
|
+
type_mapping = {
|
22
|
+
"int": "integer",
|
23
|
+
"str": "string",
|
24
|
+
"bool": "boolean",
|
25
|
+
"float": "number",
|
26
|
+
"list": "array", # Basic support for lists
|
27
|
+
"dict": "object", # Basic support for dicts
|
28
|
+
}
|
29
|
+
|
30
|
+
parameters = {"type": "object", "properties": {}, "required": []}
|
31
|
+
sig = inspect.signature(fn)
|
32
|
+
|
33
|
+
for name, type_hint in fn.__annotations__.items():
|
34
|
+
if name == "return":
|
35
|
+
continue
|
36
|
+
param_type_name = getattr(type_hint, "__name__", str(type_hint))
|
37
|
+
schema_type = type_mapping.get(param_type_name, "string")
|
38
|
+
|
39
|
+
parameters["properties"][name] = {"type": schema_type}
|
40
|
+
|
41
|
+
if sig.parameters[name].default is inspect.Parameter.empty:
|
42
|
+
parameters["required"].append(name)
|
43
|
+
|
44
|
+
if not parameters["required"]:
|
45
|
+
del parameters["required"]
|
46
|
+
|
47
|
+
fn_schema: dict = {
|
48
|
+
"type": "function",
|
49
|
+
"function": {
|
50
|
+
"name": fn.__name__,
|
51
|
+
"description": fn.__doc__,
|
52
|
+
"parameters": parameters,
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
return fn_schema
|
57
|
+
|
58
|
+
|
59
|
+
def validate_arguments(tool_call_args: dict, tool_schema: dict) -> dict:
|
60
|
+
"""
|
61
|
+
Validates and converts arguments in the input dictionary based on the tool's JSON schema.
|
62
|
+
NOTE: This is a simplified validator. For production, use a robust JSON Schema validator.
|
63
|
+
|
64
|
+
Args:
|
65
|
+
tool_call_args (dict): The arguments provided for the tool call (usually strings from LLM).
|
66
|
+
tool_schema (dict): The JSON schema for the tool's parameters.
|
67
|
+
|
68
|
+
Returns:
|
69
|
+
dict: The arguments dictionary with values converted to the correct types if possible.
|
70
|
+
|
71
|
+
Raises:
|
72
|
+
ValueError: If conversion fails for a required argument.
|
73
|
+
"""
|
74
|
+
properties = tool_schema.get("function", {}).get("parameters", {}).get("properties", {})
|
75
|
+
validated_args = {}
|
76
|
+
|
77
|
+
type_mapping = {
|
78
|
+
"integer": int,
|
79
|
+
"string": str,
|
80
|
+
"boolean": bool,
|
81
|
+
"number": float,
|
82
|
+
"array": list,
|
83
|
+
"object": dict
|
84
|
+
}
|
85
|
+
|
86
|
+
for arg_name, arg_value in tool_call_args.items():
|
87
|
+
prop_schema = properties.get(arg_name)
|
88
|
+
if not prop_schema:
|
89
|
+
# Argument not defined in schema, potentially skip or warn
|
90
|
+
print(f"Warning: Argument '{arg_name}' not found in tool schema.")
|
91
|
+
validated_args[arg_name] = arg_value # Pass through unknown args for now
|
92
|
+
continue
|
93
|
+
|
94
|
+
expected_type_name = prop_schema.get("type")
|
95
|
+
expected_type = type_mapping.get(expected_type_name)
|
96
|
+
|
97
|
+
if expected_type:
|
98
|
+
try:
|
99
|
+
if not isinstance(arg_value, expected_type):
|
100
|
+
if expected_type is bool and isinstance(arg_value, str):
|
101
|
+
if arg_value.lower() in ['true', '1', 'yes']:
|
102
|
+
validated_args[arg_name] = True
|
103
|
+
elif arg_value.lower() in ['false', '0', 'no']:
|
104
|
+
validated_args[arg_name] = False
|
105
|
+
else:
|
106
|
+
raise ValueError(f"Cannot convert string '{arg_value}' to boolean.")
|
107
|
+
# Basic handling for array/object assuming JSON string
|
108
|
+
elif expected_type in [list, dict] and isinstance(arg_value, str):
|
109
|
+
try:
|
110
|
+
validated_args[arg_name] = json.loads(arg_value)
|
111
|
+
if not isinstance(validated_args[arg_name], expected_type):
|
112
|
+
raise ValueError(f"Decoded JSON for '{arg_name}' is not the expected type '{expected_type_name}'.")
|
113
|
+
except json.JSONDecodeError:
|
114
|
+
raise ValueError(f"Argument '{arg_name}' with value '{arg_value}' is not valid JSON for type '{expected_type_name}'.")
|
115
|
+
else:
|
116
|
+
validated_args[arg_name] = expected_type(arg_value)
|
117
|
+
else:
|
118
|
+
# Type is already correct
|
119
|
+
validated_args[arg_name] = arg_value
|
120
|
+
except (ValueError, TypeError) as e:
|
121
|
+
raise ValueError(f"Error converting argument '{arg_name}' with value '{arg_value}' to type '{expected_type_name}': {e}")
|
122
|
+
else:
|
123
|
+
# Unknown type in schema, pass through
|
124
|
+
validated_args[arg_name] = arg_value
|
125
|
+
|
126
|
+
# Check for missing required arguments (optional, depends on strictness)
|
127
|
+
# required_args = tool_schema.get("function", {}).get("parameters", {}).get("required", [])
|
128
|
+
# for req_arg in required_args:
|
129
|
+
# if req_arg not in validated_args:
|
130
|
+
# raise ValueError(f"Missing required argument: '{req_arg}'")
|
131
|
+
|
132
|
+
|
133
|
+
return validated_args
|
134
|
+
|
135
|
+
class Tool:
|
136
|
+
"""
|
137
|
+
A class representing a tool that wraps a callable and its schema.
|
138
|
+
Handles both synchronous and asynchronous functions.
|
139
|
+
|
140
|
+
Attributes:
|
141
|
+
name (str): The name of the tool (function).
|
142
|
+
fn (Callable): The function that the tool represents (can be sync or async).
|
143
|
+
fn_schema (dict): Dictionary representing the function's schema in JSON Schema format.
|
144
|
+
fn_signature (str): JSON string representation of the function's signature (legacy, kept for potential compatibility).
|
145
|
+
"""
|
146
|
+
|
147
|
+
def __init__(self, name: str, fn: Callable, fn_schema: dict):
|
148
|
+
self.name = name
|
149
|
+
self.fn = fn
|
150
|
+
self.fn_schema = fn_schema
|
151
|
+
self.fn_signature = json.dumps(fn_schema)
|
152
|
+
|
153
|
+
def __str__(self):
|
154
|
+
return json.dumps(self.fn_schema, indent=2)
|
155
|
+
|
156
|
+
async def run(self, **kwargs) -> Any:
|
157
|
+
"""
|
158
|
+
Executes the tool (function) with provided arguments asynchronously.
|
159
|
+
Validates arguments against the tool's JSON schema before execution.
|
160
|
+
Handles both sync and async tool functions appropriately.
|
161
|
+
|
162
|
+
Args:
|
163
|
+
**kwargs: Keyword arguments provided for the tool call.
|
164
|
+
|
165
|
+
Returns:
|
166
|
+
The result of the function call, or an error string.
|
167
|
+
"""
|
168
|
+
parameter_schema = self.fn_schema.get("function", {}).get("parameters", {})
|
169
|
+
|
170
|
+
# --- Use jsonschema for validation ---
|
171
|
+
try:
|
172
|
+
# Validate the incoming arguments against the parameter schema
|
173
|
+
# Note: jsonschema validates, it doesn't coerce types like the old function
|
174
|
+
jsonschema.validate(instance=kwargs, schema=parameter_schema)
|
175
|
+
# If validation passes, kwargs are structurally correct according to schema
|
176
|
+
|
177
|
+
# Type Coercion/Conversion might still be needed depending on self.fn
|
178
|
+
# If self.fn uses Pydantic models or type hints, it might handle coercion.
|
179
|
+
# Or, you could apply specific conversions based on schema after validation if needed.
|
180
|
+
# For now, assume self.fn or Pydantic handles coercion post-validation.
|
181
|
+
validated_kwargs = kwargs # Use original kwargs after validation passes
|
182
|
+
|
183
|
+
except jsonschema.ValidationError as e:
|
184
|
+
print(f"Argument validation failed for tool {self.name}: {e.message}")
|
185
|
+
return f"Error: Invalid arguments provided - {e.message}"
|
186
|
+
except Exception as e: # Catch other potential validation setup errors
|
187
|
+
print(f"An unexpected error occurred during argument validation for tool {self.name}: {e}")
|
188
|
+
return f"Error: Argument validation failed."
|
189
|
+
# --- End jsonschema validation ---
|
190
|
+
|
191
|
+
# --- Execute the function (sync or async) ---
|
192
|
+
try:
|
193
|
+
if inspect.iscoroutinefunction(self.fn):
|
194
|
+
return await self.fn(**validated_kwargs)
|
195
|
+
else:
|
196
|
+
func_with_args = functools.partial(self.fn, **validated_kwargs)
|
197
|
+
return await anyio.to_thread.run_sync(func_with_args)
|
198
|
+
except Exception as e:
|
199
|
+
# Catch errors during the actual tool execution
|
200
|
+
print(f"Error executing tool {self.name}: {e}")
|
201
|
+
# Consider logging traceback here
|
202
|
+
return f"Error executing tool: {e}"
|
203
|
+
|
204
|
+
|
205
|
+
|
206
|
+
def tool(fn: Callable):
|
207
|
+
"""
|
208
|
+
A decorator that wraps a function (sync or async) into a Tool object,
|
209
|
+
including its JSON schema.
|
210
|
+
|
211
|
+
Args:
|
212
|
+
fn (Callable): The function to be wrapped.
|
213
|
+
|
214
|
+
Returns:
|
215
|
+
Tool: A Tool object containing the function, its name, and its schema.
|
216
|
+
"""
|
217
|
+
|
218
|
+
def wrapper():
|
219
|
+
fn_schema = get_fn_signature(fn)
|
220
|
+
if not fn_schema or 'function' not in fn_schema or 'name' not in fn_schema['function']:
|
221
|
+
raise ValueError(f"Could not generate valid schema for function {fn.__name__}")
|
222
|
+
return Tool(
|
223
|
+
name=fn_schema["function"]["name"],
|
224
|
+
fn=fn,
|
225
|
+
fn_schema=fn_schema
|
226
|
+
)
|
227
|
+
|
228
|
+
return wrapper()
|
229
|
+
|
@@ -0,0 +1,241 @@
|
|
1
|
+
# --- START OF ASYNC MODIFIED tool_agent.py (Init Fix) ---
|
2
|
+
|
3
|
+
import json
|
4
|
+
import asyncio
|
5
|
+
from typing import List, Dict, Any, Optional
|
6
|
+
|
7
|
+
from colorama import Fore
|
8
|
+
from dotenv import load_dotenv
|
9
|
+
from groq import AsyncGroq
|
10
|
+
|
11
|
+
from clap.tool_pattern.tool import Tool
|
12
|
+
from clap.mcp_client.client import MCPClientManager
|
13
|
+
from clap.utils.completions import build_prompt_structure
|
14
|
+
from clap.utils.completions import ChatHistory
|
15
|
+
from clap.utils.completions import completions_create
|
16
|
+
from clap.utils.completions import update_chat_history
|
17
|
+
from mcp import types as mcp_types
|
18
|
+
|
19
|
+
load_dotenv()
|
20
|
+
|
21
|
+
NATIVE_TOOL_SYSTEM_PROMPT = """
|
22
|
+
You are a helpful assistant. Use the available tools (local or remote) if necessary to answer the user's request.
|
23
|
+
If you use a tool, you will be given the results, and then you should provide the final response to the user based on those results.
|
24
|
+
If no tool is needed, answer directly.
|
25
|
+
"""
|
26
|
+
|
27
|
+
class ToolAgent:
|
28
|
+
"""
|
29
|
+
A simple agent that uses native tool calling asynchronously to answer user queries.
|
30
|
+
Supports both local Python tools and remote MCP tools via an MCPClientManager.
|
31
|
+
It makes one attempt to call tools if needed, processes the results,
|
32
|
+
and then generates a final response.
|
33
|
+
"""
|
34
|
+
|
35
|
+
def __init__(
|
36
|
+
self,
|
37
|
+
tools: Optional[Tool | List[Tool]] = None,
|
38
|
+
mcp_manager: Optional[MCPClientManager] = None,
|
39
|
+
mcp_server_names: Optional[List[str]] = None,
|
40
|
+
model: str = "llama-3.3-70b-versatile",
|
41
|
+
system_prompt: str = NATIVE_TOOL_SYSTEM_PROMPT,
|
42
|
+
) -> None:
|
43
|
+
self.client = AsyncGroq()
|
44
|
+
self.model = model
|
45
|
+
self.system_prompt = system_prompt
|
46
|
+
|
47
|
+
if tools is None:
|
48
|
+
self.local_tools = []
|
49
|
+
elif isinstance(tools, list):
|
50
|
+
self.local_tools = tools
|
51
|
+
else:
|
52
|
+
self.local_tools = [tools]
|
53
|
+
|
54
|
+
self.local_tools_dict = {tool.name: tool for tool in self.local_tools}
|
55
|
+
self.local_tool_schemas = [tool.fn_schema for tool in self.local_tools]
|
56
|
+
|
57
|
+
self.mcp_manager = mcp_manager
|
58
|
+
self.mcp_server_names = mcp_server_names or []
|
59
|
+
self.remote_tools_dict: Dict[str, mcp_types.Tool] = {}
|
60
|
+
self.remote_tool_server_map: Dict[str, str] = {}
|
61
|
+
|
62
|
+
|
63
|
+
async def _get_combined_tool_schemas(self) -> List[Dict[str, Any]]:
|
64
|
+
"""Fetches remote tools and combines their schemas with local ones."""
|
65
|
+
all_schemas = list(self.local_tool_schemas) # Start with local schemas
|
66
|
+
self.remote_tools_dict = {} # Reset remote tools for this run
|
67
|
+
self.remote_tool_server_map = {}
|
68
|
+
|
69
|
+
if self.mcp_manager and self.mcp_server_names:
|
70
|
+
fetch_tasks = [
|
71
|
+
self.mcp_manager.list_remote_tools(name)
|
72
|
+
for name in self.mcp_server_names
|
73
|
+
]
|
74
|
+
results = await asyncio.gather(*fetch_tasks, return_exceptions=True)
|
75
|
+
|
76
|
+
for server_name, result in zip(self.mcp_server_names, results):
|
77
|
+
if isinstance(result, Exception):
|
78
|
+
print(f"{Fore.RED}Error listing tools from MCP server '{server_name}': {result}{Fore.RESET}")
|
79
|
+
continue
|
80
|
+
|
81
|
+
if isinstance(result, list):
|
82
|
+
for tool in result:
|
83
|
+
if isinstance(tool, mcp_types.Tool):
|
84
|
+
if tool.name in self.local_tools_dict:
|
85
|
+
print(f"{Fore.YELLOW}Warning: Remote tool '{tool.name}' from server '{server_name}' conflicts with local tool. Local tool will be used.{Fore.RESET}")
|
86
|
+
continue
|
87
|
+
if tool.name in self.remote_tools_dict:
|
88
|
+
print(f"{Fore.YELLOW}Warning: Remote tool '{tool.name}' from server '{server_name}' conflicts with another remote tool from server '{self.remote_tool_server_map[tool.name]}'. Skipping duplicate.{Fore.RESET}")
|
89
|
+
continue
|
90
|
+
|
91
|
+
self.remote_tools_dict[tool.name] = tool
|
92
|
+
self.remote_tool_server_map[tool.name] = server_name
|
93
|
+
|
94
|
+
translated_schema = {
|
95
|
+
"type": "function",
|
96
|
+
"function": {
|
97
|
+
"name": tool.name,
|
98
|
+
"description": tool.description or "",
|
99
|
+
"parameters": tool.inputSchema
|
100
|
+
}
|
101
|
+
}
|
102
|
+
all_schemas.append(translated_schema)
|
103
|
+
else:
|
104
|
+
print(f"{Fore.YELLOW}Warning: Received non-Tool object from {server_name}: {type(tool)}{Fore.RESET}")
|
105
|
+
|
106
|
+
print(f"{Fore.BLUE}Total tools available to LLM: {len(all_schemas)}{Fore.RESET}")
|
107
|
+
return all_schemas
|
108
|
+
|
109
|
+
async def process_tool_calls(self, tool_calls: List[Any]) -> List[Dict[str, Any]]:
|
110
|
+
"""
|
111
|
+
Processes tool calls requested by the LLM asynchronously, dispatches execution
|
112
|
+
to local or remote tools, and collects results formatted as 'tool' role messages.
|
113
|
+
"""
|
114
|
+
observation_messages = []
|
115
|
+
if not isinstance(tool_calls, list):
|
116
|
+
print(f"{Fore.RED}Error: Expected a list of tool_calls, got {type(tool_calls)}{Fore.RESET}")
|
117
|
+
return observation_messages
|
118
|
+
|
119
|
+
tasks = [self._execute_single_tool_call(tc) for tc in tool_calls]
|
120
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
121
|
+
|
122
|
+
for result in results:
|
123
|
+
if isinstance(result, dict):
|
124
|
+
if len(result) == 1:
|
125
|
+
tool_call_id, result_str = list(result.items())[0]
|
126
|
+
observation_messages.append(
|
127
|
+
build_prompt_structure(role="tool", content=result_str, tool_call_id=tool_call_id)
|
128
|
+
)
|
129
|
+
else:
|
130
|
+
print(f"{Fore.RED}Error: Unexpected result format from tool execution: {result}{Fore.RESET}")
|
131
|
+
elif isinstance(result, Exception):
|
132
|
+
print(f"{Fore.RED}Error during concurrent tool execution: {result}{Fore.RESET}")
|
133
|
+
else:
|
134
|
+
print(f"{Fore.RED}Error: Unexpected item in tool execution results: {result}{Fore.RESET}")
|
135
|
+
|
136
|
+
return observation_messages
|
137
|
+
|
138
|
+
async def _execute_single_tool_call(self, tool_call: Any) -> Dict[str, Any]:
|
139
|
+
"""Helper to execute a single tool call (local or remote)."""
|
140
|
+
tool_call_id = getattr(tool_call, 'id', 'error_no_id')
|
141
|
+
function_call = getattr(tool_call, 'function', None)
|
142
|
+
tool_name = getattr(function_call, 'name', 'error_unknown_name')
|
143
|
+
result_str = f"Error: Processing failed for tool call '{tool_name}' (id: {tool_call_id})."
|
144
|
+
|
145
|
+
try:
|
146
|
+
if not function_call:
|
147
|
+
raise ValueError("Invalid tool_call object structure: missing 'function'.")
|
148
|
+
|
149
|
+
arguments_str = getattr(function_call, 'arguments', '{}')
|
150
|
+
arguments = json.loads(arguments_str)
|
151
|
+
|
152
|
+
if tool_name in self.local_tools_dict:
|
153
|
+
tool = self.local_tools_dict[tool_name]
|
154
|
+
print(f"{Fore.GREEN}\nExecuting Local Tool: {tool_name}{Fore.RESET}")
|
155
|
+
print(f"Tool call ID: {tool_call_id}")
|
156
|
+
print(f"Arguments: {arguments}")
|
157
|
+
result = await tool.run(**arguments)
|
158
|
+
elif tool_name in self.remote_tool_server_map and self.mcp_manager:
|
159
|
+
server_name = self.remote_tool_server_map[tool_name]
|
160
|
+
print(f"{Fore.CYAN}\nExecuting Remote MCP Tool: {tool_name} on {server_name}{Fore.RESET}")
|
161
|
+
print(f"Tool call ID: {tool_call_id}")
|
162
|
+
print(f"Arguments: {arguments}")
|
163
|
+
result = await self.mcp_manager.call_remote_tool(server_name, tool_name, arguments)
|
164
|
+
else:
|
165
|
+
print(f"{Fore.RED}Error: Tool '{tool_name}' not found locally or in known remote servers.{Fore.RESET}")
|
166
|
+
result_str = f"Error: Tool '{tool_name}' is not available."
|
167
|
+
return {tool_call_id: result_str}
|
168
|
+
|
169
|
+
if not isinstance(result, (str, int, float, bool, list, dict, type(None))):
|
170
|
+
result_str = str(result)
|
171
|
+
else:
|
172
|
+
try: result_str = json.dumps(result)
|
173
|
+
except TypeError: result_str = str(result)
|
174
|
+
print(f"{Fore.GREEN}Tool '{tool_name}' result: {result_str[:100]}...{Fore.RESET}")
|
175
|
+
|
176
|
+
except json.JSONDecodeError:
|
177
|
+
print(f"{Fore.RED}Error: Could not decode arguments for tool {tool_name}: {arguments_str}{Fore.RESET}")
|
178
|
+
result_str = f"Error: Invalid arguments JSON provided for {tool_name}"
|
179
|
+
except Exception as e:
|
180
|
+
print(f"{Fore.RED}Error processing or running tool {tool_name} (id: {tool_call_id}): {e}{Fore.RESET}")
|
181
|
+
result_str = f"Error executing tool {tool_name}: {e}"
|
182
|
+
|
183
|
+
return {tool_call_id: result_str}
|
184
|
+
|
185
|
+
async def run(
|
186
|
+
self,
|
187
|
+
user_msg: str,
|
188
|
+
) -> str:
|
189
|
+
"""
|
190
|
+
Handles the asynchronous interaction: user message -> LLM (tool decision) ->
|
191
|
+
execute tools (local or remote) -> LLM (final response).
|
192
|
+
"""
|
193
|
+
combined_tool_schemas = await self._get_combined_tool_schemas()
|
194
|
+
|
195
|
+
initial_user_message = build_prompt_structure(role="user", content=user_msg)
|
196
|
+
chat_history = ChatHistory(
|
197
|
+
[
|
198
|
+
build_prompt_structure(role="system", content=self.system_prompt),
|
199
|
+
initial_user_message,
|
200
|
+
]
|
201
|
+
)
|
202
|
+
|
203
|
+
print(f"{Fore.CYAN}\n--- Calling LLM for Tool Decision ---{Fore.RESET}")
|
204
|
+
assistant_message_1 = await completions_create(
|
205
|
+
self.client,
|
206
|
+
messages=list(chat_history),
|
207
|
+
model=self.model,
|
208
|
+
tools=combined_tool_schemas,
|
209
|
+
tool_choice="auto"
|
210
|
+
)
|
211
|
+
|
212
|
+
update_chat_history(chat_history, assistant_message_1)
|
213
|
+
|
214
|
+
final_response = "Agent encountered an issue."
|
215
|
+
|
216
|
+
if hasattr(assistant_message_1, 'tool_calls') and assistant_message_1.tool_calls:
|
217
|
+
print(f"{Fore.YELLOW}\nAssistant requests tool calls:{Fore.RESET}")
|
218
|
+
observation_messages = await self.process_tool_calls(assistant_message_1.tool_calls)
|
219
|
+
print(f"{Fore.BLUE}\nObservations prepared for LLM: {observation_messages}{Fore.RESET}")
|
220
|
+
|
221
|
+
for obs_msg in observation_messages:
|
222
|
+
update_chat_history(chat_history, obs_msg)
|
223
|
+
|
224
|
+
print(f"{Fore.CYAN}\n--- Calling LLM for Final Response ---{Fore.RESET}")
|
225
|
+
assistant_message_2 = await completions_create(
|
226
|
+
self.client,
|
227
|
+
messages=list(chat_history),
|
228
|
+
model=self.model,
|
229
|
+
)
|
230
|
+
final_response = str(assistant_message_2.content) if assistant_message_2.content else "Agent did not provide a final response after using tools."
|
231
|
+
|
232
|
+
elif assistant_message_1.content is not None:
|
233
|
+
print(f"{Fore.CYAN}\nAssistant provided direct response (no tools used):{Fore.RESET}")
|
234
|
+
final_response = assistant_message_1.content
|
235
|
+
else:
|
236
|
+
print(f"{Fore.RED}Error: Assistant message has neither content nor tool calls.{Fore.RESET}")
|
237
|
+
final_response = "Error: Received an unexpected empty response from the assistant."
|
238
|
+
|
239
|
+
print(f"{Fore.GREEN}\nFinal Response:\n{final_response}{Fore.RESET}")
|
240
|
+
return final_response
|
241
|
+
|
clap/tools/__init__.py
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
|
2
|
+
from .web_search import duckduckgo_search
|
3
|
+
from .web_crawler import scrape_url, extract_text_by_query # Removed smart_extract
|
4
|
+
from .email_tools import send_email, fetch_recent_emails
|
5
|
+
|
6
|
+
__all__ = [
|
7
|
+
"duckduckgo_search",
|
8
|
+
"scrape_url",
|
9
|
+
"extract_text_by_query",
|
10
|
+
"send_email",
|
11
|
+
"fetch_recent_emails",
|
12
|
+
]
|
13
|
+
|