feat: api tool config section + agent refactor for more llm fields

This commit is contained in:
Siddhant Rai
2025-02-03 06:07:10 +05:30
parent 9319ec5bb2
commit a5b2eb3a28
13 changed files with 1048 additions and 250 deletions

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,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():
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:
call_args[param] = details["value"]
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"]
),
)
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, **call_args)
result = tool.execute_action(action_name, **parameters)
call_id = getattr(call, "id", None)
return result, call_id

View File

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

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;

View File

@@ -2,26 +2,36 @@ import React from 'react';
import userService from '../api/services/userService';
import ArrowLeft from '../assets/arrow-left.svg';
import CircleCheck from '../assets/circle-check.svg';
import CircleX from '../assets/circle-x.svg';
import Trash from '../assets/trash.svg';
import Dropdown from '../components/Dropdown';
import Input from '../components/Input';
import { UserTool } from './types';
import AddActionModal from '../modals/AddActionModal';
import { ActiveState } from '../models/misc';
import { APIActionType, APIToolType, UserToolType } from './types';
export default function ToolConfig({
tool,
setTool,
handleGoBack,
}: {
tool: UserTool;
setTool: (tool: UserTool) => void;
tool: UserToolType | APIToolType;
setTool: (tool: UserToolType | APIToolType) => void;
handleGoBack: () => void;
}) {
const [authKey, setAuthKey] = React.useState<string>(
tool.config?.token || '',
'token' in tool.config ? tool.config.token : '',
);
const [actionModalState, setActionModalState] =
React.useState<ActiveState>('INACTIVE');
const handleCheckboxChange = (actionIndex: number, property: string) => {
setTool({
...tool,
actions: tool.actions.map((action, index) => {
actions:
'actions' in tool
? tool.actions.map((action, index) => {
if (index === actionIndex) {
return {
...action,
@@ -39,7 +49,8 @@ export default function ToolConfig({
};
}
return action;
}),
})
: [],
});
};
@@ -50,8 +61,8 @@ export default function ToolConfig({
name: tool.name,
displayName: tool.displayName,
description: tool.description,
config: { token: authKey },
actions: tool.actions,
config: tool.name === 'api_tool' ? tool.config : { token: authKey },
actions: 'actions' in tool ? tool.actions : [],
status: tool.status,
})
.then(() => {
@@ -64,6 +75,36 @@ export default function ToolConfig({
handleGoBack();
});
};
const handleAddNewAction = (actionName: string) => {
const newAction: APIActionType = {
name: actionName,
method: 'GET',
url: '',
description: '',
body: {
properties: {},
type: 'object',
},
headers: {
properties: {},
type: 'object',
},
query_params: {
properties: {},
type: 'object',
},
active: true,
};
const toolCopy = tool as APIToolType;
setTool({
...toolCopy,
config: {
...toolCopy.config,
actions: { ...toolCopy.config.actions, [actionName]: newAction },
},
});
};
return (
<div className="mt-8 flex flex-col gap-4">
<div className="mb-4 flex items-center gap-3 text-eerie-black dark:text-bright-gray text-sm">
@@ -84,13 +125,14 @@ export default function ToolConfig({
</p>
</div>
<div className="mt-1">
{Object.keys(tool?.config).length !== 0 && (
{Object.keys(tool?.config).length !== 0 && tool.name !== 'api_tool' && (
<p className="text-sm font-semibold text-eerie-black dark:text-bright-gray">
Authentication
</p>
)}
<div className="flex mt-4 flex-col sm:flex-row items-start sm:items-center gap-2">
{Object.keys(tool?.config).length !== 0 && (
{Object.keys(tool?.config).length !== 0 &&
tool.name !== 'api_tool' && (
<div className="relative w-96">
<span className="absolute left-5 -top-2 bg-white px-2 text-xs text-gray-4000 dark:bg-[#26272E] dark:text-silver">
API Key / Oauth
@@ -122,11 +164,25 @@ export default function ToolConfig({
</div>
<div className="flex flex-col gap-4">
<div className="mx-1 my-2 h-[0.8px] w-full rounded-full bg-[#C4C4C4]/40 lg:w-[95%] "></div>
<div className="w-full flex flex-row items-center justify-between gap-2">
<p className="text-base font-semibold text-eerie-black dark:text-bright-gray">
Actions
</p>
<div className="flex flex-col gap-10">
{tool.actions.map((action, actionIndex) => {
<button
onClick={() => {
setActionModalState('ACTIVE');
}}
className="border border-solid border-purple-30 text-purple-30 dark:border-purple-30 dark:text-purple-30 transition-colors hover:bg-[#6F3FD1] hover:text-white dark:hover:bg-purple-30 dark:hover:text-white rounded-full text-sm px-5 py-1"
>
Add action
</button>
</div>
{tool.name === 'api_tool' ? (
<APIToolConfig tool={tool as APIToolType} setTool={setTool} />
) : (
<div className="flex flex-col gap-12">
{'actions' in tool &&
tool.actions.map((action, actionIndex) => {
return (
<div
key={actionIndex}
@@ -198,7 +254,10 @@ export default function ToolConfig({
(param, index) => {
const uniqueKey = `${actionIndex}-${param[0]}`;
return (
<tr key={index} className="text-nowrap font-normal">
<tr
key={index}
className="text-nowrap font-normal"
>
<td>{param[0]}</td>
<td>{param[1].type}</td>
<td>
@@ -239,11 +298,13 @@ export default function ToolConfig({
parameters: {
...act.parameters,
properties: {
...act.parameters.properties,
...act.parameters
.properties,
[param[0]]: {
...act.parameters
.properties[param[0]],
description: e.target.value,
description:
e.target.value,
},
},
},
@@ -273,7 +334,8 @@ export default function ToolConfig({
parameters: {
...act.parameters,
properties: {
...act.parameters.properties,
...act.parameters
.properties,
[param[0]]: {
...act.parameters
.properties[param[0]],
@@ -301,6 +363,602 @@ export default function ToolConfig({
);
})}
</div>
)}
<AddActionModal
modalState={actionModalState}
setModalState={setActionModalState}
handleSubmit={handleAddNewAction}
/>
</div>
</div>
);
}
function APIToolConfig({
tool,
setTool,
}: {
tool: APIToolType;
setTool: (tool: APIToolType) => void;
}) {
const [apiTool, setApiTool] = React.useState<APIToolType>(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 (
<div className="flex flex-col gap-16">
{apiTool.config.actions &&
Object.entries(apiTool.config.actions).map(
([actionName, action], actionIndex) => {
return (
<div
key={actionIndex}
className="w-full border border-silver dark:border-silver/40 rounded-xl"
>
<div className="h-10 bg-[#F9F9F9] dark:bg-[#28292D] rounded-t-xl border-b border-silver dark:border-silver/40 flex items-center justify-between px-5 flex-wrap">
<p className="font-semibold text-eerie-black dark:text-bright-gray">
{action.name}
</p>
<label
htmlFor={`actionToggle-${actionIndex}`}
className="relative inline-block h-6 w-10 cursor-pointer rounded-full bg-gray-300 dark:bg-[#D2D5DA33]/20 transition [-webkit-tap-highlight-color:_transparent] has-[:checked]:bg-[#0C9D35CC] has-[:checked]:dark:bg-[#0C9D35CC]"
>
<input
type="checkbox"
id={`actionToggle-${actionIndex}`}
className="peer sr-only"
checked={action.active}
onChange={() => handleActionToggle(actionName)}
/>
<span className="absolute inset-y-0 start-0 m-[3px] size-[18px] rounded-full bg-white transition-all peer-checked:start-4"></span>
</label>
</div>
<div className="mt-8 px-5">
<div className="relative w-full">
<span className="absolute left-5 -top-2 bg-white px-2 text-xs text-gray-4000 dark:bg-raisin-black dark:text-silver">
URL
</span>
<Input
type="text"
value={action.url}
onChange={(e) => {
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"
></Input>
</div>
</div>
<div className="mt-4 px-5 py-2">
<div className="relative w-full">
<span className="absolute left-5 -top-2 z-10 bg-white px-2 text-xs text-gray-4000 dark:bg-raisin-black dark:text-silver">
Method
</span>
<Dropdown
options={['GET', 'POST', 'PUT', 'DELETE']}
selectedValue={action.method}
onSelect={(value: string) => {
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"
/>
</div>
</div>
<div className="mt-4 px-5 py-2">
<div className="relative w-full">
<span className="absolute left-5 -top-2 bg-white px-2 text-xs text-gray-4000 dark:bg-raisin-black dark:text-silver">
Description
</span>
<Input
type="text"
value={action.description}
onChange={(e) => {
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"
></Input>
</div>
</div>
<div className="mt-4 px-5 py-2">
<APIActionTable
apiAction={action}
handleActionChange={handleActionChange}
/>
</div>
</div>
);
},
)}
</div>
);
}
function APIActionTable({
apiAction,
handleActionChange,
}: {
apiAction: APIActionType;
handleActionChange: (
actionName: string,
updatedAction: APIActionType,
) => void;
}) {
const [action, setAction] = React.useState<APIActionType>(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) => (
<tr key={index} className="text-nowrap font-normal">
<td className="relative">
{editingPropertyKey.section === section &&
editingPropertyKey.oldKey === key ? (
<div className="flex flex-row items-center justify-between gap-2">
<input
value={newPropertyKey}
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"
onChange={(e) => setNewPropertyKey(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleRenameProperty();
}
}}
/>
<div className="mt-1">
<button
onClick={handleRenameProperty}
className="mr-1 w-5 h-5"
>
<img
src={CircleCheck}
alt="check"
className="w-5 h-5"
/>
</button>
<button
onClick={handleRenamePropertyCancel}
className="w-5 h-5"
>
<img src={CircleX} alt="cancel" className="w-5 h-5" />
</button>
</div>
</div>
) : (
<input
value={key}
className="min-w-[175.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"
onFocus={() => handleRenamePropertyStart(section, key)}
readOnly
/>
)}
</td>
<td>{param.type}</td>
<td>
<label className="ml-[10px] flex cursor-pointer items-start gap-4">
<div className="flex items-center">
<input
checked={param.filled_by_llm}
type="checkbox"
className="size-4 rounded border-gray-300 bg-transparent"
onChange={(e) =>
handlePropertyChange(
section,
key,
'filled_by_llm',
e.target.checked,
)
}
/>
</div>
</label>
</td>
<td className="w-10">
<input
value={param.description}
className="bg-transparent border border-silver dark:border-silver/40 outline-none px-2 py-1 rounded-lg text-sm"
onChange={(e) =>
handlePropertyChange(
section,
key,
'description',
e.target.value,
)
}
></input>
</td>
<td>
<input
value={param.value}
disabled={param.filled_by_llm}
onChange={(e) =>
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' : ''}`}
></input>
</td>
<td
style={{
width: '50px',
minWidth: '50px',
maxWidth: '50px',
padding: '0',
}}
className="border-b border-silver dark:border-silver/40"
>
<button
onClick={() => handlePorpertyDelete(section, key)}
className="w-4 h-4 opacity-60 hover:opacity-100"
>
<img src={Trash} alt="delete" className="w-4 h-4"></img>
</button>
</td>
</tr>
),
)}
{addingPropertySection === section ? (
<tr>
<td>
<input
value={newPropertyKey}
onChange={(e) => 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"
/>
</td>
<td colSpan={4} className="text-right">
<button
onClick={handleAddProperty}
className="bg-purple-30 text-white hover:bg-[#6F3FD1] rounded-full px-5 py-[4px] mr-1 text-sm"
>
{' '}
Add{' '}
</button>
<button
onClick={handleAddPropertyCancel}
className="border border-solid border-red-500 text-red-500 hover:bg-red-500 hover:text-white rounded-full px-5 py-[4px] text-sm"
>
{' '}
Cancel{' '}
</button>
</td>
<td
style={{
width: '50px',
minWidth: '50px',
maxWidth: '50px',
padding: '0',
}}
></td>
</tr>
) : (
<tr>
<td colSpan={5}>
<button
onClick={() => handleAddPropertyStart(section)}
className="flex items-start rounded-full px-5 py-[4px] border border-solid border-purple-30 text-purple-30 dark:border-purple-30 dark:text-purple-30 transition-colors hover:bg-[#6F3FD1] hover:text-white dark:hover:bg-purple-30 dark:hover:text-white text-nowrap text-sm"
>
Add New Field
</button>
</td>
<td
style={{
width: '50px',
minWidth: '50px',
maxWidth: '50px',
padding: '0',
}}
></td>
</tr>
)}
</>
);
};
return (
<div className="flex flex-col gap-6">
<div>
<h3 className="mb-1 text-base font-normal text-eerie-black dark:text-bright-gray">
Headers
</h3>
<table className="table-default">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Filled by LLM</th>
<th>Description</th>
<th>Value</th>
<th
style={{
width: '50px',
minWidth: '50px',
maxWidth: '50px',
padding: '0',
}}
></th>
</tr>
</thead>
<tbody>{renderPropertiesTable('headers')}</tbody>
</table>
</div>
<div>
<h3 className="mb-1 text-base font-normal text-eerie-black dark:text-bright-gray">
Query Parameters
</h3>
<table className="table-default">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Filled by LLM</th>
<th>Description</th>
<th>Value</th>
<th
style={{
width: '50px',
minWidth: '50px',
maxWidth: '50px',
padding: '0',
}}
></th>
</tr>
</thead>
<tbody>{renderPropertiesTable('query_params')}</tbody>
</table>
</div>
<div className="mb-6">
<h3 className="mb-1 text-base font-normal text-eerie-black dark:text-bright-gray">
Body
</h3>
<table className="table-default">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Filled by LLM</th>
<th>Description</th>
<th>Value</th>
<th
style={{
width: '50px',
minWidth: '50px',
maxWidth: '50px',
padding: '0',
}}
></th>
</tr>
</thead>
<tbody>{renderPropertiesTable('body')}</tbody>
</table>
</div>
</div>
);

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 } };
};