Merge pull request #1608 from siiddhantt/feat/api-tool

feat: API Tool
This commit is contained in:
Alex
2025-02-03 11:48:57 +00:00
committed by GitHub
15 changed files with 1150 additions and 270 deletions

View File

@@ -57,25 +57,35 @@ class GoogleLLM(BaseLLM):
for tool_data in tools_list:
if tool_data["type"] == "function":
function = tool_data["function"]
genai_function = dict(
name=function["name"],
description=function["description"],
parameters={
"type": "OBJECT",
"properties": {
k: {
**v,
"type": v["type"].upper() if v["type"] else None,
}
for k, v in function["parameters"]["properties"].items()
parameters = function["parameters"]
properties = parameters.get("properties", {})
if properties:
genai_function = dict(
name=function["name"],
description=function["description"],
parameters={
"type": "OBJECT",
"properties": {
k: {
**v,
"type": v["type"].upper() if v["type"] else None,
}
for k, v in properties.items()
},
"required": (
parameters["required"]
if "required" in parameters
else []
),
},
"required": (
function["parameters"]["required"]
if "required" in function["parameters"]
else []
),
},
)
)
else:
genai_function = dict(
name=function["name"],
description=function["description"],
)
genai_tool = types.Tool(function_declarations=[genai_function])
genai_tools.append(genai_tool)

View File

@@ -26,6 +26,21 @@ class Agent:
tools_by_id = {str(tool["_id"]): tool for tool in user_tools}
return tools_by_id
def _build_tool_parameters(self, action):
params = {"type": "object", "properties": {}, "required": []}
for param_type in ["query_params", "headers", "body", "parameters"]:
if param_type in action and action[param_type].get("properties"):
for k, v in action[param_type]["properties"].items():
if v.get("filled_by_llm", True):
params["properties"][k] = {
key: value
for key, value in v.items()
if key != "filled_by_llm" and key != "value"
}
params["required"].append(k)
return params
def _prepare_tools(self, tools_dict):
self.tools = [
{
@@ -33,31 +48,16 @@ class Agent:
"function": {
"name": f"{action['name']}_{tool_id}",
"description": action["description"],
"parameters": {
**action["parameters"],
"properties": {
k: {
key: value
for key, value in v.items()
if key != "filled_by_llm" and key != "value"
}
for k, v in action["parameters"]["properties"].items()
if v.get("filled_by_llm", False)
},
"required": [
key
for key in action["parameters"]["required"]
if key in action["parameters"]["properties"]
and action["parameters"]["properties"][key].get(
"filled_by_llm", False
)
],
},
"parameters": self._build_tool_parameters(action),
},
}
for tool_id, tool in tools_dict.items()
for action in tool["actions"]
if action["active"]
for action in (
tool["config"]["actions"].values()
if tool["name"] == "api_tool"
else tool["actions"]
)
if action.get("active", True)
]
def _execute_tool_action(self, tools_dict, call):
@@ -65,18 +65,59 @@ class Agent:
tool_id, action_name, call_args = parser.parse_args(call)
tool_data = tools_dict[tool_id]
action_data = next(
action for action in tool_data["actions"] if action["name"] == action_name
action_data = (
tool_data["config"]["actions"][action_name]
if tool_data["name"] == "api_tool"
else next(
action
for action in tool_data["actions"]
if action["name"] == action_name
)
)
for param, details in action_data["parameters"]["properties"].items():
if param not in call_args and "value" in details:
call_args[param] = details["value"]
query_params, headers, body, parameters = {}, {}, {}, {}
param_types = {
"query_params": query_params,
"headers": headers,
"body": body,
"parameters": parameters,
}
for param_type, target_dict in param_types.items():
if param_type in action_data and action_data[param_type].get("properties"):
for param, details in action_data[param_type]["properties"].items():
if param not in call_args and "value" in details:
target_dict[param] = details["value"]
for param, value in call_args.items():
for param_type, target_dict in param_types.items():
if param_type in action_data and param in action_data[param_type].get(
"properties", {}
):
target_dict[param] = value
tm = ToolManager(config={})
tool = tm.load_tool(tool_data["name"], tool_config=tool_data["config"])
print(f"Executing tool: {action_name} with args: {call_args}")
result = tool.execute_action(action_name, **call_args)
tool = tm.load_tool(
tool_data["name"],
tool_config=(
{
"url": tool_data["config"]["actions"][action_name]["url"],
"method": tool_data["config"]["actions"][action_name]["method"],
"headers": headers,
"query_params": query_params,
}
if tool_data["name"] == "api_tool"
else tool_data["config"]
),
)
if tool_data["name"] == "api_tool":
print(
f"Executing api: {action_name} with query_params: {query_params}, headers: {headers}, body: {body}"
)
result = tool.execute_action(action_name, **body)
else:
print(f"Executing tool: {action_name} with args: {call_args}")
result = tool.execute_action(action_name, **parameters)
call_id = getattr(call, "id", None)
return result, call_id

View File

@@ -0,0 +1,54 @@
import json
import requests
from application.tools.base import Tool
class APITool(Tool):
"""
API Tool
A flexible tool for performing various API actions (e.g., sending messages, retrieving data) via custom user-specified APIs
"""
def __init__(self, config):
self.config = config
self.url = config.get("url", "")
self.method = config.get("method", "GET")
self.headers = config.get("headers", {"Content-Type": "application/json"})
self.query_params = config.get("query_params", {})
def execute_action(self, action_name, **kwargs):
return self._make_api_call(
self.url, self.method, self.headers, self.query_params, kwargs
)
def _make_api_call(self, url, method, headers, query_params, body):
if query_params:
url = f"{url}?{requests.compat.urlencode(query_params)}"
if isinstance(body, dict):
body = json.dumps(body)
try:
print(f"Making API call: {method} {url} with body: {body}")
response = requests.request(method, url, headers=headers, data=body)
response.raise_for_status()
try:
data = response.json()
except ValueError:
data = None
return {
"status_code": response.status_code,
"data": data,
"message": "API call successful.",
}
except requests.exceptions.RequestException as e:
return {
"status_code": response.status_code if response else None,
"message": f"API call failed: {str(e)}",
}
def get_actions_metadata(self):
return []
def get_config_requirements(self):
return {}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="1 6 38 28" xmlns="http://www.w3.org/2000/svg">
<path d="M3,33.5c-0.827,0-1.5-0.673-1.5-1.5V8c0-0.827,0.673-1.5,1.5-1.5h34c0.827,0,1.5,0.673,1.5,1.5v24 c0,0.827-0.673,1.5-1.5,1.5H3z" style="fill: rgb(7, 106, 255);"/>
<path d="M37,7c0.551,0,1,0.449,1,1v24c0,0.551-0.449,1-1,1H3c-0.551,0-1-0.449-1-1V8c0-0.551,0.449-1,1-1 H37 M37,6H3C1.895,6,1,6.895,1,8v24c0,1.105,0.895,2,2,2h34c1.105,0,2-0.895,2-2V8C39,6.895,38.105,6,37,6L37,6z" style="fill: rgb(7, 106, 255);"/>
<path d="M 19.296 13.226 C 20.066 13.06 21.108 12.955 22.147 12.955 C 23.772 12.955 25.153 13.185 26.047 14.038 C 26.88 14.766 27.255 15.931 27.255 17.118 C 27.255 18.638 26.798 19.718 26.07 20.489 C 25.196 21.426 23.801 21.842 22.656 21.842 C 22.47 21.842 22.302 21.842 22.115 21.821 L 22.115 27.045 L 19.297 27.045 L 19.297 13.226 L 19.296 13.226 Z M 22.114 19.616 C 22.259 19.637 22.405 19.637 22.571 19.637 C 23.945 19.637 24.55 18.657 24.55 17.347 C 24.55 16.119 24.049 15.162 22.78 15.162 C 22.532 15.162 22.281 15.203 22.114 15.266 L 22.114 19.616 Z M 29.158 12.955 L 31.976 12.955 L 31.976 27.045 L 29.158 27.045 L 29.158 12.955 Z M 15.001 27.045 L 17.887 27.045 L 14.91 12.955 L 11.342 12.955 L 8.024 27.045 L 10.91 27.045 L 11.524 24.227 L 14.408 24.227 L 15.001 27.045 Z M 13 15.547 L 13.068 15.547 C 13.205 16.467 13.409 17.888 13.568 18.745 L 14.021 21.409 L 11.942 21.409 L 12.457 18.746 C 12.614 17.93 12.841 16.488 13 15.547 Z" style="fill: rgb(255, 255, 255);"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="rgb(34 197 94)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-check"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>

After

Width:  |  Height:  |  Size: 281 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="rgb(239 68 68)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-x"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>

After

Width:  |  Height:  |  Size: 293 B

View File

@@ -50,11 +50,11 @@ body.dark {
@layer components {
.table-default {
@apply block w-full table-auto content-start justify-center rounded-xl border border-silver dark:border-silver/40 text-center dark:text-bright-gray overflow-auto;
@apply block w-full table-auto justify-center rounded-xl border border-silver dark:border-silver/40 text-center dark:text-bright-gray overflow-auto;
}
.table-default th {
@apply p-4 font-normal text-gray-400 text-nowrap; /* Remove border-r */
@apply p-4 font-normal text-gray-400 text-nowrap;
}
.table-default th {
@@ -62,15 +62,15 @@ body.dark {
}
.table-default th:last-child {
flex: 0; /* Ensure the last column does not stretch unnecessarily */
flex: 0;
}
.table-default td {
@apply border-t w-full border-silver dark:border-silver/40 px-4 py-2; /* Remove border-r */
@apply border-t w-full border-silver dark:border-silver/40 px-4 py-2;
}
.table-default td:last-child {
@apply border-r-0; /* Ensure no right border on the last column */
@apply border-r-0;
}
.table-default th,

View File

@@ -0,0 +1,75 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import Exit from '../assets/exit.svg';
import Input from '../components/Input';
import { ActiveState } from '../models/misc';
export default function AddActionModal({
modalState,
setModalState,
handleSubmit,
}: {
modalState: ActiveState;
setModalState: (state: ActiveState) => void;
handleSubmit: (actionName: string) => void;
}) {
const { t } = useTranslation();
const [actionName, setActionName] = React.useState('');
return (
<div
className={`${
modalState === 'ACTIVE' ? 'visible' : 'hidden'
} fixed top-0 left-0 z-30 h-screen w-screen bg-gray-alpha flex items-center justify-center`}
>
<article className="flex w-11/12 sm:w-[512px] flex-col gap-4 rounded-2xl bg-white shadow-lg dark:bg-[#26272E]">
<div className="relative">
<button
className="absolute top-3 right-4 m-2 w-3"
onClick={() => {
setModalState('INACTIVE');
}}
>
<img className="filter dark:invert" src={Exit} />
</button>
<div className="p-6">
<h2 className="font-semibold text-xl text-jet dark:text-bright-gray px-3">
New Action
</h2>
<div className="mt-6 relative px-3">
<span className="absolute left-5 -top-2 bg-white px-2 text-xs text-gray-4000 dark:bg-[#26272E] dark:text-silver">
Action Name
</span>
<Input
type="text"
value={actionName}
onChange={(e) => setActionName(e.target.value)}
borderVariant="thin"
placeholder={'Enter name'}
></Input>
</div>
<div className="mt-8 flex flex-row-reverse gap-1 px-3">
<button
onClick={() => {
handleSubmit(actionName);
setModalState('INACTIVE');
}}
className="rounded-3xl bg-purple-30 px-5 py-2 text-sm text-white transition-all hover:bg-[#6F3FD1]"
>
Add
</button>
<button
onClick={() => {
setModalState('INACTIVE');
}}
className="cursor-pointer rounded-3xl px-5 py-2 text-sm font-medium hover:bg-gray-100 dark:bg-transparent dark:text-light-gray dark:hover:bg-[#767183]/50"
>
{t('modals.configTool.closeButton')}
</button>
</div>
</div>
</div>
</article>
</div>
);
}

View File

@@ -1,11 +1,12 @@
import React, { useRef } from 'react';
import { useTranslation } from 'react-i18next';
import userService from '../api/services/userService';
import Exit from '../assets/exit.svg';
import { ActiveState } from '../models/misc';
import { AvailableTool } from './types';
import ConfigToolModal from './ConfigToolModal';
import { useOutsideAlerter } from '../hooks';
import { useTranslation } from 'react-i18next';
import { ActiveState } from '../models/misc';
import ConfigToolModal from './ConfigToolModal';
import { AvailableToolType } from './types';
export default function AddToolModal({
message,
@@ -18,12 +19,11 @@ export default function AddToolModal({
setModalState: (state: ActiveState) => void;
getUserTools: () => void;
}) {
const [availableTools, setAvailableTools] = React.useState<AvailableTool[]>(
[],
);
const [selectedTool, setSelectedTool] = React.useState<AvailableTool | null>(
null,
);
const [availableTools, setAvailableTools] = React.useState<
AvailableToolType[]
>([]);
const [selectedTool, setSelectedTool] =
React.useState<AvailableToolType | null>(null);
const [configModalState, setConfigModalState] =
React.useState<ActiveState>('INACTIVE');
const modalRef = useRef<HTMLDivElement>(null);
@@ -46,7 +46,7 @@ export default function AddToolModal({
});
};
const handleAddTool = (tool: AvailableTool) => {
const handleAddTool = (tool: AvailableToolType) => {
if (Object.keys(tool.configRequirements).length === 0) {
userService
.createTool({

View File

@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import Exit from '../assets/exit.svg';
import Input from '../components/Input';
import { ActiveState } from '../models/misc';
import { AvailableTool } from './types';
import { AvailableToolType } from './types';
import userService from '../api/services/userService';
export default function ConfigToolModal({
@@ -15,13 +15,13 @@ export default function ConfigToolModal({
}: {
modalState: ActiveState;
setModalState: (state: ActiveState) => void;
tool: AvailableTool | null;
tool: AvailableToolType | null;
getUserTools: () => void;
}) {
const { t } = useTranslation();
const [authKey, setAuthKey] = React.useState<string>('');
const handleAddTool = (tool: AvailableTool) => {
const handleAddTool = (tool: AvailableToolType) => {
userService
.createTool({
name: tool.name,
@@ -75,7 +75,7 @@ export default function ConfigToolModal({
<div className="mt-8 flex flex-row-reverse gap-1 px-3">
<button
onClick={() => {
handleAddTool(tool as AvailableTool);
handleAddTool(tool as AvailableToolType);
}}
className="rounded-3xl bg-purple-30 px-5 py-2 text-sm text-white transition-all hover:bg-[#6F3FD1]"
>

View File

@@ -1,13 +1,13 @@
import React, { useEffect, useRef } from 'react';
import Exit from '../assets/exit.svg';
import { WrapperModalProps } from './types';
import { WrapperModalPropsType } from './types';
export default function WrapperModal({
children,
close,
isPerformingTask,
}: WrapperModalProps) {
}: WrapperModalPropsType) {
const modalRef = useRef<HTMLDivElement>(null);
useEffect(() => {

View File

@@ -1,4 +1,4 @@
export type AvailableTool = {
export type AvailableToolType = {
name: string;
displayName: string;
description: string;
@@ -10,7 +10,7 @@ export type AvailableTool = {
}[];
};
export type WrapperModalProps = {
export type WrapperModalPropsType = {
children?: React.ReactNode;
isPerformingTask?: boolean;
close: () => void;

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@ import { useDarkTheme } from '../hooks';
import AddToolModal from '../modals/AddToolModal';
import { ActiveState } from '../models/misc';
import ToolConfig from './ToolConfig';
import { UserTool } from './types';
import { APIToolType, UserToolType } from './types';
export default function Tools() {
const { t } = useTranslation();
@@ -18,8 +18,10 @@ export default function Tools() {
const [searchTerm, setSearchTerm] = React.useState('');
const [addToolModalState, setAddToolModalState] =
React.useState<ActiveState>('INACTIVE');
const [userTools, setUserTools] = React.useState<UserTool[]>([]);
const [selectedTool, setSelectedTool] = React.useState<UserTool | null>(null);
const [userTools, setUserTools] = React.useState<UserToolType[]>([]);
const [selectedTool, setSelectedTool] = React.useState<
UserToolType | APIToolType | null
>(null);
const getUserTools = () => {
userService
@@ -47,7 +49,7 @@ export default function Tools() {
});
};
const handleSettingsClick = (tool: UserTool) => {
const handleSettingsClick = (tool: UserToolType) => {
setSelectedTool(tool);
};

View File

@@ -19,7 +19,19 @@ export type LogData = {
timestamp: string;
};
export type UserTool = {
export type ParameterGroupType = {
type: 'object';
properties: {
[key: string]: {
type: 'string' | 'integer';
description: string;
value: string | number;
filled_by_llm: boolean;
};
};
};
export type UserToolType = {
id: string;
name: string;
displayName: string;
@@ -47,3 +59,23 @@ export type UserTool = {
active: boolean;
}[];
};
export type APIActionType = {
name: string;
url: string;
description: string;
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
query_params: ParameterGroupType;
headers: ParameterGroupType;
body: ParameterGroupType;
active: boolean;
};
export type APIToolType = {
id: string;
name: string;
displayName: string;
description: string;
status: boolean;
config: { actions: { [key: string]: APIActionType } };
};