mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 08:33:20 +00:00
feat: api tool config section + agent refactor for more llm fields
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
1
frontend/src/assets/circle-check.svg
Normal file
1
frontend/src/assets/circle-check.svg
Normal 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 |
1
frontend/src/assets/circle-x.svg
Normal file
1
frontend/src/assets/circle-x.svg
Normal 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 |
@@ -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,
|
||||
|
||||
75
frontend/src/modals/AddActionModal.tsx
Normal file
75
frontend/src/modals/AddActionModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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]"
|
||||
>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 } };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user