From a5b2eb3a28418832024b2aa46e2a51a3c7682862 Mon Sep 17 00:00:00 2001 From: Siddhant Rai Date: Mon, 3 Feb 2025 06:07:10 +0530 Subject: [PATCH] feat: api tool config section + agent refactor for more llm fields --- application/tools/agent.py | 76 +- application/tools/implementations/api_tool.py | 9 +- frontend/src/assets/circle-check.svg | 1 + frontend/src/assets/circle-x.svg | 1 + frontend/src/index.css | 10 +- frontend/src/modals/AddActionModal.tsx | 75 ++ frontend/src/modals/AddToolModal.tsx | 22 +- frontend/src/modals/ConfigToolModal.tsx | 8 +- frontend/src/modals/WrapperModal.tsx | 4 +- frontend/src/modals/types/index.ts | 4 +- frontend/src/settings/ToolConfig.tsx | 1044 ++++++++++++++--- frontend/src/settings/Tools.tsx | 10 +- frontend/src/settings/types/index.ts | 34 +- 13 files changed, 1048 insertions(+), 250 deletions(-) create mode 100644 frontend/src/assets/circle-check.svg create mode 100644 frontend/src/assets/circle-x.svg create mode 100644 frontend/src/modals/AddActionModal.tsx diff --git a/application/tools/agent.py b/application/tools/agent.py index 740e1689..de8ad725 100644 --- a/application/tools/agent.py +++ b/application/tools/agent.py @@ -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,26 +48,7 @@ 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", True) - }, - "required": [ - key - for key in action["parameters"]["required"] - if key in action["parameters"]["properties"] - and action["parameters"]["properties"][key].get( - "filled_by_llm", True - ) - ], - }, + "parameters": self._build_tool_parameters(action), }, } for tool_id, tool in tools_dict.items() @@ -79,21 +75,49 @@ class Agent: ) ) - 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"]["actions"][action_name] + { + "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"] ), ) - print(f"Executing tool: {action_name} with args: {call_args}") - result = tool.execute_action(action_name, **call_args) + 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 diff --git a/application/tools/implementations/api_tool.py b/application/tools/implementations/api_tool.py index da105e39..5d0fec70 100644 --- a/application/tools/implementations/api_tool.py +++ b/application/tools/implementations/api_tool.py @@ -15,11 +15,16 @@ class APITool(Tool): 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, kwargs) + return self._make_api_call( + self.url, self.method, self.headers, self.query_params, kwargs + ) - def _make_api_call(self, url, method, headers, body): + 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: diff --git a/frontend/src/assets/circle-check.svg b/frontend/src/assets/circle-check.svg new file mode 100644 index 00000000..f0e8390d --- /dev/null +++ b/frontend/src/assets/circle-check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/circle-x.svg b/frontend/src/assets/circle-x.svg new file mode 100644 index 00000000..d6bdd2c3 --- /dev/null +++ b/frontend/src/assets/circle-x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css index b8cf596e..85d6fced 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -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, diff --git a/frontend/src/modals/AddActionModal.tsx b/frontend/src/modals/AddActionModal.tsx new file mode 100644 index 00000000..c52d89f8 --- /dev/null +++ b/frontend/src/modals/AddActionModal.tsx @@ -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 ( +
+
+
+ +
+

+ New Action +

+
+ + Action Name + + setActionName(e.target.value)} + borderVariant="thin" + placeholder={'Enter name'} + > +
+
+ + +
+
+
+
+
+ ); +} diff --git a/frontend/src/modals/AddToolModal.tsx b/frontend/src/modals/AddToolModal.tsx index c1bdb052..d4706431 100644 --- a/frontend/src/modals/AddToolModal.tsx +++ b/frontend/src/modals/AddToolModal.tsx @@ -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( - [], - ); - const [selectedTool, setSelectedTool] = React.useState( - null, - ); + const [availableTools, setAvailableTools] = React.useState< + AvailableToolType[] + >([]); + const [selectedTool, setSelectedTool] = + React.useState(null); const [configModalState, setConfigModalState] = React.useState('INACTIVE'); const modalRef = useRef(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({ diff --git a/frontend/src/modals/ConfigToolModal.tsx b/frontend/src/modals/ConfigToolModal.tsx index 4a8ca881..f26029fc 100644 --- a/frontend/src/modals/ConfigToolModal.tsx +++ b/frontend/src/modals/ConfigToolModal.tsx @@ -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(''); - const handleAddTool = (tool: AvailableTool) => { + const handleAddTool = (tool: AvailableToolType) => { userService .createTool({ name: tool.name, @@ -75,7 +75,7 @@ export default function ConfigToolModal({
+
+ {tool.name === 'api_tool' ? ( + + ) : ( +
+ {'actions' in tool && + tool.actions.map((action, actionIndex) => { + return ( +
+
+

+ {action.name} +

+ +
+
+ { + setTool({ + ...tool, + actions: tool.actions.map((act, index) => { + if (index === actionIndex) { + return { + ...act, + description: e.target.value, + }; + } + return act; + }), + }); + }} + borderVariant="thin" + > +
+
+ + + + + + + + + + + + {Object.entries(action.parameters?.properties).map( + (param, index) => { + const uniqueKey = `${actionIndex}-${param[0]}`; + return ( + + + + + + + + ); + }, + )} + +
Field NameField TypeFilled by LLMFIeld descriptionValue
{param[0]}{param[1].type} + + + { + setTool({ + ...tool, + actions: tool.actions.map( + (act, index) => { + if (index === actionIndex) { + return { + ...act, + parameters: { + ...act.parameters, + properties: { + ...act.parameters + .properties, + [param[0]]: { + ...act.parameters + .properties[param[0]], + description: + e.target.value, + }, + }, + }, + }; + } + return act; + }, + ), + }); + }} + > + + { + setTool({ + ...tool, + actions: tool.actions.map( + (act, index) => { + if (index === actionIndex) { + return { + ...act, + parameters: { + ...act.parameters, + properties: { + ...act.parameters + .properties, + [param[0]]: { + ...act.parameters + .properties[param[0]], + value: e.target.value, + }, + }, + }, + }; + } + return act; + }, + ), + }); + }} + > +
+
+
+ ); + })} +
+ )} + + + + ); +} + +function APIToolConfig({ + tool, + setTool, +}: { + tool: APIToolType; + setTool: (tool: APIToolType) => void; +}) { + const [apiTool, setApiTool] = React.useState(tool); + + const handleActionChange = ( + actionName: string, + updatedAction: APIActionType, + ) => { + setApiTool((prevApiTool) => { + const updatedActions = { ...prevApiTool.config.actions }; + updatedActions[actionName] = updatedAction; + return { + ...prevApiTool, + config: { ...prevApiTool.config, actions: updatedActions }, + }; + }); + }; + + const handleActionToggle = (actionName: string) => { + setApiTool((prevApiTool) => { + const updatedActions = { ...prevApiTool.config.actions }; + const updatedAction = { ...updatedActions[actionName] }; + updatedAction.active = !updatedAction.active; + updatedActions[actionName] = updatedAction; + return { + ...prevApiTool, + config: { ...prevApiTool.config, actions: updatedActions }, + }; + }); + }; + + React.useEffect(() => { + setApiTool(tool); + }, [tool]); + + React.useEffect(() => { + setTool(apiTool); + }, [apiTool]); + return ( +
+ {apiTool.config.actions && + Object.entries(apiTool.config.actions).map( + ([actionName, action], actionIndex) => { return (
{ - setTool({ - ...tool, - actions: tool.actions.map((act, index) => { - if (index === actionIndex) { - return { ...act, active: !act.active }; - } - return act; - }), - }); - }} + onChange={() => handleActionToggle(actionName)} />
-
- { - setTool({ - ...tool, - actions: tool.actions.map((act, index) => { - if (index === actionIndex) { - return { - ...act, - description: e.target.value, - }; - } - return act; - }), - }); - }} - borderVariant="thin" - > +
+
+ + URL + + { + setApiTool((prevApiTool) => { + const updatedActions = { + ...prevApiTool.config.actions, + }; + const updatedAction = { + ...updatedActions[actionName], + }; + updatedAction.url = e.target.value; + updatedActions[actionName] = updatedAction; + return { + ...prevApiTool, + config: { + ...prevApiTool.config, + actions: updatedActions, + }, + }; + }); + }} + borderVariant="thin" + placeholder="Enter url" + > +
-
- - - - - - - - - - - - {Object.entries(action.parameters?.properties).map( - (param, index) => { - const uniqueKey = `${actionIndex}-${param[0]}`; - return ( - - - - - - - - ); - }, - )} - -
Field NameField TypeFilled by LLMFIeld descriptionValue
{param[0]}{param[1].type} - - - { - setTool({ - ...tool, - actions: tool.actions.map( - (act, index) => { - if (index === actionIndex) { - return { - ...act, - parameters: { - ...act.parameters, - properties: { - ...act.parameters.properties, - [param[0]]: { - ...act.parameters - .properties[param[0]], - description: e.target.value, - }, - }, - }, - }; - } - return act; - }, - ), - }); - }} - > - - { - setTool({ - ...tool, - actions: tool.actions.map( - (act, index) => { - if (index === actionIndex) { - return { - ...act, - parameters: { - ...act.parameters, - properties: { - ...act.parameters.properties, - [param[0]]: { - ...act.parameters - .properties[param[0]], - value: e.target.value, - }, - }, - }, - }; - } - return act; - }, - ), - }); - }} - > -
+
+
+ + Method + + { + setApiTool((prevApiTool) => { + const updatedActions = { + ...prevApiTool.config.actions, + }; + const updatedAction = { + ...updatedActions[actionName], + }; + updatedAction.method = value as + | 'GET' + | 'POST' + | 'PUT' + | 'DELETE'; + updatedActions[actionName] = updatedAction; + return { + ...prevApiTool, + config: { + ...prevApiTool.config, + actions: updatedActions, + }, + }; + }); + }} + size="w-56" + rounded="3xl" + border="border" + /> +
+
+
+
+ + Description + + { + setApiTool((prevApiTool) => { + const updatedActions = { + ...prevApiTool.config.actions, + }; + const updatedAction = { + ...updatedActions[actionName], + }; + updatedAction.description = e.target.value; + updatedActions[actionName] = updatedAction; + return { + ...prevApiTool, + config: { + ...prevApiTool.config, + actions: updatedActions, + }, + }; + }); + }} + borderVariant="thin" + placeholder="Enter description" + > +
+
+
+
); - })} -
+ }, + )} +
+ ); +} + +function APIActionTable({ + apiAction, + handleActionChange, +}: { + apiAction: APIActionType; + handleActionChange: ( + actionName: string, + updatedAction: APIActionType, + ) => void; +}) { + const [action, setAction] = React.useState(apiAction); + const [newPropertyKey, setNewPropertyKey] = React.useState(''); + const [addingPropertySection, setAddingPropertySection] = React.useState< + 'headers' | 'query_params' | 'body' | null + >(null); + const [editingPropertyKey, setEditingPropertyKey] = React.useState<{ + section: 'headers' | 'query_params' | 'body' | null; + oldKey: string | null; + }>({ section: null, oldKey: null }); + + const handlePropertyChange = ( + section: 'headers' | 'query_params' | 'body', + key: string, + field: 'value' | 'description' | 'filled_by_llm', + value: string | number | boolean, + ) => { + setAction((prevAction) => { + const updatedProperties = { + ...prevAction[section].properties, + [key]: { + ...prevAction[section].properties[key], + [field]: value, + }, + }; + return { + ...prevAction, + [section]: { + ...prevAction[section], + properties: updatedProperties, + }, + }; + }); + }; + + const handleAddPropertyStart = ( + section: 'headers' | 'query_params' | 'body', + ) => { + setEditingPropertyKey({ section: null, oldKey: null }); + setAddingPropertySection(section); + setNewPropertyKey(''); + }; + const handleAddPropertyCancel = () => { + setAddingPropertySection(null); + setNewPropertyKey(''); + }; + const handleAddProperty = () => { + if (addingPropertySection && newPropertyKey.trim() !== '') { + setAction((prevAction) => { + const updatedProperties = { + ...prevAction[addingPropertySection].properties, + [newPropertyKey.trim()]: { + type: 'string', + description: '', + value: '', + filled_by_llm: false, + }, + }; + return { + ...prevAction, + [addingPropertySection]: { + ...prevAction[addingPropertySection], + properties: updatedProperties, + }, + }; + }); + setNewPropertyKey(''); + setAddingPropertySection(null); + } + }; + + const handleRenamePropertyStart = ( + section: 'headers' | 'query_params' | 'body', + oldKey: string, + ) => { + setAddingPropertySection(null); + setEditingPropertyKey({ section, oldKey }); + setNewPropertyKey(oldKey); + }; + const handleRenamePropertyCancel = () => { + setEditingPropertyKey({ section: null, oldKey: null }); + setNewPropertyKey(''); + }; + const handleRenameProperty = () => { + if ( + editingPropertyKey.section && + editingPropertyKey.oldKey && + newPropertyKey.trim() !== '' && + newPropertyKey.trim() !== editingPropertyKey.oldKey + ) { + setAction((prevAction) => { + const { section, oldKey } = editingPropertyKey; + if (section && oldKey) { + const { [oldKey]: oldProperty, ...restProperties } = + prevAction[section].properties; + const updatedProperties = { + ...restProperties, + [newPropertyKey.trim()]: oldProperty, + }; + return { + ...prevAction, + [section]: { + ...prevAction[section], + properties: updatedProperties, + }, + }; + } + return prevAction; + }); + setEditingPropertyKey({ section: null, oldKey: null }); + setNewPropertyKey(''); + } + }; + + const handlePorpertyDelete = ( + section: 'headers' | 'query_params' | 'body', + key: string, + ) => { + setAction((prevAction) => { + const { [key]: deletedProperty, ...restProperties } = + prevAction[section].properties; + return { + ...prevAction, + [section]: { + ...prevAction[section], + properties: restProperties, + }, + }; + }); + }; + + React.useEffect(() => { + setAction(apiAction); + }, [apiAction]); + + React.useEffect(() => { + handleActionChange(action.name, action); + }, [action]); + const renderPropertiesTable = ( + section: 'headers' | 'query_params' | 'body', + ) => { + return ( + <> + {Object.entries(action[section].properties).map( + ([key, param], index) => ( + + + {editingPropertyKey.section === section && + editingPropertyKey.oldKey === key ? ( +
+ setNewPropertyKey(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleRenameProperty(); + } + }} + /> +
+ + +
+
+ ) : ( + handleRenamePropertyStart(section, key)} + readOnly + /> + )} + + {param.type} + + + + + + handlePropertyChange( + section, + key, + 'description', + e.target.value, + ) + } + > + + + + handlePropertyChange(section, key, 'value', e.target.value) + } + className={`bg-transparent border border-silver dark:border-silver/40 outline-none px-2 py-1 rounded-lg text-sm ${param.filled_by_llm ? 'opacity-50' : ''}`} + > + + + + + + ), + )} + {addingPropertySection === section ? ( + + + setNewPropertyKey(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleAddProperty(); + } + }} + placeholder="New property key" + className="min-w-[130.5px] w-full flex items-start bg-transparent border border-silver dark:border-silver/40 outline-none px-2 py-1 rounded-lg text-sm" + /> + + + + + + + + ) : ( + + + + + + + )} + + ); + }; + return ( +
+
+

+ Headers +

+ + + + + + + + + + + + {renderPropertiesTable('headers')} +
NameTypeFilled by LLMDescriptionValue
+
+
+

+ Query Parameters +

+ + + + + + + + + + + + {renderPropertiesTable('query_params')} +
NameTypeFilled by LLMDescriptionValue
+
+
+

+ Body +

+ + + + + + + + + + + + {renderPropertiesTable('body')} +
NameTypeFilled by LLMDescriptionValue
); diff --git a/frontend/src/settings/Tools.tsx b/frontend/src/settings/Tools.tsx index bb0bd3f7..bce5da74 100644 --- a/frontend/src/settings/Tools.tsx +++ b/frontend/src/settings/Tools.tsx @@ -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('INACTIVE'); - const [userTools, setUserTools] = React.useState([]); - const [selectedTool, setSelectedTool] = React.useState(null); + const [userTools, setUserTools] = React.useState([]); + 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); }; diff --git a/frontend/src/settings/types/index.ts b/frontend/src/settings/types/index.ts index 322bdfeb..2e7079c7 100644 --- a/frontend/src/settings/types/index.ts +++ b/frontend/src/settings/types/index.ts @@ -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 } }; +};