Skip to content
5 changes: 4 additions & 1 deletion agent/component/agent_with_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ def __init__(self):
self.max_rounds = 5
self.description = ""
self.custom_header = {}
self.tool_timeout = 10


class Agent(LLM, ToolBase):
Expand Down Expand Up @@ -111,7 +112,9 @@ def __init__(self, canvas, id, param: LLMParam):
self.tool_meta.append(mcp_tool_metadata_to_openai_tool(meta, function_name=indexed_name))
self.tools[indexed_name] = MCPToolBinding(tool_call_session, tnm)
self.callback = partial(self._canvas.tool_use_callback, id)
self.toolcall_session = LLMToolPluginCallSession(self.tools, self.callback)
self.toolcall_session = LLMToolPluginCallSession(
self.tools, self.callback, default_timeout=float(self._param.tool_timeout or 10)
)
if self.tool_meta:
self.chat_mdl.bind_tools(self.toolcall_session, self.tool_meta)

Expand Down
8 changes: 5 additions & 3 deletions agent/tools/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,16 @@ class ToolMeta(TypedDict):


class LLMToolPluginCallSession(ToolCallSession):
def __init__(self, tools_map: dict[str, object], callback: partial):
def __init__(self, tools_map: dict[str, object], callback: partial, default_timeout: float = 10):
self.tools_map = tools_map
self.callback = callback
self.default_timeout = default_timeout

def tool_call(self, name: str, arguments: dict[str, Any], timeout: float | int = 10) -> Any:
def tool_call(self, name: str, arguments: dict[str, Any], timeout: float | int | None = None) -> Any:
return asyncio.run(self.tool_call_async(name, arguments, request_timeout=timeout))

async def tool_call_async(self, name: str, arguments: dict[str, Any], request_timeout: float | int = 10) -> Any:
async def tool_call_async(self, name: str, arguments: dict[str, Any], request_timeout: float | int | None = None) -> Any:
request_timeout = self.default_timeout if request_timeout is None else request_timeout

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)(base\.py$|agent_with_tools\.py$|agent-form/index\.tsx$)' || true

echo "== base.py outline =="
ast-grep outline agent/tools/base.py 2>/dev/null || true

echo "== base.py relevant lines =="
cat -n agent/tools/base.py | sed -n '1,140p'

echo "== agent_with_tools.py relevant lines =="
cat -n agent/component/agent_with_tools.py | sed -n '1,180p'

echo "== agent-form schema relevant lines =="
cat -n web/src/pages/agent/form/agent-form/index.tsx | sed -n '1,140p'

echo "== search timeout usages =="
rg -n "tool_timeout|default_timeout|request_timeout|toolcall_session|ToolPluginCallSession|LLMToolPluginCallSession|NumberInputStepper|min" agent web -S

Repository: infiniflow/ragflow

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== tracked target files =="
git ls-files | rg '(^|/)agent/tools/base\.py$|(^|/)agent/component/agent_with_tools\.py$|(^|/)web/src/pages/agent/form/agent-form/index\.tsx$' || true

echo "== base.py relevant lines =="
sed -n '1,160p' agent/tools/base.py

echo "== agent_with_tools.py relevant lines =="
sed -n '80,145p' agent/component/agent_with_tools.py

echo "== agent-form relevant lines =="
sed -n '55,95p' web/src/pages/agent/form/agent-form/index.tsx

echo "== focused timeout/tool_timeout usages (files only first) =="
rg -n -S --glob '!docs/**' --glob '!*.png' --glob '!*.jpg' --glob '!*.svg' --glob '!*.txt' \
  'tool_timeout|default_timeout|request_timeout|LLMToolPluginCallSession|NumberInputStepper|min' agent web -s || true

echo "== behavioral probe: current truthiness/default expressions =="
python3 - <<'PY'
cases = [None, 0, "0", "14", -1, float("nan"), float("inf")]
for case in cases:
    agent_fallback = float(case or 10) if case is not None else float(10)
    base_default = 10 if case is None else case
    print({"case": case, "agent_with_tools_fallback_value": agent_fallback, "tool_call_default_if_none": base_default})
PY

Repository: infiniflow/ragflow

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== exact git status for target files =="
git status --short -- agent/tools/base.py agent/component/agent_with_tools.py web/src/pages/agent/form/agent-form/index.tsx || true

echo "== exact tracked target files =="
git ls-files -- agent/tools/base.py agent/component/agent_with_tools.py web/src/pages/agent/form/agent-form/index.tsx

echo "== exact target file sizes =="
wc -l agent/tools/base.py agent/component/agent_with_tools.py web/src/pages/agent/form/agent-form/index.tsx

echo "== sed exact target ranges =="
sed -n '1,145p' agent/tools/base.py
printf '\n--- agent_with_tools.py ---\n'
sed -n '90,130p' agent/component/agent_with_tools.py
printf '\n--- agent-form/schema ---\n'
sed -n '60,90p' web/src/pages/agent/form/agent-form/index.tsx

echo "== focused timeout/tool_timeout matches in tracked files =="
rg -n -S \
  'tool_timeout|default_timeout|request_timeout|LLMToolPluginCallSession|NumberInputStepper|z\.coerce\.number\(\)\.optional|z\.number\(\).*optional|NumberInputStepper' \
  agent/tools/base.py agent/component/agent_with_tools.py web/src/pages/agent/form/agent-form/index.tsx || true

printf '%s\n' '--- behavioral probe: truthiness/default expressions ---'
python3 - <<'PY'
cases = [
    ("missing", None, 10),
    ("0", 0, float(0)),
    ("empty string", "", float(0)),
    ("14", 14, float(14)),
    ("negative", -1, float(-1)),
    ("nan", float("nan"), float("nan")),
    ("inf", float("inf"), float("inf")),
]
print("agent/component fallback: float(value or 10) if value is not None else float(10)")
print("agent/tools default: 10 if request_timeout is None else request_timeout")
for label, value, fallback in cases:
    agent_value_or_10 = float(value or 10)
    default_value = 10 if value is None else value
    print(label, "value=", repr(value), "agent fallback=", repr(fallback), "base default=", repr(default_value), "Python timeout passes?", timeout_is_valid(default_value))
PY

Repository: infiniflow/ragflow

Length of output: 11777


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== common/mcp_tool_call_conn.py outline =="
ast-grep outline common/mcp_tool_call_conn.py 2>/dev/null || true

echo "== common/mcp_tool_call_conn.py relevant lines =="
sed -n '1,240p' common/mcp_tool_call_conn.py

echo "== focused call signature checks =="
rg -n -S \
  'class MCPToolBinding|class MCPToolCallSession|class ToolCallSession|def (tool_call|tool_call_async|invoke)|thread_pool_exec|request_timeout|timeout' \
  common/mcp_tool_call_conn.py

Repository: infiniflow/ragflow

Length of output: 13718


Validate tool_timeout at component construction.

The form accepts optional values, including zero, and agent/component/agent_with_tools.py#L115-L117 converts falsy values to 10. agent/tools/base.py#L60 then uses whatever default came from construction, so invalid settings can hide behind the fallback or propagate to MCP calls. Reject values below the intended minimum (for example, less than 1) before creating LLMToolPluginCallSession.

📍 Affects 3 files
  • agent/tools/base.py#L51-L60 (this comment)
  • agent/component/agent_with_tools.py#L115-L117
  • web/src/pages/agent/form/agent-form/index.tsx#L79-L79
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/tools/base.py` around lines 51 - 60, Validate the timeout in the tool
component constructor before creating LLMToolPluginCallSession, rejecting values
below the minimum of 1 instead of allowing invalid defaults to propagate. In
agent/tools/base.py around the constructor and default_timeout assignment,
enforce this validation; update agent/component/agent_with_tools.py lines
115-117 to stop converting falsy tool_timeout values to 10 and pass the
validated value consistently. The form field at
web/src/pages/agent/form/agent-form/index.tsx line 79 requires no direct change
unless needed to preserve submission of zero for server-side validation.

assert name in self.tools_map, f"LLM tool {name} does not exist"
logging.info(f"[ToolCall] invoke name={name} arguments={str(arguments)[:200]}")
if not isinstance(arguments, Mapping):
Expand Down
64 changes: 64 additions & 0 deletions test/unit_test/agent/tools/test_llm_tool_plugin_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#
# Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Verify that LLMToolPluginCallSession applies its configurable default timeout to MCP tool calls."""

import asyncio
import time
from functools import partial

from agent.tools.base import LLMToolPluginCallSession
from common.mcp_tool_call_conn import MCPToolBinding


class _BlockingSession:
"""Fake MCP session that waits until the caller-provided timeout elapses, mimicking MCPToolCallSession."""

def tool_call(self, name: str, arguments: dict, timeout: float = 10) -> str:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
time.sleep(0.005)
return "done"


def _noop_callback(*args, **kwargs):
pass


def _make_session(default_timeout):
return LLMToolPluginCallSession(
{"transcribe_0": MCPToolBinding(_BlockingSession(), "transcribe")},
partial(_noop_callback),
default_timeout=default_timeout,
)


def test_default_timeout_is_applied_to_mcp_tool_call():
session = _make_session(default_timeout=0.1)
start = time.monotonic()
result = asyncio.run(session.tool_call_async("transcribe_0", {}))
elapsed = time.monotonic() - start
assert result == "done"
# Without the session default the 10s tool_call default would run far longer.
assert elapsed < 1.0


def test_explicit_request_timeout_overrides_default():
session = _make_session(default_timeout=0.5)
start = time.monotonic()
result = asyncio.run(session.tool_call_async("transcribe_0", {}, request_timeout=0.05))
elapsed = time.monotonic() - start
assert result == "done"
assert elapsed < 0.3
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
3 changes: 3 additions & 0 deletions web/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2602,6 +2602,9 @@ Best for: Documents with flowing, contextually connected content — such as boo
maxRounds: 'Max reflection rounds',
delayAfterError: 'Delay after error',
maxRetries: 'Max retry rounds',
toolTimeout: 'Tool timeout',
toolTimeoutTip:
'Timeout in seconds for a single tool call (including MCP tools). Increase it for long-running tools.',
maxSteps: 'Max steps',
headless: 'Headless',
enableDefaultExtensions: 'Enable default extensions',
Expand Down
3 changes: 3 additions & 0 deletions web/src/locales/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1599,6 +1599,9 @@ export default {
maxRounds: 'Максимальное количество раундов рефлексии',
delayAfterError: 'Задержка после ошибки',
maxRetries: 'Максимальное количество попыток повтора',
toolTimeout: 'Таймаут вызова инструмента',
toolTimeoutTip:
'Таймаут в секундах для одного вызова инструмента (включая MCP). Увеличьте для длительно выполняющихся инструментов.',
advancedSettings: 'Расширенные настройки',
addTools: 'Добавить инструменты',
sysPromptDefaultValue: `
Expand Down
3 changes: 3 additions & 0 deletions web/src/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2247,6 +2247,9 @@ NER:使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系
maxRounds: '最大反思轮数',
delayAfterError: '错误后延迟',
maxRetries: '最大重试轮数',
toolTimeout: '工具调用超时',
toolTimeoutTip:
'单次工具调用(含 MCP 工具)的超时秒数。长时间运行的工具请调大该值。',
maxSteps: '最大步数',
headless: '无头模式',
enableDefaultExtensions: '启用默认扩展',
Expand Down
1 change: 1 addition & 0 deletions web/src/pages/agent/constant/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,7 @@ export const initialAgentValues = {
exception_default_value: '',
tools: [],
mcp: [],
tool_timeout: 10,
cite: true,
showStructuredOutput: false,
outputs: {
Expand Down
28 changes: 28 additions & 0 deletions web/src/pages/agent/form/agent-form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { Input, NumberInput } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Separator } from '@/components/ui/separator';
import { Switch } from '@/components/ui/switch';
import NumberInputStepper from '@/components/originui/number-input';
import { useFindLlmByUuid } from '@/hooks/use-llm-request';
import { zodResolver } from '@hookform/resolvers/zod';
import { get } from 'lodash';
Expand Down Expand Up @@ -49,6 +50,7 @@ import {
useHandleShowStructuredOutput,
useShowStructuredOutputDialog,
} from './use-show-structured-output-dialog';
import { useGetAgentMCPIds } from './use-get-tools';
import { useValues } from './use-values';
import { useWatchFormChange } from './use-watch-change';

Expand All @@ -74,6 +76,7 @@ const FormSchema = z.object({
exception_method: z.string().optional(),
exception_goto: z.array(z.string()).optional(),
exception_default_value: z.string().optional(),
tool_timeout: z.coerce.number().optional(),
...LargeModelFilterFormSchema,
cite: z.boolean().optional(),
showStructuredOutput: z.boolean().optional(),
Expand Down Expand Up @@ -118,6 +121,8 @@ function AgentForm({ node }: INextOperatorForm) {
name: 'exception_method',
});

const { mcpIds } = useGetAgentMCPIds();

const showStructuredOutput = useWatch({
control: form.control,
name: 'showStructuredOutput',
Expand Down Expand Up @@ -248,6 +253,29 @@ function AgentForm({ node }: INextOperatorForm) {
</FormItem>
)}
/>
{mcpIds.length > 0 && (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We recommend using RAGFlowFormItem to reduce code and ensure consistent styles.

<FormField
control={form.control}
name={`tool_timeout`}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel tooltip={t('flow.toolTimeoutTip')}>
{t('flow.toolTimeout')}
</FormLabel>
<FormControl>
<div className="flex gap-2 items-center">
<NumberInputStepper
value={field.value}
onChange={field.onChange}
min={1}
/>{' '}
{t('flow.seconds')}
</div>
</FormControl>
</FormItem>
)}
/>
)}
{hasSubAgentOrTool(edges, node?.id) && (
<FormField
control={form.control}
Expand Down
1 change: 1 addition & 0 deletions web/src/pages/agent/form/agent-form/use-values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export function useValues(node?: RAGFlowNodeType) {

return {
...omitToolsAndMcp(formData),
tool_timeout: get(formData, 'tool_timeout', 10),
prompts: get(formData, 'prompts.0.content', ''),
};
}, [defaultValues, node?.data?.form]);
Expand Down