mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-09-08 10:14:35 +08:00
83d09b16ceefae4349b06801ef0a49d62c50e6d8
504 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3cba34d67f |
fix(agent/tools): port Crawler to ToolBase so it can load and run (#16415)
### What problem does this PR solve? Closes #16414. The **Crawler** agent tool (`agent/tools/crawler.py`) was never ported to the modern `ToolBase`/`_invoke` interface during the agent module redesign, so it was broken in three independent ways: 1. **Crashed on construction.** `CrawlerParam` extends `ToolParamBase`, whose `__init__` reads `self.meta["parameters"]`, but `CrawlerParam` defined no `meta`. Constructing it raised `AttributeError: 'CrawlerParam' object has no attribute 'meta'`. Because `agent/canvas.py` instantiates `component_class(component_name + "Param")()` while loading a canvas, **any agent containing a Crawler node failed to load.** 2. **`_invoke` missing.** It extends `ToolBase` (whose `invoke()` dispatches to `self._invoke`) but only implemented the legacy `_run`, so `_invoke` resolved to `ComponentBase._invoke` → `NotImplementedError`. 3. **`be_output` removed.** `_run` called `Crawler.be_output(...)`, which no longer exists on the base classes. ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) ### Changes - Add a `ToolMeta` to `CrawlerParam` (defined before `super().__init__()`, matching every other ported tool such as `ArXivParam`/`TavilyExtractParam`) advertising a required `query` parameter — the URL to crawl, default `{sys.query}`, consistent with the `{sys.query}` convention shared by the other tools. - Replace the legacy `_run`/`be_output` with `_invoke`/`set_output`, writing the extracted page content to `formalized_content` (errors surfaced via `_ERROR`), consistent with the other tools. - Preserve the existing SSRF guard (`assert_url_is_safe` + `pin_dns_global`). - Add regression tests (`test/unit_test/agent/component/test_crawler.py`) covering param construction, validation, and the tool descriptor. Same class of defect as #16329 (DeepL). Backend-only; no frontend changes. --------- Co-authored-by: Zhichang Yu <yuzhichang@gmail.com> |
||
|
|
6a4b9be426 | Refactor: reformat all code for lefthook using ruff and gofmt (#16585) | ||
|
|
83540185e1 |
fix(agent/tools): port AkShare to ToolBase so it works as an Agent tool (#16417)
### What problem does this PR solve? Closes #16416. The **AkShare** agent tool (`agent/tools/akshare.py`) was never ported to the modern `ToolBase`/`_invoke` interface during the agent module redesign and was still written against the removed legacy `_run`/`be_output` API, so it was non-functional: 1. **Adding it to an Agent raised `AttributeError`.** `AkShare` extended `ComponentBase` (not `ToolBase`) and `AkShareParam` defined no `meta`, so it had no `get_meta()`. `agent/component/agent_with_tools.py` builds each tool's function descriptor via `cpn.get_meta()`, so constructing an Agent that includes the AkShare tool raised `AttributeError: 'AkShare' object has no attribute 'get_meta'`. 2. **It could never run.** `invoke()` dispatches to `self._invoke`, but `AkShare` only implemented the legacy `_run`, so `_invoke` fell through to `ComponentBase._invoke` → `NotImplementedError`. `_run` also called `be_output(...)`, which no longer exists on the base classes. ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) ### Changes - Port `AkShareParam` to `ToolParamBase` with a `ToolMeta` (defined before `super().__init__()`, matching `ArXivParam`/`TavilyExtractParam`) exposing a required `query` parameter — the stock symbol to look up, default `{sys.query}`. `query` matches the `{sys.query}` convention shared by the other tools. - Rewrite the component with `_invoke`/`set_output("formalized_content", ...)` (errors surfaced via `_ERROR`), keeping `top_n` and importing `akshare` lazily. - Add regression tests (`test/unit_test/agent/component/test_akshare.py`) covering param construction, validation, and the tool descriptor. Same class of defect as #16329 (DeepL) and #16414 (Crawler). Backend-only; no frontend changes. --------- Co-authored-by: Zhichang Yu <yuzhichang@gmail.com> |
||
|
|
93f6d647d4 | Fix the sandbox exec image cannot show and download (#16577) | ||
|
|
27c9a093bd |
Fix: close MCP sessions after canvas execution to prevent connection leaks (#13295)
### What problem does this PR solve? Closes #12962 MCPToolCallSessions created during agent execution (in `Agent.__init__`) are never explicitly closed. Each session starts its own event loop thread and opens an SSE/HTTP connection to the MCP server. When the canvas goes out of scope, these threads and connections remain alive indefinitely, accumulating over time and causing resource exhaustion after prolonged use. ### Solution 1. Add a `Graph.close()` method that iterates all components, finds MCPToolCallSessions held by Agent tools, and calls `close_sync()` on each to properly shut down the event loop, thread, and connection. 2. Call `canvas.close()` in `finally` blocks after `canvas.run()` completes in `canvas_service.py` and `canvas_app.py`. 3. Move MCP session cleanup to `finally` blocks in `test_tool` endpoint (`mcp_server_app.py`) and `get_mcp_tools` (`api_utils.py`) to ensure sessions are closed even on exceptions. ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) --------- Co-authored-by: conflict-resolver <conflict-resolver@local> Co-authored-by: Zhichang Yu <yuzhichang@gmail.com> |
||
|
|
742188c3bb |
feat(agent): report accurate aggregated token usage and propagate session/user + input/output to Langfuse for agent runs (#16420)
### What problem does this PR solve?
_Briefly describe what this PR aims to solve. Include background context
that will help reviewers understand the purpose of the PR._
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
- [x] Other (please describe):
## Summary
Agent (Canvas) runs previously did not surface token usage in the SSE
stream, and RAGFlow's own Langfuse generations for agent runs were
missing the prompt/completion split and the session/user correlation.
This made it impossible for an external caller (or Langfuse) to
reconcile an agent turn's cost with the upstream provider (e.g.
OpenRouter), because a single turn can issue several distinct LLM calls
(query rewriting / cross-language translation, multi-round tool
reasoning, nested sub-agents, and the final answer).
This PR introduces a per-run token usage sink so that **every** LLM call
in a run is aggregated and reported once, and enriches Langfuse
generations with the prompt/completion split plus session/user
attributes.
## What changes
### 1. Per-run token usage sink (`common/token_utils.py`)
- Adds two `contextvars`: `token_usage_sink` (a mutable per-run
accumulator) and `langfuse_run_attrs` (session_id/user_id for the run).
- Adds `record_run_token_usage(...)` (thread-safe via a lock, because
`thread_pool_exec` copies the context into worker threads that share the
sink dict) and `usage_from_response(...)` which extracts a
`{prompt_tokens, completion_tokens, total_tokens}` split from
OpenAI/OpenRouter-style responses.
### 2. Provider layer captures the prompt/completion split
(`rag/llm/chat_model.py`)
- `LiteLLMBase` and `Base` now store `self.last_usage`
(prompt/completion/total) for the most recent chat call, in both the
plain and tool-calling paths.
- Streaming requests set `stream_options.include_usage = True` (LiteLLM
path) so the authoritative usage arrives on the final chunk; this is
read even on the usage-only chunk that carries no `choices`.
- Fixes a multi-round accounting bug in `*_with_tools`: token totals
were **overwritten** by each round (`total_tokens = tol`) instead of
accumulated, undercounting multi-round tool conversations. Each round is
now committed to a running aggregate.
### 3. LLMBundle reports usage once, per call
(`api/db/services/llm_service.py`)
- New `_report_usage(total_tokens)` records the call's usage into the
active run sink and returns the prompt/completion/total split for
Langfuse. The split is only used when it is consistent with the
authoritative total; otherwise only the total is reported.
- All three chat entry points (`async_chat`, `async_chat_streamly`,
`async_chat_streamly_delta`) now emit `usage_details` with
`input`/`output`/`total` instead of total-only.
- `_start_langfuse_observation` now applies `session_id`/`user_id` from
the per-run context (`langfuse_run_attrs`) so agent-run generations are
correctly grouped, even though agent LLMBundles are constructed without
those attributes.
### 4. Canvas installs the sink and emits the aggregate
(`agent/canvas.py`)
- `Canvas.run()` installs a fresh `token_usage_sink` and
`langfuse_run_attrs` (from `user_id`/`session_id`) at the start of every
turn.
- `message_end` now includes an aggregated `usage` object:
`{prompt_tokens, completion_tokens, total_tokens, calls}` covering all
LLM calls in the run.
### 5. Pass session id into the run
(`api/db/services/canvas_service.py`)
- `completion()` forwards `session_id` to `Canvas.run()` for Langfuse
session correlation.
## Why a context variable
LLM calls in an agent run originate from many places that each build
their own `LLMBundle` (e.g. `cross_languages`/`keyword_extraction`
helpers, the Agent component, and nested sub-agents invoked as tools). A
run-scoped context variable is the only non-invasive chokepoint that
captures all of them exactly once, including nested agents (which run in
the same async context) and thread-pool tools (the executor copies the
context).
## Behavior / compatibility
- No public API or wire-format removal: `message_end` gains an
additional optional `usage` field; existing consumers are unaffected.
- When a provider does not return authoritative usage, behavior falls
back to the previous token estimate (total only, no split).
- Non-agent flows (Dataflow `Pipeline`, sync `Graph.run`) are untouched.
## Testing
- [x] Simple agent answer: `message_end.usage.total_tokens` matches
provider usage.
- [x] Agent with cross-language retrieval: aggregate equals the sum of
both provider calls.
- [x] Tool-calling agent (multi-round): total accumulates across rounds.
- [x] Nested agent (agent-as-tool): sub-agent tokens included in the
parent run total.
- [x] Langfuse: agent generations show input/output split and are
grouped by session/user.
---------
Co-authored-by: yzc <yuzhichang@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
9bf57600cf |
feat(agent): add BGPT structured literature evidence search tool (#16050)
## Summary Adds a first-class **BGPT** Agent tool (backend + UI) in response to [#15997](https://github.com/infiniflow/ragflow/issues/15997#issuecomment-4703864227). BGPT calls `POST https://bgpt.pro/api/mcp-search` and returns structured study evidence from full-text papers — not just titles/abstracts. Each result is formatted for RAGFlow citations with: - methods - sample size / population - results - limitations - conflicts of interest - data availability - study blind spots - `how_to_falsify` ## Why this shape - Mirrors existing literature tools (`PubMed`, `ArXiv`) and HTTP tools (`SearXNG`). - Works on the free tier (no API key required for first 50 results). - Optional `api_key` and `days_back` in the node/tool config. - Surfaces both `formalized_content` and raw `json` outputs (like SearXNG). ## Files - `agent/tools/bgpt.py` — REST client + evidence formatter - Frontend: Operator enum, forms, tool picker, canvas accordion, en/zh locales, icon ## Demo / docs Runnable claim-interrogation demo: https://github.com/connerlambden/bgpt-mcp/blob/main/EVIDENCE_DEMO.md ## Test plan - [ ] Add BGPT node on Agent canvas, run query `GLP-1 alcohol craving`, verify `formalized_content` includes limitations/COI fields - [ ] Add BGPT as Agent sub-tool under Search, verify tool-calling works - [ ] Confirm empty query / try-run returns gracefully - [ ] Optional: paid-tier `api_key` path --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Zhichang Yu <yuzhichang@gmail.com> |
||
|
|
508f6226f8 |
fix(agent): filter TuShare news with upstream keyword input (#16361)
## Summary TuShare required non-empty upstream input but filtered fetched news with the static `keyword` param (default empty string), so agent-provided keywords were ignored. Use `self._param.keyword or ans` when filtering, matching how AkShare uses upstream input for its query. Fixes #16360 ## Test plan - [x] `test_tushare_filters_with_upstream_keyword_when_param_empty` mocks the API and asserts only rows matching the upstream keyword are returned --------- Co-authored-by: yzc <yuzhichang@gmail.com> Co-authored-by: Harsh Kashyap <harshkashyap@Harshs-MacBook-Pro.local> |
||
|
|
828c5789f6 |
fix(agent/tools): GoogleScholar empty json output and ignored top_n (#16419)
### What problem does this PR solve? Closes #16418. `scholarly.search_pubs(...)` returns a **lazy generator**, but `agent/tools/googlescholar.py` treated it as a re-iterable, bounded list: ```python scholar_client = scholarly.search_pubs(kwargs["query"], ...) # lazy generator self._retrieve_chunks(scholar_client, ...) # (1) iterates -> exhausts it self.set_output("json", list(scholar_client)) # (2) already empty -> [] ``` 1. **`json` output was always empty.** `_retrieve_chunks` iterates `scholar_client`, exhausting the generator; `list(scholar_client)` then returns `[]`. 2. **`top_n` was never applied.** Unlike `ArXiv` (`max_results=self._param.top_n`), the unbounded generator was passed straight to `_retrieve_chunks`, which has no internal limit — so the tool kept paginating well past Top N (until an error, rate-limit/block, or `COMPONENT_EXEC_TIMEOUT`). ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) ### Changes - Materialize at most `top_n` results once with `itertools.islice`, and reuse that list for both `_retrieve_chunks` and the `json` output. - Add regression tests (`test/unit_test/agent/component/test_googlescholar.py`, stubbing `scholarly.search_pubs`) covering the `top_n` bound, the non-empty `json` output, and the empty-query short-circuit. Verified: against `main` the new tests fail with `assert 30 == 5` (top_n ignored) and `assert 0 == 5` (empty json); with this fix all pass. Backend-only. --------- Co-authored-by: Zhichang Yu <yuzhichang@gmail.com> |
||
|
|
06b07bbfd6 |
Add CAJAL scientific paper agent template (#14641)
### What problem does this PR solve? Closes https://github.com/infiniflow/ragflow/issues/14571. Adds CAJAL as a first-class local scientific-writing option in RAGFlow: - registers `agnuxo/cajal-4b-p2pclaw` as a known Ollama chat model with a 32K context setting - adds a built-in “CAJAL scientific paper agent” template under the existing agent template catalog - preconfigures the agent for grounded scientific writing: retrieval first, citation traceability, LaTeX-ready output, and explicit limitations when evidence is missing - adds unit coverage to ensure the template normalizes through RAGFlow’s production template loader, keeps graph form data in sync, and exposes the Ollama model option Behavior/evidence gathered for the requested model: - Hugging Face model metadata for `Agnuxo/CAJAL-4B-P2PCLAW` reports `pipeline_tag=text-generation` and tags including `gguf`, `llama.cpp`, `vllm`, `scientific-research`, `papers`, `academic-writing`, `latex`, and `license:apache-2.0`. - The model card documents CAJAL as a 4B scientific paper generation model with 32K context, local inference, LaTeX/citation specialization, and CPU-only support around 5 tok/s on Ryzen 7 5800X. - Local CPU generation could not be completed on this machine because the advertised Ollama model name is not currently resolvable from Ollama’s registry: both `https://registry.ollama.ai/v2/agnuxo/cajal-4b-p2pclaw/manifests/latest` and `https://registry.ollama.ai/v2/library/agnuxo/cajal-4b-p2pclaw/manifests/latest` returned `404 Not Found`; the Hugging Face repo tree currently exposes an 8.4 GB `model.safetensors` but no GGUF artifact in `main`. The template therefore targets the documented Ollama model name for users who have the local CAJAL deployment/model file available. Verification run locally: ```bash python3 -m pytest test/test_cajal_template_unit.py -q # 3 passed in 0.34s python3 - <<'PY' import json, glob for f in sorted(glob.glob('agent/templates/*.json') + ['conf/llm_factories.json']): with open(f, encoding='utf-8') as fp: json.load(fp) print('json_ok') PY # json_ok python3 -m ruff check test/test_cajal_template_unit.py # All checks passed! git diff --check ``` `uv run pytest test/testcases/test_web_api/test_agent_app/test_cajal_template_unit.py -q` was also attempted first, but dependency setup failed before test collection while building `ormsgpack==1.5.0` from uv with a package metadata parse error. Clearing uv’s `ormsgpack` cache and retrying reproduced the same build failure, so the focused unit test was run with the system Python environment instead. ### Type of change - [ ] Bug Fix (non-breaking change which fixes an issue) - [x] New Feature (non-breaking change which adds functionality) - [ ] Documentation Update - [ ] Refactoring - [ ] Performance Improvement - [ ] Other (please describe): --------- Co-authored-by: sxxtony <sxxtony@users.noreply.github.com> Co-authored-by: yzc <yzc@users.noreply.github.com> Co-authored-by: Zhichang Yu <yuzhichang@gmail.com> |
||
|
|
38f8f8a656 |
fix: handle non-serializable objects in agent canvas SSE and state se… (#14210)
…rialization Agent components (llm.py, agent_with_tools.py, message.py) store functools.partial objects as deferred streaming handles in their output slots. When the canvas state gets serialized for SSE events, Redis commits, or logging, these partials — plus non-copyable objects like Langfuse clients — crash json.dumps and deepcopy. Changes: - canvas_app.py: add default=str to json.dumps for SSE event serialization (lines 238, 296) - canvas.py: wrap deepcopy calls in try/except to handle non-copyable objects (Langfuse clients, etc.), add default=str to final json.dumps - base.py: add default=str to ComponentParamBase.__str__ to handle non-serializable objects in component parameters Closes #14229 ### What problem does this PR solve? _Briefly describe what this PR aims to solve. Include background context that will help reviewers understand the purpose of the PR._ ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) - [ ] New Feature (non-breaking change which adds functionality) - [ ] Documentation Update - [ ] Refactoring - [ ] Performance Improvement - [ ] Other (please describe): Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: yzc <yuzhichang@gmail.com> |
||
|
|
e23f63bd93 |
fix(agent): prevent empty LLM user message after prompt fitting (#16413)
## Summary - Treat `max_tokens=0` as unset (`or 8192`) when building model context budgets, fixing agents that silently zeroed prompts when a vLLM model had `max_tokens: 0` in tenant config - Replace trailing same-role canvas history in `LLM._sys_prompt_and_msg` instead of skipping the current user prompt - Add `LLM.fit_messages()` validation after `message_fit_in` on agent paths so empty user content fails fast with a clear error instead of reaching vLLM Fixes #16411 ## Root cause Agent canvas workflow called `message_fit_in` with `int(max_length * 0.97)`. When `max_length` was `0`, both system and user content were trimmed to empty strings. The `[HISTORY STREAMLY]` log showing only `{"role":"user","content":""}` matches this. A secondary bug skipped appending the formatted user prompt when history ended with a `user` role message. ## Test plan - [x] Added `test/unit_test/agent/component/test_llm_prompt.py` for role-replace, validation, and zero-budget fitting - [x] Added `test_message_fit_in_zero_budget_preserves_non_empty_messages` in `test_generator_message_fit_in.py` - [ ] CI unit tests - [ ] Manual: agent canvas `begin → Retrieval → Agent → Message` with vLLM Qwen3; confirm user message reaches LLM Made with [Cursor](https://cursor.com) --------- Co-authored-by: Taranum Wasu <taranumwasu@Taranums-MacBook-Pro.local> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
dc8b6d767c |
fix(agent): inject uploaded attachments into LLM context (#15215) (#15220)
## Summary Fixes #15215 — attachments uploaded to an agent were not reaching the LLM. When a user uploads a file in an agent chat, `canvas.run` parses it into the `sys.files` global (text content for documents, `data:image/...` URIs for images — see `agent/canvas.py:752-768`). But the LLM/Agent component's `_prepare_prompt_variables` only substitutes variables the user's prompt template explicitly references via `{var}` placeholders. The default prompt is `[{"role": "user", "content": "{sys.query}"}]` with no `{sys.files}`, so the parsed attachment content never reaches the model. In the reporter's logs, this is why the agent saw only the bare query `附件 摘要 attachment summary` and went searching the dataset instead of reading the uploaded PDF. ## Fix `agent/component/llm.py` — added `_collect_sys_files()` and an auto-injection step in `_prepare_prompt_variables`: - If `sys.files` is non-empty **and** neither `sys_prompt` nor any entry in `prompts` already contains `{sys.files}` (no double-injection), split the entries into text vs. `data:image/...` URIs. - Image URIs are merged into `self.imgs`, which the existing logic uses to switch the chat model to `IMAGE2TEXT` and pass `images=...` to `async_chat`. - Text content is appended to the last `user` role message in `msg`, mirroring how `dialog_service.async_chat_solo` handles attachments for the non-agent chat path (`api/db/services/dialog_service.py:318-321`). Both `LLM._invoke_async` and `Agent._invoke_async` (tool-using) go through `_prepare_prompt_variables`, so plain LLM nodes and Agent nodes are fixed in both streaming and non-streaming paths. ## Test plan - [ ] Upload a PDF attachment to an agent with the default `{sys.query}` prompt and ask "summarize the attachment" — the model should answer from the file content rather than searching the knowledge base. - [ ] Upload an image attachment to an agent and ask about its contents — the model should switch to the vision-capable LLM and answer from the image. - [ ] Verify that an agent whose prompt **does** include `{sys.files}` still works and does **not** include the file content twice. - [ ] Verify that an agent run with no attachments behaves unchanged. - [ ] Run `uv run pytest` to make sure no existing tests regress. ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) - [ ] New Feature (non-breaking change which adds functionality) - [ ] Documentation Update - [ ] Refactoring - [ ] Performance Improvement - [ ] Other (please describe): --------- Co-authored-by: yzc <yuzhichang@gmail.com> |
||
|
|
b6dbb2f71e | fix: update variable completeness check to allow None parameter (#16389) | ||
|
|
78db4e949b |
feat(agent): add module-level debug logging for canvas execution flow (#16200)
Summary Add module-level debug logging to track Agent canvas execution flow (Closes #9306), enabling developers to diagnose component invocation, input/output states, and variable resolution without modifying production code. Also fix related bugs in message.py: re.sub backreference issue and unawaited _save_to_memory coroutine causing silent memory save failures. Changes agent/canvas.py: log workflow start, component invocation, and component completion agent/component/agent_with_tools.py: log Agent parameter resolution and LLM invocation path; standardize json.dumps usage agent/component/base.py: log get_input() variable resolution branches agent/component/message.py: fix re.sub backreference issue; properly await _save_to_memory coroutine Design Uses module-level loggers (logging.getLogger(__name__)) to support selective debugging: LOG_LEVELS=agent=DEBUG Zero performance impact in production (INFO level by default) Works with existing PUT /system/config/log API for runtime level changes Closes #9306 Note: While adding debug logging, I discovered and fixed two related bugs in message.py: - re.sub replacement value was interpreted as regex backreference instead of literal string - _save_to_memory coroutine was not properly awaited, causing silent failures --------- Co-authored-by: wills <willsgao@163.com> |
||
|
|
660970b253 |
fix(agent): add SSRF guard to Invoke HTTP component (#15426)
## Summary Closes #15425. The agent **Invoke** (HTTP Request) component now calls `assert_url_is_safe` and `pin_dns` before `requests.*`, matching Crawler and SearXNG. ## Changes - `agent/component/invoke.py`: SSRF guard + DNS pinning on outbound requests. - `test_invoke_component_unit.py`: unit test blocks loopback URL without calling `requests.get`. ## Test plan - [x] `pytest test/testcases/test_web_api/test_canvas_app/test_invoke_component_unit.py::test_invoke_blocks_loopback_url_with_ssrf_guard` (requires project test env / `ZHIPU_AI_API_KEY` in CI) --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Zhichang Yu <yuzhichang@gmail.com> |
||
|
|
e256d91ade |
fix: guard SSRF in ExeSQL agent tool DB host (#15609)
### What problem does this PR solve? Closes #15608. The ExeSQL agent tool (`agent/tools/exesql.py`) opens database connections to a node-author-controlled host/port with no SSRF validation. The sibling `test_db_connection` endpoint already validates the host via `common.ssrf_guard.assert_host_is_safe` (added by PR #14860), but the tool that actually performs the connection at agent run time was left unguarded — so the guard is bypassed simply by running the agent. An agent author can point the host at `127.0.0.1`, `169.254.169.254` (cloud metadata), or any internal RFC1918 host/port, turning ExeSQL into an internal port-scanner / metadata-fetch primitive. ### Fix Mirror the accepted endpoint guard: validate (and resolve) the host once, before the `db_type` dispatch, and connect to the validated public IP so a later DNS change cannot rebind the host to an internal address. - Add `from common.ssrf_guard import assert_host_is_safe`. - `safe_host = assert_host_is_safe(self._param.host)` before the dispatch (rejects loopback, link-local/metadata, RFC1918, and unresolvable hosts). - Substitute the validated IP into all 6 driver branches: mysql/mariadb, oceanbase, postgres, mssql, trino, IBM DB2. Adds `test/unit_test/agent/tools/test_exesql_ssrf.py` covering loopback, link-local/metadata, RFC1918, and empty-host rejection (before any connection), plus an allowed host dialing the validated IP. ### Validation - `python3 -m py_compile agent/tools/exesql.py` - `ruff check agent/tools/exesql.py test/unit_test/agent/tools/test_exesql_ssrf.py` - `pytest test/unit_test/agent/tools/test_exesql_ssrf.py` — 5 passed ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) --------- Co-authored-by: Zhichang Yu <yuzhichang@gmail.com> |
||
|
|
0d7ad0ed0c |
Feat/agent thinking switch (#15446)
### What problem does this PR solve? This PR adds an Agent LLM setting to control thinking mode for official providers that expose a thinking switch. Related to #12842. Closes #15445. Some providers expose thinking controls through provider-specific request fields, but Agent LLM settings did not have a unified option for users to enable or disable thinking mode. This PR adds a `Thinking` selector with: - System default - Enabled - Disabled <img width="452" height="278" alt="8566b0b4-0546-4c8a-913d-f9bbd38319f6" src="https://github.com/user-attachments/assets/25b497f7-1ba0-4bfe-940d-6fe79287d6ab" /> <img width="471" height="971" alt="8a0a6bee-f45f-48d5-bd83-17af260de3db" src="https://github.com/user-attachments/assets/41ad43c1-5087-48f1-bf37-f2ca14c2be2f" /> Initial support is limited to the verified official providers: - Qwen / DashScope: `enable_thinking` - Kimi / Moonshot: `thinking.type` - GLM / ZHIPU-AI: `thinking.type` For LiteLLM-based providers, provider-specific fields are forwarded through `extra_body` before `drop_params` filtering so the request parameters are preserved. ### Type of change - [x] New Feature (non-breaking change which adds functionality) --------- Co-authored-by: jiashi <jiashi19@outlook.com> Co-authored-by: Zhichang Yu <yuzhichang@gmail.com> |
||
|
|
6a4de82a80 |
fix(agent): restore be_output and test DeepL error return (#16363)
## Summary #16332 fixed the missing `return` in DeepL's except branch, but `ComponentBase.be_output` was removed during the agent refactor (#9113) while several components still call it. DeepL (and other tools) would raise `AttributeError` before any error message could be returned. - Restore `ComponentBase.be_output` as `pd.DataFrame([{"content": v}])` (same as pre-refactor behavior) - Add regression test that `_run` returns the `**Error**:` message when translation fails Related to #16329 ## Test plan - [x] `test_run_returns_error_on_translation_failure` - [x] Existing `test_deepl.py` check() tests still pass --------- Co-authored-by: Harsh Kashyap <harshkashyap@Harshs-MacBook-Pro.local> Co-authored-by: Zhichang Yu <yuzhichang@gmail.com> |
||
|
|
14174b2364 |
fix(agent): add HTTP timeout to external API tools (#15436)
### What problem does this PR solve? Closes #15435 Several agent tools call external HTTP APIs through `requests` with no request timeout. When an upstream host accepts the connection but never responds (a slow or overloaded API, a half open connection, a stuck load balancer), the call blocks forever. These tools run inside agent canvas execution, so a single stalled socket freezes the entire agent run with no recovery. Ten call sites were affected: - `agent/tools/qweather.py` (4 calls) - `agent/tools/jin10.py` (4 calls) - `agent/tools/tushare.py` (1 call) - `agent/tools/github.py` (1 call) The `github.py` tool already carried the `@timeout` decorator from `common/connection_utils.py`, but that does not protect against this case. In the default configuration the decorator waits on its result queue with no timeout, and a daemon thread blocked inside a socket read cannot be killed, so the run still hangs. The per request timeout added here is what actually bounds the call. This is the same bug class as the merged Go stream timeout fix, surfacing in the Python tool layer. Changes: - Pass `timeout=DEFAULT_TIMEOUT` on all 10 calls, reusing the existing shared constant in `common/http_client.py` (configurable via `HTTP_CLIENT_TIMEOUT`) so there is one source of truth rather than scattered literals. - Add an AST based unit test at `test/unit_test/agent/tools/test_http_timeout.py` that scans every tool module and fails if any `requests` or `httpx` request call omits a `timeout`, guarding current and future call sites. Verification: - Reproduced the indefinite block against a stalling local server, and confirmed that adding a timeout raises `ReadTimeout` promptly. - Confirmed the `@timeout` decorator does not interrupt a blocked no timeout request in its default configuration. - The new test flags exactly the 10 original call sites on the pre fix code and passes (22 modules) after the fix. ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) - [ ] New Feature (non-breaking change which adds functionality) - [ ] Documentation Update - [ ] Refactoring - [ ] Performance Improvement - [ ] Other (please describe): --------- Co-authored-by: Zhichang Yu <yuzhichang@gmail.com> |
||
|
|
f57f3b4b3a |
feat(agent): add Pipeline chunker component for pre-chunking workflows (#14773) (#15068)
### What problem does this PR solve? Closes #14773. Today, Pipeline (`rag/flow/`) chunking strategies only run as part of a dataset ingestion that always embeds and indexes the result. There is no way to drive Pipeline-style chunking from an Agent workflow without paying that vectorization/persistence cost. This PR adds a single new Agent component, `PipelineChunker`, that: - Takes one or more file references (from `Begin` / `UserFillUp` uploads) as input. - Runs the existing `rag.app.*` chunking strategies (`naive`, `paper`, `qa`, `manual`, `book`, `presentation`, `laws`, `table`, `one`, `email`, `picture`, `audio`, `resume`, `tag`) against each file. - Emits the resulting chunks as `chunks: list[str]` and `chunks_full: list[dict]` for downstream Agent nodes. - Performs **no embedding and no persistence** — chunks live only in canvas variables for the duration of the run, exactly as requested in the issue. The component is auto-discovered by `agent/component/__init__.py`; no registry edits required. Chunker functions are imported lazily so the component itself does not pull `deepdoc` / OCR / VLM at component-discovery time. File resolution mirrors the existing `ExcelProcessor` convention. Out of scope for this PR (potential follow-ups): - Vectorization / KB persistence (explicit ask in the issue). - Frontend canvas UI for the new component. - Bridging to the newer Pydantic-based `rag/flow/chunker/TokenChunker` (consumes a parser node's structured output rather than a raw file — a separate, larger feature). ### Type of change - [ ] Bug Fix (non-breaking change which fixes an issue) - [x] New Feature (non-breaking change which adds functionality) - [ ] Documentation Update - [ ] Refactoring - [ ] Performance Improvement - [ ] Other (please describe): --- ## Files changed - `agent/component/pipeline_chunker.py` — new component (~180 lines) - `test/unit_test/agent/test_pipeline_chunker.py` — unit tests (~120 lines) ## Test plan - [x] `ruff check` on changed files — clean. - [x] `ruff format` applied to the new component file. - [x] `python -m py_compile` on both new files — both compile. - [x] New unit test file carries `pytestmark = pytest.mark.p2` so it runs under marker-filtered CI. - [x] Every new function, method, and class has a docstring (CodeRabbit 80% docstring-coverage gate). - [x] `python -m pytest test/unit_test/agent/test_pipeline_chunker.py -x -q` — **7 passed in 1.95s** locally. Tests stub `api.db.services.file_service` and `rag.app.*` so they exercise the parameter validation and parser-id lookup table without requiring the full backend / model stack. ## Manual integration plan (post-merge) 1. Drop the component into an Agent canvas after a `Begin` node with a file input. 2. Set `parser_id = "naive"` (or any other strategy) and reference the file input in `inputs`. 3. Wire the `chunks` output into a downstream `LLM` / `Message` / `Iteration` node — chunks are available as plain text without any embedding or KB write. Co-authored-by: John Baillie <johnbaillie2007@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Zhichang Yu <yuzhichang@gmail.com> |
||
|
|
faef22c18a |
Harden closed-advisory fixes (#16409)
## Summary - harden reopened advisory fixes across REST connector, invoke, document downloads, and markdown rendering - add targeted regression coverage for redirect-safe SSRF handling, invoke SSRF checks, document access control, and markdown sanitization - verify each referenced GHSA against the original GitHub advisory text and align the closed-advisory plan with the implemented remediation ## What changed - add tenant access checks to document download endpoints to avoid cross-tenant document disclosure - add per-hop SSRF validation, DNS pinning, redirect handling, and redirect limits to the REST API connector - ensure invoke requests validate and pin the resolved host and never follow redirects implicitly - keep the generic rate-limited request path wrapped, not just GET and POST helpers - sanitize markdown HTML before rendering in the highlight markdown component ## Validation - `cd web && npm test -- --runInBand src/components/highlight-markdown/__tests__/index.test.tsx` - `.venv/bin/python -m pytest -q test/unit_test/data_source/test_rest_api_connector.py` - targeted `test/testcases/test_web_api/...` unit additions were reviewed, but the suite cannot be executed end-to-end in this environment because parent `test/testcases/conftest.py` requires a local service on `127.0.0.1:9380` ## Notes - all GHSA entries referenced by the plan were checked against the original GitHub advisory text, not sampled - the closed-advisory plan document was updated locally during review, but is intentionally not included in this PR |
||
|
|
195bfffb5e |
fix(security): address 93 CodeQL code-scanning alerts across 61 files (#16407)
## Summary Resolves all 93 open alerts at https://github.com/infiniflow/ragflow/security/code-scanning by rule: | Rule | Count | Treatment | |------|-------|-----------| | py/clear-text-logging-sensitive-data | 23 | Real fix — log scrubbing | | go/path-injection | 15 | Real fix where possible, suppression with rationale | | go/request-forgery | 8 | Suppression with rationale (operator-controlled URLs) | | go/clear-text-logging | 10 | Real fix — log scrubbing | | go/unsafe-quoting | 5 | Real fix — escape or refactor | | go/sql-injection | 3 | Real fix — orderby whitelist + CodeQL comment | | go/uncontrolled-allocation-size | 2 | Real fix — cap to 1024 | | go/incorrect-integer-conversion | 3 | Real fix — ParseInt + range check | | go/insecure-hostkeycallback | 1 | Real fix — known_hosts file | | go/disabled-certificate-check | 2 | Suppression with rationale | | go/command-injection | 1 | Suppression (sanitized via shq()) | | go/email-injection | 1 | Suppression with rationale | | go/cookie-httponly-not-set | 1 | Suppression (SPA bootstrap) | | js/stack-trace-exposure | 1 | Real fix — generic client message | | js/prototype-pollution-utility | 1 | Real fix — reject __proto__/constructor/prototype | | py/weak-sensitive-data-hashing | 1 | Real fix — MD5 → SHA-256 | | py/incomplete-url-substring-sanitization | 3 | Real fix — urlparse(hostname) | | py/paramiko-missing-host-key-validation | 1 | Real fix — load_system_host_keys + RejectPolicy | | cpp/integer-multiplication-cast-to-long | 2 | Real fix — cast to size_t | ## Real fixes (with measurable security improvement) **SSH host key verification (Go + Python)** Replace `InsecureIgnoreHostKey()` / `paramiko.AutoAddPolicy()` with proper host key verification against a known_hosts file (configurable via `SSH_KNOWN_HOSTS` env / `known_hosts` config field; fail-closed when unset). Loads `~/.ssh/known_hosts` first via `load_system_host_keys()` so existing setups keep working. **SQL injection in `user_canvas`** Add `userCanvasOrderableColumns` whitelist + `userCanvasOrderClause` helper. Both `GetList()` and `ListByTenantIDs()` now route the user-supplied `orderby` query param through the helper, defaulting to `create_time` on miss. **SQL injection in `pipeline_operation_log`** Existing whitelist documented via CodeQL comment. **Real SQL injection in `infinity/chunk.go:931`** Escape `'` → `''` on user-controlled `questionText` before splicing into `filter_fulltext(...)` SQL filter. **Real SQL injection in `elasticsearch/sql.go:75`** Defense-in-depth escape on tokenizer output before splicing into `MATCH(...)`. **Python code injection in `result_protocol.go`** Replace raw JSON literal embedding into Python/JS expressions with base64 + `json.loads` / `JSON.parse(Buffer.from(..., 'base64').toString('utf8'))`. Eliminates both the unsafe-quoting sink and the brittleness of mixing JSON true/false/null with Python syntax. **URL substring check bypass in `embedding_model.py`** Replace `if "dashscope-intl.aliyuncs.com" in u` with `urlparse(u).hostname == "dashscope-intl.aliyuncs.com"` so a base_url like `https://attacker.example/?u=dashscope-intl.aliyuncs.com` cannot bypass the routing. **Prototype pollution in `setNestedValue` (TS)** Reject `__proto__`/`constructor`/`prototype` keys before any assignment. **Integer overflow** - scrypt params via `ParseInt` + non-positive check (`internal/common/password.go`) - `topN` and `n` caps to 1024 (retrieval_service.go, dataset.go) - `nalloc*statesize` cast to `size_t` (cpp/re2/onepass.cc) **Cookie httponly** Set explicitly with rationale: this is the OAuth bootstrap cookie intentionally read by the SPA. **Stack trace exposure** Replace `error.message` in HTTP 500 response with generic `"internal error"`; full error still logged server-side via `console.error`. **Weak hashing** MD5 → SHA-256 for deterministic `conv_id` derivation (`conversation_service.py`). **Log scrubbing** Remove or redact user-controlled / sensitive content from clear-text logs across 8 ingestion parsers, `llm_service.py` ×11, `tenant_llm_service.py` ×7, `misc_utils.py` ×4, `redis_conn.py` ×10, `conftest.py` ×4, `init_data.py`, `dataset_api_service.py`, `generator.py`, `mysql_migration.py`, `cli.go`, `user_command.go`, `pdf_parser.go`. Most patterns converted to parameterized logging (`logging.info("...: %d", n)`) or static messages. ## CodeQL suppressions (each with rationale) For alerts where the data flow is genuinely safe but CodeQL can't see the context — operator-controlled URLs, sanitized inputs, etc. — I added `// codeql[go/<rule>] <rationale>` annotations rather than dismissing them, so future readers can audit the rationale inline: - `internal/agent/component/invoke.go:135` — Invoke is a generic canvas HTTP client - `internal/service/langfuse.go` ×2 — host is per-tenant operator config - `internal/service/file.go:1184` — already SSRF-guarded by `assertURLSafe` - `internal/utility/mcp_client.go` ×3 — already `AssertURLSafe` + IP-pinned - `internal/entity/models/bedrock.go` — sigv4-signed request, URL can't be tampered - `internal/service/deep_researcher.go:269` — `callback` is SSE display string, not SQL - `internal/engine/infinity/chunk.go:346` — UUIDs can't contain `'` (RFC 4122) - `internal/cli/common_command.go` ×2 — CLI trusts operator-configured URL - `internal/utility/smtp.go:194` — msg is server-built, not user form input - `internal/entity/models/*` ×14 (path-injection) — audio file paths are caller-supplied ## Test plan - ✅ All 13 modified Go packages build cleanly - ✅ 663 tests pass across `internal/agent/sandbox`, `internal/common`, `internal/agent/component`, `internal/engine/infinity`, `internal/dao` - ✅ All 11 modified Python files parse via `ast.parse` - ✅ TypeScript `tsc --noEmit` clean on the modified `use-provider-fields.tsx` - ✅ `node --check` clean on the modified JS file 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
f58fae5fb7 |
feat(go-agent): Ported retrieval node, added Keenable web search tool (#16396)
Ported retrieval node, added Keenable web search tool - [x] New Feature (non-breaking change which adds functionality) |
||
|
|
f80d4c7843 | fix: tighten loop validation (#16374) | ||
|
|
fe14cc35cf |
fix(agent/tools): DeepL component fails validation and drops errors (#16332)
### What problem does this PR solve?
`DeepLParam.check()` validated `self.top_n`, but DeepL has no such
parameter (it is not defined on the param class or its base), so
`check()` always raised `AttributeError` and a DeepL component could
never pass validation. Removed the bogus `top_n` check.
Also fixed the `_run` except branch, which computed
`be_output("**Error**...")` but never returned it, silently dropping the
error message.
Closes #16329
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Add test cases
### Testing
Added `test/unit_test/agent/component/test_deepl.py` covering
`DeepLParam.check()` with valid defaults and rejection of invalid
source/target languages.
|
||
|
|
3747a6bfeb |
fix(agent/tools): PubMed tool always returns "Unknown Authors" (#16330)
### What problem does this PR solve? Fixes the PubMed tool always emitting `Authors: Unknown Authors`. The `safe_find` closure in `_format_pubmed_content` was hardcoded to search from the article root, so the per-author `LastName`/`ForeName` lookups never matched. `safe_find` now accepts an optional `base` node (defaults to `child`, preserving the existing field lookups), and the author loop passes the current `<Author>` element. Closes #16328 ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) - [x] Add test cases ### Testing Added `test/testcases/test_web_api/test_canvas_app/test_pubmed_unit.py` covering per-author parsing, intact title/journal/DOI fields, and the no-authors fallback. Before: `Authors: Unknown Authors` After: `Authors: Furqan Khan, Jane Smith` |
||
|
|
b9445c67e2 |
fix(agent): coerce None Switch inputs before string operators (#16320)
## Summary - Coerce `None` canvas values to `""` before string comparison operators in `Switch.process_operator`. - Prevents `AttributeError` when upstream components yield `None` and the Switch uses contains/start with/end with. ## Test plan - [x] `.v/bin/python -m ruff check agent/component/switch.py test/unit_test/agent/component/test_switch.py` - [x] `.v/bin/python -m pytest test/unit_test/agent/component/test_switch.py -q` (3 passed) Fixes #16315 --------- Co-authored-by: Harsh Kashyap <harshkashyap@Harshs-MacBook-Pro.local> |
||
|
|
824c88423c |
fix(agent): log Wikipedia disambiguation and page errors instead of s… (#16207)
## Problem The Wikipedia tool silently swallows all exceptions with `except Exception: pass`, making it impossible to debug failures when fetching Wikipedia pages. ## Fix Replace the bare `except Exception: pass` with specific exception handling: - `DisambiguationError`: log available options - `PageError`: log page not found - `Exception`: log unexpected errors with full traceback Co-authored-by: wills <willsgao@163.com> Co-authored-by: Zhichang Yu <yuzhichang@gmail.com> |
||
|
|
10d02e54a8 |
Add Keenable web search tool to the agent (#16233)
Adds Keenable as a web search tool in the agent, alongside the existing Tavily/DuckDuckGo/SearXNG/Google tools. The main difference from the other search tools is that it doesn't require an API key. By default it uses Keenable's keyless public endpoint, so it works out of the box. Providing a key (in the tool config) switches to the authenticated endpoint and lifts the rate limits. ### Changes - Backend: `agent/tools/keenable.py` — `KeenableSearch`, follows the Tavily/DuckDuckGo tool shape (results go through `_retrieve_chunks`). Auto-registered by `agent/tools/__init__.py`. - Frontend: wired into the agent builder — operator + icon, config form (optional API key, search mode, site filter, top N), the search tool menu, and the existing api_key export sanitizer. ### Config - API key: optional. Blank = keyless free tier; set it to lift limits / enable `realtime` mode. - `site`: restrict to a single domain. - `mode`: `pro` (default) or `realtime`. ### Notes `KEENABLE_API_URL` can override the API base (HTTPS enforced; defaults to `https://api.keenable.ai`). The tool only sends the query (no URL fetch), so there's no SSRF surface. Verified the frontend with `vite build` and the backend search path against the public endpoint. |
||
|
|
a9eca9de82 | fix: guard against missing component IDs in Switch Flow path to prevent NoneType crash (#16279) | ||
|
|
3f805a64f1 |
feat(agent): align Go agent behavior with Python (except retrieval component) (#16225)
## Summary
Aligns the **Go agent runtime/canvas/components/tools** behavior with
the **Python `agent/` implementation** so the same stored canvas DSL
produces the same execution result on either side. Every component,
tool, and runtime primitive in `internal/agent/` is now driven by the
same semantics as its Python counterpart — variable resolution, template
substitution, control flow, error reporting, retry/cancel, and stream
event shapes.
The **retrieval component is the one explicit exception** in this PR. It
is being reworked in a separate change and is excluded from this
alignment pass; the wrapper slot (`universe_a_wrappers.go →
newRetrievalComponent`) is preserved.
## Scope of alignment
### Components (all aligned with `agent/component/`)
`Begin` · `Message` · `LLM` (incl. ChatTemplateKwargs,
MessageHistoryWindowSize, VisualFiles, Cite, OutputStructure,
JSONOutput, TopP, MaxRetries, DelayAfterError, credentials) · `Agent`
(react + tool artifact capture + `Reset()` interface-assert) · `Switch`
(12/12 operators, Python-equivalent semantics) · `Categorize` · `Invoke`
· `Iteration` · `Loop` (macro-expansion through `workflowx.AddLoopNode`)
· `UserFillUp` (Python-equivalent interrupt/resume via eino
`compose.Interrupt`/`ResumeWithData`) · `FillUp` · `DataOperations` ·
`ListOperations` · `StringTransform` · `VariableAggregator` ·
`VariableAssigner` · `Browser` (full stagehand runtime parity) ·
`DocsGenerator` · `ExcelProcessor`.
### Tools (all aligned with `agent/tools/`)
`Retrieval` (wrapper slot only — logic out of scope) · `MCPToolAdapter`
(streamable-HTTP) · `CodeExec` (sandbox bridge with
`code_exec_contract.go` matching Python contract) · `AkShare` · `ArXiv`
· `Crawler` · `DeepL` · `DuckDuckGo` · `Email` · `ExeSQL` · `GitHub` ·
`Google` · `GoogleScholar` · `Jin10` · `PubMed` · `QWeather` · `SearXNG`
· `Tavily` · `Tushare` · `Wencai` · `Wikipedia` · `YahooFinance` —
uniform `eino tool.InvokableTool` interface, SSRF protection, shared
HTTP client.
### Canvas execution engine (`internal/agent/canvas/`)
Aligned with Python's `agent/canvas.py`:
- **Scheduler** (`scheduler.go`): state pre/post handlers, node lambdas,
per-component timeout resolver (4-level: per-class env → per-class table
→ uniform env → 600s fallback), `legacyNoOpNames`.
- **Loop subgraph** (`loop_subgraph.go`): Python-equivalent
`AddLoopNode` macro expansion + condition translation.
- **Multibranch** (`multibranch.go`): `Switch` / `Categorize` routing
via `compose.NewGraphMultiBranch` — same branch selection semantics as
Python.
- **Parallel subgraph** (`parallel_subgraph.go`): matches Python's
parallel fan-out contract.
- **Interrupt/Resume** (`interrupt_resume.go`): `UserFillUpNodeBody` /
`IsInterruptError` / `ExtractInterruptContexts` — replaces the
deprecated Python sentinel chain with eino's native interrupt API,
preserving the same external behavior.
- **Checkpoint** (`checkpoint_store.go`): `RedisCheckPointStore`
Get/Set/Delete, with business metadata (status / canvas_id /
parent_run_id) on a parallel Redis Hash.
- **RunTracker** (`run_tracker.go`): Start / MarkSucceeded / MarkFailed
/ MarkCancelled / AttachCheckpoint — same lifecycle as the Python run
record.
- **Cancel** (`cancel.go`): Redis pub/sub watch.
- **Stream** (`stream.go`): SSE channel with `messages` / `waiting` /
`errors` / `done` events, same shape as Python's `agent.canvas.RunEvent`
payload.
### DSL bridge (`internal/agent/dsl/`)
- `normalize.go`: v1↔v2 collapsed into a single wire format — Python and
Go consume the same stored JSON.
- `reset.go`: per-run state reset matches Python's `Canvas.reset()`
semantics.
- Testdata mirrors Python's `agent_msg.json` / `all.json` / etc.
### Runtime (`internal/agent/runtime/`)
- `CanvasState` / `NewCanvasState` / `GetVar` / `SetVar` / `ReadVars`:
same `{{cpn_id@param}}` resolution model.
- `ResolveTemplate` (regex fast path + gonja fallback) — Python
Jinja-style semantics.
- `selector.go`, `metrics.go`, `component.go`: shared runtime contracts.
## Out of scope (intentionally)
- **`Retrieval` component logic** — wrapped only; full parity lands in a
follow-up PR.
- **Frontend** — only minor dsl-bridge / canvas UX fixes ride along.
- **CLI / admin / model registry** — orthogonal to agent behavior.
## How alignment is verified
`internal/service/agent_run_e2e_test.go` exercises the **full production
chain** against real Python-shaped DSL fixtures:
```
loadCanvasForUser → versionDAO.GetLatest → decodeCanvasFromDSL →
canvas.Compile → cc.Workflow.Invoke → answer extraction
```
using in-memory SQLite + miniredis (no Docker). Covers:
- `TestRunAgent_RealCanvas_BeginMessage` — happy path, `{{sys.query}}`
resolution
- `TestRunAgent_RealCanvas_WaitForUserResume` — two-run resume cycle
(Python-equivalent)
- `TestRunAgent_RealCanvas_CompileFails` — unknown component name →
sanitized error (Python-equivalent)
- `TestRunAgent_RealCanvas_InvokeFails` — unresolvable template ref
(Python-equivalent)
- `TestRunAgent_RunTracker_AttachCheckpoint_CallSequence` —
Start→AttachCheckpoint→MarkSucceeded lifecycle
`internal/handler/agent_test.go` — SSE streaming parity (`Content-Type:
text/event-stream`, `data: {…}\n\n`, trailing `data: [DONE]\n\n`,
OpenAI-compatible non-stream `choices`).
`internal/agent/canvas/fixture_compile_test.go` + per-component tests
pin the Python-equivalent outputs.
```
go test -count=1 -v -run 'TestRunAgent_RealCanvas|TestRunAgent_RunTracker' ./internal/service/
```
## Design reference
`docs/develop/agent-go-port-design.md` (1329 lines, last cross-checked
2026-06-17) — module layout, per-component / per-tool inventory,
corner-case catalogue, and the actionable backlog (Section 14, including
the retrieval alignment follow-up).
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
3fa15c0e2f |
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
|
||
|
|
906618fb30 |
Fix Agent chat Minimax content in thinking (#15937)
Fix Agent chat Minimax content in thinking |
||
|
|
c50f9c59aa |
fix: allow zero message history window and clear history for new sessions (#15897) (#15902)
### What problem does this PR solve? Two bugs in the Agent Categorize component: 1. The backend rejected `message_history_window_size = 0` while frontend allowed it, causing API errors. 2. When calling the agent API without a `session_id`, a new session was created but retained history from previous conversations. ### Type of change - [x] Bug Fix ### How has this been tested? - Issue 1: `CategorizeParam().check()` now accepts `0` and rejects negative values. - Issue 2: `canvas.clear_history()` is called for new sessions (no `session_id`), ensuring fresh conversation state. Verified via UI and API that a second call without `session_id` does not remember the first conversation. ### Related Issue Closes #15897 Co-authored-by: RAGFlow Dev <dev@ragflow.local> Co-authored-by: Wang Qi <wangq8@outlook.com> |
||
|
|
2980981da2 |
fix: route visual agent calls to image model (#15906)
### What problem does this PR solve? Ensure agent components with image inputs route to `image2text` models instead of staying on the chat path, so visual requests use the CV wrapper when supported. ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) |
||
|
|
9aa81e7cad |
Fix paddle ocr / minerU cannot add (#15858)
Fix paddle ocr / minerU cannot add |
||
|
|
17f27b9df2 |
fix(browser): show resolved variables in workflow run log input (#15325)
### What problem does this PR solve? Browser parsed sys.query from prompts but never called set_input_value, so node_finished inputs displayed null in the agent orchestration run log. Additionally, Browser’s tenant-model path could trigger unsupported structured-output modes (response_format/tool_choice) for some OpenAI-compatible providers (notably DeepSeek thinking models), causing step failures. ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
6cba5a544a |
fix(agent): skip empty switch conditions (#15691)
## What - make `Switch` ignore conditions that have no evaluable items - add a regression for blank `cpn_id` items falling through to the else branch - keep the existing non-empty `and` condition behavior covered Fixes #15643. ## Verified - `python -m py_compile agent\component\switch.py test\unit_test\agent\component\test_switch.py` - `python -m pytest test\unit_test\agent\component\test_switch.py -q` -> `2 passed` - `python -m ruff check agent\component\switch.py test\unit_test\agent\component\test_switch.py` - `git diff --check` I also checked `python -m ruff format --check` on the touched files. It would reformat pre-existing style in `agent/component/switch.py` beyond this bug fix, so I kept the patch scoped instead of reformatting the whole file. |
||
|
|
c0e00a7f6e |
Fix: agent template smart_customer_service_specialist.json (#15565)
### What problem does this PR solve? agent template smart_customer_service_specialist.json ### Type of change - [x] Refactoring |
||
|
|
36357a6afd |
Fix: model provider (#15517)
### What problem does this PR solve? Fix: - Handle siliconflow and siliconflow_intl api_key ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) |
||
|
|
dc4b82523b |
Feat: tenant llm provider (#14595)
### What problem does this PR solve? Python implementation of the Go-based model_provider API suite. ### Type of change - [x] New Feature (non-breaking change which adds functionality) --------- Co-authored-by: bill <yibie_jingnian@163.com> |
||
|
|
43cbfd447a |
Fix: ExeSQL node continues on per-statement SQL errors (#15140)
Wrap per-statement execution in both the generic and IBM DB2 loops so a failing statement reports a friendly "SQL Execution Failed" message and continues, instead of letting a raw driver exception abort the node and discard results from statements that already succeeded. Rolls back after a failure so PostgreSQL's aborted-transaction state does not cascade into every subsequent statement in the batch. ### What problem does this PR solve? Closes #14737 The **ExeSQL** agent node splits its input on `;` and runs each statement in a loop. Both execution loops — the generic one (`cursor.execute`) and the IBM DB2 one (`ibm_db.exec_immediate`) — were wrapped only in a `try/finally` for resource cleanup, with **no `except`** around statement execution. As a result, when any single statement failed (e.g. the reporter's MSSQL `('42S02', "[42S02] ... 对象名 'ASSET_AUDIT' 无效")`): - The raw, unformatted driver exception bubbled up and the node failed with an ugly `_ERROR` instead of friendly information. - **The whole node aborted** — results from statements that had already succeeded were discarded, and the remaining statements in the batch never ran. The reporter confirmed this was the real pain point: *"after reporting an exception, the previous normal query cannot be executed properly … Do not interrupt the workflow for any issues."* Connection-level failures were already wrapped with a friendly `"Database Connection Failed!"` prefix — only per-statement execution errors were missed. **This PR** wraps per-statement execution in `try/except` in both loops. A failing statement now: - records a friendly `SQL Execution Failed: <sql>\n<error>` entry into the `json` and `formalized_content` outputs (the actual DB error is kept so the user can see *what* failed), and - `continue`s to the next statement — so earlier results survive and later statements still run. After a failure in the generic loop, the connection is rolled back so PostgreSQL's aborted-transaction state does not cascade into every subsequent statement in the batch. The node returns normally (no `_ERROR` raised), so the agent workflow proceeds instead of halting. Connection failures remain fatal (correct — nothing can run without a connection). The pre-existing `break` on `cursor.rowcount == 0` is intentionally left unchanged; it is out of scope for this fix. ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) |
||
|
|
e7d45dd645 |
Feat: Expose Doc Generator file metadata as discrete outputs (#15080)
Declare doc_id, filename, mime_type, and size as separate outputs on the Document Generation component so downstream nodes (e.g., the Code component) can consume them via the variable picker. The existing download JSON blob is preserved unchanged for the Message component's download-chip rendering. ### What problem does this PR solve? The Document Generation component previously exposed only a single `download` output — a JSON-encoded blob containing the file's `doc_id`, `filename`, `mime_type`, `size`, and base64 payload. On top of that, the variable picker actively hides this `download` entry from every consumer except the Message component (because the embedded base64 is too heavy to splat into arbitrary downstream nodes). The combined effect: users wiring the Doc Generator's output into a Code component had no way to retrieve basic file info such as `file_name` or `doc_id` from the picker, blocking workflows that need to post-process the generated file (e.g., registering it elsewhere, custom delivery, follow-up API calls). This PR declares `doc_id`, `filename`, `mime_type`, and `size` as **discrete outputs** on the Document Generation component, alongside the existing `download` blob. The new fields: - Appear in the variable picker for **all** downstream nodes, including the Code component, so users can bind them directly to script arguments. - Are cheap scalars only — no base64 payload leaks into other components. - Leave the existing `download` JSON blob completely untouched, so the Message component's download-chip rendering (which parses that blob via `_is_download_info`) keeps working with no behavior change. Changes: - `agent/component/docs_generator.py` — declare the four new outputs in `DocGeneratorParam` and emit them via `set_output(...)` in `_invoke`. - `web/src/pages/agent/constant/index.tsx` — extend `initialDocGeneratorValues.outputs` with the new keys. - `web/src/pages/agent/form/doc-generator-form/index.tsx` — mirror the new outputs in the zod schema so the form is valid. No changes needed to the picker's existing `download`-hiding filter — it matches only on the literal output name `download`, so the new metadata entries fall through naturally. Reported in: https://github.com/infiniflow/ragflow/issues/14461. ### Type of change - [x] New Feature (non-breaking change which adds functionality) |
||
|
|
8f90740d2e |
feat: pass chat_template_kwargs through agent chat completion (#14542)
### What problem does this PR solve? The agent API currently does not pass chat_template_kwargs to the underlying LLM call path, so clients cannot control template-level model behavior (such as thinking-mode toggles) when invoking /agents/chat/completion. This PR adds passthrough support for chat_template_kwargs across agent execution flows (session and non-session, streaming and non-streaming) by propagating it through canvas runtime state and into LLM invocation kwargs. This addresses the feature gap raised in [Issue #14182](https://github.com/infiniflow/ragflow/issues/14182). Closes #14182 ### Type of change - [x] New Feature (non-breaking change which adds functionality) |
||
|
|
3e5b11a523 |
Feat(browser control):Add new agent component 'browser' to control browser by AI (#14888)
### What problem does this PR solve? This PR adds a new `Browser` operator to Agent workflows, enabling prompt-driven browser automation in RAGFlow.Technically based ‘Browser-Use’ It includes: - Backend browser component execution with tenant LLM integration - Upload source support (file IDs, URLs, variables, CSV/JSON array) - Downloaded file persistence to RAGFlow storage - Frontend node/operator integration, form config, icon, and i18n updates - Unit tests for upload/download and ID parsing logic - Dependency and Docker updates for browser-use runtime support ### Type of change - [x] New Feature (non-breaking change which adds functionality) |
||
|
|
b28e134944 |
Feat: add local & ssh provider in admin panel (#15039)
### What problem does this PR solve? Feat: add local & ssh provider in admin panel ### Type of change - [x] New Feature (non-breaking change which adds functionality) |
||
|
|
7edabdf7c3 |
fix(retrieval): keep manual metadata filter reusable inside Iteration (#14849)
## What problem does this PR solve? Closes #12582. When a Retrieval component sits inside an Iteration with a **manual** metadata filter that references the iteration variable (e.g. `{IterationItem:abc@item}`), every iteration reuses the value resolved on the **first** pass. Root cause: [`_resolve_manual_filter` in `agent/tools/retrieval.py`](https://github.com/infiniflow/ragflow/blob/main/agent/tools/retrieval.py#L144-L171) mutated `flt["value"]` in place. The `filters` list passed in is the live `self._param.meta_data_filter["manual"]` (see [`apply_meta_data_filter` in `common/metadata_utils.py:257-261`](https://github.com/infiniflow/ragflow/blob/main/common/metadata_utils.py#L257-L261)), so after the first iteration the param dict permanently held the resolved string instead of the original variable reference. ```text iter #1: flt["value"] = "{IterationItem:abc@item}" → resolved to "AI" after mutation: flt["value"] = "AI" ← written back into _param iter #2: flt["value"] = "AI" ← no {…} matches retrieval keeps filtering by "AI" forever ``` This PR returns a shallow copy with the resolved value instead, leaving the original filter (and its variable reference) intact for the next iteration. ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## Test plan - [ ] Build an agent: `Agent (structured output → list of areas) → Iteration → Retrieval (manual filter: Area = {IterationItem/Item}) → Message`. Run with a multi-area query and confirm each iteration's Retrieval result matches its own item, not the first item. - [ ] Regression: Retrieval with a manual metadata filter outside an Iteration still resolves the variable correctly on each request. - [ ] Regression: Retrieval with no metadata filter and with `auto` / `semi_auto` filters behave unchanged. |
||
|
|
f169ab4b39 |
feat(tts): cache synthesized speech in Redis to avoid redundant calls (#14851)
## What problem does this PR solve? Closes #12017. TTS output is deterministic for a given `(model, text)` pair, so re-running the same text through the same TTS model produces the same bytes — yet `Canvas.tts` and `dialog_service.tts` re-synthesized on every request. That's slow and wastes provider quota whenever the same assistant response is replayed, shared across users, or repeated within a session. ### Change New helper `rag/utils/tts_cache.py` with `synthesize_with_cache(tts_mdl, cleaned_text)`: - **Key:** `tts:cache:{model_id}:{sha256(text)}` — separate namespace per model, identical cleaned text reuses a single entry across both call sites. - **Value:** the hex-encoded audio blob both call sites already returned. No format change for downstream consumers. - **TTL:** 7 days by default, configurable via `RAGFLOW_TTS_CACHE_TTL_SECONDS`. - **Failure modes:** a Redis hiccup falls back to direct synthesis; a failed synthesis still returns `None` (existing contract preserved). [`Canvas.tts`](https://github.com/infiniflow/ragflow/blob/main/agent/canvas.py#L683-L724) and [`dialog_service.tts`](https://github.com/infiniflow/ragflow/blob/main/api/db/services/dialog_service.py#L1367-L1380) now route through the helper; the per-file bytes-accumulation/hex-encode loop has been removed in favor of one shared implementation. ## Type of change - [x] New Feature (non-breaking change which adds functionality) ## Test plan - [ ] **Cache hit, chat path:** Configure a dialog with TTS enabled, ask the same question twice with `stream=false`. Verify the second response returns the same `audio_binary` and that the second invocation doesn't hit the TTS provider (e.g., observe provider-side logs / usage counters; check no `LLMBundle.tts can't update token usage` log line on the second run). - [ ] **Cache hit, agent path:** Same exercise via a Conversational Agent that includes a Message component playing back the answer. - [ ] **Cache isolation per model:** Switch tenant's `tts_id` between two models, run the same text against each — confirm the second model's first synthesis still happens (no cross-model hits). - [ ] **TTL override:** Set `RAGFLOW_TTS_CACHE_TTL_SECONDS=120`, confirm the entry expires after 2 minutes. - [ ] **Redis unavailable:** Stop Redis (or break the connection). Verify the TTS endpoint still works — synthesis falls back to direct calls, with a `TTS cache lookup failed` / `TTS cache store failed` warning logged. - [ ] **Failure path:** Configure a TTS model with an invalid API key, ensure the response still returns successfully with `audio_binary=None` (no regression vs. current behavior). |
||
|
|
ff318aba7a |
fix: correct literal_eval dispatch and bool isinstance ordering in agent components (#13988)
## Summary This PR fixes 3 bugs in agent components: ### Bug 1: `DataOperations._invoke()` dispatches `"literal_eval"` to wrong handler **File:** `agent/component/data_operations.py`, line 76 The `_invoke()` method compares `self._param.operations` against `"recursive_eval"` (line 76), but the valid value defined in `DataOperationsParam.__init__()` (line 29) and validated in `check()` (line 43) is `"literal_eval"`. This means selecting the `literal_eval` operation from the frontend would never match, and the method `_literal_eval()` would never be called. **Fix:** Change `"recursive_eval"` to `"literal_eval"` in the dispatch condition. ### Bug 2: `VariableAssigner._clear()` — `bool` branch unreachable **File:** `agent/component/variable_assigner.py`, lines 95–100 In Python, `bool` is a subclass of `int` (`True` is `isinstance(True, int) == True`). The `isinstance(variable, int)` check on line 95 catches boolean values before the `isinstance(variable, bool)` check on line 99, making the bool branch unreachable. A boolean variable would be cleared to `0` instead of `False`. **Fix:** Move the `isinstance(variable, bool)` check before `isinstance(variable, int)`. ### Bug 3: `LoopItem.evaluate_condition()` — `bool` branch unreachable **File:** `agent/component/loopitem.py`, lines 67–93 Same issue as Bug 2: `isinstance(var, (int, float))` on line 67 catches boolean values before `isinstance(var, bool)` on line 85. Boolean variables would be evaluated with numeric operators (`=`, `≠`, `>`, etc.) instead of boolean operators (`is`, `is not`). **Fix:** Move the `isinstance(var, bool)` check before `isinstance(var, (int, float))`. ## Test plan - [ ] Verify `DataOperations` with `literal_eval` operation correctly invokes `_literal_eval()` - [ ] Verify `VariableAssigner._clear()` returns `False` for boolean variables (not `0`) - [ ] Verify `LoopItem.evaluate_condition()` uses boolean operators for `True`/`False` values 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Fixed operation routing logic to correctly dispatch the "literal_eval" operation to its handler. * **Refactor** * Reorganized conditional branch ordering in agent components to improve code structure and maintainability without affecting functional behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |