Compare commits

...

1035 Commits

Author SHA1 Message Date
balibabu
999eb533a9 Feat: Adjust the color and size of the graph based on the data. (#16868) 2026-07-14 10:20:31 +08:00
Zhichang Yu
d279aee1ff Go ports of workflow and chunker fixes (#16878)
Ports two Python fixes to Go: the variable_ref_patt underscore/colon fix
(#16792) and the TokenChunker upstream-chunks fix (#16825). Keeps Go
behavior aligned with upstream Python.
2026-07-13 23:32:07 +08:00
Yash Raj Pandey
e54e7ec7ef Fix: TokenChunker discards TitleChunker chunks when output_format is 'chunks' (#16825)
Fixes #16812

### Problem
In the `rag/flow` ingestion pipeline, when `TitleChunker` feeds
`TokenChunker`, the chapter-aware chunks are silently discarded and the
parser's raw flat json is re-chunked instead.

`TitleChunker` emits `output_format="chunks"` and writes its
chapter-aware output to the `chunks` field
(`rag/flow/chunker/title_chunker/common.py`,
`set_output("output_format", "chunks")`). But `TokenChunker._invoke`
only handles `output_format` in `["markdown", "text", "html"]`, then
falls through to the `# json` path which reads
`from_upstream.json_result`. There is no branch for `"chunks"`, so
`from_upstream.chunks` is never read.

Downstream effects reported in #16812: PageIndex/TOC extraction receives
flat line-level text instead of structured chapter blocks
(incorrect/duplicate/missing chapters), and retrieval quality degrades
because chunks are no longer aligned to document structure.

### Fix
Select the source list based on `output_format`, mirroring the exact
pattern already used in `title_chunker/common.py`:

```python
json_result = (from_upstream.chunks if from_upstream.output_format == "chunks" else from_upstream.json_result) or []
```

`chunks` items share the same dict shape as `json_result` items (both
consumed via `.get("text")`, `.get("doc_type_kwd")`, etc.), so they flow
through the existing token-sizing path unchanged. One-line change, no
behavior change for the `json`/`markdown`/`text`/`html` paths.

### Test
Adds `rag/flow/tests/test_token_chunker.py`, an isolated unit test that
runs the real `TokenChunker._invoke` (heavy deps stubbed; real pydantic
schema used when available) and asserts that with
`output_format="chunks"` the upstream `chunks` are consumed rather than
the raw parser `json`.

Verified RED -> GREEN: the test fails against the current code (reads
the raw json) and passes with the fix.

Signed-off-by: Yash Raj Pandey <yashpn62@gmail.com>
2026-07-13 22:04:56 +08:00
Taranum Wasu
d48fd37ff1 fix(agent): allow underscores in variable_ref_patt component_id (#16758) (#16792)
## Summary

`ComponentBase.variable_ref_patt` (and its duplicate in
`agent.canvas.Graph.get_value_with_variable`) is the regex the canvas
runtime uses to find `cpn_id@var_nm` template refs in component prompts.

The `cpn_id` half was constrained to `[a-zA-Z:0-9]+`, which silently
dropped underscores. Component ids emitted by the frontend all contain
underscores (`userfillup_abc`, `retrieval_xyz`, `llm_0`, `message_0`,
…), so any template ref like `{userfillup_abc@line}` failed to match.
The placeholder then leaked through to the LLM verbatim, and the Agent
answered only its system-prompt directive.

This is exactly the "unconsidered await response" symptom in #16758:

```
Begin(Task) -> Await response -> Agent -> Message
```

Widen `cpn_id` from `[a-zA-Z:0-9]+` to `[a-zA-Z0-9_]+`. Bare `{line}`
(no cpn_id) remains unrecognised so it stays literal until the user
wires it up — matching the existing `VARIABLE_REF_PATTERN` shape used by
`agent.dsl_migration` for the same purpose.

## Changes

- `agent/component/base.py` — fix `variable_ref_patt` class attribute.
- `agent/canvas.py` — same fix applied to the inline regex inside
`Graph.get_value_with_variable` (kept as the literal regex to avoid
coupling the two unrelated sites).
-
`test/testcases/test_web_api/test_canvas_app/test_variable_ref_pattern_unit.py`
— new regression test pinning both the regex shape and end-to-end
resolution.

## Regression coverage

```
test_variable_ref_patt_matches_underscored_component_ids     PASSED
test_variable_ref_patt_still_matches_legacy_ids              PASSED
test_get_input_elements_from_text_resolves_underscored_id    PASSED
test_string_format_substitutes_underscored_ref                PASSED
test_variable_ref_patt_does_not_match_bare_var_name          PASSED
```

All five regression tests fail against the pre-fix regex (verified via
`git stash` round trip — drop fix, tests fail, restore fix, tests pass).

The two targeted existing tests in the same directory
(`test_fillup_unit.py`, `test_iterationitem_unit.py`) continue to pass.

## Repro before the fix

```python
import re
patt = r"\{* *\{([a-zA-Z:0-9]+@[A-Za-z0-9_.-]+|sys\.[A-Za-z0-9_.]+|env\.[A-Za-z0-9_.]+)\} *\}*"
list(re.finditer(patt, "{userfillup_abc@line}"))
# => []   # <-- bug
```

## Repro after the fix

```python
import re
patt = r"\{* *\{([a-zA-Z0-9_]+@[A-Za-z0-9_.-]+|sys\.[A-Za-z0-9_.]+|env\.[A-Za-z0-9_.]+)\} *\}*"
list(re.finditer(patt, "{userfillup_abc@line}"))
# => [<re.Match object; span=(0, 24), match='{userfillup_abc@line}'>]
```

Fixes #16758

## Test plan

- [x] New unit tests pass
- [x] Reverse-apply the fix and confirm the regression tests fail (they
do)
- [x] `test_fillup_unit.py` (existing sibling suite) still passes
- [x] `test_iterationitem_unit.py` (existing sibling suite) still passes
- [ ] Project CI green

---------

Co-authored-by: Taranum01 <taranum01@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 22:03:48 +08:00
Jin Hai
844e3ea64d Go and Python: fix IDOR issue of search completions (#16864)
### Summary

In Go and python implementation, the dataset / KB id isn't validated if
it is accessible by this user.

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-13 21:00:19 +08:00
balibabu
45862bf95d Feat: If the interval between two outputs exceeds 600ms, a loading state is displayed at the end. (#16861)
### Summary

Feat: If the interval between two outputs exceeds 600ms, a loading state
is displayed at the end.
2026-07-13 18:02:29 +08:00
Jin Hai
8bc18154d2 Go: refactor and add version type (#16863)
### Summary

```
RAGFlow(admin)> show version;
+--------------+-----------------------+
| field        | value                 |
+--------------+-----------------------+
| version      | v0.26.4-84-g547bc8614 |
| version_type | open source           |
+--------------+-----------------------+
```

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-13 18:00:35 +08:00
buua436
09abe5f513 fix: ensure database model indexes (#16860) 2026-07-13 17:34:28 +08:00
Haruko386
466f33e6b4 fix: support component brwoser (#16848)
### Summary

As title
Unable to test it since I don't have apiKey for `openai` , `Anthropic`
and `Gemini`

---

<img width="1130" height="557" alt="image"
src="https://github.com/user-attachments/assets/11570c75-68f3-490d-8186-4ecbcd8b8f40"
/>
2026-07-13 16:57:56 +08:00
Hz_
d03b09b6a3 fix(go-agent): Align Go WenCai and SearXNG agent components with python (#16854)
## Summary

- Align Go WenCai and SearXNG behavior, schemas, and node parameters
with Python.
- Add the `WenCai` and `SearXNG` Canvas components and register their
tool factories.
- Match Python's current WenCai behavior by returning an empty report
while its upstream request is disabled.
- Add SearXNG request validation, SSRF-safe DNS pinning, raw result
preservation, and reference rendering.
- Support context cancellation, error envelopes, and lock-safe retrieval
references.

  ## Tests

  Passed:

  - `bash build.sh --test ./internal/agent/tool/...`
  - `bash build.sh --test ./internal/agent/component/...`
  - `bash build.sh --test ./internal/agent/runtime/...`
  - `bash build.sh --test ./internal/agent/...`
  - `cd web && npm run type-check`
  
  
<img width="1900" height="1102" alt="image"
src="https://github.com/user-attachments/assets/ec77d217-d9fd-455a-96ec-9aabf6841109"
/>
  
<img width="1900" height="1102" alt="image"
src="https://github.com/user-attachments/assets/52ac129f-cb65-453d-ae48-cc518803ac23"
/>
2026-07-13 16:50:17 +08:00
euvre
3bfad1f00e fix: correct model type mappings and improve system setting persistence (#16501) 2026-07-13 16:42:55 +08:00
Sbaaoui Idriss
d35e957252 fix: test drift on go specific proxy scheme (#16796)
### Summary

certain tests fail because of test drift and were fixed, other because
of go issues

---------

Co-authored-by: Wang Qi <wangq8@outlook.com>
2026-07-13 16:41:38 +08:00
Jack
cfacaccad7 Refactor: message processing (#16852)
### Summary

1. refactor message processing
2. delete un-used componentIndexMap
3. unfold (delete) internal/ingestion/task/task_handler.go
2026-07-13 16:32:34 +08:00
boskodev790
80a7a87427 fix(agent): port QWeather to ToolBase so it works as an Agent tool (#16692)
### Summary

Port the **QWeather** agent tool to the modern `ToolBase` / `_invoke`
interface. It was still written against the removed legacy
`ComponentBase` / `_run` / `be_output` API, so it was non-functional as
an Agent tool — adding it to an Agent raised `AttributeError` because it
had no `get_meta()`. This is the same defect that was fixed for the
AkShare tool in #16417.

**Changes**
- `QWeatherParam` now extends `ToolParamBase` with a `meta` exposing a
`query` (location) parameter, and adds `get_input_form()`. Existing
config (`web_apikey`, `lang`, `type`, `user_type`, `time_period`) is
preserved.
- `QWeather` now extends `ToolBase` and implements `_invoke(**kwargs)`
with the standard retry loop, cancellation checks,
`set_output("formalized_content", ...)`, and `thoughts()`. The weather /
indices / air-quality branches and the API error-code messages are kept.
- Added `test/unit_test/agent/component/test_qweather.py` covering the
restored `meta`, param validation, the weather-now and multi-day and
indices branches, the empty-query short-circuit, and the location-lookup
error message.

**Testing**
- `ruff check agent/tools/qweather.py
test/unit_test/agent/component/test_qweather.py` — clean
- `ruff format --check` — clean
- `pytest test/unit_test/agent/component/test_qweather.py`
2026-07-13 16:31:19 +08:00
Hz_
2a482f3ae7 fix(go-agent): align GitHub and Invoke Canvas components (#16849)
## Summary

- Add the GitHub Canvas component with tool registration and reference
propagation.
- Align the Invoke component with the Python contract for node config,
input form, response output, and timing fields.
- GitHub search and HTTP Invoke now work correctly in the Go Canvas
runtime.

## Tests

- `bash build.sh --test ./internal/agent/tool/...`
- `bash build.sh --test ./internal/agent/component/...`

Note: the untracked go_ragflow_cli file is not part of the PR changes.

<img width="1813" height="1102" alt="image"
src="https://github.com/user-attachments/assets/f69cef32-59a0-4287-a06b-6843d85198cf"
/>


<img width="1813" height="1102" alt="image"
src="https://github.com/user-attachments/assets/b37dfc31-bc9b-4937-a38e-d2184bb157fe"
/>
2026-07-13 15:48:07 +08:00
chanx
547bc86141 Fixed an issue where cited webpages could not be opened during online searches. (#16840) 2026-07-13 15:25:57 +08:00
Jin Hai
9f403ac3ca Go: more APIs (#16850)
### Summary

as title.

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-13 15:24:17 +08:00
chanx
f21368057b fix(setting-model): fix instance display and api_key loss after auto-save (#16853) 2026-07-13 15:20:01 +08:00
balibabu
2b9569ff51 Feat: Added support for session graph and session essence templates. (#16851)
### Summary

Feat: Added support for session graph and session essence templates.
2026-07-13 14:48:02 +08:00
qinling0210
d549194562 Implement builtin chunk method as ingestion pipeline in GO (#16822)
### Summary

Implement builtin chunk mehtod as ingestion pipeline in GO
2026-07-13 13:51:40 +08:00
Zhichang Yu
ed27b5f9f8 uv tool install lefthook (#16842) 2026-07-13 13:01:33 +08:00
Lynn
b0cac0ac9d Fix: Align part of Go provider APIs with Python APIs (#16823) 2026-07-13 11:20:02 +08:00
Jack
3c0f8fc192 Refactor: refactor dataflow_service.go and guard nats message re-delivery (#16826)
### Summary

1. refactor dataflow_service.go 
2. guard nats message re-delivery
3. support document parse cancelling & re-run
2026-07-13 11:08:04 +08:00
Hz_
347a45967e fix(go-agent): ArXiv component registration and request parity (#16808)
## Summary
- register the Go `ArXiv` canvas component and add its input form
- align the Go ArXiv request/schema with Python by keeping only `query`
in runtime args and moving `top_n`/`sort_by` to node params
- keep ArXiv results consistent for canvas output and tool response
handling

## Test
- `bash build.sh --test ./internal/agent/tool
./internal/agent/component`

<img width="1817" height="972" alt="image"
src="https://github.com/user-attachments/assets/7f726dfa-a996-4561-b481-cb0b44bec81c"
/>
2026-07-13 10:19:50 +08:00
dependabot[bot]
a7ef7be056 build(deps): bump github.com/xuri/excelize/v2 from 2.10.1 to 2.11.0 (#16839) 2026-07-13 09:43:19 +08:00
Jin Hai
891261e108 Go: add dummy admin functions (#16830)
As title.

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-11 23:38:22 +08:00
Jin Hai
bdef878821 Go: fix nats and go command (#16828)
### Summary
1. update docker compose file to start NATS healthy
2. Add two commands
```
RAGFlow(admin)> live;
SUCCESS
RAGFlow(admin)> health;
+---------------+-------+
| field         | value |
+---------------+-------+
| storage       | ok    |
| message_queue | ok    |
| status        | ok    |
| db            | ok    |
| redis         | ok    |
| doc_engine    | ok    |
+---------------+-------+
```

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-11 19:37:57 +08:00
Hz_
156a11c56b fix(go-agent): Added PubMed component support (#16817)
## Summary

- Merge upstream main and retain PubMed component support.
- Preserve newly registered tool components and update registry
verification.

## Tests

- `bash build.sh --test ./internal/agent/component/...`
- `bash build.sh --test ./internal/agent/tool/...`

<img width="1817" height="972" alt="image"
src="https://github.com/user-attachments/assets/9fcb9448-9e26-41b9-940c-a9bfde9835e9"
/>

---------

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-07-11 17:49:59 +08:00
Harsh Kashyap
8a3699fa87 fix(agent): clear component inputs on canvas re-run (#16790)
### What problem does this PR solve?

Issue [#16758](https://github.com/infiniflow/ragflow/issues/16758) —
clicking a chunk whose data references a single-line variable from an
Await-Response (UserFillUp) component, the Agent's `user_prompt` is
being resolved against the **previous** canvas run's captured value
instead of the current run's value. The system-prompt path works only
because the system prompt is computed upstream and re-reads the value on
the new run.

### Root cause

`Canvas._run_impl` reset every path component with `only_output=True`,
so `_param.inputs` was never cleared between runs.
`ComponentBase.get_input()` calls `set_input_value(var, resolved)` at
line 482, which writes the resolved variable into
`self._param.inputs[var]["value"]`. On the next canvas run, that input
was never cleared, so the previous run's resolved value stuck around.
The Agent's `kwargs.get("user_prompt")` then read the stale string and
forwarded it to the LLM, which produced the "Understood. Please provide
the text..." fallback because the prompt looked empty.

### What changed?

- `agent/canvas.py` — differentiate `begin` (still `only_output=True`,
since it has no inputs and the webhook payload branch below populates
`request` explicitly) from non-begin path components (reset with
`only_output=False`, which clears both `inputs` and `outputs`).
- `test/unit_test/agent/test_canvas_input_reset.py` — new pytest module.
Pinned the contract: non-begin path components receive
`only_output=False`. The fix is small enough to verify with a stub
canvas rather than a full canvas-runtime test (the existing agent
conftest hits an unrelated `scholarly` import on Python 3.13, so a real
canvas import would require fixing that first).

### Backward compatibility

- `Begin` behaviour unchanged.
- All non-begin path components: previously persisted inputs across runs
(the bug); now reset between runs. Components that were relying on stale
inputs (none found in the existing test suite) would lose that as a side
effect, but that is the entire point of the fix.
- No API surface change. No backend change.

### Testing

```
$ uv run pytest test/unit_test/agent/test_canvas_input_reset.py -v
collected 4 items
test/unit_test/agent/test_canvas_input_reset.py::test_begin_is_reset_with_only_output_true PASSED
test/unit_test/agent/test_canvas_input_reset.py::test_non_begin_path_components_are_reset_with_only_output_false PASSED
test/unit_test/agent/test_canvas_input_reset.py::test_only_path_components_are_reset PASSED
test/unit_test/agent/test_canvas_input_reset.py::test_inputs_reset_flag_is_passed_to_non_begin_components PASSED
4 passed in 0.14s
```

`python3 -m py_compile agent/canvas.py` clean. Existing agent test files
(`test_switch.py`, `test_llm_prompt.py`) hit a pre-existing `scholarly`
import error on Python 3.13 (unrelated to this PR), so I couldn't run
the full agent suite. Recommend fixing the `scholarly` import
separately.

### Files changed

- `agent/canvas.py` (+9 / −1)
- `test/unit_test/agent/test_canvas_input_reset.py` (new, +104)

Fixes #16758

---------

Co-authored-by: Harsh Kashyap <harshkashyap@Harshs-MacBook-Pro.local>
Co-authored-by: Zhichang Yu <yuzhichang@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-11 16:33:37 +08:00
Taranum Wasu
0ee02fb6d8 [Fix] Rename StandardizeImag -> StandardizeImage to fix deepdoc OCR preprocessing (#7316) (#16785)
Fixes #7316.

## Problem

`deepdoc/vision/operators.py` defines the image-standardize
preprocessing op as `class StandardizeImag` (missing the final `e`), but
every caller — including
`deepdoc/vision/recognizer.py::Recognizer.preprocess` — looks the class
up by the canonical string `"StandardizeImage"` via:

```python
op_type = new_op_info.pop("type")  # "StandardizeImage"
preprocess_ops.append(getattr(operators, op_type)(**new_op_info))
```

So `getattr(operators, "StandardizeImage")` raised `AttributeError`, and
the "StandardizeImage" preprocessing step silently never ran for any
image pipeline that used the dynamic dispatch (LayoutLMv3 and friends).
The user-visible symptom is that the standardize step is missing
entirely from the preprocessing chain, so the model gets un-normalized
images.

## Production fix

```diff
-class StandardizeImag:
+class StandardizeImage:
     """normalize image
     Args:
         mean (list): im - mean
         std (list): im / std
         is_scale (bool): whether need im / 255
         norm_type (str): type in ['mean_std', 'none']
     """
```

That's the entire production change — a one-character class rename. The
misnamed `StandardizeImag` had no other references in the codebase
(verified via `git grep`), so removing it is safe; every caller uses the
canonical `"StandardizeImage"` string and will now resolve correctly.

## Tests

New `test/unit_test/deepdoc/vision/test_operators_standardize_image.py`
with six regression tests, all green locally:

```
test_standardize_image_class_resolves_by_canonical_name            PASSED
test_standardize_image_callable_matches_legacy_alias_name          PASSED
test_standardize_image_normalizes_input_with_mean_std_and_is_scale PASSED
test_standardize_image_skips_scaling_when_is_scale_false           PASSED
test_standardize_image_norm_type_none_passes_image_through         PASSED
test_standardize_image_via_module_getattr_dispatch_path            PASSED
6 passed in 0.18s
```

The tests:
1. **Pin the dispatch contract** (`hasattr(operators,
"StandardizeImage")`) — this is the exact check the recognizer's
`getattr` would do, so any future regression fails the same way the
runtime would.
2. **Pin that the misspelled name is gone** — if a downstream caller
ever relied on it, this fails loudly.
3–5. **Behavioural coverage** of the three documented code paths:
`is_scale=True, norm_type="mean_std"`, `is_scale=False,
norm_type="mean_std"`, and `norm_type="none"`.
6. **End-to-end via the same `getattr(operators, "StandardizeImage")`
call** the recognizer uses, with a real numpy image, so any rename or
removal surfaces as `AttributeError` instead of silently skipping the
step.

Verified both ways:
- Without the fix → **all 6 tests fail** (Python even suggests
`'StandardizeImag' → 'StandardizeImage'`)
- With the fix → all 6 pass in 0.15s

The test file follows the project's existing pattern
(`test/unit_test/deepdoc/parser/test_html_parser.py`): load the target
module via `importlib.util.spec_from_file_location`, stub the only
project-internal import (`rag.utils.lazy_image`), and assert against the
loaded module — no full RAGFlow runtime required.

## Risk

Very low. The class is renamed; no public Python API was using the
misnamed class. The only reference path is the `"StandardizeImage"`
string in `recognizer.py:270`, which now resolves correctly.

## Out of scope

- No other ops in `operators.py` are affected; checked all the others
(DecodeImage, NormalizeImage, Permute, etc.) and they all use correct
names.
- The dynamic-dispatch lookups in `recognizer.py` for `LinearResize`,
`StandardizeImage`, `Permute`, `PadStride` all use the same dispatch
path; only the `StandardizeImage` key was broken. No other keys need
fixing.

Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Taranum01 <Taranum01@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Zhichang Yu <yuzhichang@gmail.com>
2026-07-11 16:32:03 +08:00
Jin Hai
e6e99b86a6 PY: fix admin error (#16827)
### Summary

Sync code from EE

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-11 14:57:26 +08:00
Öndery
4060cd1440 fix(agent): Await Response pauses on every Loop iteration (#16794)
## What

An **Await Response** (`UserFillUp`) node placed inside a **Loop** now
pauses and waits for a fresh user response on **every** iteration,
instead of only on the first one.

## Problem

When a `UserFillUp` node lives inside a `Loop`, it only paused for input
on the first iteration. On subsequent iterations the loop ran straight
through, silently reusing the answer the user gave the first time.

Root cause is in `UserFillUp._invoke` / the canvas wait-check
(`agent/canvas.py`). The wait-check decides whether to pause by calling
`Canvas._is_input_field_satisfied` on the node's form fields — a field
counts as satisfied as soon as its `value` is not `None`:

```python
@staticmethod
def _is_input_field_satisfied(field):
    ...
    if value is None:
        return False
    return True
```

The same component object is reused across loop iterations, and
`UserFillUp._invoke` writes the answer into
`self._param.inputs[...]["value"]` via `set_input_value`. Nothing
cleared those values when the node was re-entered for the next
iteration, so:

| Iteration | Entry (no answer yet) | Field value | Satisfied? | Result
|
|---|---|---|---|---|
| 1 | fresh | `None` | no | pauses  |
| 1 | resume w/ answer | `answer` | yes | continues  |
| 2 | fresh | `answer` (**stale**) | yes | continues  (should pause) |

## Fix

When a `UserFillUp` is entered without a fresh user answer
(`merged_inputs` is empty), clear the retained form values so the
wait-check treats the form as unsatisfied and pauses again:

```python
merged_inputs = self._merge_runtime_inputs(kwargs.get("inputs", {}))
if not merged_inputs:
    self._clear_form_values()
```

- Fresh entry / new loop iteration → no answer supplied → values cleared
→ node pauses and waits.
- Resume with an answer → `merged_inputs` is non-empty → values applied
normally, nothing cleared.
- Non-loop behavior is unchanged: the first entry already had `None`
values, so clearing is a no-op there.

`Begin` overrides `_invoke` and is unaffected.

## Tests

Added to
`test/testcases/test_web_api/test_canvas_app/test_fillup_unit.py`:

- `test_user_fillup_clears_stale_values_on_reentry_without_answer` — a
retained value is cleared on a fresh entry with no answer (loop
re-entry).
- `test_user_fillup_keeps_values_when_answer_supplied` — a supplied
answer is applied and not cleared.

All unit tests pass and `ruff check` is clean.

## Scope

This targets the Python agent runtime (`agent/`). It is independent of
any other in-flight Await Response change.
2026-07-10 23:24:04 +08:00
Jack
5e60fcec9f Refactor: Refine ingestion task state transitions (#16814)
### Summary

Refine ingestion task state transitions
2026-07-10 22:47:51 +08:00
hyotaek kim
9b60870fd6 feat: make blob storage size threshold configurable (#16806)
### Summary

- Make `BLOB_STORAGE_SIZE_THRESHOLD` configurable through an environment
variable.
  - Preserve the existing 20 MiB default.
  - Add tests for the default and configured values.

  ### Why

Blob storage, Seafile, and WebDAV connectors currently use a hardcoded
20 MiB limit. Self-hosted users
cannot raise this limit without modifying the source code inside the
container.

  ### Testing

  - `test/unit_test/data_source/test_config.py`: 2 passed
- `ruff check common/data_source/config.py
test/unit_test/data_source/test_config.py`

  Fixes #16634
2026-07-10 21:36:12 +08:00
buua436
d291e4641d fix: improve pipeline compilation, template updates, and document resets (#16815)
### What problem does this PR solve?

- Clear stale pipeline IDs and generated data when updating documents
without `pipeline_id`.
- Support tree compilation results in pipeline workflows.
- Update compilation templates in place while preserving existing
template IDs.
- Improve duplicate-template validation messages.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-07-10 21:17:19 +08:00
Yingfeng
30739b7f8e Fix compilation workflow (#16819)
Dataset_nav can not support large number of documents, introducing AHC
clustering as well as retrieval engine as the clustering database
2026-07-10 21:11:19 +08:00
chanx
ea9b3789ce fix(prompt-editor): prevent premature variable path merge during typing (#16811)
### Summary

fix(prompt-editor): prevent premature variable path merge during typing

---------

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-07-10 20:57:59 +08:00
Jin Hai
0aa2993d55 Go: format code (#16813)
### Summary

Aligned to EE

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-10 20:30:00 +08:00
Jin Hai
106c2d0a41 Go: add show users plan quota (#16818)
### Summary

```
RAGFlow(admin)> show users plan quota 100;
+---------+------------------------------------------+
| field   | value                                    |
+---------+------------------------------------------+
| quota   | 100                                      |
| command | show_users_plan_quota                    |
| error   | 'Show users plan quota' is not supported |
+---------+------------------------------------------+
```

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-10 20:27:33 +08:00
maoyifeng
8ced3d1f47 fix ci (#16824)
### Summary

fix ci
2026-07-10 20:20:29 +08:00
Öndery
e53235ed3d fix(docker): [URGENT] Docker build fails for all PRs - missing web/scripts/prepare.js during npm install (#16821)
## 🚨 Urgent: this breaks the Docker image build for **every** open PR

The `ragflow_tests_elasticsearch` and `ragflow_tests_infinity` jobs —
which build the image before running tests — currently **fail on all
pull requests**, not just this one. CI is effectively red repo-wide
until this lands. **Please review and merge ASAP.**

Example failing runs (from an unrelated PR):
-
https://github.com/infiniflow/ragflow/actions/runs/29086020168/job/86341032173
-
https://github.com/infiniflow/ragflow/actions/runs/29086020168/job/86341032196

## Symptom

```
Error: Cannot find module '/ragflow/web/scripts/prepare.js'
npm error command sh -c node scripts/prepare.js
ERROR: process "/bin/bash -c cd web && NODE_OPTIONS=..." did not complete successfully: exit code: 1
```

## Root cause

`web/package.json` defines:

```json
"prepare": "node scripts/prepare.js"
```

npm runs the `prepare` lifecycle script during `npm install`. But the
frontend-deps layer in the `Dockerfile` only copies the package
manifests before installing:

```dockerfile
COPY web/package.json web/package-lock.json web/.npmrc ./web/
RUN ... cd web && npm install     # runs `node scripts/prepare.js` → file not present yet
```

Since `web/scripts/prepare.js` is not in the image at that point, `npm
install` aborts and the whole build fails.

## Fix

Copy `web/scripts` before `npm install` so the `prepare` script is
present:

```dockerfile
COPY web/package.json web/package-lock.json web/.npmrc ./web/
COPY web/scripts ./web/scripts
RUN ... cd web && npm install
```

- Minimal and safe: `scripts/prepare.js` only installs lefthook git
hooks and already no-ops (wrapped in try/catch) inside the image where
there is no Git repo.
- Preferred over `--ignore-scripts`, which would also disable
dependencies' legitimate install scripts.
- `web/scripts` changes rarely, so build-cache impact on this layer is
negligible.

## Verification

Build reaches the frontend `npm install` step without the
`MODULE_NOT_FOUND` error. No application code is touched — this is a
build-infrastructure fix only.
2026-07-10 19:15:57 +08:00
Jin Hai
2a83ad6cb2 Go: Fix error code (#16807)
### Summary

As title.

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-10 17:02:25 +08:00
Hz_
07a3523b09 fix(go-agent): unknown component "Wikipedia" canvas error. (#16784)
## Summary

- Register Wikipedia component + tool alias
`wikipedia`/`wikipedia_search`
- Use `generator=search` to get title/summary/url in one request (was
N+1)
- Node params `top_n`/`language` with validation
- Return `formalized_content` for downstream
- tests pass

<img width="1817" height="972" alt="image"
src="https://github.com/user-attachments/assets/f6d79599-6d1f-4ea6-84f7-ac06d0de13b0"
/>
2026-07-10 16:12:09 +08:00
Zhichang Yu
fb42e5531d Refactor: drop dead canvas runtime selector and tokenizer embedding wiring (#16809)
Two refactors on the Go port (agent-go-port):

- Remove the dead per-tenant canvas runtime selector (write-only Redis
scaffolding with no runtime callers) and its dependent metrics/admin
code.
- Move the tokenizer embedding-model id from the shared ingestion
globals into a Tokenizer-scoped setup, and wire the production embedder
resolver in the ingestion task package.

32 files changed, 861 insertions, 1228 deletions.
2026-07-10 15:46:45 +08:00
balibabu
d317742975 Feat: Rewrite wiki template with reui (#16797) 2026-07-10 15:44:04 +08:00
Jack
7db39822db Feature: user select pipeline support (#16788)
### Summary

Feature: user select pipeline support
2026-07-10 14:30:28 +08:00
Jin Hai
0b01171a86 Go: Update response message (#16803)
### Summary

As title.

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-10 14:26:54 +08:00
maoyifeng
c33a5a9d49 GO:Cli fix show user storage display truncation (#16805)
### Summary

GO: Cli  fix show user storage display truncation
2026-07-10 13:59:07 +08:00
Hz_
5797f81fea feat(go-agent): merge Google Scholar node params with runtime inputs (#16802)
## Summary

- align the Go Google Scholar component with the Python-side config
pattern
- merge node-level params with runtime inputs so canvas defaults are
preserved and per-run inputs can override them
- add tests covering node param fallback and runtime override behavior

## Verification

- `bash build.sh --test ./internal/agent/component/... -run
TestGoogleScholar`

<img width="1873" height="1165" alt="image"
src="https://github.com/user-attachments/assets/67198c6f-6a0e-43bf-a500-8e88d82b8751"
/>
2026-07-10 13:30:21 +08:00
buua436
28340f6218 GO: improve model info parsing and add model_id/tenant context to list response (#16804) 2026-07-10 13:29:01 +08:00
chanx
868e524f29 fix: pass ownerTenantId to LLMLabel and related components for improved model fetching (#16800) 2026-07-10 13:27:14 +08:00
dependabot[bot]
eca8e87f44 build(deps): bump mistune from 3.2.0 to 3.3.0 (#16799)
Bumps [mistune](https://github.com/lepture/mistune) from 3.2.0 to 3.3.0.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-10 12:24:56 +08:00
dependabot[bot]
a1a2ba8f6c build(deps): bump soupsieve from 2.8.1 to 2.8.4 (#16798)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-10 12:24:21 +08:00
Jin Hai
15d1ee7654 Go: set gin mode in admin server (#16801)
### Summary

As title.

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-10 12:19:10 +08:00
Jin Hai
add7b9486f Go: merge duplicate codes (#16783)
### Summary

1. merge heartbeat function.
2. introduce all environments

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-10 11:58:32 +08:00
Harsh Kashyap
289ca28ce2 Fix OpenAI agent stream chunk shape (#16402)
### What problem does this PR solve?

Closes #8175.

The Agent OpenAI-compatible streaming path uses `get_data_openai(...,
stream=True)`, but that helper currently returns a minimal chunk shape.
The main OpenAI-compatible chat endpoint already includes chunk metadata
such as `created`, `system_fingerprint`, `usage`, `logprobs`, and
assistant role/tool placeholders.

This PR aligns the Agent stream helper with that existing
OpenAI-compatible chunk shape while keeping the current `delta.content`
behavior and existing reference injection path intact.

### 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):

### Verification

- `./.venv/bin/python -m pytest
test/unit_test/api/utils/test_api_utils.py -q`
- `python3 -m py_compile api/utils/api_utils.py
test/unit_test/api/utils/test_api_utils.py`
- `uvx ruff check api/utils/api_utils.py
test/unit_test/api/utils/test_api_utils.py`

---------

Co-authored-by: Harsh Kashyap <harshkashyap@Harshs-MacBook-Pro.local>
2026-07-10 10:59:32 +08:00
Zhichang Yu
12787996d1 feat(agent): Go ingestion pipeline progress mirroring and DeepDOC parser hardening (#16795)
feat(ingestion): mirror Go pipeline progress into the document table;
harden resume guards
- pipeline: bind the owning document via WithDocumentID; after each
TrackProgress event aggregate ingestion_task_log progress and mirror
progress/run/progress_msg back into the document table, so GET
/api/v1/datasets/{dataset_id}/documents reflects live Go pipeline
progress without a bespoke endpoint.
- canvas: extend the S3 resume guard to reject legacy no-op nodes (e.g.
ExitLoop) so component_total equals the count of progress-reporting
components and the aggregate percent can reach 100%.
- runtime/canvas: route progress through TrackProgress; add interrupt
test coverage (r3_interrupt_test.go).
- dao/entity: add IngestionTask.DocumentID column and AggregateProgress
support used by the mirror; IngestionTaskLog keeps a Checkpoint column
alongside the progress fields.

feat(deepdoc): cache DocAnalyzer inference results in Redis (1h TTL)
- Redis-backed DocAnalyzerCache decorator over inference.Client; cache
key = "ddoc:cache:<method>:" + sha256 of the JPEG-encoded image bytes
(deterministic).
- TTL = 1h; hits skip the inner HTTP call and return cached JSON; inner
errors are not cached.

refactor(deepdoc): align figure cropping with Python cropout + bounded
page caches
- CropSectionByDLA mirrors Python cropout: best-overlap DLA
figure/equation region, fallback to section bbox per page, vertical
concat on gray background.
- sliding-window page-image cache bounds peak memory to the recent
window instead of the whole PDF.
- rename DLADebug -> DLARegions across parser/chunker/tests.

refactor(parser): drop lib_type selector; align NewXxxParser with
NewPDFParser
- remove config["lib_type"] lookup and the libType param/field/switch
from all nine constructors; surface the CGO-required error at
ParseWithResult time instead of construction time; drop resolveLibType,
its test, and the four lib_type constants.

feat(utility): add a reusable workerpool for bounded concurrent
execution
- internal/utility/workerpool.go (+ tests).

refactor: translate Chinese prose comments to English in non-harness Go
files.

chore: upgrade github.com/cloudwego/eino from v0.9.9 to v0.9.12.
2026-07-10 10:36:10 +08:00
buua436
74bbbba3e0 fix: align model default handling (#16782) 2026-07-10 10:34:19 +08:00
Hz_
d48a5622df fix(go-agent): align Go DuckDuckGo component with canvas input form (#16775)
## Summary

- register the Go `DuckDuckGo` canvas component and restore its dynamic
input form metadata
- align the Go component input/output surface with the current canvas
usage for `query`, `channel`, and `top_n`
- fix DuckDuckGo news search in Go by fetching the required `vqd` token
before calling `news.js`, and add targeted regression tests

## Testing

Passed:
- `bash build.sh --test ./internal/agent/tool/... -run 'DuckDuckGo'`
- `bash build.sh --test ./internal/agent/component/... -run
'DuckDuckGo|TestVerifyRegistration_P1'`
- `bash build.sh --test ./internal/agent/component/... -run
'DuckDuckGo'`

Not run:
- frontend tests
- frontend build
- full Go test suite

<img width="1776" height="1092" alt="image"
src="https://github.com/user-attachments/assets/9f3f8e4b-f6b4-4915-b96c-3c5b8c7b8b30"
/>
2026-07-10 10:11:13 +08:00
chanx
8236f2cabb Fix: update LayoutRecognizeFormField to accept ownerTenantId and refactor model handling in LLM requests (#16781) 2026-07-10 09:36:25 +08:00
chanx
0095fa048f fix: Show full text on hover when text overflows the cards on the list page. (#16787) 2026-07-10 09:25:05 +08:00
Hz_
5c8b51cbbf fix(go-agent): add Google search wrapper component and tool registry (#16768)
### Summary

- Implemented googleComponent wrapper to bridge the canvas component
contract with Eino's SerpApi-backed GoogleTool.
- Added parameter alias mapping (query to q, max_results to num) and
content formatting logic to match Python search result representation.
- Registered the "Google" component and the "google" tool factory in the
Go agent runtime to support web search nodes.

<img width="1776" height="1092" alt="image"
src="https://github.com/user-attachments/assets/e295ab88-e48c-4fe2-bcb7-47ca5b977c9b"
/>
2026-07-09 18:58:55 +08:00
balibabu
0083ad0deb Feat: Add a data compilation layer. (#16777)
### Summary

Feat: Add a data compilation layer.
2026-07-09 17:49:16 +08:00
Lynn
5de823eab9 Fix: delete unused tenant_llm testcase (#16786) 2026-07-09 17:48:02 +08:00
Jack
8bd62573eb Fix: delete tasklet (#16778)
### Summary

1. Fix: delete tasklet
2. Remove panic in text processing
2026-07-09 17:31:28 +08:00
Hz_
b29a4a61eb fix(ao-agent): add Tavily input_form, wire real search component, and port TavilyExtract to Go (#16693)
### Summary

- TavilySearch now stores api_key from component params and injects it
into tool calls when runtime inputs omit it.
- TavilyExtract and BGPT now follow the same stored api_key behavior.
- Canvas decoding now recovers api_key from graph.nodes[].data.form when
components[].obj.params.api_key is empty, matching frontend payload
behavior without changing frontend data.
- Added regression tests for graph form key recovery and stored key
injection / caller key precedence.

Tests: build.sh --test ./internal/agent/component ./internal/service —
all pass.

<img width="1476" height="850" alt="image"
src="https://github.com/user-attachments/assets/0be31587-c1ba-4f3e-b43a-4fe0fca5a44c"
/>

<img width="1476" height="850" alt="image"
src="https://github.com/user-attachments/assets/e3edd92c-c62e-4db4-abe2-772bdf4fe1b2"
/>
2026-07-09 16:38:42 +08:00
Jack
8ac80284c4 Feat: add embedding support (#16769)
### Summary

Feat: add embedding support
2026-07-09 16:24:39 +08:00
Lynn
0ba1d37a10 Feat: optional url v1 (#16774) 2026-07-09 15:53:06 +08:00
chanx
a9420a7832 Fix: resolve shared embedding/LLM model selection errors (#16773) 2026-07-09 15:17:49 +08:00
Lynn
cc94639555 Fix: get_by_id (#16765) 2026-07-09 14:52:41 +08:00
Lynn
7fa2a0b607 Fix: rm role_id in user.go (#16771) 2026-07-09 14:44:08 +08:00
buua436
6a77523bf0 refa: resolve tenant model refs consistently (#16744) 2026-07-09 14:02:08 +08:00
Yingfeng
794fcc2517 Fix docs (#16770) 2026-07-09 13:38:29 +08:00
Yingfeng
b9ed0773ec Fix naive chunking for windows (#16767)
`\r` is ignored for splitting boundaries
2026-07-09 12:02:19 +08:00
qinling0210
ae96e636e9 Handle searching dataset without embedding model (#16742)
### Summary

Handle searching dataset without embedding model

In this PR, Searching datasets with different embedding models or
searching dataset with/without embedding models are not allowed. We will
improve the behavior later.
2026-07-09 11:38:55 +08:00
Lynn
1430d0e431 Fix: provider name (#16733) 2026-07-09 10:19:10 +08:00
balibabu
575984877f Fix: Rapid clicking results in multiple message requests being sent. (#16739) 2026-07-09 09:57:54 +08:00
euvre
3ec9187cd2 fix(web): prevent 'last saved at' label from vertical stacking in agent home card (#16756) 2026-07-09 09:46:03 +08:00
chanx
080dd84fed Feat: apply prose typography styling to markdown preview (#16752) 2026-07-09 09:45:52 +08:00
chanx
36f053a248 fix: Fixed the empty state styling on the home page. (#16755) 2026-07-09 09:45:43 +08:00
euvre
70019810a1 fix(web): show memory owner name in shared memory card (#16751) 2026-07-08 20:13:07 +08:00
euvre
74d3508a37 Fix: prevent auto-incrementing memory name suffix when only permissions change (#16750) 2026-07-08 20:12:45 +08:00
euvre
a41fef49d0 fix(web): hide folder tab in agent JSON import uploader (#16754) 2026-07-08 20:11:55 +08:00
Kevin Hu
2c59d07bdb Feat: add wiki folder (#16749)
### Summary

Add wiki folders.
2026-07-08 20:08:14 +08:00
qinling0210
8e3bbad4da Port agent PRs to GO - 5 (#16667)
### Summary

Port 

https://github.com/infiniflow/ragflow/pull/15376
https://github.com/infiniflow/ragflow/pull/16401
https://github.com/infiniflow/ragflow/pull/15484
https://github.com/infiniflow/ragflow/pull/16685
2026-07-08 19:54:29 +08:00
Virus
0f08dc070d fix: update nginx version. fix CVE-2026-9256 (#16732)
### Summary
Updates the NGINX package used in the RAGFlow Docker image from
`1.31.0-1~noble` to `1.31.2-1~noble`.

NGINX 1.31.0 is affected by CVE-2026-9256. NGINX 1.31.2 includes
the corresponding security fix and is available from the official
NGINX mainline repository for Ubuntu Noble.

### References
- nginx security advisories:
https://nginx.org/en/security_advisories.html
- Vendor advisory: https://my.f5.com/manage/s/article/K000161377

### Fix
Single-line change in `Dockerfile:62`:

```diff
-ARG NGINX_VERSION=1.31.0-1~noble
+ARG NGINX_VERSION=1.31.2-1~noble

Co-authored-by: duanyuan <duanyaun@uyuyue.com>
2026-07-08 19:19:06 +08:00
Jack
0dd0ac06f8 Feature: task executor migration to go (#16549)
### Summary

Feature: Integrate parser
2026-07-08 19:08:11 +08:00
Wang Qi
c8d1b21ae3 Fix Build-in metadata not working (#788) (#16748) 2026-07-08 19:06:54 +08:00
Hz_
e548108fff fixed(go-agent): agent-publish (#16713)
## Summary

This PR updates Go agent publish logic to persist the parent canvas
update and canvas-version save in the same transaction.

## Changes:

  - Reuse SaveOrReplaceLatest semantics for published versions
  - Add SaveOrReplaceLatestTx for transactional publish flow
  - Keep canvas release update and version persistence atomic
- Add a focused publish test covering canvas and released version state

## Tested:

```
  bash build.sh --test -run 'Test(PublishAgentUpdatesCanvasAndReleasedVersion|UpdateAgentDSLCreatesAndReplacesDraftVersion|
  UpdateAgentDSLDoesNotOverwriteLatestReleasedVersion)$' ./internal/service ./internal/dao
```

<img width="1476" height="850" alt="image"
src="https://github.com/user-attachments/assets/2c576581-1143-420b-8750-a77aa3c4292d"
/>
2026-07-08 18:47:01 +08:00
Lynn
330033d7c2 Fix(go): adapt to new db columns (#16745) 2026-07-08 18:02:11 +08:00
Jin Hai
21266286cb Go: add more commands and GCS supports (#16741)
### Summary

1. GCS supports
2. More commands
```
RAGFlow(admin)> ping store;
SUCCESS
RAGFlow(admin)> ping engine;
SUCCESS
RAGFlow(admin)> ping cache;
SUCCESS
```

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-08 17:49:02 +08:00
Yingfeng
dc95b1d291 Fix skill search (#16743) 2026-07-08 17:17:50 +08:00
chanx
3d167204e7 fix: issue with memory error message display (#16738) 2026-07-08 16:47:14 +08:00
chanx
b9432bb43f Feat: add filter in chat and search page (#16707) 2026-07-08 16:47:01 +08:00
dependabot[bot]
5baf4089bf build(deps): bump golang.org/x/crypto from 0.51.0 to 0.52.0 (#16736)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from
0.51.0 to 0.52.0.
2026-07-08 12:51:47 +08:00
Wang Qi
18ea093344 Dev: need set the stable version (#16730)
Resolve: #16729
2026-07-08 11:05:09 +08:00
Jin Hai
40cb581d16 Go: clean release.yml (#16725)
Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-08 10:41:30 +08:00
euvre
699a25c19c fix(service): allow updating memory_type when memory is empty (#16668) 2026-07-08 10:03:58 +08:00
Lynn
0ae5961e1c Feat: v0.27.0 model provider (#16604) 2026-07-08 09:47:29 +08:00
Jin Hai
cb93883f3f Go: fix cgo build (#16724)
Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-07 21:15:14 +08:00
Jin Hai
2b95fa2ba4 Go: remove cgo when build cli (#16723)
### Summary

Fix

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-07 21:05:07 +08:00
Jin Hai
fe835fd19c Go: fix release (#16722)
### Summary

Remove ragflow-cli building dependency

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-07 20:57:48 +08:00
Jin Hai
9e87fc6036 Go: fix ragflow-cli building (#16721) 2026-07-07 20:44:10 +08:00
writinwaters
49d9f6f98e Docs: Added v0.26.4 release notes. (#16720) 2026-07-07 20:14:16 +08:00
Jin Hai
568bac673b Go: add migrate database flag (#16719)
### Summary

--migrate will trigger database migration
```
./ragflow_main --api --migrate
```

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-07 20:11:40 +08:00
Liu An
4da9429451 Docs: Update version references to v0.26.4 in READMEs and docs (#16716) 2026-07-07 19:36:58 +08:00
euvre
41801ad2b8 fix: prevent memory name from auto-appending (1) on description update (#16714) 2026-07-07 19:34:51 +08:00
Öndery
28a41ed070 fix(task_executor): fix Langfuse flush/shutdown deadlock that freezes document parsing (#16502) 2026-07-07 19:06:30 +08:00
Yingfeng
6cd03d7a70 Fix broken logo for gitee mirrors (#16709) 2026-07-07 18:12:50 +08:00
Wang Qi
705754ea8b Fix PageIndex is not working (#16704)
Follow on PR #16515
2026-07-07 18:09:05 +08:00
Yingfeng
7db3eea37b Fix broken camo cache for french (#16706) 2026-07-07 17:50:06 +08:00
Yingfeng
ee7edddea3 Fix broken cache for release badge (#16705) 2026-07-07 17:43:29 +08:00
Yingfeng
f8f049e663 Fix docker pulls badge (#16702) 2026-07-07 17:26:47 +08:00
Jin Hai
7df7384b21 Go: refactor UUID functions (#16695)
As title

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-07 17:22:08 +08:00
chanx
5236c8f659 fix: update similarity threshold fallback to use nullish coalescing (#16700) 2026-07-07 17:03:03 +08:00
Wang Qi
2de5940325 Fix cannot run raptor (#16694) 2026-07-07 17:02:04 +08:00
Wang Qi
b82169fba1 Fix: ValueError: Operation on closed image (#16697) 2026-07-07 17:00:40 +08:00
Hz_
ac5860c3e2 fix(go-pipline): list agents incorrectly filtering out ingestion pipelines (#16698) 2026-07-07 16:32:40 +08:00
Hz_
332d34c495 fix(agent): save draft version on agent update (#16691) 2026-07-07 16:32:11 +08:00
chanx
b7945f3a64 Fix: Referenced files not displaying. (#16696) 2026-07-07 16:29:17 +08:00
chanx
dd2f27d6a3 fix: Restrict the agent to using memory compatible with the embedding model. (#16699) 2026-07-07 16:28:58 +08:00
Jin Hai
9eba45249c Go: fix development guide (#16678)
### Summary

Go development guide

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-07 15:09:39 +08:00
chanx
f082675e6f Fix: Prevent text overflow in confirm delete dialog (#16689) 2026-07-07 14:50:25 +08:00
chanx
5aa3e81a93 fix: remove duplicate error toast on memory update failure (#16690) 2026-07-07 14:50:09 +08:00
Wang Qi
3660b98ae9 fix: strip reasoning model thinking tags from document exports (#16687)
Co-authored-by: noob <yixiao121314@outlook.com>
2026-07-07 12:17:53 +08:00
Wang Qi
a0bda639e0 Fix Agent Chat not working (#16688)
Follow on this PR: #15484 it break the Agent chat
2026-07-07 12:10:52 +08:00
天海蒼灆
318045dda5 feat(agent): support JSON object input on begin node (#16685)
### Summary

Add object as a begin-node parameter type with JSON editor UI, webhook
schema support, and backend parsing in UserFillUp.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 11:40:57 +08:00
euvre
b4540672e4 fix(go): seed built-in agent templates for Go backend (#16666)
### Summary

The Go backend never seeded the `canvas_template` table, so the "Create
agent from template" page was blank when the frontend proxies to the Go
API (`API_PROXY_SCHEME=go`). This PR adds `SeedCanvasTemplates()` in
`internal/dao`, invoked from `InitDB()` after migrations, which loads
`agent/templates/*.json` and mirrors Python's `add_graph_templates()`
behavior.

Changes:
- Add `internal/dao/canvas_template_seed.go` to parse and upsert
built-in templates.
- Call `SeedCanvasTemplates()` in `InitDB()`.
- Add `CanvasTypes` (`JSONSlice`) to `entity.CanvasTemplate` so the
frontend can filter/group by category.
- Skip seeding gracefully when the templates directory is absent.

This fixes the blank template catalogue in Go mode.
2026-07-07 11:25:53 +08:00
Hz_
d03a360fb1 fix(go-agent): add BGPT component and input form (#16684)
## Summary
Adds the missing input form metadata for the Go BGPT canvas component.

## Root Cause

The standalone BGPT component was registered in Go, but it did not
implement GetInputForm(). During component trial run, the backend asks
the component for its input_form. Since BGPT had none, the API returned:

component has no input_form: BGPT:<node_id>

Python BGPT already exposes the query input form, so the Go component
needed the same contract.

## Change

Added GetInputForm() to the Go BGPT component with a single query line
input.
Added test coverage to ensure BGPT exposes the input form.

## Validation

Backend:
bash build.sh --test -run TestBGPT ./internal/agent/component

<img width="1369" height="1184" alt="image"
src="https://github.com/user-attachments/assets/f99e4a81-2359-42e5-80bb-dcc4e6a63fea"
/>

<img width="1736" height="1152" alt="image"
src="https://github.com/user-attachments/assets/c11240a5-2c42-4d08-88e3-c6dfbf49eedb"
/>
2026-07-07 11:15:05 +08:00
weifanglab
7a4e1b6034 refactor: use slices.Contains to simplify code (#16680)
### Summary

There is a [new function](https://pkg.go.dev/slices@go1.21.0#Contains)
added in the go1.21 standard library, which can make the code more
concise and easy to read.

Signed-off-by: weifanglab <weifanglab@outlook.com>
2026-07-07 11:12:38 +08:00
Hz_
863b35db7f fix(go-agent-web): correct BGPT canvas form watcher usage (#16682)
## Summary

Fixes a page crash when opening the BGPT node configuration in the
canvas.

## Root Cause

BGPT was using the tool-form watcher call pattern in a normal canvas
component form.

Tool forms use:

useWatchFormChange(form)

Canvas component forms use:

useWatchFormChange(node?.id, form)

Tool is not equal to component. The BGPT canvas component imported the
component-level hook but called it like a tool-form hook, so the form
argument became undefined and React Hook Form tried to read control from
a null context.

## Change

Updated the BGPT canvas form to pass the node id and form instance
correctly.

## Validation

Ran ESLint for the changed file:
npx eslint src/pages/agent/form/bgpt-form/index.tsx

<img width="1369" height="1184" alt="image"
src="https://github.com/user-attachments/assets/a40c5202-7394-4f26-9da2-08329dcc7fbf"
/>
2026-07-07 11:08:50 +08:00
Shuo Liu
ab5958f518 fix: encapsulate terminal color output and add cross-platform color detection (#16672) 2026-07-07 09:41:26 +08:00
S
f477d3329d Fix: ValueError: too many values to unpack in list_tenant_added_models for model IDs containing '@' (#16467) (#16468) 2026-07-07 09:40:27 +08:00
Rodger Blom
d8cefcf052 feat: add native Dutch language support for BM25 tokenization (#14140)
## Summary
- Add language-aware Snowball stemmer to `RagTokenizer` supporting 16
languages (Dutch, German, French, Spanish, etc.)
- Thread the KB `language` parameter through the full tokenization
pipeline (14 parser modules + task executor)
- Add Dutch to the frontend language lists and cross-language form

## Problem
RAGFlow uses the English Porter stemmer + WordNet lemmatizer for **all**
BM25 tokenization, regardless of the knowledge base language setting.
This produces incorrect stems for non-English text. For example:

| Dutch word | Dutch stemmer | English Porter |
|---|---|---|
| documenten | document | documenten (unchanged!) |
| gebruikers | gebruiker | gebruik (over-stemmed) |
| instellingen | instell | instellingen (unchanged!) |

This degrades BM25 recall for any non-English knowledge base.

## Solution
NLTK already ships Snowball stemmers for 16 languages. This PR:

1. **`rag/nlp/rag_tokenizer.py`**: Overrides `tokenize()` with
`set_language()` and `_normalize_token()` that selects the correct NLTK
Snowball stemmer. Falls back to Porter for unmapped languages (Chinese,
Japanese, Korean, etc. — these use character-based tokenization anyway).
2. **`rag/nlp/__init__.py`** + **14 `rag/app/*.py` parsers** +
**`rag/svr/task_executor.py`**: Threads the `language` parameter through
`tokenize()`, `tokenize_chunks()`, `tokenize_table()`, and all callers.
3. **Frontend**: Adds Dutch (`Nederlands`) to `LanguageList`,
`LanguageMap`, `LanguageAbbreviationMap`, `LanguageTranslationMap`,
cross-language form field, and `en.ts` locale.

## Backward Compatibility
- Default language is `"English"`, preserving existing behavior for all
current users
- Languages without a Snowball stemmer mapping fall back to Porter (no
change)
- No new dependencies — NLTK Snowball is already bundled
2026-07-06 23:39:56 +08:00
Yingfeng
dd20561fca Feat: add event sourcing and replay to harness (#16326)
### Motivation

This PR evolves the harness from a pure execution runtime into an
**observable, replayable agent evaluation platform**. The current
`harness/graph` checkpoint mechanism is insufficient for true
event-sourced introspection—we need append-only event logs capturing
every tool call, state transition, memory write, and approval decision,
enabling deterministic replay, fork/diff, postmortem analysis, and
time-travel debugging.

### Key Design Goals

1. **Event-Sourced Execution Model**  
Replace coarse checkpoints with granular, append-only event logs. Every
operation becomes a durable event: tool invocation, state mutation,
memory update, human approval. This unlocks deterministic replay,
branching execution histories, and regression datasets derived directly
from production failures.

2. **First-Class Replay & Evaluation Loop**  
Replay is not an afterthought—it is a core primitive. A single live run
seeds an offline corpus that supports: repeated playback, model
substitution, tool result mocking, and strategy comparison. The harness
graduates from "executor" to "continuous evaluation platform" where
failed production traces convert directly into offline regression
suites.

3. **Operational Observability**  
   Beyond raw traces, expose metrics that prove stability over time:
   - Tool success / failure rates
   - Approval latency distributions
   - Retry frequencies
   - Checkpoint restore reliability
   - Memory retrieval quality
   - Cost per completed task
   - Fork replay pass rates

The underlying thesis: the bottleneck for most agent systems is not
execution capability, but the inability to **demonstrate continuous,
measurable improvement**.


### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-07-06 23:31:54 +08:00
OrbisAI Security
82f3735770 fix: upgrade crawl4ai to 0.9.0 (GHSA-r253-r9jw-qg44) (#16426)
## Summary
Upgrade crawl4ai from 0.8.9 to 0.9.0 to fix GHSA-r253-r9jw-qg44.

## Vulnerability
| Field | Value |
|-------|-------|
| **ID** | GHSA-r253-r9jw-qg44 |
| **Severity** | CRITICAL |
| **Scanner** | trivy |
| **Rule** | `GHSA-r253-r9jw-qg44` |
| **File** | `uv.lock` |
| **Assessment** | Likely exploitable |

**Description**: Crawl4AI: Unauthenticated RCE via Chromium
launch-argument injection in browser_config.extra_args

## Evidence

**Scanner confirmation**: trivy rule `GHSA-r253-r9jw-qg44` flagged this
pattern.

**Production code**: This file is in the production codebase, not
test-only code.

## Threat Model Context

This is a web service - vulnerabilities in request handlers are directly
exploitable by remote attackers.

## Changes
- `pyproject.toml`
- `uv.lock`

## Verification
- [x] Build passes
- [x] Scanner re-scan confirms fix
- [x] LLM code review passed

---
*This change addresses a pattern flagged by static analysis. The code
path handles user-influenced input and the fix reduces the attack
surface against both manual and automated exploitation.*

---
*Automated security fix by [OrbisAI Security](https://orbisappsec.com)*

Co-authored-by: Ling Qin <qinling0210@163.com>
2026-07-06 21:28:19 +08:00
Hernandez Avelino
5a8660df23 [Bug]: Workflow agent completions default stream=True when stream is omitted (#15484)
## Summary

Closes #15483.

Default workflow/session agent completions to non-streaming when
`stream` is omitted.

## Changes

- `api/apps/restful_apis/agent_api.py`: `req.get("stream", False)` on
workflow paths.

## Test plan

- [ ] POST workflow completion without `stream`; assert JSON response.
2026-07-06 21:27:22 +08:00
Mattie Schraeder
8a19c6aa5a Make RAPTOR GMM robust on small reduced clusters (#16632) 2026-07-06 21:09:35 +08:00
Mei Zhihan
85b565244d fix(mcp): handle dict response in list_chats when /chats API returns paginated envelope (#16639) 2026-07-06 21:06:22 +08:00
Mattie Schraeder
0c2fb622e9 Collapse small RAPTOR layers in one step instead of one node per layer (#16633) 2026-07-06 21:06:04 +08:00
OSHA-B
779bf52549 fix: handle missing ES/OpenSearch index in check_embedding (HTTP 500 on empty dataset) (#16650) 2026-07-06 20:30:16 +08:00
Mattie Schraeder
8fb8e4197c fix(graphrag): filter negative-judgment and misattributed relationship edges (#16541) 2026-07-06 20:26:13 +08:00
Zhichang Yu
55af1d70f3 Align Go parser backends and PDF pipeline with Python (#16676)
Ports remaining Go parser wiring and PDF backends, adds tenant-aware VLM
dispatch, aligns post-processing with Python, and adds end-to-end
pipeline coverage with a generated six-page PDF.
2026-07-06 19:50:54 +08:00
euvre
3044283442 fix(go): add missing 'resume' chunk method for new tenants (#16660) 2026-07-06 19:21:14 +08:00
Hz_
8ee3f097b9 fix(go-document): keep upload partial success data as array (#16661)
### Summary

Keep `data` as the uploaded document array when dataset document upload
partially succeeds.

This matches the Python API behavior and allows parse-on-creation to run
for successfully uploaded files when other files in the same folder are
unsupported.
2026-07-06 19:15:29 +08:00
Hz_
7e0ccee7e2 fix(go-agent): missing input form for ExeSQL and Browser agent nodes (#16675)
## Summary
- Add Go dynamic input form support for ExeSQL and Browser components.
- Align their input form metadata with the Python implementation.
- Add regression tests for `/components/:component_id/input-form`.
2026-07-06 19:15:09 +08:00
Hz_
b2e82a42d6 fix(go-agent): Yahoofinance input and run (#16658)
## Summary

Debugging YahooFinance component in agent canvas returns "unknown
component" and "no input_form".
YahooFinance was only registered as an eino tool, not as a runtime
component. The component factory only searches the runtime registry.
- `universe_a_wrappers.go`: add `yahooFinanceComponent` wrapper
delegating to `agenttool.YahooFinanceTool` with `GetInputForm()`
- `fixture_stubs.go`: register `"YahooFinance"` component

## TEST
`go build` and `go test ./internal/agent/component/...` all pass.
2026-07-06 19:14:50 +08:00
Kevin Hu
52f985f43e Refactor: Remove redundant functions. (#16671)
### Summary

Remove redundant functions.
2026-07-06 19:02:25 +08:00
euvre
bfb9641128 fix(go): uploaded documents should be enabled by default (#16674) 2026-07-06 19:01:32 +08:00
Wang Qi
3a247dbb3c Fix filter to use Chinese (#16673) 2026-07-06 18:20:42 +08:00
Jin Hai
b3d536c48e Go: merge functions (#16622)
### Summary

Merge HTTP response functions into common/response.go

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-06 18:14:05 +08:00
Wang Qi
ed52255868 Fix Agent session lost <think></think> tag (#16670) 2026-07-06 17:47:38 +08:00
Wang Qi
57625c919a Fix Tag weight should be greater than 0 (#16657) 2026-07-06 16:06:27 +08:00
balibabu
290ab557a5 Fix: Layout of the agent prompt dropdown menu is messed up. (#16653) 2026-07-06 14:48:42 +08:00
Harsh Kashyap
98189cd20a Fix OpenAI response created timestamp (#16401)
## What this fixes

Closes #16400.

`get_data_openai()` currently returns `created: null` when callers do
not pass a timestamp, and it replaces explicit timestamp values with the
current time. This makes non-streaming OpenAI-compatible responses
inconsistent with the expected integer `created` timestamp field.

## Change

- Preserve explicit `created` values when provided.
- Default non-streaming responses to `int(time.time())` when `created`
is not provided.
- Add focused unit coverage for default timestamps, explicit timestamps,
and unchanged streaming chunk shape.

## Verification

- `./.venv/bin/python -m pytest
test/unit_test/api/utils/test_api_utils.py -q`
- `python3 -m py_compile api/utils/api_utils.py
test/unit_test/api/utils/test_api_utils.py`
- `uvx ruff check api/utils/api_utils.py
test/unit_test/api/utils/test_api_utils.py`

---------

Co-authored-by: Harsh Kashyap <harshkashyap@Harshs-MacBook-Pro.local>
2026-07-06 14:16:16 +08:00
Crystora
b633ff0435 feat(go): drop empty and duplicate chunks in postprocess filter (#16049)
### What problem does this PR solve?

The Go chunk pipeline's `PostprocessOperator` `filter` stage
(`internal/ingestion/chunk/postprocess.go`) only filtered by length
(`min_length`/`max_length`). It could not drop empty/whitespace-only
chunks or duplicate chunks — both standard RAG post-processing steps
(blank chunks shouldn't be indexed; identical chunks waste embedding
compute and add redundant retrieval results).

This adds two optional, default-off booleans to the `filter` config:

- `drop_empty` — drop chunks whose content is empty or whitespace-only.
- `drop_duplicates` — drop chunks whose exact content already appeared
(order-preserving; the first occurrence is kept).

They compose with the existing length bounds and are reflected in
`String()` for plan explainability. Also adds the first unit tests for
the postprocess filter (length bounds, drop_empty, drop_duplicates,
combined, exact-content matching, and config parsing).

Validation: `gofmt` clean, `go vet ./internal/ingestion/chunk/` clean,
`go build` ok, `go test ./internal/ingestion/chunk/` — all tests pass.

Closes #16048

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

Co-authored-by: Ling Qin <qinling0210@163.com>
2026-07-06 14:14:31 +08:00
Crystora
4effc242fd feat(go): add length split strategy with overlap to chunk pipeline (#16047)
### What problem does this PR solve?

The Go ingestion chunk pipeline's `SplitOperator`
(`internal/ingestion/chunk/split.go`) supported only `sentence`, `char`,
and `paragraph` strategies, but not **fixed-size (length) chunking with
overlap** — the canonical RAG strategy for bounding chunk length while
preserving cross-boundary context.

This adds a `length` strategy alongside the existing ones, configurable
via DSL `params`:

- `chunk_size` — target window size in **runes** (rune-aware:
multi-byte/CJK text is windowed by character, never split mid-rune).
- `overlap` — runes carried from the end of each window into the next.

The window advances by `chunk_size - overlap`. `chunk_size` falls back
to a default (256) when unset/non-positive, and `overlap` is clamped to
`[0, chunk_size-1]` so the window always advances and the operation
terminates. Implementation follows the existing
`splitByChar`/`splitByParagraph` pattern and reuses `DetectLanguage` for
chunk metadata.

It also adds `split_test.go` — the first unit tests for the `chunk`
package — covering basic windowing, overlap, overlap
clamping/termination, rune-awareness (CJK), default sizing, no-overlap
reconstruction, empty input, and DSL param parsing.

Validation: `gofmt` clean, `go vet ./internal/ingestion/chunk/` clean,
`go build` ok, `go test ./internal/ingestion/chunk/` — all tests pass.

Closes #16046

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

Co-authored-by: bittoby <218712309+bittoby@users.noreply.github.com>
2026-07-06 14:14:21 +08:00
Hz_
c4166a91e0 fix(go-agent): align agent debug input form with Python (#16654)
## Summary

- derive Go Agent debug input forms from prompt variable references
instead of Agent meta fields
- seed `sys.*` debug params into `CanvasState.Sys` so single-component
debug resolves prompt variables like Python
- restore Agent test-run parity for form rendering and debug execution

## Tests

- `go test ./internal/agent/component -run
'TestAgent_(GetInputForm_UsesPromptReferences|GetInputForm_DeduplicatesPromptReferences|Meta_DefaultsToEmpty|Reset_NoTools)$'`
- `go test ./internal/handler -run
'Test(DebugComponent_SeedsSysInputsIntoCanvasState|DebugComponent_HappyPath_Begin|GetComponentInputForm_HappyPath)$'`

AFTER:
<img width="669" height="456" alt="image"
src="https://github.com/user-attachments/assets/4fd86559-aafc-4027-91ae-6e666137ee1b"
/>
2026-07-06 13:29:10 +08:00
chanx
c9f064d5fd fix(metadata): inline value edits not persisted to backend (#16655) 2026-07-06 13:04:04 +08:00
OSHA-B
d607b55c24 fix(nlp): prevent dotted-number cross-references from being classified as headings in Laws chunker (#16626) 2026-07-06 13:02:58 +08:00
Wang Qi
48ef1f4965 Dev: Fix nats host (#16656) 2026-07-06 11:50:37 +08:00
jony376
aaade3530e fix(api): cap memory message limit and top_n at REST_API_MAX_PAGE_SIZE (#15376)
## Related issues

Closes #15375

### What problem does this PR solve?

`GET /api/v1/messages` and `GET /api/v1/messages/search` accepted
unbounded `limit` / `top_n` query parameters while other REST list
endpoints enforce `REST_API_MAX_PAGE_SIZE` (100) via
`validate_rest_api_page_size()`. Oversized values can trigger expensive
memory index queries and large result sets (DoS risk).

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

### Changes

| File | Change |
|------|--------|
| `api/apps/restful_apis/memory_api.py` | Cap `limit` and `top_n` with
`validate_rest_api_page_size`; return argument error when exceeded |
|
`test/testcases/test_web_api/test_message_app/test_message_routes_unit.py`
| Regression tests for oversized `limit` / `top_n` |

### Test plan

- [x] Unit tests added
- [ ] `pytest
test/testcases/test_web_api/test_message_app/test_message_routes_unit.py`

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 11:20:13 +08:00
Sohaib Ahmed
534a3a7faa fix: Docling parser extracts mathematical formulas (#16645) 2026-07-06 11:17:46 +08:00
qinling0210
3d2f60c34f Port agent PRs to GO - 4 (#16652)
### Summary

Port

https://github.com/infiniflow/ragflow/pull/15399
https://github.com/infiniflow/ragflow/pull/16469
2026-07-06 10:58:40 +08:00
Carlo Beltrame
09eb9dbd21 Switch the default minio image in the helm chart as well (#16322)
Follow-up from #13896
Fixes #13840

### What problem does this PR solve?

In #13896, only the docker-compose-base.yml was adjusted. However, in
the Helm chart, the unmaintained minio/minio image is still referenced.
This PR syncs the Helm chart with the docker compose setup again.

I also added a line to AGENTS.md, so agents should know to do this
automatically in the future.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [ ] New Feature (non-breaking change which adds functionality)
- [x] Documentation Update
- [ ] Refactoring
- [ ] Performance Improvement
- [ ] Other (please describe):

Co-authored-by: Zhichang Yu <yuzhichang@gmail.com>
2026-07-06 10:32:19 +08:00
AI-Mart
fe2f3b60a1 feat(agent): expose thinking mode control per LLM node in Agent canvas (#16640)
## Summary

Add **per-node thinking mode control** for LLM components in RAGFlow
Agent canvas, supporting Qwen3/Qwen3.6/B200M thinking-capable models.
Users can now independently configure thinking mode
(thinking/non-thinking) for each LLM node via the existing UI dropdown.

## Motivation

When Qwen3.6-27B (and other thinking-capable models like Qwen3-32B,
B200M) are used in RAGFlow Agent nodes, different nodes need different
thinking behavior:
- **Reasoning nodes** (complex analysis, math, coding): thinking mode ON
- **Simple nodes** (direct Q&A, intent classification): thinking mode
OFF

The web UI already has a `thinking` dropdown (default/enabled/disabled)
in LLM settings, and the LLM backend `_apply_model_family_policies()`
already supports `enable_thinking`. **The missing link was `gen_conf()`
not forwarding the parameter.**

This 3-line fix completes the chain.

## Changes

**`agent/component/llm.py`** — `LLMParam.gen_conf()`:

```python
if hasattr(self, "thinking") and self.thinking and self.thinking != "default":
    conf["thinking"] = self.thinking
```

## End-to-end flow

```
UI dropdown: default / enabled / disabled
  → DSL: {"thinking": "enabled"}
    → LLMParam.thinking = "enabled"
      → gen_conf() returns {"thinking": "enabled"}
        → _apply_model_family_policies()
          → extra_body {enable_thinking: true}
            → Model API call with thinking ON
```

## Backward compatibility

- **Fully backward compatible** — only 3 lines added, nothing changed
- When `thinking` is "default" or not set, existing behavior is
preserved
- Qwen3 models default to `enable_thinking: false` (non-thinking),
unchanged

## Related issues

- Closes #16321 (thinking content leaks in non-streaming agent API
responses)
- Closes #13957 (how to view model reasoning process in agent API)

## Testing

- Verified in
`test/unit_test/rag/llm/test_chat_model_thinking_policy.py` that
thinking policy already tested for Qwen3 models
- The 3-line change passes through existing tested code path
(`_apply_model_family_policies`)

Co-authored-by: Hermes Agent <hermes-agent@agent.local>
2026-07-06 10:19:27 +08:00
Hz_
358152f758 fix(go-document): add document and file access checks (#16592)
## Summary
Adds ownership/access checks before updating or deleting documents,
setting document metadata, and reading file contents from storage. Also
adds tests for authorized and unauthorized access paths.
2026-07-06 10:13:46 +08:00
Hz_
e5d217993b fix(go-skill): Elasticsearch skill search field mapping (#16611)
## Summary

Fix Elasticsearch-backed skill search by mapping skill search fields to
their indexed token fields.

`name`, `tags`, `description`, and `content` are stored for display but
are not searchable in the skill ES mapping. Search queries now target
`name_tks`, `tags_tks`, `description_tks`, and `content_tks`.

## Testing

- Ran Go unit tests:

```bash
/usr/local/go/bin/go test -count=1 ./internal/engine/elasticsearch
```

- Frontend verification:
    1. Open /files/skills.
    2. Enter a skill space.
3. Reindex the skill space if existing skills were created before this
fix.
    4. Search by skill name or description keyword.
    5. Confirm matching skills are returned.
2026-07-06 10:05:34 +08:00
euvre
0265ffbc53 fix(agent): enable single-component debug for Agent in Go backend (#16606)
### Summary

This PR fixes two issues that prevented the Agent component's
single-component debug/test run from working under the Go backend:

1. **Dynamic input_form generation**: Some components (e.g. `Agent`) do
not store a static `input_form` in the DSL. The Go handler now falls
back to the runtime component's `GetInputForm()` method, matching
Python's `Canvas.get_component_input_form` behavior. This resolves the
frontend 102 error: `component has no input_form`.

2. **Tenant ID injection for debug**: Single-component debug runs use a
fresh `CanvasState` that previously lacked `tenant_id`.
`AgentComponent.Invoke` resolves LLM credentials via the tenant tables,
so the debug run failed with `api key is required`. The handler now
seeds `state.Sys["tenant_id"]` with the authenticated user's ID,
mirroring Python's `@add_tenant_id_to_kwargs` decorator.

### Changes

- `internal/handler/agent_component.go`:
- Added `componentInputForm` helper that first reads the static
`input_form` and, if missing, instantiates the component and calls
`GetInputForm()`.
- In `DebugComponent`, set `debugState.Sys["tenant_id"] = user.ID`
before invoking the component.
2026-07-06 09:57:00 +08:00
euvre
8b065d3ddd fix(agent): collect CodeExec artifacts from ReAct tool responses (#16609)
### Summary

The Go backend Agent component was not returning artifacts produced by
the CodeExec tool. While the Python agent collects the "`_ARTIFACTS`"
envelope from tool responses and appends artifact markdown to the final
content, the Go agent only returned the assistant text, so generated
images were missing from the chat output.

### Changes

- Wire `react.WithMessageFuture()` in `runEinoReActAgent` and store the
resulting `MessageFuture` in the invocation context.
- After the ReAct loop finishes, drain the future and extract
``_ARTIFACTS`` entries from every tool response message.
- Support reading the tool payload from both `msg.Content` and
`msg.UserInputMultiContent` to match eino's tool contract.
- De-duplicate artifacts by URL and render images as `!` and other files
as download links.
- Add `agent_artifact_test.go` with a regression test that simulates a
CodeExec-style tool response carrying an image artifact and verifies it
is collected and formatted.

### Verification

- `go test ./internal/agent/component/... -run
TestAgent_ReActAgent_CollectsArtifactsFromCodeExecTool` passes.
- `go test ./internal/agent/component/... -count=1` compiles; the only
failure is an unrelated DNS-pinning timeout test
(`TestInvoke_ProxyDNSPin`).
- `gofmt` clean for modified files.

### Related

Fixes the behavior shown in the screenshot where the Go agent ignored
the CodeExec-generated PNG artifact.
2026-07-05 20:53:43 +08:00
Jack
1d3c100acb Refactor: pdf parser (#16625)
### Summary

PDF parser refactor
2026-07-05 20:45:35 +08:00
Zhichang Yu
014c3f634f Align Go ingestion boundaries with Python (#16647)
Moves doc_id blob resolution into Parser, tightens chunker/tokenizer to
Python output_format semantics, updates extractor list handling, and
fixes real-template integration tests.
2026-07-05 20:43:52 +08:00
Mattie Schraeder
0fcfb38365 Cap RAPTOR UMAP n_neighbors to prevent OOM on large datasets (#16627)
## Problem
`raptor.py` computes `n_neighbors = int((len(embeddings) - 1) ** 0.8)`
and
passes it to `umap.UMAP(...)`. In a dataset-scope RAPTOR build the first
layer's `embeddings` is the entire KB's chunk set, so this is
effectively
unbounded: ~93k chunks → n_neighbors ≈ 9,446.

UMAP's k-NN graph is `N × n_neighbors`; at these values the raw neighbor
arrays alone are ~14 GB (93k × 9446 × 16 B), and the symmetrized fuzzy
simplicial set + spectral init push peak well past 30 GB. The task
executor is OOM-killed inside `fit_transform` before any clustering runs
—
the log shows "Task has been received" with no "Cluster one layer" line
—
after which the unacked task re-queues and OOMs again in a loop.

The line above already flags this: `# Degrade too much ??`.

## Fix
Cap `n_neighbors` at 100. UMAP's neighborhood size has strongly
diminishing returns well below this (default 15; a few dozen already
captures global structure), so the ceiling preserves — likely improves —
cluster quality while bounding memory to O(N). Mirrors the existing
`n_components=min(12, len(embeddings) - 2)` clamp two lines down.

​```diff
-        n_neighbors = int((len(embeddings) - 1) ** 0.8)
+        n_neighbors = min(int((len(embeddings) - 1) ** 0.8), 100)
​```

## Repro
Dataset-scope RAPTOR over a KB with ~90k+ chunks on a box with <~64 GB
available: executor OOM-killed in the first-layer UMAP `fit_transform`.
With the cap, first-layer UMAP peaks in low single-digit GB and the
build
proceeds to completion.

## Scope
Only affects large dataset-scope builds; file-scope RAPTOR already had
n_neighbors well under 100. No behavior change beyond the ceiling.
2026-07-04 17:47:43 +08:00
Wang Qi
a0e65637eb Delete canvas_app.py and evaluation_service.py (#16614)
Follow on PR #13295
2026-07-03 21:03:54 +08:00
Kevin Hu
cf634b92b4 Feat: Put some wiki templates. (#16617)
### Summary

Add a few of wiki templates.
2026-07-03 20:52:27 +08:00
Wang Qi
06aa169df7 Update development script (#16623) 2026-07-03 20:34:30 +08:00
Liu An
63a4ed55d8 docs: update Docker build instructions for deps image (#16620)
### Summary

update Docker build instructions for deps image
2026-07-03 19:57:12 +08:00
monsterDavid
7da4f200e5 fix(agent): enable MCP file preview via doc_id (#15399)
## Summary
MCP-wrapped agents could only force-download files looked up by
`doc_id`. This adds an explicit preview path and inline response headers
for previewable file types.

- **New** `GET /api/v1/agents/attachments/{attachment_id}/preview` —
inline preview for PDFs, images, and other safe types (pass `ext` and/or
`mime_type`)
- **Improved** `GET /api/v1/documents/{doc_id}/preview` — sets inline
disposition using the document filename
- **Improved** attachment download routing — resolves `mime_type` /
`ext` query params (no default `markdown`), supports
`disposition=inline`
- **DocGenerator output** — includes URL-encoded `preview_url` for MCP
clients
- **Legacy `/document/download/...` aliases** — still use download
semantics; MCP clients should call `/preview` explicitly

Fixes #15398

## Test plan
- [x] `pytest test/unit_test/api/utils/test_file_response_headers.py`
(6/6)

---------

Co-authored-by: MkDev11 <mkdev11@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Ling Qin <qinling0210@163.com>
2026-07-03 19:56:01 +08:00
maoyifeng
0f4f2135f3 Go:cli move _order _columns sort group (#16615)
### Summary
1. Move common functions to format.go
2. modify show name spaces to _
3. move _order _columns column sort group;
4. add dao empty enterprise file
2026-07-03 19:37:53 +08:00
Jin Hai
6b571694df Go: Update error info (#16619)
As title.

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-03 19:37:25 +08:00
S
1861087787 fix(agent): defend against @ in var names at all template-split sites (#16469)
## Summary

While fixing #16467 (IterationItem crash on `@` in user-defined output
keys), an audit of `agent/**/*.py` revealed **three additional sites**
with the same vulnerability. This PR hardens all of them with
`maxsplit=1` and adds regression tests.

This is **defense-in-depth hardening**, not a behavior change. The
current `variable_ref_patt` regex constrains `var_nm` to
`[A-Za-z0-9_.-]+`, so single-`@` templates resolve exactly as before.
The `maxsplit=1` only kicks in if the trailing side itself contains `@`
— currently unreachable from the public DSL surface, but trivially
exploitable the moment a user-defined output key happens to contain `@`
(e.g. `user@email`) or the regex is ever relaxed.

> **Note on issue scope**: The primary fix for #16467 (the
`list_tenant_added_models` `ValueError` crash on `@` in model names) is
in PR #16468. This PR is a **follow-up hardening sweep** of the same
vulnerability class found in `agent/` during that audit; it does not
duplicate or replace #16468.

## Sites hardened

| File | Line | Method |
|------|------|--------|
| `agent/canvas.py` | 206 | `Graph.get_variable_value` |
| `agent/canvas.py` | 256 | `Graph.set_variable_value` |
| `agent/component/base.py` | 533 |
`ComponentBase.get_input_elements_from_text` |
| `agent/component/iterationitem.py` | 88 |
`IterationItem.output_collation` |

All now use `split("@", 1)` with an inline comment explaining the
rationale. The trailing side keeps any embedded `@`.

## Sites already safe (audited but left alone)

| File | Reason safe |
|------|------------|
| `agent/canvas.py:708` (`is_reff`) | Pre-checks `len(arr) != 2` |
| `agent/component/categorize.py` | Uses `rsplit` |
| `agent/component/iteration.py` | Pre-validates via regex |
| Other call sites | `rsplit` or regex pre-validation |

## Regression tests

9 new tests across 2 files, all `pytest.mark.p2`:

| File | Tests |
|------|-------|
| `test/unit_test/agent/test_canvas_at_split.py` | 6 —
`get_variable_value`, `set_variable_value`, round-trip, single-`@`,
missing-component |
| `test/unit_test/agent/component/test_iterationitem_at_split.py` | 3 —
`output_collation` with `@` in var, single-`@`, non-matching cid |

Each test was **verified to fail with `ValueError: too many values to
unpack (expected 2)`** when the corresponding fix is temporarily
reverted, confirming the tests actually catch the bug rather than just
exercising the happy path.

## Test results

```
9 passed in 0.04s
```

Full agent unit suite also clean (38 passed, 3 skipped; 6 unrelated
pre-existing collection errors from missing `peewee`/`requests` in local
venv — not caused by this PR).

## Related

- Issue: #16467
- Primary fix PR: #16468 (closes the issue)
- This PR: defense-in-depth follow-up, intentionally non-blocking on
#16467

---------

Co-authored-by: skbs-eng <skbs-eng@users.noreply.github.com>
2026-07-03 19:26:27 +08:00
Haruko386
fd7fb6669a fix: cannot get query in agent-log (#16610)
### Summary

As title

bug:


fixed:
<img width="1827" height="1286" alt="image"
src="https://github.com/user-attachments/assets/0cdc391c-43d7-4330-bc34-3aefe5d4f4ee"
/>
2026-07-03 18:56:32 +08:00
Jin Hai
83d09b16ce Fix Go: list providers order issue. (#16616)
### Summary

As title.

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-03 18:27:32 +08:00
Haruko386
dde8b6d54c fix: get team's search in own search-list (#16599)
### Summary

As title:
2026-07-03 18:26:03 +08:00
Haruko386
226d0ff77c fix: get team merber's chat (#16597)
### Summary

As title
2026-07-03 18:25:31 +08:00
Haruko386
488574fd80 fix: get all memory in team with permission=me (#16593)
### Summary

As title:
2026-07-03 18:25:04 +08:00
Yingfeng
706fa4e87a Feat: add gbrain compile template for session/memory data (#16613) 2026-07-03 18:22:29 +08:00
qinling0210
ffc4d29a06 Port agent PRs to GO - 3 (#16596)
### Summary

Port
https://github.com/infiniflow/ragflow/pull/16415
https://github.com/infiniflow/ragflow/pull/16417
2026-07-03 18:03:23 +08:00
Yingfeng
8db68e3eec Refactor(harness): remove naive inline graph engine , unify graph execution under single pregel engine (#16608) 2026-07-03 17:50:30 +08:00
Muhammad Furqan
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>
2026-07-03 17:15:48 +08:00
Jin Hai
1880e65e99 Go: refactor (#16602)
### Summary

1. update doc
2. refactor route code

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-03 17:00:43 +08:00
chanx
79518973e5 Fix: optimize folder data handling in MoveDialog component (#16580) 2026-07-03 16:13:57 +08:00
euvre
4effd057f0 i18n: localize visual input file label in agent form (#16594) 2026-07-03 15:31:27 +08:00
Jin Hai
a4c370c5ba Go: fix 'list services' (#16598)
### Summary

```
RAGFlow(admin)> list services;
+-----------------------------------------------------------------------------+-----------+----+---------------+------+---------------+-----------+
| extra                                                                       | host      | id | name          | port | service_type  | status    |
+-----------------------------------------------------------------------------+-----------+----+---------------+------+---------------+-----------+
| map[database:1 mq_type:redis password:infini_rag_flow]                      | localhost | 0  | redis         | 6379 | message_queue | alive     |
| map[password:infini_rag_flow retrieval_type:elasticsearch username:elastic] | localhost | 1  | elasticsearch | 1200 | retrieval     | alive     |
|                                                                             | 0.0.0.0   | 2  | nats          | 4222 | message_queue | CONNECTED |
| map[meta_type:mysql password:infini_rag_flow username:root]                 | localhost | 3  | mysql         | 3306 | meta_data     | alive     |
| map[password:infini_rag_flow store_type:minio user:rag_flow]                | localhost | 4  | minio         | 9000 | file_store    | alive     |
+-----------------------------------------------------------------------------+-----------+----+---------------+------+---------------+-----------+

```

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-03 15:08:37 +08:00
euvre
994b603374 fix: prevent duplicate MCP server name when editing (#16588) 2026-07-03 14:30:43 +08:00
euvre
7b341539e7 fix: prevent exporting empty MCP server selection (#16589) 2026-07-03 14:22:17 +08:00
Hz_
ac5d0c4615 fix(go-file): KB counter drift when deleting files with linked documents (#16584)
## Summary

Use `DocumentService.RemoveDocumentKeepFile` when deleting files that
are linked to documents.

  ## Change

  - inject `DocumentService` into `FileService`
  - replace direct document deletion in `deleteSingleFile`
  - remove the obsolete file-local engine deletion helper

  ## Result

Deleting a file now cleans up linked documents through the same service
path used elsewhere, keeping KB counters and document engine cleanup
consistent.
2026-07-03 14:07:54 +08:00
Haruko386
ee942711c4 fix: unable to fetch tools for MCP (#16583) 2026-07-03 14:05:42 +08:00
Haruko386
b2e4740acd fix: unable to import mcp from local (#16590)
### Summary

As title
2026-07-03 14:05:07 +08:00
Haruko386
383d059969 fix: agent chat completions can not use (#16570)
### Summary

As title
<img width="2370" height="2039" alt="image"
src="https://github.com/user-attachments/assets/4cccf543-3908-49ee-8101-c5068fbf53ec"
/>
2026-07-03 13:25:14 +08:00
euvre
e65bac238e fix: preserve existing links when bulk linking files to knowledge bases (#16587) 2026-07-03 13:17:19 +08:00
Wang Qi
6a4b9be426 Refactor: reformat all code for lefthook using ruff and gofmt (#16585) 2026-07-03 12:53:39 +08:00
Yingfeng
19fcb4a981 Fix harness DAG slow-branch test cased by nil initialization of pregel engine (#16591) 2026-07-03 12:53:25 +08:00
euvre
918229613a fix: prevent duplicate 'skills' and '.knowledgebase' folders caused by race conditions (#16568) 2026-07-03 12:06:45 +08:00
Muhammad Furqan
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>
2026-07-03 11:39:26 +08:00
Jin Hai
1aa8abe373 Go: file syncer service framework (#16579)
### Summary

./ragflow_main --syncer to start file syncer


config yaml file has following config
```
file_syncer:
  max_concurrent_syncs: 4 # concurrent file sync threads
  sync_interval: 3 # check interval

```

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-03 11:14:02 +08:00
Kevin Hu
62f94cd59b Feat: Add knowledge compilation workflows (#16515)
## Summary
- Add knowledge compilation template APIs, services, and builtin
template seed data
- Add advanced knowledge compile structure/artifact/RAPTOR workflow
support
- Update parsing, dataset/document APIs, and supporting services for
compilation workflows
2026-07-02 23:22:07 +08:00
Jin Hai
7d64a78f83 Go: unify three services into one binary (#16462)
### Summary

Plan to start api_server, admin_server and ingestor in one binary:
- ./ragflow_main --admin
- ./ragflow_main --api
- ./ragflow_main --ingestor

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-02 21:21:10 +08:00
Liu An
32c5cb16e9 Docs: Update version references to v0.26.3 in READMEs and docs (#16574) 2026-07-02 20:55:15 +08:00
Wang Qi
93f6d647d4 Fix the sandbox exec image cannot show and download (#16577) 2026-07-02 20:49:51 +08:00
maoyifeng
4a81b9cfde fix workflow file type Identify (#16576)
fix workflow file type Identify
2026-07-02 20:41:14 +08:00
Lynn
bc54903bf6 Fix: display model_id in memory_list (#16567) 2026-07-02 20:28:27 +08:00
chanx
9a6d30bfe6 Fix: send agent log date filters as local wall-clock strings (#16575) 2026-07-02 20:23:15 +08:00
qinling0210
dcbd0d260c Port agent PRs to GO - 2 (#16565)
### Summary

Port the following PRs to GO in this PR

https://github.com/infiniflow/ragflow/pull/16420
https://github.com/infiniflow/ragflow/pull/13295
2026-07-02 20:20:11 +08:00
qinling0210
24118ac0d1 Fix chat thinking & Figure issue in GO (#16558)
### Summary

Fix chat thinking & Figure issue
2026-07-02 20:19:50 +08:00
Hz_
42aba36c1b fix(go): chunk stats after chunk deletion (#16553)
## Summary
- Decrement document and knowledgebase chunk counts after chunks are
deleted
- Keep token counts unchanged because deleted chunk token totals are not
available
- Add tests for stats update, zero-delete behavior, error handling, and
transaction rollback
2026-07-02 19:54:42 +08:00
Hz_
dfd95c9c5c fix(go): Add tenant filter to file queries (#16526)
## Summary

- Add `tenant_id` filtering to `FileDAO.Query`.
- Pass tenant IDs through existing file query call sites.
- Prevent cross-tenant filename and folder duplicate checks.
2026-07-02 19:54:22 +08:00
Jin Hai
11dfea489d Fix Go: fix minio port issue (#16552)
### Summary

1. env 'MINIO_PORT' is used for MINIO external access, which shouldn't
be used in Go config.
2. After RAGFlow 1.0 release, MINIO_PORT will be used for docker compose
internal usage. new ENV MINIO_EXTERNAL_PORT will be used for external
access.

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-02 19:15:58 +08:00
euvre
fc9116578c Fix: PDF page count detection for compressed PDFs (#16487) 2026-07-02 19:08:49 +08:00
Wang Qi
f7e39a09dc Fix graphrag generate error - AttributeError: 'RedisDB' object has no attribute 'mget' (#16573) 2026-07-02 19:06:16 +08:00
Jack
6ea95807be Fix: disable agent tests (#16562)
### Summary

Per discussion with @yuzhichang , disable agent test firstly.


https://github.com/infiniflow/ragflow/actions/runs/28562749273/job/84704079689?pr=16521
[0.094ms] [rows:0] SELECT * FROM `tenant_model_instance` WHERE
provider_id = "provider-1" AND instance_name = "default" ORDER BY
`tenant_model_instance`.`id` LIMIT 1
  --- FAIL: TestInvoke_ProxyDNSPin (2.00s)
invoke_test.go:375: dial error = Invoke: do: Get "http://8.8.8.8/api":
context deadline exceeded; want pinned proxy IP 192.88.99.1:9999
(connection-refused is acceptable; an absent IP means the dialer fell
through to the default resolver and the pinning regression went
undetected)
2026/07/02 14:34:58
/home/infiniflow/runners_work/tower01-9dd627fd9c44/ragflow/ragflow/internal/dao/kb.go:79
record not found
[0.358ms] [rows:0] SELECT * FROM `knowledgebase` WHERE id = "da1" AND
status = "1" ORDER BY `knowledgebase`.`id` LIMIT 1
2026/07/02 14:34:58
/home/infiniflow/runners_work/tower01-9dd627fd9c44/ragflow/ragflow/internal/dao/kb.go:79
record not found
[0.283ms] [rows:0] SELECT * FROM `knowledgebase` WHERE id = "da1" AND
status = "1" ORDER BY `knowledgebase`.`id` LIMIT 1
2026/07/02 14:34:58
/home/infiniflow/runners_work/tower01-9dd627fd9c44/ragflow/ragflow/internal/dao/kb.go:79
record not found
[0.523ms] [rows:0] SELECT * FROM `knowledgebase` WHERE id = "da1" AND
status = "1" ORDER BY `knowledgebase`.`id` LIMIT 1
2026/07/02 14:34:58 ExpectPing will have no effect as monitoring pings
is disabled. Use MonitorPingsOption to enable.
  FAIL
  FAIL    ragflow/internal/agent/component    2.759s
  ok      ragflow/internal/agent/component/io    0.026s
2026-07-02 18:50:20 +08:00
Renzo
7d422ba67d feat(go): implement chatbots/<dialog_id>/info and searchbots/detail (#15420)
### What problem does this PR solve?

Part of #15240 (rewriting the RAGFlow API server in Go).

Implements the two public bot endpoints from
`api/apps/restful_apis/bot_api.py`:

- **`GET /api/v1/chatbots/<dialog_id>/info`** (`chatbots_inputs`) —
returns `{title, avatar, prologue, has_tavily_key}` for a dialog the
authenticated tenant owns (tenant match + `status == VALID`), otherwise
`"Authentication error: no access to this chatbot!"`.
- **`GET /api/v1/searchbots/detail`** (`detail_share_embedded`) —
returns search-app detail for a `search_id` the tenant can access.
Permission is checked across the tenant's joined tenants; denial returns
`"Has no permission for this operation."` (operating error, `data:
false`) and a missing app returns `"Can't find this Search App!"`.

Both endpoints authenticate with an SDK **beta token** (`Authorization:
Bearer <beta>`) rather than a session — the token is resolved to a
tenant via `APIToken.query(beta=token)`, backed by a new
`APITokenDAO.GetByBeta`. Because they perform their own token-based
auth, the routes are registered on the unauthenticated route group
(mirroring the Python blueprint, which has no `@login_required`).

Both live in a new `internal/handler/bot.go` + `internal/service/bot.go`
since they share the same source module. Handler unit tests cover the
auth, success, and error-mapping paths.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

---------

Co-authored-by: Claude Code <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Ling Qin <qinling0210@163.com>
2026-07-02 18:46:00 +08:00
Jack
7ae18a45ee Fix: correct download_deps.py path in error messages and add native libs doc (#16557)
## Summary

Fix error messages in `build.sh` and add documentation in
`internal/development.md` for downloading native static libraries
(pdfium, pdf_oxide, office_oxide).

## Changes

- `build.sh`: change error hint from `uv run download_deps.py` to `uv
run ragflow_deps/download_deps.py` (correct path from project root)
- `internal/development.md`: add section 2.1 documenting how to download
native libs and install lld
2026-07-02 18:41:39 +08:00
maoyifeng
3e7e5f4f6a add web and build start steps (#16572)
### Summary

update ci
2026-07-02 18:17:06 +08:00
writinwaters
ce8941ded4 Docs: Added v0.26.3 release notes. (#16566) 2026-07-02 17:50:14 +08:00
chanx
16b8c79a2b Fix: hide model settings button and related functionality (#16563) 2026-07-02 17:49:52 +08:00
chanx
2ef78189ce Fix: pass mcp to useExportMcp for correct JSON export filename (#16564) 2026-07-02 17:49:46 +08:00
chanx
c44d56f1bb Fix: enhance reference handling in SessionChat component (#16571) 2026-07-02 17:48:48 +08:00
Hz_
d31640a7a2 fix(go): shared chatbot session id length (#16559)
## Summary
- use the project-standard 32-character ID generator when creating
shared chatbot sessions
- fix MySQL insert failures caused by writing 36-character UUID strings
into `api_4_conversation.id`
2026-07-02 17:42:33 +08:00
Jack
c8cf0c967d Feat: add DOCX parser (#16521)
### Summary

Add DOCX parser - go.
2026-07-02 16:31:09 +08:00
Haruko386
9c8d8c7b83 fix: unable to load pic in chunk result (#16485)
### Summary

As title:
2026-07-02 16:05:49 +08:00
Haruko386
3a5bc1371a fix: unable to build go backend (#16542) 2026-07-02 15:57:51 +08:00
Haruko386
92e8eb5fe7 fix: add search keywords and filter for datasets-search (#16550) 2026-07-02 15:57:07 +08:00
Wang Qi
4130091b69 [Python] 1, Fix to allow single login, 2, update password to force re-login (#16556) 2026-07-02 15:47:51 +08:00
Hz_
cbb24944e8 fix(go): clear task cancel signals and chunk counters on rerunWithDelete (#16544) 2026-07-02 15:46:11 +08:00
Hz_
fa1b52ca74 fix(go): prevent moving folders into themselves (#16522) 2026-07-02 15:45:30 +08:00
maoyifeng
404ef4ce87 workflow steps separated to go or python (#16561)
add new workflow yml,  steps separated to go or python
2026-07-02 15:02:11 +08:00
Jin Hai
0b9ab12c58 Go: fix lint (#16533)
### Summary

as title.

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-02 13:44:05 +08:00
grandpig
17e3e34e78 refactor: use WaitGroup.Go to simplify code (#16539)
### Summary

Adopt sync.WaitGroup.Go (Go 1.25) to simplify tracked goroutine
spawning. This replaces the error-prone trio of wg.Add(1), go func(),
and defer wg.Done() with a single, self-contained call.

More info: https://github.com/golang/go/issues/63796

Signed-off-by: grandpig <grandpig@outlook.com>
2026-07-02 13:41:53 +08:00
Hz_
d0d0339428 fix(go): agent settings update clearing DSL (#16495)
### Summary

This PR fixes a Go backend bug where updating agent settings, such as
description, could clear the agent DSL.

Root cause:
PUT /api/v1/agents/:canvas_id only bound the dsl field in Go. When the
frontend submitted settings without dsl, the service still updated the
canvas with an empty DSL value.

Changes:

- Treat agent updates as partial patches.
- Preserve existing DSL when dsl is not present in the request.
- Update only specified user_canvas fields instead of saving the full
row.
- Add a regression test for settings updates preserving DSL.

Test:

`go test ./internal/service ./internal/handler`

Co-authored-by: Zhichang Yu <yuzhichang@gmail.com>
2026-07-02 13:41:24 +08:00
Hz_
a67026f714 fix(go): agent explore thumbnail loading for multiple doc_ids (#16514)
## Summary
- align the Go `/api/v1/thumbnails` endpoint with the frontend request
format for repeated `doc_ids`
- return thumbnail mappings for multiple documents instead of failing on
a single missing document
- preserve Python-compatible thumbnail formatting, including base64
thumbnail passthrough
2026-07-02 12:35:10 +08:00
Hz_
cb8012e30b fix(go): accept disabled chunk filter in list chunks handler (#16532)
### Summary

Fixes a bug in the Go chunk list handler where the available` query
parser rejected `false` and `0` even though they were documented as
supported values.`

This caused requests from the "Disabled" chunk filter to return HTTP 400
and broke the chunk list page when filtering disabled chunks.
2026-07-02 12:07:19 +08:00
Haruko386
b4825166a7 fix: JSONMap scan in dataset index chunking config (#16489)
### Summary

As title

This PR fixes dataset index task creation failing with unsupported data
type: entity.JSONMap when loading document chunking config.

#### issues:
```
2026/06/30 15:19:40 /home/infiniflow/Documents/development/ragflow/internal/dao/document.go:162 
[error] unsupported data type: ragflow/internal/entity.JSONMap
```

#### Changes:
+ Adds the missing GORM type:longtext tag to ParserConfig in
DocumentDAO.GetChunkingConfig.
+ Adds a DAO regression test covering GetChunkingConfig joins across
document, knowledgebase, and tenant while scanning parser_config.
2026-07-02 12:06:53 +08:00
Haruko386
d6b1c5937b fix: get duplicate datasetID when get-Chat (#16498)
### Summary

As title

```go
// Resolve kb_ids to kb_names
	kbNames, datasetIDs := s.getDatasetNamesAndIDs(chat.KBIDs)

        // duplicated add datasetID(removed)
	for _, kbID := range chat.KBIDs {
		datasetID, ok := kbID.(string)
		if !ok {
			continue
		}
		datasetIDs = append(datasetIDs, datasetID)
	}
```
2026-07-02 12:06:29 +08:00
Haruko386
ee45c97b0b fix: unadble to add metadata for file in kb (#16523)
### Summary

As title

Before, it return `update success` but never insert or update any
metadata

fixed:

```go
	_, err = s.docEngine.InsertMetadata(nil, []map[string]interface{}{
		{
			"id":          docID,
			"kb_id":       doc.KbID,
			"meta_fields": meta,
		},
	}, tenantID)
```
2026-07-02 12:06:05 +08:00
Br1an
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>
2026-07-02 10:57:24 +08:00
Zhichang Yu
ba552f64b9 Stabilize timeout tests with semantic assertions (#16537)
Replace fragile wall-clock timeout assertions with semantic checks for
deadline errors, retry suppression, and event ordering. Keep only
lower-bound timing checks where they prove backoff behavior. This
reduces CPU-load flakes without weakening regression coverage.
2026-07-02 10:56:38 +08:00
euvre
3195d6fa89 fix: improve Normal role badge visibility with proper styling (#16528) 2026-07-02 10:47:01 +08:00
Wang Qi
7abc69434f [Go] Fix to allow duplicate key for provider (#16543) 2026-07-02 10:34:36 +08:00
Hz_
9b83d0f154 fix(go): document count in kb (#16490)
### Summary
This PR fixes incorrect dataset document counters in the Go service.

Several document creation paths inserted document records directly
through documentDAO.Create, bypassing the shared InsertDocument logic
that increments knowledgebase.doc_num. As a result, datasets could
contain documents while doc_num remained 0.
2026-07-02 10:34:14 +08:00
Hz_
0de69e5bba feat(go-api) sessions message update (#16517)
### Summary
```
/api/v1/chats/<chat_id>/sessions/<session_id>/messages/<msg_id> DELETE
/api/v1/chats/<chat_id>/sessions/<session_id>/messages/<msg_id>/feedback PUT
```
Migrates the chat session message delete and feedback APIs to the Go
server, matching the Python behavior for authorization, session
ownership checks, message/reference updates, and feedback validation.
2026-07-02 10:33:27 +08:00
Jack
5bc4753d1e Feat/oss parser no post (#16464)
### Summary

Remove dead code
2026-07-02 09:46:33 +08:00
qinling0210
133b1e15fd Port agent PRs to GO (#16529)
### Summary

Port the following PRs to GO in this PR

https://github.com/infiniflow/ragflow/pull/14210
https://github.com/infiniflow/ragflow/pull/14641
https://github.com/infiniflow/ragflow/pull/15220
https://github.com/infiniflow/ragflow/pull/15228
https://github.com/infiniflow/ragflow/pull/15384
https://github.com/infiniflow/ragflow/pull/15754
https://github.com/infiniflow/ragflow/pull/16413
https://github.com/infiniflow/ragflow/pull/16483
https://github.com/infiniflow/ragflow/pull/16419
https://github.com/infiniflow/ragflow/pull/16361   
https://github.com/infiniflow/ragflow/pull/16050
2026-07-02 09:45:01 +08:00
Öndery
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>
2026-07-02 09:35:28 +08:00
Jack
42a0faad18 Fix: use .a to replace .so for pdfium/pdf_oxide/office_oxide (#16496)
### Summary

Use .a to replace .so for pdfium/pdf_oxide/office_oxide

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-01 21:21:31 +08:00
OSHA-B
b0e6007131 perf: batch-embed entity/edge names in set_graph() to fix stall on large graphs (#16205) (#16472)
## Problem

When building or updating a knowledge graph with a large number of
entities and edges, `set_graph()` in `rag/graphrag/utils.py` creates one
`asyncio` task per entity and one per edge, each calling
`embd_mdl.encode([single_name])` — a single-item HTTP request to the
embedding server.

For a graph with 17,000+ nodes and edges (real case reported in #16205),
this generates **34,000+ individual embedding API round-trips** instead
of ~266 batched calls at the default `_INSERT_BULK_SIZE=128`. The
asyncio gather over thousands of tasks makes the embedding server the
bottleneck; under load, a single slow/failed call aborts all remaining
tasks, causing the pipeline to stall and never complete.

Closes #16205. Related: #15921.

## Root Cause

```python
# Before (in set_graph, node loop):
tasks = [asyncio.create_task(graph_node_to_chunk(n, ...)) for n in nodes]
# Each task calls embd_mdl.encode([single_name]) — 1 HTTP call per node
```

`graph_node_to_chunk` checks the embed cache first, but the cache is
cold on first build, so every task makes a live API call.

## Fix

Pre-warm the embedding cache with batched calls before spawning tasks.
Each batch pre-warm calls `embd_mdl.encode(batch_of_128)` once,
populating the cache. Then every individual task hits the cache and
makes zero embedding API calls.

- Only encodes names not already in cache (no-op on warm cache / small
incremental updates)
- Uses existing project idioms: `thread_pool_exec`, `chat_limiter`,
`_INSERT_BULK_SIZE`, `get_embed_cache`, `set_embed_cache`
- Mirrors the `ENABLE_TIMEOUT_ASSERTION` timeout pattern from
`graph_node_to_chunk`
- Zero behavior change: per-task encode logic remains as a correct
fallback

## Result

| Graph size | Before | After |
|---|---|---|
| 17,576 edges | ~17,576 embedding calls → stall | ~138 batched calls |
| 17,509 nodes | ~17,509 embedding calls → stall | ~137 batched calls |
| **Total** | **~35,000 calls** | **~275 calls** |

---------

Co-authored-by: Oti_B <oti@mac.speedport.ip>
2026-07-01 20:45:20 +08:00
Haruko386
4a72c973e8 fix: return call failed when LLM not available (#16518)
### Summary

As title
2026-07-01 20:10:42 +08:00
euvre
fb0376561f fix: normalize Q&A parser ID key to lowercase 'qa' (#16530) 2026-07-01 19:33:18 +08:00
Jin Hai
f4f9e4466b Go CLI: fix list provider models (#16493)
### Summary

As title.

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-01 19:08:17 +08:00
Haruko386
9eacbb418f fix: unable to open filter in agent page(no agent tags...) (#16531)
### Summary

As title
2026-07-01 19:07:15 +08:00
blackflytech
fcf2ca869b refactor: replace context.WithCancel with t.Context (#16509)
### Summary

The addition of the Context method to Go's testing.T provides
significant improvements for writing concurrent tests. It allows better
management of goroutines, ensuring they properly exit and preventing
issues like deadlocks and unfinished processes.

By using Context, errors and cancellations can be handled more
effectively, making tests more robust and easier to reason about. This
change also enables tighter integration between tests and the
application code, especially for systems that span multiple concurrent
components. Overall, it simplifies test code and enhances test stability
and maintainability.

More info: [golang/go#18368](https://github.com/golang/go/issues/18368)

Signed-off-by: blackflytech <blackflytech@outlook.com>
2026-07-01 18:37:11 +08:00
qinling0210
5ba25a5267 Fix GetProjectRoot in GO (#16520)
### Summary

Fix GetProjectRoot in GO
2026-07-01 18:17:53 +08:00
euvre
81cfcdf2d3 feat(frontend): add AuthenticatedImg component for authorized image requests (#16525) 2026-07-01 17:02:44 +08:00
ZF
97a4c64cc8 fix(harness): truncate text on rune boundary to keep UTF-8 valid (#16511)
### Summary

`truncateText` in the `reduction` and `summarization` middlewares
truncates with `s[:maxLen]`, which slices by byte. When `maxLen` lands
inside a multi-byte character (common with CJK or other non-ASCII
content flowing through the agent), the string is cut mid-rune and the
tail byte(s) become invalid UTF-8. That broken text then goes into the
reduced context / summary prompt.

`TruncateToolResult` in the same `reduction` package already avoids this
by slicing on a rune boundary and even notes it in a comment. This PR
makes the two `truncateText` helpers do the same, so they stay
consistent with the existing helper.

Both functions keep their existing output shape (summarization still
appends `...`). Added a small unit test in each package covering ASCII
truncation and a CJK string, asserting the result stays valid UTF-8.
2026-07-01 16:45:26 +08:00
Harsh Kashyap
d770217b25 fix(api): fall back to factory max_tokens for tenant models (#16364) 2026-07-01 16:00:13 +08:00
qinling0210
7862f69f39 Implement chat completions in go (#16491)
### Summary

POST   /api/v1/chat/completions
2026-07-01 15:52:52 +08:00
Harsh Kashyap
b8e960e6c8 fix(qa): preserve final CSV pair row number (#16433) 2026-07-01 14:52:08 +08:00
Harsh Kashyap
b42414b64a fix(deepdoc): parse bodyless HTML fragments (#16423) 2026-07-01 14:45:22 +08:00
connerlambden
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>
2026-07-01 13:52:24 +08:00
Harsh Kashyap
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>
2026-07-01 13:51:39 +08:00
Harsh Kashyap
572f1ea9f4 fix(web): sanitize agent rerun modal HTML against stored XSS (#16516) 2026-07-01 13:38:31 +08:00
Lynn
400476f0b3 Feat: SoMark (#16482)
Follow #15486
Co-authored-by: limuting <limuting233@gmail.com>
Co-authored-by: lutianyi <lutianyi233@163.com>
Co-authored-by: justinychuang <huangyicheng@soulcode.cn>
Co-authored-by: maybehokori <138367708+maybehokori@users.noreply.github.com>
2026-07-01 13:29:28 +08:00
Lynn
b6fa5ce4ea Fix: ollama provider (#16519) 2026-07-01 13:24:31 +08:00
Wang Qi
8f24b30652 [Go] Add API /api/v1/chat/recommendation and consolidate with /api/v1/searchbots/related_questions (#16500) 2026-07-01 13:17:16 +08:00
Muhammad Furqan
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>
2026-07-01 10:47:39 +08:00
Yingfeng
6648fe4151 Fix g++ 11 incompatibility issue (#16512) 2026-07-01 10:16:47 +08:00
saltsalt123
f60245f199 feat(mcp): add ragflow_list_datasets and ragflow_list_chats tools (#15384)
## Summary

Add two new MCP tools to the RAGFlow MCP server:

1. **ragflow_list_datasets** - List all accessible datasets with IDs,
names, descriptions
2. **ragflow_list_chats** - List all accessible chat assistants with
IDs, names, descriptions

### Implementation
- Added `list_chats()` method to `RAGFlowConnector`
- Registered both tools in `list_tools()` and `call_tool()`
- Follows existing `ragflow_retrieval` pattern for error handling

### Usage via langchain-mcp-adapters

---------

Co-authored-by: saltsalt123 <saltsalt123@users.noreply.github.com>
Co-authored-by: yzc <yuzhichang@gmail.com>
2026-07-01 09:36:52 +08:00
sxxtony
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>
2026-07-01 09:35:37 +08:00
RazmikGevorgyan
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>
2026-07-01 09:33:41 +08:00
Taranum Wasu
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>
2026-07-01 09:30:54 +08:00
Harsh Kashyap
45fc7feab4 fix(common/time_utils): correct None/empty timestamp fallback and ISO 8601 parsing (#16483)
Recovery PR for #16173 after the fork branch was accidentally reset
during rewrite-cleanup.

Cherry-picked onto current `main`:
- fix(common/time_utils): correct fallback timestamp and ISO-8601
normalization
- fix(common/time_utils): preserve zero timestamps and mark regression
tests
- test(common/time_utils): make fallback assertions deterministic

Supersedes closed #16173 — same branch
`Harsh23Kashyap/fix/time-utils-edgecases`, rebuilt per @yuzhichang
recovery steps in
https://github.com/infiniflow/ragflow/pull/16173#issuecomment-4829663835

---------

Co-authored-by: Harsh Kashyap <harshkashyap@Harshs-MacBook-Pro.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 22:30:44 +08:00
Lynn
b53b693f22 Fix: CI (#16504)
### Summary

Fix race condition in parallel lefthook hooks causing ETXTBSY error
2026-06-30 22:14:11 +08:00
Jack
8e1dc4f308 revert: roll back tests.yml CI changes from PR #16391 (#16505)
## Summary

Two changes to make Go build \& run independent of native libraries
(office_oxide, pdfium, pdf_oxide).

## 1. Make native libraries optional (build.sh + Go source)

## 2. Roll back tests.yml CI changes from PR #16391
2026-06-30 21:50:37 +08:00
Yingfeng
5af361ed68 Add spacy based ner and relationship extractor for both python and Go version with equivalent outputs (#16456)
As title
2026-06-30 21:40:24 +08:00
Hz_
3633d08495 feat(go-api): Migrate Box web OAuth connector APIs to Go (#16480)
This PR migrates the Box web OAuth flow from Python to Go for:

  - POST /api/v1/connectors/box/oauth/web/start
  - GET /api/v1/connectors/box/oauth/web/callback
  - POST /api/v1/connectors/box/oauth/web/result
2026-06-30 18:10:36 +08:00
Yingfeng
63bdf5c5b1 Fix harness streaming emit (#16486) 2026-06-30 18:06:03 +08:00
天海蒼灆
3c946a7e58 fix(agent): add canvas_type filter and field to list_agents API (#15754)
### What problem does this PR solve?

GET /api/v1/agents (list_agents) already supports filtering by
canvas_category, keywords, tags, and owner_ids, but it does not support
canvas_type — even though canvas_type is a persisted field on UserCanvas
and is already accepted on agent create/update APIs.

This gap causes two issues:

Filtering — clients cannot list agents by business category (e.g.
Marketing, Agent, Ingestion Pipeline) without fetching all agents and
filtering client-side.
Response payload — list_agents did not return canvas_type in each canvas
item, so consumers had to call GET /api/v1/agents/{id} per agent to read
it.
This PR adds optional canvas_type query parameter support and includes
canvas_type in the list response.
### Type of change

- [√] 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):
2026-06-30 17:43:26 +08:00
Wang Qi
d2ecd57c59 Fix: UI cannot start up (#16497) 2026-06-30 17:09:09 +08:00
Haruko386
b3af9fc068 fix: remove dup-prefix in bot_routes (#16492) 2026-06-30 17:02:58 +08:00
Rene Arredondo
09dc4c8841 fix(agent): return session_id when chat completion produces no events (#15169) (#15228)
## Summary

Fixes #15169 — `POST /api/v1/agents/chat/completions` returned
`data: {}` with no `session_id` when the agent produced no events
(e.g. the reporter's payload sent `"query": ""`).

## Root cause

For `{"agent_id": "...", "query": "", "stream": false}`:

1. No `session_id` in the request → new-session branch at
   `agent_api.py:1278`.
2. `session_id = get_uuid()` at `agent_api.py:1294`.
3. Falls into `_run_workflow_session`.
4. `canvas.run(query="")` produces no events, so `final_ans`
   stays `{}`.
5. Non-streaming path then hit:

   ```python
   if not final_ans:
       await commit_runtime_replica()
       return get_result(data={})
   ```

   `session_id` was allocated but silently dropped on the way out.

The streaming path had the same shape (only a bare `[DONE]` was
yielded — no SSE event carrying `session_id`). The
session-continuation path at `agent_api.py:1463` had the same bug
for callers that passed `session_id` and got `{}` back.

The successful (non-empty) paths were fine because every canvas
event has `ans["session_id"] = session_id` attached before being
yielded / captured into `final_ans` (see
`agent_api.py:255` and `:303`).

## Fix

Three minimal changes, all in
`api/apps/restful_apis/agent_api.py`:

1. **`_run_workflow_session` (non-streaming)**:
   `return get_result(data={"session_id": session_id})` instead of
   `data={}`.
2. **`_run_workflow_session` (SSE)**: if the canvas loop emits no
   events, yield one
   `data:{"session_id": "...", "data": {}}` event before
   `[DONE]`, so the client receives the id over the wire.
3. **`agent_chat_completion` session-continuation**: echo the
   caller-supplied `session_id` back in the empty-events case
   instead of `{}`.

No change needed on the happy paths — they already attach
`session_id` to every event.

## Test plan

- [ ] Repro from the issue: `POST /api/v1/agents/chat/completions`
      with `{"agent_id": "<id>", "query": "", "stream": false}`.
      Response `data` should now contain `session_id`.
- [ ] Same payload with `"stream": true`. SSE stream should
      contain one event with `session_id` before `data:[DONE]`.
- [ ] Same shape but with a real, non-empty `"query"` (new
      session). Response should be unchanged from before — every
      event still carries `session_id`, final response still
      includes it on `final_ans`.
- [ ] Pass an existing `session_id` plus `"query": ""`. Response
      should echo that `session_id` back instead of `{}`.
- [ ] Pass an existing `session_id` plus a normal query. Response
      should be unchanged from before.
- [ ] `openai-compatible: true` path is untouched — sanity-check
      it still works.
- [ ] 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):
2026-06-30 16:41:44 +08:00
Wang Qi
3bb976b383 [Go] Add /api/v1/searchbots/mindmap and /api/v1/chat/mindmap (#16443) 2026-06-30 16:35:33 +08:00
Zhichang Yu
4c54cefd29 Port 14 upstream agent security / correctness fixes to Go canvas (#16455)
Mirrors 14 merged upstream PRs into the Go agent port.

PRs ported:
  - #15609 ExeSQL SSRF guard + DNS pin
  - #15436 HTTP timeout on external API tools
  - #16363 be_output restore + DeepL error path
  - #15644 switch no longer matches empty condition
  - #15374 session_id bind to path agent_id (DAO idor guard)
  - #16169 sandbox artifact ownership gate
  - #15457 tenant ownership on agentbots
  - #15145 rerun agent document access check
- #15446 thinking switch (component portion; provider policy lives in
internal/llm)
  - #15426 Invoke URL/proxy SSRF + DNS pin + no-redirects
  - #15238 agentbot thinking-logs beta endpoint
  - #14589 UserFillUp SSE event propagation
  - #14890 anonymous webhook opt-in
- #15068 PipelineChunker new component (text/file_ref/parser_id
dispatch; file-format extraction is a follow-up)

40 files, +2355 / -58 lines. 33 new tests, all targeted package suites
pass (1721 + 4 skipped); 1 pre-existing flaky test unrelated.
2026-06-30 16:28:48 +08:00
Rene Arredondo
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>
2026-06-30 15:48:59 +08:00
Jin Hai
bd56a1473f Go CLI: merge function (#16458)
### Summary

1. remove unused code.

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-30 15:47:26 +08:00
chanx
9542e6d530 fix: adjust width of messageItemSectionLeft to fit-content (#16488) 2026-06-30 15:37:22 +08:00
Wang Qi
2018eec0dc Fix: allow any host for url for development (#16459) 2026-06-30 10:19:04 +08:00
dependabot[bot]
540acb4892 build(deps): bump crawl4ai from 0.8.9 to 0.9.0 (#16470) 2026-06-30 09:34:48 +08:00
maoyifeng
5276baf1f9 Go CLI: add admin_command response table funtion (#16454)
### Summary

Go CLI: add admin_command  response table funtion

---------

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-06-30 00:25:39 +08:00
Jin Hai
6370fce3f0 Go CLI: add show users plan summary (#16463)
### Summary

```
RAGFlow(admin)> show users plan summary;
+---------+----------------------------------------------------------------+
| field   | value                                                          |
+---------+----------------------------------------------------------------+
| command | show_users_plan_summary                                        |
| error   | 'Show users plan summary' is implemented in enterprise edition |
+---------+----------------------------------------------------------------+
```

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-29 22:28:45 +08:00
Attili-sys
5fc254eb2e Feature big query connector (#15871)
### What problem does this PR solve?

This PR adds Google BigQuery as a first-class data source connector in
RAGFlow.

It enables users to ingest and sync BigQuery data using the same
row-to-document model used by relational database connectors: selected
content columns become document text, metadata columns become document
metadata, an optional ID column provides stable document IDs, and an
optional timestamp column enables cursor-based incremental sync.

The connector supports service-account JSON credentials, table mode,
custom query mode, GoogleSQL queries, cursor-based incremental sync,
deleted-row pruning support, configurable query limits such as
`maximum_bytes_billed`, dry-run validation, batch loading, stable
document IDs, and BigQuery-aware value serialization.
2026-06-29 22:08:40 +08:00
Jin Hai
1087a25f22 Revert "feat(go-api): Add Go chat session message delete and feedback APIs" (#16465)
Reverts infiniflow/ragflow#16442
2026-06-29 21:37:11 +08:00
writinwaters
c1175137e4 Docs: Added an FAQ (#16466)
### Summary

Added an FAQ.
2026-06-29 21:20:48 +08:00
Haruko386
1c0cdd84ce feat[Go]: implement searches/<search_id>/completions POST (#16440)
### Summary

As title

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-29 20:07:12 +08:00
Jin Hai
7c1edca15e Go CLI: fix api commands (#16457)
### Summary

As title.

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-29 19:09:32 +08:00
Wang Qi
48b77022f4 [Go] Fix beta auth for /documents/images/:image_id and /documents/:id/preview and /thumbnails (#16453) 2026-06-29 19:08:49 +08:00
Hz_
a553886989 feat(go-api): Add Go chat session message delete and feedback APIs (#16442)
### Summary

```
/api/v1/chats/<chat_id>/sessions/<session_id>/messages/<msg_id> DELETE
/api/v1/chats/<chat_id>/sessions/<session_id>/messages/<msg_id>/feedback PUT
```

Migrates the chat session message delete and feedback APIs to the Go
server, matching the Python behavior for authorization, session
ownership checks, message/reference updates, and feedback validation.

### Testing

  - `/usr/local/go/bin/go test ./internal/service ./internal/handler`
- Verified through the frontend page for deleting chat messages and
updating message feedback
2026-06-29 19:05:50 +08:00
Hz_
a10a2d8769 fix(py): chat message reference deletion index (#16436)
Fix the reference index used when deleting a chat message pair.

Each user/assistant message pair shares one reference entry, while the
first assistant prologue has no reference. Using `i // 2` correctly
removes the reference for the deleted pair and avoids deleting the
previous turn's reference.
2026-06-29 19:05:25 +08:00
Haruko386
445a13ee9a fix: new chat cannot be edit (#16434)
### What problem does this PR solve?

As title
main fix:

```go
if _, ok := req["meta_data_filter"]; !ok || req["meta_data_filter"] == nil {
	req["meta_data_filter"] = map[string]interface{}{}
}
```


### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
2026-06-29 19:04:59 +08:00
Haruko386
43f75fdfc7 fix: unable to upload avatar for search (#16437)
### What problem does this PR solve?

As title

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-29 19:04:30 +08:00
Haruko386
c5e10a1578 fix: auth middleware double responses on early rejection (#16444)
### Summary

As title:
2026-06-29 19:02:37 +08:00
Jack
98323e7910 Refactor: oss parser go refactor (#16391)
### What problem does this PR solve?

Package refactor and PDF post process.

### Type of change

- [x] Refactoring

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-29 18:46:41 +08:00
Wang Qi
c0f64295c2 [Go] Fix searchbot retrieval_test accept kb_id as array, fix model recognize (#16452) 2026-06-29 17:17:20 +08:00
Jin Hai
3202ec6abf Go CLI: refactor commands (#16447)
### Summary

1. Move debug commands to dev file.
2. Refactor some commands syntax

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-29 17:03:26 +08:00
Wang Qi
ec5cd6b1c0 [Go] Fix searchbot BETA auth (#16450) 2026-06-29 16:44:21 +08:00
chanx
ca17808f12 fix: user-setting modal fixes and DOMPurify cleanup (#16449)
### Summary
  fix: user-setting modal fixes and DOMPurify cleanup
- HighlightMarkdown: drop post-process DOMPurify pass (ineffective after
preprocessLaTeX; Coderabbit CRITICAL
#3486038798)
- SettingTeam: add invite-only-registered-users hint to add-user modal
- SettingModel: reset provider loading state when add-provider modal
closes
- MCP edit dialog: set maskClosable=false to prevent accidental
dismissal
- Form: switch FormDescription color from text-muted-foreground to
text-text-disabled
2026-06-29 16:38:23 +08:00
Wang Qi
9b726a519e Fix: failed to get embedding model by embd_id: model config not found BAAI/bge-m3@...@SILICONFLOW (#16445) 2026-06-29 15:40:29 +08:00
Harsh Kashyap
ebd4f4e633 fix(rag/nlp): handle non-numbered DOCX heading styles (#16219)
## What problem does this PR solve?

DOCX parsing could crash when a paragraph used a `Heading`-prefixed
style without a trailing numeric level, such as `Heading`, `Heading1`,
or `Heading Title`.

`docx_question_level()` assumed every heading style looked like `Heading
N` and called `int(p.style.name.split(" ")[-1])`. For non-numbered
heading styles, that raises `ValueError` and breaks Manual, Q&A, and
Laws chunking.

This PR parses heading levels safely and falls back to level 1 for
Heading-prefixed styles without an explicit numeric suffix.

Closes #16163.

## Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Test Case (non-breaking change which adds test coverage)
2026-06-29 15:21:17 +08:00
Wang Qi
6e82e2726d Guard /datasets/{dataset_id}/chunks cannot parse ingestion pipeline, use /documents/ingest instead (#16395) 2026-06-29 13:45:29 +08:00
euvre
a339e8a579 feat: handle partial upload success in document batch upload (#16438) 2026-06-29 13:06:14 +08:00
Jin Hai
d56c17b1e6 Fix PR template (#16439)
### What problem does this PR solve?

Update Github PR template

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-29 12:03:28 +08:00
writinwaters
6d6e32a1d0 Docs: Updated release date and cli installation commands (#16435)
### What problem does this PR solve?

Updated release date and cli installation commands.

### Type of change

- [x] Documentation Update
2026-06-29 11:32:09 +08:00
Jin Hai
d4ef3d21d1 Go CLI: Add create and drop commands (#16430)
### What problem does this PR solve?

1. Add CREATE and DROP DATASET / MEMORY / AGENT / SEARCH / CHAT.
2. Add option to build.sh to strip RAGFlow binary.

### Type of change

- [x] Refactoring

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-29 11:13:14 +08:00
buua436
b6dbb2f71e fix: update variable completeness check to allow None parameter (#16389) 2026-06-29 10:51:57 +08:00
Carl Harris
61ac1c1dff refactor: enhance UI components and improve layout (#15984) 2026-06-29 10:40:28 +08:00
Willsgao
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>
2026-06-29 09:45:17 +08:00
Rene Arredondo
dc07b6ca8f Feat: add duplicate action to agent list (#14769) (#14856)
closes #14769 


### 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

- [ ] 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: Zhichang Yu <yuzhichang@gmail.com>
2026-06-29 09:45:17 +08:00
vincimirror
9f6f0c5582 Fix: clean up iteration child nodes and edges on delete (#13889) (#14033)
### What problem does this PR solve?

Fixes a workflow editor bug where deleting an Iteration Box could leave
orphan child nodes and dangling edges in client state. Those stale
references could be exported with the workflow and later cause rendering
errors, broken connections, and unstable editing behavior.

### Root Cause

Iteration deletion logic only removed the container, its direct
children, and some internal edges. It did not consistently remove the
full descendant subtree or all edges connected to deleted child nodes,
and the keyboard delete path was not expanded to include Iteration
descendants.

### 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>
2026-06-29 09:45:17 +08:00
jony376
8fb692f10a fix(agent): enforce document access on POST /api/v1/agents/rerun (#15145)
## Related issues

Closes #15144

### What problem does this PR solve?

`POST /api/v1/agents/rerun` loaded a pipeline operation log by UUID via
`PipelineOperationLogService.get_documents_info` with no authorization,
then wiped chunks, reset document counters, deleted tasks, and re-queued
dataflow for the victim document.

Any authenticated user who knew a victim's pipeline log id could disrupt
parsing on documents they did not own.

### 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):

### Changes

| File | Change |
|------|--------|
| `api/apps/restful_apis/agent_api.py` | Call
`DocumentService.accessible(doc["id"], tenant_id)` before destructive
rerun operations; deny with generic `"Document not found."` |
|
`test/unit_test/api/apps/restful_apis/test_rerun_agent_authorization.py`
| Unit tests: cross-tenant log rejected, missing/unauthorized same
message, authorized rerun proceeds |

### Security notes

- **CWE-639:** Closes cross-tenant pipeline rerun / chunk wipe via
leaked log UUID.
- `tenant_id` from `@add_tenant_id_to_kwargs` is `current_user.id`;
`DocumentService.accessible` covers team-shared KBs.

### Test plan

- [ ] `pytest
test/unit_test/api/apps/restful_apis/test_rerun_agent_authorization.py`
- [ ] Manual: attacker cannot rerun victim pipeline log id

```bash
cd ragflow
uv run pytest test/unit_test/api/apps/restful_apis/test_rerun_agent_authorization.py -q
```

---------

Co-authored-by: Zhichang Yu <yuzhichang@gmail.com>
2026-06-29 09:45:17 +08:00
Tim Wang
f0f10b6092 Fix: UserFillUp interactive forms not working in agent explore mode (#14589)
## Summary

- **Backend**: `_iter_session_completion_events` in `agent_api.py` was
filtering out `user_inputs` and `workflow_finished` SSE events, causing
agents with UserFillUp components to silently fail in explore mode — the
interactive form never appeared, while the same agent worked correctly
in run (editor) mode.
- **Frontend**: `SessionChat` component in explore mode was missing
`DebugContent` children rendering inside `MessageItem`, so even if the
backend forwarded the events, the form UI would not render. Added
`DebugContent`, `MarkdownContent`, `useAwaitCompentData` hook, and
input-disabling logic to match the run mode's `chat/box.tsx` behavior.

## What was changed

### Backend (`api/apps/restful_apis/agent_api.py`)
- Line 266: Added `"user_inputs"` and `"workflow_finished"` to the
allowed event filter in `_iter_session_completion_events`

### Frontend (`web/src/pages/agent/explore/components/session-chat.tsx`)
- Added imports: `DebugContent`, `MarkdownContent`,
`useAwaitCompentData`, `useParams`
- Added `sendFormMessage` from `useSendSessionMessage()` hook
- Added `useAwaitCompentData` hook for form state management
- Added `DebugContent` as `MessageItem` children for the latest
assistant message (renders UserFillUp form)
- Added `MarkdownContent` + submitted values display for previous
assistant messages
- Updated `NextMessageInput` disabled states to respect `isWaitting`
(form submission in progress)

## Test plan

- [x] Agent with UserFillUp component (e.g., email draft with
send/edit/cancel options) shows interactive form in **explore mode**
- [x] Same agent continues to work correctly in **run (editor) mode**
- [x] Form submission sends data back to the agent and workflow
continues
- [x] Input field is disabled while waiting for form submission
- [ ] Agents without UserFillUp components are unaffected in explore
mode

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Zhichang Yu <yuzhichang@gmail.com>
2026-06-29 09:45:17 +08:00
kpdev
212429bf9d fix(api): gate sandbox artifact download on agent session ownership (#16169)
Fixes #16168

## Summary
- Add session-scoped authorization for `GET
/api/v1/documents/artifact/<filename>`
- Allow download only when the artifact filename appears in the caller's
`api_4_conversation` message and
`UserCanvasService.accessible(dialog_id, user_id)` passes
- Deny with generic `"Artifact not found."` before storage access (no
cross-user enumeration)
- Return 4xx when the blob is missing (existing behavior preserved)

## Approach
Sandbox artifacts are runtime CodeExec outputs, not KB documents — this
uses the same session gate pattern as `agent_chat_completion`, not
`DocumentService.accessible`.

## Test plan
- [x] Unit: denied when filename not referenced in user sessions
- [x] Unit: denied when agent canvas is not accessible
- [x] Unit: authorized user receives bytes; missing blob returns
`"Artifact not found."`
- [ ] `pytest
test/testcases/test_web_api/test_document_app/test_document_metadata.py
-k get_artifact`

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Zhichang Yu <yuzhichang@gmail.com>
2026-06-29 09:45:16 +08:00
Hernandez Avelino
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>
2026-06-29 09:45:16 +08:00
Renzo
6079ded70b fix: require explicit anonymous webhook access (#14890)
### What problem does this PR solve?

Fixes #14882

Agent webhook execution currently fails open when the saved webhook
`security` block is missing/empty, or when `auth_type` is set to `none`.
This allows unauthenticated webhook invocation without an explicit
operator opt-in.

This PR makes anonymous webhook access explicit:
- Rejects missing or empty webhook security config.
- Requires `allow_anonymous: true` when `auth_type` is `none`.
- Preserves explicit anonymous webhooks by having the frontend serialize
`allow_anonymous: true` when the user selects `None` auth.
- Updates webhook unit tests to cover both denied implicit-anonymous
configs and allowed explicit-anonymous configs.

### Type of change

- [x] Bug Fix
- [x] Security hardening
- [x] Test

### Tests

- [x] `ZHIPU_AI_API_KEY=dummy uv run python -m pytest
--confcutdir=test/testcases/test_web_api/test_agent_app
test/testcases/test_web_api/test_agent_app/test_agents_webhook_unit.py`
- [x] `uv run ruff check api/apps/restful_apis/agent_api.py
test/testcases/test_web_api/test_agent_app/test_agents_webhook_unit.py`
- [x] `npm exec eslint src/pages/agent/utils.ts
src/pages/agent/form/begin-form/schema.ts`

---------

Co-authored-by: Zhichang Yu <yuzhichang@gmail.com>
2026-06-29 09:45:16 +08:00
philluiz2323
43a9d53c72 fix(agent): enforce tenant ownership on agentbots completions/inputs (#15457)
### What problem does this PR solve?

Fixes #15456.

The SDK agent-bot routes `POST /api/v1/agentbots/<agent_id>/completions`
and `GET /api/v1/agentbots/<agent_id>/inputs`
(`api/apps/restful_apis/bot_api.py`) authenticate the caller with a beta
API token — which only yields the caller's `tenant_id` — but then load
and run the agent named in the URL **without verifying the agent belongs
to the caller's tenant**. `UserCanvasService.get_agent_dsl_with_release`
even accepts a `tenant_id` it never uses, and `begin_inputs` calls
`get_by_id` directly. Any holder of a single valid beta token could
therefore run another tenant's agent (leaking its DSL/prompts/tool
config) or read another tenant's agent metadata and begin input form,
just by substituting a victim `agent_id`.

This PR adds the project's existing ownership gate,
`UserCanvasService.accessible(agent_id, tenant_id)`, to both endpoints
right after token authentication — mirroring the checks already enforced
on the equivalent first-party routes in
`api/apps/restful_apis/agent_api.py` (lines 75/578/775) and on the
sibling `chatbot_completions` / `create_agent_session` /
`delete_agent_session` handlers in the same file. On failure it returns
the same `Can't find agent by ID: <id>` message already used by
`begin_inputs`, so it does not reveal whether an `agent_id` exists in
another tenant.

Added a regression test
(`test/unit_test/api/apps/restful_apis/test_agentbots_access_control.py`,
following the existing stubbed-loader pattern from
`test_get_agent_session.py`) asserting that an inaccessible `agent_id`
is rejected before the agent is loaded (`begin_inputs`) or executed
(`completions`), and that an accessible agent still proceeds.

### 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>
2026-06-29 09:45:16 +08:00
Rene Arredondo
7ecc0908ef fix(agent): authenticate "Thinking" button in shared/embedded chat via beta token (#14985) (#15238)
## Summary

Fixes #14985 — clicking the **Thinking** button in a shared/embedded
chat returns 401 and bounces the user to the login page, even though
the same share page can chat with the agent just fine.

## Root cause

In shared chat, `useGetSharedChatSearchParams` binds `conversationId`
to the URL's `shared_id` query param — which is the **beta APIToken**,
not the real agent id. That `conversationId` propagates through the
component tree:

```tsx
<WorkFlowTimeline canvasId={conversationId}>
  → useFetchMessageTrace(canvasId)
  → GET /api/v1/agents/<sharedId>/logs/<messageId>
```

But `/agents/<agent_id>/logs/<message_id>` is decorated with
`@login_required` (`api/apps/restful_apis/agent_api.py:842-846`).
The share page only holds the beta token — there is no session JWT
— so the request 401s and quart-auth redirects to the login page.
The reporter's server log matches exactly:

```
load_user from jwt got exception No b'.' found in value
load_user: No APIToken found for token=ULG10SWG3E...
Unauthorized request (quart_auth)
GET /api/v1/agents/394013f8d42211f0bad6123fa55e8ed9/logs/96fd72e2-... 1.1 401
```

The `394013f8...` segment in the URL is the `shared_id` (beta
token), not an actual agent id. `_load_user` already accepts the
regular `APIToken.token` field, but not `APIToken.beta`, by design
— beta is a much weaker share-link credential than a personal API
key.

The sibling endpoints `/agentbots/<id>/completions` and
`/agentbots/<id>/inputs` already use the right auth pattern for
this scope (beta-token via `_get_sdk_authorization_token` →
`APIToken.query(beta=token)`). Trace just didn't have a parallel.

## Fix

### Backend (`api/apps/restful_apis/bot_api.py`)

Added a beta-token sibling endpoint:

```
GET /api/v1/agentbots/<shared_id>/logs/<message_id>
```

- Same auth shape as the existing `agentbots` endpoints.
- The `<shared_id>` path segment is a client-supplied label only.
  The real `agent_id` used to build the Redis key
  (`<agent_id>-<message_id>-logs`) is taken from
  `APIToken.dialog_id` on the looked-up token, so the endpoint
  never trusts client-supplied identifiers for the data lookup.
- Returns the same `{data: ...}` shape as the existing
  `/agents/<id>/logs/<message_id>` endpoint, so the frontend
  doesn't need to reshape the response.

### Frontend

- `web/src/utils/api.ts`: added `sharedTrace(sharedId, messageId)`
  URL builder.
- `web/src/services/agent-service.ts`: added
  `fetchSharedTrace({ shared_id, message_id })`.
- `web/src/hooks/use-agent-request.ts`: `useFetchMessageTrace`
  takes an optional `isShare` argument. When set, it calls
  `fetchSharedTrace`; `isShare` is also folded into the
  `queryKey` so the two modes never share cached results.
- `web/src/pages/agent/log-sheet/workflow-timeline.tsx`:
  forwards the already-existing `isShare` prop into the hook.

All other existing call sites of `useFetchMessageTrace` (webhook
timeline, pipeline log, dataflow result) pass no `isShare`
argument → undefined → falsy → unchanged behavior.

## Test plan

- [ ] In the regular Agent UI (logged-in user): open the trace /
      log sheet for any message and click into "Thinking" — the
      timeline should still load via `/agents/<id>/logs/<msg>`,
      same as before.
- [ ] From the Agent page, click **Chat in new tab** to open
      `/chat/share?shared_id=<token>&from=agent`. Send a message,
      wait for a response, then click **Thinking** on the
      assistant turn. The trace panel should load instead of
      redirecting to the login page.
- [ ] Same flow but with the agent embedded in an iframe ("Embed
      into webpage") — confirm there is no login redirect.
- [ ] In DevTools → Network, confirm the share-chat trace request
      goes to `/api/v1/agentbots/<sharedId>/logs/<msgId>` and
      returns 200 with the same JSON shape as the logged-in path.
- [ ] Confirm the chat completions, inputs, and upload flows in
      the share page still work — they were not touched.
- [ ] Send a bogus / expired beta token to the new endpoint and
      confirm it returns the standard "Authentication error: API
      key is invalid!" response (no traceback, no 500).
- [ ] 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: Zhichang Yu <yuzhichang@gmail.com>
2026-06-29 09:45:16 +08:00
jony376
7b81f63653 fix(agent): bind session_id to path agent_id on GET/DELETE agent sessions (#15374)
## Related issues

Closes #15128

### What problem does this PR solve?

`GET` and `DELETE` `/api/v1/agents/<agent_id>/sessions/<session_id>`
verified canvas access for `agent_id` in the URL but loaded/deleted
sessions only by `session_id`, without checking `conv.dialog_id ==
agent_id`.

Any user with access to **any** agent could read or delete another
agent's `API4Conversation` session (messages, references, DSL, etc.)
when they knew the session UUID.

Agent completions in the same file already enforce this binding; chat
sessions do too — these two routes were inconsistent.

### 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):

### Changes

| File | Change |
|------|--------|
| `api/apps/restful_apis/agent_api.py` | Require `conv.dialog_id ==
agent_id` in `get_agent_session` and `delete_agent_session_item`; return
generic `"Session not found!"` on mismatch |
| `test/unit_test/api/apps/restful_apis/test_get_agent_session.py` | Add
IDOR regression tests for GET/DELETE; fix success fixture to include
`dialog_id`; track `delete_by_id` calls |

### Test plan

- [x] Unit tests added for GET/DELETE IDOR and success paths
- [ ] `pytest
test/unit_test/api/apps/restful_apis/test_get_agent_session.py`

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Zhichang Yu <yuzhichang@gmail.com>
2026-06-29 09:45:16 +08:00
seekmistar01
608fc5df4d fix(agent): Switch no longer matches an empty condition (all([]) is True) (#15644)
## Summary
Fixes the agent `Switch` component matching an **empty/all-skipped
condition** unconditionally because `all([]) is True`.

## Root cause
`res` only accumulates for items with a non-empty `cpn_id` (blank ones
`continue`). For a condition with empty `items` (or all-blank `cpn_id`),
`res == []`, and `if all(res):` is `True`, so the Switch routes to that
condition's `to` target before reaching the else/`end_cpn_ids` branch.

## Fix
```diff
-            if all(res):
+            if res and all(res):
```
An empty result set no longer counts as a match; genuinely-satisfied
"and" conditions still route (the real `all(res)` path is preserved).

## Files changed
- `agent/component/switch.py`
- `test/unit_test/agent/component/test_switch_empty_condition.py` (new)

## Verification
- `ruff check` / `ruff format --check` — clean
- Added unit tests (mirroring the existing `_FakeCanvas` component-test
pattern): an empty/all-skipped "and" condition now falls through to
`end_cpn_ids`; a genuinely-satisfied "and" condition still routes to its
target.
- Local full pytest not run (heavy RAG deps); CI validates.

## Note
Implemented with LLM assistance (model: claude-opus-4-8).

Closes #15643

---------

Co-authored-by: seekmistar01 <seekmistar01@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Zhichang Yu <yuzhichang@gmail.com>
2026-06-29 09:45:16 +08:00
philluiz2323
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>
2026-06-29 09:45:16 +08:00
jiashi19
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>
2026-06-29 09:45:16 +08:00
Harsh Kashyap
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>
2026-06-29 09:45:16 +08:00
cleanjunc
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>
2026-06-29 09:45:16 +08:00
Khaostica
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>
2026-06-29 09:45:16 +08:00
Zhichang Yu
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
2026-06-29 09:45:16 +08:00
Zhichang Yu
ee165c5dd7 build(codeql): exclude office_oxide CGO files so Go analysis completes (#16410)
## Problem

The CodeQL Go analysis was failing on the entire codebase with:

  fatal error: office_oxide.h: No such file or directory

because six ingestion parser files (`doc`, `docx`, `ppt`, `pptx`, `xls`,
`xlsx`) import `github.com/yfedoseev/office_oxide/go`, a CGO binding to
a Rust library. The CodeQL runner image doesn't ship the
`office_oxide.h` native header, so the Go AST build aborts before CodeQL
can analyze anything.

This means **no Go-language alerts have been re-evaluated** since the
suppression comments were added in #16407 and #16408. The most recent
CodeQL run fixed 51 alerts (all Python/JS), but every Go alert stayed
open, including ones in files that have nothing to do with office_oxide.

## Fix

Add a `.github/codeql/codeql-config.yml` that uses `paths-ignore` to
skip the six parser files. The rest of the Go tree is pure Go (no CGO)
and analyzes cleanly.

The parser files are also excluded from local `go test` / `go build`
when the office_oxide C library isn't installed, so this brings CodeQL
in line with the existing toolchain.

## Expected outcome

After this PR merges, the next CodeQL run on main will:

1. Complete successfully (Go analysis no longer aborts)
2. Re-evaluate the alerts in the remaining files
3. Match the existing `// codeql[go/...] suppression comments` added in
#16407 and #16408
4. Close those alerts

This should drop the open-alert count from 44 to near zero (the 6 Python
clear-text-logging and 1 JS prototype-pollution alerts that were added
in #16408 will also be re-evaluated).

## Why not just install office_oxide in the CodeQL runner?

- The `office_oxide` Go binding is a 3rd-party module
(`github.com/yfedoseev/office_oxide/go`) with CGO that pulls in a Rust
crate
- The CodeQL runner uses a stock Go toolchain that doesn't include the C
library
- Installing it would require modifying the GitHub-managed CodeQL
workflow, which is owned by GitHub and not easily customizable
- The parsers are also unimplemented stubs (each `Parse` function logs
the filename and returns `nil` after my earlier clear-text-logging fix),
so they have no security-relevant code to scan anyway

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-29 09:45:16 +08:00
Zhichang Yu
0c3952147c fix(codeql): close remaining 44 CodeQL alerts post-merge (#16408)
## Summary

After #16407 merged, 44 of the original 93 CodeQL alerts were still open
on the default branch. This PR closes the remaining ones by:

1. **Moving 32 existing `// codeql[...]` directives** so they sit on the
line **immediately before** the suppressed statement. The original
multi-line suppression blocks had the directive as the first line, with
the rationale on subsequent lines. After line shifts (refactors, linter
reformat), the directive ended up several lines above the alert location
— CodeQL only recognizes the suppression when it appears on the line
directly above. (32 alerts across 27 files.)

2. **Adding 9 new `// codeql[...]` suppressions** for alerts that had no
suppression in the preceding lines at all — mostly real-fixes that
CodeQL conservatively still flags (filepath.Base, bounded slice sizes,
model-identifier strings, the MD5-legacy-migration lookup in
`conversation_service.py`).

## Files changed

- `api/db/services/conversation_service.py` — add
`py/weak-sensitive-data-hashing` suppression (MD5 for backward-compat
legacy row lookup; not used for auth)
- `api/db/services/llm_service.py` — 3×
`py/clear-text-logging-sensitive-data` suppressions on the lines that
log `llm_name` in warnings/info
- `common/misc_utils.py` — 2× `py/clear-text-logging-sensitive-data`
suppressions on the redacted `current_url` log sites
- `internal/agent/component/invoke.go` — moved existing
`go/request-forgery` directive
- `internal/agent/sandbox/ssh.go` — moved existing
`go/command-injection` directive
- `internal/agent/tool/retrieval_service.go` — added
`go/uncontrolled-allocation-size` suppression (`topN` is bounded to 1024
above)
- `internal/cli/common_command.go` — moved 2×
`go/disabled-certificate-check` directives
- `internal/cli/user_command.go` — added `go/clear-text-logging`
suppression (filepath.Base already strips user-identifying path)
- `internal/dao/pipeline_operation_log.go` — moved 2× `go/sql-injection`
directives
- `internal/dao/user_canvas.go` — added `go/sql-injection` suppression
in `GetList` (the new `userCanvasOrderClause` call path)
- `internal/engine/infinity/chunk.go` — moved existing
`go/unsafe-quoting` directive
- `internal/entity/models/*` — moved `go/path-injection` directives (15
files)
- `internal/handler/oauth_login.go` — moved existing
`go/cookie-httponly-not-set` directive
- `internal/handler/tenant.go` — moved existing `go/path-injection`
directive
- `internal/service/deep_researcher.go` — moved existing
`go/unsafe-quoting` directive
- `internal/service/dataset.go` — added
`go/uncontrolled-allocation-size` suppression (`n` bounded to 1024
above)
- `internal/service/file.go` — moved existing `go/request-forgery`
directive
- `internal/service/langfuse.go` — moved 2× `go/request-forgery`
directives
- `internal/utility/mcp_client.go` — moved 3× `go/request-forgery`
directives
- `internal/utility/smtp.go` — moved existing `go/email-injection`
directive
- `rag/prompts/generator.py` — added
`py/clear-text-logging-sensitive-data` suppression
- `web/.../use-provider-fields.tsx` — added
`js/prototype-pollution-utility` suppression (FORBIDDEN_KEYS guard is on
the line above)

## Why the previous PR left alerts open

`// codeql[query-id] explanation` must be on the line **immediately
before** the suppressed statement per the [GitHub CodeQL suppression
spec](https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/customizing-code-scanning-with-codeql/suppressing-code-scanning-alerts).
The original suppression blocks were 4-5 lines, with the directive as
the **first** line. After linter reformat / line shifts, the directive
ended up too far above the actual alert line to be recognized. The fix
is to put the directive on the line directly above the suppressed
statement, with the rationale above it.

## Test plan

- All 9 modified Python files `ast.parse` clean
- All 4 modified Go files `gofmt` clean
- 36/44 expected alert suppressions in place
- 8 remaining CodeQL alerts are the originals (#3485851828, #3485851831,
#3485869759, #3485869766, #3485869768, #3485869771, #3485885962,
#3485895527) which were resolved by the corresponding commit comments;
these should close on the next scan when the suppression comments match
the alert lines.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-29 09:45:16 +08:00
Zhichang Yu
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)
2026-06-29 09:45:16 +08:00
Zhichang Yu
dfe2dc346d feat[Go]: port agent attachment download, chatbot + agentbot completion/info endpoints from Python (#16405)
## Summary

Ports five Python agent APIs to Go under the v1 Gin router:

- `GET  /api/v1/agents/attachments/<attachment_id>/download`
- `POST /api/v1/chatbots/<dialog_id>/completions`  (SSE)
- `GET  /api/v1/chatbots/<dialog_id>/info`
- `POST /api/v1/agentbots/<agent_id>/completions` (SSE)
- `GET  /api/v1/agentbots/<agent_id>/inputs`

Mirrors the existing Python wire shape (`{code, message,
data:{answer,reference,...}}` per Python `canvas_service.completion`) so
the iframe SDK and existing JS widgets keep working.

## Behavioural parity with Python

| # | Concern | How it's met |
|---|---------|--------------|
| R0 | Bot routes must not require regular user session | Routes mount
on `apiNoAuth` (router.go:198-202), with `BetaAuthMiddleware` only |
| R3 | Two SSE formats in Go drift | F2: `AgentChatCompletions` and
`AgentbotCompletion` share `service.WriteChatbotRunEvent` |
| R7 | `GetBySessionID` returns `(nil, nil)` on miss | Defensive
nil-check before `session.UserID != tenantID` |
| R8 | Begin component name vs ID | `FindBeginComponentID` resolves name
→ ID first, then `ExtractComponentInputForm(dsl, beginID)` |
| R9 | Defensive PromptConfig parsing | `stringFromMap` helper used for
`prologue` and `tavily_api_key` |
| R10 | `BetaAuthMiddleware` Bearer-prefix pre-filter | Removed —
`GetUserByToken` is called unconditionally, falls back to
`GetUserByBetaAPIToken` |
| F8 | Multi-turn chatbot history | `ChatbotCompletion` reads prior
turns from `session.Message`, appends user turn, calls LLM, persists new
pair via new `API4ConversationDAO.Update` |
| F9 | UUID gate stricter than plan | Removed — only `filepath.Base` +
CR/LF/quote header sanitization remains |
| H2 | Defence-in-depth IDOR | `AgentbotCompletion` calls `loadCanvas`
before delegating to `RunAgent` |
| M2 | SSE error leakage | `WriteChatbotFrame` emits generic `"an
internal error occurred"`; real error logged via `common.Error` |

## Verification

```bash
$ go vet ./...                                     # clean (only pre-existing issues)
$ go build ./...                                   # success
$ go test ./internal/handler/ ./internal/service/ ./internal/agent/dsl/ ./internal/common/ ./internal/dao/
ok  ragflow/internal/handler     0.617s
ok  ragflow/internal/service     1.729s
ok  ragflow/internal/agent/dsl   0.008s
ok  ragflow/internal/common      0.087s
ok  ragflow/internal/dao         0.083s
```

1199 tests pass across 5 packages.

## Known follow-ups (out of scope for this PR)

- **F1**: token-level streaming in `ChatbotCompletion` (currently emits
one frame per turn)
- **F3**: per-route `auth_types` attribute in Go (currently applied via
route group middleware)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-29 09:45:16 +08:00
Zhichang Yu
477f2fcebd feat[Go]: port agent webhook trigger, agent file upload/download, component input-form + debug endpoints from Python (#16403)
port agent webhook trigger, agent file upload/download, component
input-form + debug endpoints from Python
- [x] New Feature (non-breaking change which adds functionality)
2026-06-29 09:45:16 +08:00
Zhichang Yu
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)
2026-06-29 09:45:16 +08:00
Liu An
f86a0e7386 Docs: Update version references to v0.26.2 in READMEs and docs (#16387) 2026-06-29 09:45:16 +08:00
Haruko386
9d18f33296 fix: remove dup-method (#16393)
### What problem does this PR solve?

As title

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-26 20:51:10 +08:00
Wang Qi
3a829fb6dd Fix VLM PDF parser only parse first 12 pages, and default page range for PDF files align with backend (#16394)
1. Fix VLM parser only parse first 12 pages
2. Fix frontend default pages 1 - 100000, keep aligned with backend.
2026-06-26 20:15:25 +08:00
Haruko386
a57a841a11 feat[Go]: implement Create-Chat/Session, Delete-Session (#16386)
### What problem does this PR solve?

As title:
implement:
```go
chats.POST("", r.chatHandler.Create)
chats.POST("/:chat_id/sessions", r.chatSessionHandler.CreateSession)
chats.DELETE("/:chat_id/sessions", r.chatSessionHandler.DeleteSessions)
```

bug fixed:

f80d4c7843/internal/handler/chat.go (L84)
↓
```go
result, err := h.chatService.ListChats(userID, "1", keywords, page, pageSize, orderby, desc)
```

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
- [x] Refactoring
2026-06-26 19:23:45 +08:00
Hz_
e3063da390 feat(go-api): add chat update endpoints (#16378)
## Summary

- Added Go API route `PUT /api/v1/chats/:chat_id` to align with Python
`PUT /api/v1/chats/<chat_id>` chat update behavior.
- Added Go API route `PATCH /api/v1/chats/:chat_id` to align with Python
`PATCH /api/v1/chats/<chat_id>` partial chat update behavior.
- Added matching handler and service logic for owner checks, tenant
validation, persisted-field filtering, read-only field filtering,
`dataset_ids` to `kb_ids` conversion, and PATCH shallow merge semantics
for `prompt_config` and `llm_setting`.
2026-06-26 19:22:57 +08:00
Haruko386
a1f1dd5007 feat[Go]: implement Add messages for Go (#16375)
### What problem does this PR solve?

As title

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-06-26 19:21:52 +08:00
Jin Hai
f763044889 Go CLI: Fix show admin server and api server (#16382)
### What problem does this PR solve?

RAGFlow(api/default)> show admin server;

RAGFlow(api/default)> show api server 'default';

RAGFlow(admin)> show admin server;

RAGFlow(admin)> show api server 'default';

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-26 19:16:14 +08:00
Tim Wang
ca96d61e73 Feat: Add New API model provider for OpenAI-compatible gateways (#15991)
## Summary

Add support for **"New API"** as a model provider, enabling connection
to [New API](https://github.com/QuantumNous/new-api) /
[one-api](https://github.com/songquanpeng/one-api) compatible gateways
that aggregate multiple LLM backends behind a unified OpenAI-compatible
`/v1` endpoint.

### Features

- **All model types**: Chat, Embedding, Rerank, Image2Text, TTS,
Speech2Text
- **List Models discovery**: `NewAPI(OpenAIAPICompatible)` class in
`model_meta.py` queries the gateway's `/v1/models` to auto-discover
available models via the native `GET /api/v1/providers/<name>/models`
endpoint
- **Model parameter editing**: Pencil icon on each discovered model row
to edit `model_type`, `max_tokens`, and `features` (e.g. tool call
support) before submitting
- **Custom model addition**: "Add Custom Model" button at the bottom of
the List Models dropdown for models not returned by the API
- **Gear icon settings**: Enabled the Settings gear button on provider
instances to manage models on existing instances (viewMode)
- **viewMode credential passthrough**: Fixed List Models in viewMode —
merges `initialValues` credentials when `api_key`/`base_url` fields are
hidden by `hideWhenInstanceExists`

### Changes

**Backend** (8 files):
- `rag/llm/chat_model.py` — `NewAPIChat(Base)` class
- `rag/llm/embedding_model.py` — `NewAPIEmbed(OpenAIEmbed)` class (no
auto `/v1` append)
- `rag/llm/rerank_model.py` — `NewAPIRerank(Base)` class (uses `/rerank`
endpoint)
- `rag/llm/cv_model.py` — `NewAPICv(GptV4)` class
- `rag/llm/tts_model.py` — `NewAPITTS(OpenAITTS)` class
- `rag/llm/sequence2txt_model.py` — `NewAPISeq2txt(GPTSeq2txt)` class
- `rag/llm/model_meta.py` — `NewAPI(OpenAIAPICompatible)` class for List
Models discovery
- `conf/llm_factories.json` — New API factory entry with all model type
tags

**Frontend** (8 files + 1 new SVG):
- `web/src/assets/svg/llm/new-api.svg` — New API logo icon
- `web/src/constants/llm.ts` — `LLMFactory.NewAPI` enum + `IconMap`
entry
- `web/src/components/svg-icon.tsx` — `NewAPI` added to `svgIcons`
-
`web/src/pages/user-setting/setting-model/modal/provider-modal/field-config/local-llm-configs.ts`
— New API `buildLocalConfig`
-
`web/src/pages/user-setting/setting-model/modal/provider-modal/constants.ts`
— `LIST_MODEL_PROVIDERS` includes NewAPI
- `web/src/pages/user-setting/setting-model/components/used-model.tsx` —
Enable Settings gear button
-
`web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-list-models-picker.ts`
— viewMode credential merge + model editing state/handlers
-
`web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-list-models-options.tsx`
— Pencil edit icon per model row
-
`web/src/pages/user-setting/setting-model/modal/provider-modal/index.tsx`
— `AddCustomModelDialog` import + edit dialog rendering

**Note on Go implementation**: A Go model driver (`NewAPIModel`
delegating to `OpenAIModel`) has been prepared but is deferred until the
Go runtime is enabled in a future release (current v0.26.0 images use
`API_PROXY_SCHEME=python` and do not compile Go binaries). Will submit
as a follow-up PR.

## Related

- Depends on: #15996 (provider instance API improvements — server-side
credential lookup, idempotent `add_model`, security fixes — required for
viewMode gear icon and batch model submission)

## Test plan

- [ ] Add New API provider with api_key and base_url pointing to an
OpenAI-compatible gateway
- [ ] Click "List Models" — should discover and display available models
from `/v1/models`
- [ ] Click pencil icon on a model — should open edit dialog to change
model_type, max_tokens, features
- [ ] Select multiple models and click OK — should add all selected
models
- [ ] Click gear icon on the added instance — should open viewMode with
List Models working
- [ ] In viewMode, select new models including pre-existing ones, click
OK — should succeed (requires #15996)
- [ ] Verify all model types work: create a Chat assistant, Embedding
KB, Rerank setting

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Tim Wang <wanghualoong@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-26 18:47:20 +08:00
chanx
10140b1d02 fix: adjust table height and button position in DatasetTable component (#16390) 2026-06-26 18:46:55 +08:00
Wang Qi
638b59fbcd Fix handle move file failed (#16384)
Follow on PR: #16350
2026-06-26 18:46:21 +08:00
balibabu
d14d2068c4 Fix: If the type of the loop variable in the Loop operator is set to object, an error occurs when clicking the Variable Replicator operator inside it. (#16388) 2026-06-26 18:44:56 +08:00
Lynn
bf1eabea72 Feat: support new qwen model (#16385) 2026-06-26 17:30:16 +08:00
buua436
f80d4c7843 fix: tighten loop validation (#16374) 2026-06-26 16:29:08 +08:00
chanx
9610173a74 feat: add log icon to parsing status display (#16383) 2026-06-26 16:13:01 +08:00
Wang Qi
985e3c1db5 Fix document progress not set to fail when embedding model error (#16381) 2026-06-26 16:11:54 +08:00
Öndery
8081a77c7c Fix missing move and copy methods in Python RAGFlowS3 storage implementation (#16350) 2026-06-26 15:51:24 +08:00
Jin Hai
2667995b25 Go CLI: Fix show model and list models (#16380)
### What problem does this PR solve?

```
RAGFlow(api/default)> show model 'WiseDiag-Z1 Think';

RAGFlow(api/default)> list models;

RAGFlow(admin)> show model 'WiseDiag-Z1 Think';

RAGFlow(admin)> list models;
```

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-26 15:36:01 +08:00
Hz_
0de8f3e127 feat: add missing qwen models to all_models.json (#16379)
Add 19 missing qwen models and 3 aliases to all_models.json.

Models added: qwen-image-2.0-pro (2026-06-22, 2026-04-22), qwen3.5-ocr,
qwen3.7-max-2026-05-17, qwen3.5-livetranslate-flash-realtime,
qwen3.5-omni-plus/flash-realtime, qwen-deep-research-2025-12-15,
qwen-flash-character-2026-02-26, qwen-plus-2025-11-05,
qwen-deep-search-planning, qwen3-s2s-flash-realtime-2025-09-22,
qwen-max-1201/longcontext/0107, qwen-1.8b-longcontext-chat

Aliases: qwen3.5-plus-2026-04-20, qwen-turbo-0919, qwen-1.8b-chat
2026-06-26 15:35:30 +08:00
writinwaters
5af798607e Docs: Added v0.26.2 release notes. (#16373) 2026-06-26 15:18:54 +08:00
Jin Hai
8bc27d8df1 Go CLI: fix show variable (#16370)
### What problem does this PR solve?

```
RAGFlow(api/default)> show var 'mail.port';
+-----------+-----------+--------------+-------+
| data_type | name      | setting_type | value |
+-----------+-----------+--------------+-------+
| integer   | mail.port | config       | 30    |
+-----------+-----------+--------------+-------+
```

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-26 13:51:56 +08:00
Jin Hai
65afaa1292 Model config: add tools (#16371)
### What problem does this PR solve?

```
{
      "name": "glm-4-flash",
      "max_tokens": 128000,
      "model_types": [
        "chat"
      ],
      "tools": {
        "support": true
      }
}
```

```
RAGFlow(admin)> list provider 'zhipu-ai' models;
+------------+---------------+------------+---------------+----------------+-----------+-----------+
| dimensions | max_dimension | max_tokens | model_type    | name           | thinking  | tools     |
+------------+---------------+------------+---------------+----------------+-----------+-----------+
|            |               | 204800     | [chat]        | glm-5          | supported | supported |
|            |               | 204800     | [chat]        | glm-5-turbo    | supported | supported |
```

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-26 11:37:51 +08:00
Jack
70250ec88c Fix: remove deepdoc dep (#16372) 2026-06-26 11:32:16 +08:00
Yash Raj Pandey
dd2c88b768 fix(excel_parser): keep zero-valued cells when building Excel text chunks (#16287) 2026-06-26 09:30:09 +08:00
Jin Hai
58da1d6bc3 Go CLI: fix model related commands (#16368)
### What problem does this PR solve?

```
RAGFlow(api/default)> show provider 'zhipu-ai'

RAGFlow(api/default)> show provider 'zhipu-ai' instance 'test';

RAGFlow(api/default)> show provider 'zhipu-ai' instance 'test' balance;

RAGFlow(api/default)> show provider 'zhipu-ai' model 'glm-4.5';
```

### Type of change

- [x] Refactoring

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-26 07:07:49 +08:00
Jin Hai
dbefadd86a Go CLI: refactor (#16355) 2026-06-25 20:36:50 +08:00
Jack
304d9e02bb Refactor: migrate pdf_parser.py to golang (#16323)
### What problem does this PR solve?

Http API based on onnx model.
pdf_parser.py to golang

### Type of change

- [x] Refactoring
2026-06-25 20:16:16 +08:00
Harsh Kashyap
c7052f4dd1 fix(rag/nlp): treat string input as one phrase in is_english (#16308) 2026-06-25 20:07:09 +08:00
Wang Qi
5defb4e7d6 Revert "fix(deepdoc): keep zero and false Excel cells in __call__" (#16366)
Reverts infiniflow/ragflow#16318
2026-06-25 19:56:47 +08:00
Harsh Kashyap
8d3c3f868c fix(api): validate immutable document fields when value is zero (#16309) 2026-06-25 19:29:12 +08:00
Harsh Kashyap
66d86154ab fix(deepdoc): accept GFM table separators with one or more dashes (#16319) 2026-06-25 19:25:57 +08:00
Hz_
e290a0d23e feat(go-api): Langfuse API key migration behavior (#16356)
## Summary

- Align Langfuse API key set/get/delete behavior with the Python
implementation.
- Improve DAO handling for Langfuse credential save/delete flows.
- Add tests for Langfuse service error handling and API key lifecycle
behavior.
2026-06-25 19:25:55 +08:00
Yoorim Choi
46b97bd1a1 fix(web): fix layout issues with text, overflow, and spacing consistency (#16324) 2026-06-25 19:25:32 +08:00
cleanjunc
e8bb534b90 fix: naive_merge splits oversized sections and counts overlap tokens correctly (#15802) 2026-06-25 19:19:38 +08:00
Harsh Kashyap
0af5d43e8d fix(deepdoc): keep zero and false Excel cells in __call__ (#16318) 2026-06-25 19:12:57 +08:00
Haruko386
43b96223b4 feat[go]: add router for connectors/<connector_id> PATCH (#16358)
### What problem does this PR solve?

As title

/api/v1/connectors/<connector_id> PATCH was implemented in #15512

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
2026-06-25 19:07:52 +08:00
Haruko386
74597b8683 feat[Go]: implemet api: Search/Get/Update-Messages (#16307)
### What problem does this PR solve?

As title:
implement:
```
/api/v1/messages/search GET
/api/v1/messages GET
/api/v1/messages/<memory_id>:<message_id>/content GET
/api/v1/memories/<memory_id>/config GET
/api/v1/messages/<memory_id>:<message_id> PUT
```

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-06-25 19:07:34 +08:00
Harsh Kashyap
49312cace3 fix(api): align use_sql Markdown separator with Source header (#16317) 2026-06-25 19:00:01 +08:00
balibabu
1dfc24003b Fix: An empty message notification pops up at the top of the agent conversation. (#16353) 2026-06-25 17:32:24 +08:00
Wang Qi
31e50b164f Fix [ID:0] not converted to Fig. 1 (#16357) 2026-06-25 17:17:46 +08:00
Wang Qi
ac9469e5f5 Fix add VLLM without apikey will fail (#16352) 2026-06-25 17:17:29 +08:00
Wang Qi
97c519662a Add env ALLOW_ANY_HOST to skip host check (#16351) 2026-06-25 17:17:02 +08:00
maoyifeng
6e7aa75e71 Go:CLI add new response function (#16347)
### What problem does this PR solve?

 add new response function

### Type of change

- [ ] New Feature (non-breaking change which adds functionality)
2026-06-25 16:49:47 +08:00
Yash Raj Pandey
091417980e fix(html_parser): preserve original text when splitting oversized blocks (#16052)
### Bug

`RAGFlowHtmlParser.chunk_block()` splits an oversized block by slicing
the **tokenized** string and storing the joined tokens:

```python
tks_str = rag_tokenizer.tokenize(block)
...
tokens = tks_str.split(" ")
while start < len(tokens):
    chunks.append(" ".join(tokens[start:start + chunk_token_num]))  # tokenized form, not source
```

On the default (Elasticsearch) backend `rag_tokenizer.tokenize`
transforms text: it lowercases/stems Latin words and inserts spaces
between CJK characters. So any text block longer than `chunk_token_num`
is stored as garbled, lowercased, space-segmented text instead of the
source content. The small-block branch correctly stores the original
`block`, so only oversized blocks are corrupted. Affects HTML and EPUB
ingestion (both go through `chunk_block`), degrading retrieved chunks
and the answers generated from them.

### Real tokenizer behavior (infinity-sdk 0.7.0, ES backend)

```
tokenize("Hello World FOO Bar Baz Qux Jumps")  -> "hello world foo bar baz qux jump"   # lowercased + stemmed
tokenize("你好世界这是一个测试")                 -> "你好世界 这 是 一个 测试"            # spaces inserted
```

### Fix

Split the **original** text: break it into atoms (whitespace-delimited
runs for space-separated scripts, per-character for spaceless scripts
such as Chinese) and pack them into pieces of at most `chunk_token_num`
tokens. This preserves the source characters and still splits scripts
that have no whitespace — a plain whitespace split would leave CJK as
one un-splittable chunk.

### Proof (real tokenizer, before/after)

Running the old vs new split against the real `infinity.rag_tokenizer`:

```
ENGLISH "Hello World FOO Bar Baz Qux Lazy Dogs"  (chunk_token_num=4)
  OLD: ['hello world foo bar', 'baz qux jump over', 'lazi dog']          # lowercased + stemmed
  NEW: ['Hello World FOO Bar ', 'Baz Qux Jumps Over ', 'Lazy Dogs']      # preserved; each <= 4 tokens
  NEW preserves text exactly: True

CHINESE "你好世界这是一个测试用例需要被切分成多个块"  (chunk_token_num=3)
  OLD: ['你好世界 这 是', '一个 测试用例 需要', ...]                      # spurious spaces
  NEW: ['你好世', '界这是', '一个测', ...]                               # preserved; each <= 3 tokens
  NEW preserves text exactly: True
```

### Tests

Added `test/unit_test/deepdoc/parser/test_html_parser.py` (English +
Chinese oversized blocks, plus small-block merge). Before the fix the
two oversized tests fail (English shows lowercasing, Chinese shows
inserted spaces); after the fix all pass. `ruff check` clean.
2026-06-25 16:43:35 +08:00
Jin Hai
edfa9be67f Go CLI: fix list provider instance tasks (#16345) 2026-06-25 15:49:31 +08:00
balibabu
3f3a2ece3d Fix: Flexible Chat Configuration (#16293) 2026-06-25 14:56:30 +08:00
Muhammad Furqan
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.
2026-06-25 14:40:56 +08:00
Harsh Kashyap
09047d6edf fix(web): bump lodash past vulnerable range (#16281) 2026-06-25 14:40:39 +08:00
Idriss Sbaaoui
fb8e5ad4b2 Fix multimodal chat image routing for VLM channel requests (#16343) 2026-06-25 14:38:29 +08:00
Muhammad Furqan
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`
2026-06-25 14:34:37 +08:00
Harsh Kashyap
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>
2026-06-25 14:18:24 +08:00
Hz_
54fb5b0fa7 feat(go-api): add Go support for POST /api/v1/datasets/{dataset_id}/documents/{document_id}/chunks (#16256)
## Summary
Add the Go implementation of `POST
/api/v1/datasets/{dataset_id}/documents/{document_id}/chunks`.

This wires the full create-chunk path in Go:
- router and handler registration
- request/response structs
- chunk creation service logic
- embedding generation
- chunk insert into doc engine
- chunk/token counter increment
- `tag_feas` validation
- `image_base64` decoding and chunk image storage/merge
- unit tests for handler and service

## Testing
Unit tests:
- `/usr/local/go/bin/go test ./internal/handler`
- `/usr/local/go/bin/go test ./internal/service/chunk`
- `/usr/local/go/bin/go test ./internal/service`
- `/usr/local/go/bin/go test ./...`

All passed locally.

Manual curl checks:
- basic text chunk: Go passed
- chunk with `important_keywords` / `questions` / `tag_kwd` /
`tag_feas`: Go passed
- blank content validation: Go matched expected `code=102`
- invalid `image_base64` validation: Go matched expected `code=102`
- image upload and repeated image upload / merge path: Go passed twice
2026-06-25 14:15:29 +08:00
chanx
d44359826d fix(web): agent log refetch and slider percentage rounding (#16344) 2026-06-25 13:49:25 +08:00
Jin Hai
17b066e6ae Go CLI: fix list dataset files by dataset name (#16341)
### What problem does this PR solve?

```
RAGFlow(api/default)> list dataset 'ccc' files;
Total: 1
```

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-25 13:41:58 +08:00
Hz_
a6cc3023c5 feat(go-api): implement dataset document upload API (#16295)
## Summary
Migrated the dataset document upload API (`POST
/api/v1/datasets/:dataset_id/documents`) from Python to the Go backend.
It supports local file uploads (`type=local`), web page ingestion
(`type=web`), and empty document creation (`type=empty`).

## Changes
- **Router**: Registered `POST /api/v1/datasets/:dataset_id/documents`
route.
- **Handler**: Implemented `UploadDocuments` handler and its routing
functions (`uploadLocalDocuments`, `uploadWebDocument`,
`uploadEmptyDocument`).
- **Service**: Implemented `UploadLocalDocuments`, `UploadWebDocument`,
and `UploadEmptyDocument` in `DocumentService`.
- **Refactoring**: Moved permission checking logic to a shared helper
for reuse in file and document services.
- **Tests**: Added comprehensive unit tests for the new handler and
service upload paths.

## Verification
Ran and passed the test suite for service and handler packages:
- `go test ./internal/service`
- `go test ./internal/handler`
2026-06-25 13:36:49 +08:00
Hz_
ced51114f4 feat(go-api): add dataset search endpoint (#16304)
### What problem does this PR solve?


- added the new dataset search route and handler
- reused the existing shared SearchDatasets service by adapting
single-dataset requests into dataset_ids=[dataset_id]
- aligned handler error responses with Python behavior for argument/data
errors
- aligned key service error messages such as invalid search_id and mixed
embedding models
- added focused handler and service tests for request mapping and error
behavior

### Tests:

`/usr/local/go/bin/go test ./internal/service -run
'TestSearchDatasetRequestToSearchDatasetsRequest|TestDatasetServiceSearchDatasets'`
`/usr/local/go/bin/go test ./internal/handler -run
'TestDatasetsHandlerSearchDataset'`
2026-06-25 13:32:22 +08:00
Willsgao
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>
2026-06-25 13:10:29 +08:00
buua436
479a9a715e feat: unify provider id or name routing (#16336) 2026-06-25 13:04:21 +08:00
Wang Qi
d0fc75f1bb Fix when empty response not set, it report: ERROR: 'knowledge' (#16338) 2026-06-25 13:02:24 +08:00
Ilya Bogin
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.
2026-06-25 12:12:28 +08:00
Jin Hai
06d45c50cb Example: list_datasets.sh (#16335)
### Type of change

- [x] Other (please describe): example

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-25 10:36:07 +08:00
Jin Hai
7ef4a4a06a Go CLI: list provider instance models, sync and list provider (#16311)
### What problem does this PR solve?

```
RAGFlow(api/default)> list provider 'zhipu-ai' instance 'test' models sync;
+------------+---------------+------------+-------------+------------------+---------------------------------------------+
| dimensions | max_dimension | max_tokens | model_types | name             | thinking                                    |
+------------+---------------+------------+-------------+------------------+---------------------------------------------+
|            |               | 128000     | [chat]      | glm-4.5@z-ai     | map[clear_thinking:true default_value:true] |
|            |               | 128000     | [chat]      | glm-4.5-air@z-ai | map[clear_thinking:true default_value:true] |
|            |               | 202752     | [chat]      | glm-4.6@z-ai     | map[clear_thinking:true default_value:true] |
|            |               | 202752     | [chat]      | glm-4.7@z-ai     | map[clear_thinking:true default_value:true] |
|            |               | 202752     | [chat]      | glm-5@z-ai       | map[clear_thinking:true default_value:true] |
|            |               | 200000     | [chat]      | glm-5-turbo@z-ai | map[clear_thinking:true default_value:true] |
|            |               | 202752     | [chat]      | glm-5.1@z-ai     | map[clear_thinking:true default_value:true] |
|            |               |            | [chat]      | glm-5.2@z-ai     |                                             |
+------------+---------------+------------+-------------+------------------+---------------------------------------------+

RAGFlow(api/default)> list provider 'zhipu-ai' instance 'test' models;

RAGFlow(api/default)> list dataset 'aaa' ingestion tasks;

RAGFlow(api/default)> list dataset '0abe79f9423311f1ad8d38a74640adcc' documents;

```

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-25 10:01:21 +08:00
Yingfeng
5b0b86c276 More resilient graph engine (#16325)
### What problem does this PR solve?

- OpenTelemetry integration
- Checkpoint conformance tests
- State inspector API
- Callbacks
- A series of fault injection tests
- Pregel integration tests

### Type of change

- [x] Refactoring
2026-06-24 23:05:07 +08:00
Haruko386
dd46ece3bc feat[go]: datasets/<dataset_id>/chunks DELETE (#16185)
### What problem does this PR solve?

As title:

`documents.POST("/ingest", r.documentHandler.Ingest)`:

---

<img width="3750" height="2039" alt="image"
src="https://github.com/user-attachments/assets/533c1c3d-af3e-47e6-9f51-a278539b7066"
/>

`datasets.DELETE("/:dataset_id/chunks", r.chunkHandler.StopParsing)`

---

<img width="3621" height="2040" alt="image"
src="https://github.com/user-attachments/assets/022adcdb-1e47-4883-9611-1a695c34007d"
/>


### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-06-24 19:43:18 +08:00
Haruko386
c2665d4ab1 implement: <dataset_id>/embedding/check POST (#16266) 2026-06-24 19:09:43 +08:00
Haruko386
48534d5af3 fix: new dataset can not update configuration (#16291) 2026-06-24 19:08:56 +08:00
Jin Hai
1fc02606ea Go CLI: fix key commands (#16306)
### What problem does this PR solve?

```
RAGFlow(api/default)> set key 'ragflow-JgnarFSCUiV99oOvvMDei7ZzZg1cVlqGd1AMHrHeKE4';
SUCCESS
RAGFlow(api/default)> unset key;
SUCCESS

RAGFlow(api/default)> list provider 'zhipu-ai' instances;

RAGFlow(api/default)> list providers;

RAGFlow(api/default)> list available providers;

RAGFlow(api/default)> list provider 'zhipu-ai' instance 'test' models;
```

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-24 18:48:09 +08:00
Hz_
9a91564194 feat(go-api): align chat session get/update with python behavior (#16239)
## Summary

Align `/chats/:chat_id/sessions/:session_id` GET and PATCH with Python
behavior.
2026-06-24 17:34:01 +08:00
Hz_
dc8ff63f1d feat(go-api): add dataset tags endpoints (#16231)
## Summary

- add `GET /api/v1/datasets/:dataset_id/tags`
- add `PUT /api/v1/datasets/:dataset_id/tags`
- implement dataset tag listing and rename flow
- align rename tag validation and response shape with the Python API
- add handler and service tests for dataset tags

## Routes

- `GET /api/v1/datasets/:dataset_id/tags`
- `PUT /api/v1/datasets/:dataset_id/tags`

## Test

- Run specific tests for dataset tags:
```
go test -v ./internal/service ./internal/handler -run 'TestDatasetServiceListTags|TestDatasetServiceRenameTag|TestDatasetsHandlerListTags|TestDatasetsHandlerRenameTag'
```
- Run all tests for service and handler to verify no regressions:
```
go test ./internal/service ./internal/handler
```
- use curl cmd to test
2026-06-24 17:05:58 +08:00
Jin Hai
9624f70b22 Go CLI: refactor (#16299)
```
RAGFlow(api/default)> list dataset 'e93ab2c04ad111f1b17438a74640adcc' documents;
Total: 1

RAGFlow(api/default)> list datasets;


RAGFlow(api/default)> list chats;
Total: 2

RAGFlow(api/default)> list agents;
Total: 1

RAGFlow(api/default)> list searches;
Total: 1

RAGFlow(api/default)> list keys;
+----------------------------------+---------------+----------------------------------+-----------------------------------------------------+---------------+
| beta                             | create_time   | tenant_id                        | token                                               | update_time   |
+----------------------------------+---------------+----------------------------------+-----------------------------------------------------+---------------+
| GKsLEdSUkl76gJz1k_4fJpSQRIlWsiki | 1782285917523 | 2ba4881420fa11f19e9c38a74640adcc | ragflow-JgnarFSCUiV99oOvvMDei7ZzZg1cVlqGd1AMHrHeKE4 | 1782285917523 |
+----------------------------------+---------------+----------------------------------+-----------------------------------------------------+---------------+
RAGFlow(api/default)> create key;
SUCCESS

RAGFlow(api/default)> drop key 'ragflow-aA4R7AuUD158yh2LDh7IDBiqwOKFDKeTwUSQSLVdPdM';
SUCCESS
```

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-24 16:50:40 +08:00
Hz_
a8651e7f83 fix(go): normalizeDatasetID (#16301)
fix `normalizeDatasetID`
2026-06-24 15:46:37 +08:00
Hz_
e35860ad74 feat(go-api): Align document metadata batch APIs and upload_info with Python (#16269)
## Summary

  Align the Go implementations of these APIs with the Python behavior:

  - `POST /api/v1/datasets/:dataset_id/metadata/update`
  - `PATCH /api/v1/datasets/:dataset_id/documents/metadatas`
  - `POST /api/v1/documents/upload`

  ## What changed

  - Added the Go routes and handlers for the 3 APIs.
  - Aligned batch document metadata updates with Python semantics:
    - support `match` in update items
    - support list append / replace behavior
    - support deleting specific list values
    - remove metadata entirely when it becomes empty
- create metadata for documents that previously had none when updates
apply
    - count `updated` only when a document actually changes
- Aligned `documents/upload` file uploads with Python-style
`upload_info` behavior:
    - store upload-info blobs in the per-user downloads bucket
- return lightweight upload descriptors instead of normal
file-management responses
  - Improved URL upload behavior:
    - SSRF-guarded fetch with redirect validation
    - redirect limit aligned to Python behavior
    - normalize filename and MIME type
    - add `.pdf` when the fetched content is PDF
- normalize HTML content into readable text instead of storing raw HTML
shells

  ## Validation

  ### Unit tests

  Passed:

  - `go test ./internal/service`
  - `go test ./internal/handler`

  Also verified targeted cases for:

  - batch metadata update semantics
  - upload_info URL handling
  - upload_info download bucket behavior

  ### curl checks

Verified the new Go endpoints with `curl` and compared the response
shape and behavior with Python for:

  - `POST /api/v1/datasets/{dataset_id}/metadata/update`
  - `PATCH /api/v1/datasets/{dataset_id}/documents/metadatas`
  - `POST /api/v1/documents/upload`

  The Go responses were checked against Python for:
  - argument validation
  - success response shape
  - metadata update results
  - upload_info result structure
  - file vs URL input handling
2026-06-24 14:52:47 +08:00
Haruko386
97718ec779 feat[Go] implement datasets/<dataset_id>/<index_type> DELETE (#16257)
### What problem does this PR solve?

As title

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-06-24 14:47:55 +08:00
Hz_
368db6fa58 feat(go-api): migrate datasets tags aggregation API to Go (#16181)
### Description

Migrates the datasets tags aggregation API `GET
/api/v1/datasets/tags/aggregation` from Python to Go.

### Changes
- Registered the `GET /api/v1/datasets/tags/aggregation` route.
- Implemented `AggregateTags` in datasets `handler` and `service`.
- Added handler and service `unit tests`.

### Test Verification
- Verified by comparing results between Python (9380) and Go (9384)
services.
- Tested scenarios: single dataset, multiple datasets, empty parameters,
and unauthorized/invalid IDs.
- All tests and Go `unit tests` passed.
2026-06-24 14:42:10 +08:00
kpdev
68d2ca0ff1 fix(api): use dataset-owner tenant for legacy /chunks docstore cleanup (#15961) 2026-06-24 14:24:40 +08:00
Lynn
ede46e0bb8 Fix: guess volc embedding model (#16298) 2026-06-24 14:11:55 +08:00
Jin Hai
e615e4faab Go CLI: fix mode switch (#16294)
### What problem does this PR solve?

```
RAGFlow(api/default)> add admin host '127.0.0.1:9383';
SUCCESS
RAGFlow(api/default)> use admin;
SUCCESS
RAGFlow(admin)> delete api 'default';
SUCCESS
RAGFlow(admin)> delete api 'default';
CLI error: api server: default not found
RAGFlow(admin)> add api 'default' host '127.0.0.1:9384';
SUCCESS
RAGFlow(admin)> use api 'default';
SUCCESS
RAGFlow(api/default)> delete admin
SUCCESS
RAGFlow(api/default)> delete admin;
CLI error: admin server not exists
RAGFlow(api/default)> list api server;
+------------+---------------+-----------------+---------+
| api_server | api_server_ip | api_server_port | auth    |
+------------+---------------+-----------------+---------+
| default    | 127.0.0.1     | 9384            | no auth |
+------------+---------------+-----------------+---------+
RAGFlow(api/default)> add admin host '127.0.0.1:9383';
SUCCESS
RAGFlow(api/default)> show admin server;
+-------------------+-----------+
| field             | value     |
+-------------------+-----------+
| admin_server_ip   | 127.0.0.1 |
| admin_server_port | 9383      |
| auth              | no auth   |
+-------------------+-----------+
```

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-24 13:41:01 +08:00
Ambercssa
e9cdd09b67 fix(agent): handle different reference data formats (#16276) 2026-06-24 13:33:59 +08:00
Wang Qi
6046bc6a8e Fix: handle empty folder when link to datasets (#16296) 2026-06-24 13:31:32 +08:00
helloxjade
1b2da645c3 fix: deduplicate markdown table chunks (#16143) 2026-06-24 13:22:57 +08:00
Ju Boxiang
39b194453d Fix: paginate get_flatted_meta_by_kbs to support datasets with >10k documents (#16034) (#16095) 2026-06-24 13:20:07 +08:00
minion1227
14565b289a Fix: docx parsing raises ValueError on 'Heading' styles (#16284) 2026-06-24 13:16:16 +08:00
minion1227
0c19190daf Fix: MCP document metadata cache can loop forever when documents returns an empty docs page (#16285) 2026-06-24 13:09:48 +08:00
ちー
5928b8b9ae fix(document_service): prevent NoneType error on progress_msg.strip() (#16289)
### What problem does this PR solve?

When I run RAGFlow_server.py:
```
2026-06-24 10:27:01,938 ERROR    3413485 fetch task exception
Traceback (most recent call last):
  File "/home/infiniflow/Documents/development/ragflow/api/db/services/document_service.py", line 948, in _sync_progress
    if t.progress_msg.strip():
       ^^^^^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'strip'

```

fixed: 
```python
if t.progress_msg.strip():

# fix:
if (t.progress_msg or "").strip():
```

Fix crash in `_sync_progress` when `progress_msg` is `None`.

#### Root Cause
`progress_msg` from task records can be `None`, causing:

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-24 13:07:40 +08:00
buua436
ba4021a9de fix: restore dataflow rerun and detail payload (#16292) 2026-06-24 13:06:06 +08:00
OSHA-B
a9eca9de82 fix: guard against missing component IDs in Switch Flow path to prevent NoneType crash (#16279) 2026-06-24 13:01:47 +08:00
buua436
d5d9d19fbe fix: keep chat channel bindings consistent (#16274) 2026-06-24 11:51:35 +08:00
buua436
8d4f4a093b fix: restore dataflow defaults and SSE response (#16290) 2026-06-24 11:51:24 +08:00
Günter Lukas
398f488b1b fix: support Google Cloud Gemini eu/us multipoint endpoints (#15990)
fix: support Google Cloud Gemini eu/us multipoint endpoints (#15990)
2026-06-24 11:07:05 +08:00
Yoorim Choi
6a8281721f fix(i18n): fix missing i18n coverage and refine Korean translations (#16203)
### What problem does this PR solve?

This PR follows up on
[#15863](https://github.com/infiniflow/ragflow/pull/15863) (Korean i18n)
with translation refinements and i18n coverage for hardcoded strings
found in the UI.

- Refine awkward Korean phrasing (e.g. 'Chunk 만들기' → 'Chunk 생성', '유형' →
'타입', etc.)
- Apply i18n to hardcoded strings in `message-item`,
`next-message-item`, `multi-select`, `chat-prompt-engine`, and various
filter hooks
- Rename `use-selelct-filters.ts` → `use-select-filters.ts` (typo fix)

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-24 10:14:19 +08:00
Jin Hai
919f596066 Fix release (#16278)
### What problem does this PR solve?

Fix release

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-23 22:04:34 +08:00
Rander
017adf841f fix(paddleocr): support PP-OCRv6 ocrResults fallback and integrate image parsing (#16150)
## Summary

This PR fixes two issues discovered during testing of the PaddleOCR
async API refactoring:

### 1. PP-OCRv6 returns `ocrResults` instead of `layoutParsingResults`

Models like PP-OCRv6 are pure text recognition models that return
results in `ocrResults.prunedResult.rec_texts` format rather than the
`layoutParsingResults.prunedResult.parsing_res_list` format used by
layout-aware models (PaddleOCR-VL series).

**Changes:**
- `deepdoc/parser/paddleocr_parser.py`: Extract `ocrResults` alongside
`layoutParsingResults` in `_send_request()`, add fallback logic in
`_transfer_to_sections()` and `parse_image()`
- `internal/entity/models/paddleocr.go`: Add `ocrResults` struct and
fallback extraction in Go OCR handler

### 2. Image parsing not integrated into picture chunker

The `parse_image()` method existed in PaddleOCRParser but was never
called from `rag/app/picture.py` (the module that handles image file
uploads). Users configuring PaddleOCR as their layout recognizer would
still get local deepdoc OCR for images.

**Changes:**
- `rag/app/picture.py`: When `layout_recognize` is set to PaddleOCR, use
`PaddleOCROcrModel.parse_image()` instead of local OCR. Falls back
gracefully to local OCR on failure.

## Testing

Verified end-to-end in Docker:
- PaddleOCR-VL-1.6 PDF parsing:  (10 text blocks with bbox)
- PaddleOCR-VL-1.6 image parsing:  (219 chars)
- PP-OCRv6 PDF parsing with ocrResults fallback:  (10 text blocks)
- PP-OCRv6 image parsing with ocrResults fallback:  (136 chars)

## Related PRs

- #15967 (merged) - PaddleOCR async Job API refactoring + new models
- #16086 (merged) - PaddleOCR image parsing support
2026-06-23 22:02:54 +08:00
Harsh Kashyap
b4a8a90c73 fix(rag/raptor): handle max_cluster edge case in GMM cluster selection (#16199)
### What problem does this PR solve?
`_get_optimal_clusters` in `rag/raptor.py` had two edge-case issues in
GMM cluster-count selection:
1. It used `np.arange(1, max_clusters)`, which never evaluates the
upper-bound candidate (`max_clusters`).
2. When effective `max_clusters` becomes `1`, the candidate list was
empty and `argmin` crashed.

This PR makes candidate evaluation inclusive (`1..max_clusters`) and
guards the single-cluster case by returning `1` directly.

### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)

### Validation
- `pytest test/unit_test/rag/test_raptor_psi_tree_builder.py
--config-file pyproject.toml -q`
- `ruff check rag/raptor.py
test/unit_test/rag/test_raptor_psi_tree_builder.py`

### Tests added
- Regression test for `max_cluster == 1` path (no crash, returns 1)
- Regression test verifying upper-bound candidate is evaluated and can
be selected

_AI-assistance disclosure: parts of this change (bug triage and test
scaffolding) were drafted with AI assistance and fully reviewed and
verified by me._

---------

Co-authored-by: Harsh Kashyap <harshkashyap@Harshs-MacBook-Pro.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 21:07:26 +08:00
Yingfeng
706e0d2d06 Refactor harness framework (#16271)
### What problem does this PR solve?

- Tools management
- Pregel engine wrapper for better usage
- UT race
- Coding style

### Type of change

- [x] Refactoring
2026-06-23 20:18:04 +08:00
Jin Hai
4f02ba4cf4 Go: show model and list all models (#16272)
### What problem does this PR solve?
```
RAGFlow(admin)> show model 'abc';
+------------+----------------------------------------------------------------+
| field      | value                                                          |
+------------+----------------------------------------------------------------+
| command    | get_model_by_model_name                                        |
| error      | 'get model by model name' is implemented in enterprise edition |
| model_name | abc                                                            |
+------------+----------------------------------------------------------------+

RAGFlow(admin)> list models;
+-----------------+--------------------------------------------------------+
| command         | error                                                  |
+-----------------+--------------------------------------------------------+
| list_all_models | 'list all models' is implemented in enterprise edition |
+-----------------+--------------------------------------------------------+
```

### Type of change

- [x] Refactoring

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-23 19:29:06 +08:00
Jin Hai
49714865c1 Go: rename ragflow_cli to ragflow-cli (#16270)
### What problem does this PR solve?

rename ragflow cli binary

### Type of change

- [x] Refactoring

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-23 19:20:49 +08:00
Haruko386
d89e29fba8 Document[Go-develop]: update Go development docs (#16229)
### What problem does this PR solve?

Document updated:

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-23 19:19:44 +08:00
Haruko386
5046626c17 feat[Go]: implement /datasets/<dataset_id>/documents/batch-update-status (#16258)
### What problem does this PR solve?

accident close #16072

As title

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-06-23 19:19:08 +08:00
Haruko386
6cbd069ea3 feat[Go]: implement <document_id>/chunks/<chunk_id> PATCH (#16232)
### What problem does this PR solve?

Implement: 
1. `/api/v1/datasets/<dataset_id>/documents/<document_id>/chunks GET`
2.
`/api/v1/datasets/<dataset_id>/documents/<document_id>/chunks/<chunk_id>
PATCH`
3. `/api/v1/datasets/<dataset_id>/documents/<document_id>/chunks PATCH`

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-06-23 18:50:36 +08:00
maoyifeng
643cb4788f Go CLI: add response output (#16263)
### What problem does this PR solve?

Go CLI: add response output
2026-06-23 18:12:15 +08:00
Wang Qi
a4f325be24 Fix: add /v1/document/upload_info -> /api/v1/documents/upload back (#16264) 2026-06-23 17:47:55 +08:00
buua436
aba5d172bd feat: add whatsapp web qr chat channel (#16238)
Adds a WhatsApp chat channel backed by a QR-based web login flow so users can connect without manual token setup.
2026-06-23 17:45:31 +08:00
Jin Hai
e15130534f Go: default public key (#16265)
### What problem does this PR solve?

Provider default public key for CLI

### Type of change

- [x] Refactoring

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-23 17:43:26 +08:00
Jin Hai
dec2ce4a60 Go CLI: admin model framework (#16252) 2026-06-23 16:57:05 +08:00
Zhichang Yu
2362210caf refactor(log): unify Go logging to zap with rotation, strip per-package levels (#16261)
Refactor the Go agent port's logging so every log line — gin access,
agent canvas events, harness warnings, fatal boot errors — flows through
a single common.Logger (zap) backed by a rotated file, with structured
fields, level filtering, and configurable rotation.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-23 16:21:46 +08:00
VincentLambert
11e14a8353 fix: propagate contextvars through thread_pool_exec (#16247)
## Problem

`thread_pool_exec()` dispatches work via `loop.run_in_executor()`, which
submits the callable with a plain `executor.submit(func, *args)` and
does **not** copy the caller's `contextvars.Context`. So a `ContextVar`
set in the async caller is not visible inside the function running in
the worker thread.

This differs from `asyncio.to_thread()`, which runs the callable inside
a copied context. `run_in_executor()` has never propagated context
(verified on Python 3.12 and 3.13) — so this is a pre-existing gap in
the helper, **not** a regression or a Python-version compatibility
issue.

Concretely, any code that sets a `ContextVar` in async code and reads it
inside a function dispatched via `thread_pool_exec` (request tracing,
per-task state, Langfuse trace propagation, etc.) silently loses that
context.

## Fix

Copy the current context before submitting and run the callable inside
it with `ctx.run()`, matching what `asyncio.to_thread()` does:

```python
async def thread_pool_exec(func, *args, **kwargs):
    loop = asyncio.get_running_loop()
    ctx = contextvars.copy_context()
    if kwargs:
        inner = functools.partial(func, *args, **kwargs)
        return await loop.run_in_executor(_thread_pool_executor(), ctx.run, inner)
    return await loop.run_in_executor(_thread_pool_executor(), ctx.run, func, *args)
```

This explicitly **adds** ContextVar propagation to the helper (it does
not restore any prior behavior). Backward-compatible.

## Tests

`TestThreadPoolExec` covers propagation, the kwargs path, per-call
isolation and the unset-default case.

> Note: the branch name still contains `python313` for historical
reasons; the change is unrelated to any Python version.
2026-06-23 15:17:42 +08:00
balibabu
d8ee1ffaad Fix: When re-entering the agent page, the data from the previous session flashes briefly. (#16251)
Fix: When re-entering the agent page, the data from the previous session flashes briefly.
2026-06-23 14:13:47 +08:00
Haruko386
9f9433e218 fix: handle SIMDe headers installation for arm64 (#16244)
### What problem does this PR solve?

Updated the release workflow to install SIMDe headers into the MSYS2
toolchain include directory. Adjusted CMake flags to remove references
to the previous SIMDE_INCLUDE_DIR.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-23 10:37:04 +08:00
Jin Hai
b661e9c19e Go CLI: admin list providers (#16243)
### What problem does this PR solve?

```
RAGFlow(admin)> list providers;
+----------------------+-------------------------------------------------------------+
| command              | error                                                       |
+----------------------+-------------------------------------------------------------+
| list_model_providers | 'list model providers' is implemented in enterprise edition |
+----------------------+-------------------------------------------------------------+

RAGFlow(admin)> add provider 'zhipu-ai';
+-------------+-----------------------------------------------------------+
| field       | value                                                     |
+-------------+-----------------------------------------------------------+
| command     | add_model_provider                                        |
| error       | 'add model provider' is implemented in enterprise edition |
| provider_id | admin                                                     |
| user_id     | zhipu-ai                                                  |
+-------------+-----------------------------------------------------------+

RAGFlow(admin)> delete provider 'zhipu-ai';
+-------------+--------------------------------------------------------------+
| field       | value                                                        |
+-------------+--------------------------------------------------------------+
| command     | delete_model_provider                                        |
| error       | 'delete model provider' is implemented in enterprise edition |
| provider_id | admin                                                        |
| user_id     | zhipu-ai                                                     |
+-------------+--------------------------------------------------------------+

RAGFlow(admin)> add provider 'zhipu-ai' instance 'instance1';
+---------------+-----------------------------------------------------------+
| field         | value                                                     |
+---------------+-----------------------------------------------------------+
| command       | add_model_instance                                        |
| error         | 'add model instance' is implemented in enterprise edition |
| instance_name | instance1                                                 |
| provider_id   | zhipu-ai                                                  |
| user_id       | admin                                                     |
+---------------+-----------------------------------------------------------+

RAGFlow(admin)> delete provider 'zhipu-ai' instance 'test'
+-------------+--------------------------------------------------------------+
| field       | value                                                        |
+-------------+--------------------------------------------------------------+
| instances   | [test]                                                       |
| provider_id | zhipu-ai                                                     |
| user_id     | admin                                                        |
| command     | delete_model_provider                                        |
| error       | 'delete model instance' is implemented in enterprise edition |
+-------------+--------------------------------------------------------------+

RAGFlow(admin)> add provider 'zhipu-ai' instance 'instance1' model 'xxx';
+---------------+--------------------------------------------------+
| field         | value                                            |
+---------------+--------------------------------------------------+
| command       | add_model                                        |
| error         | 'add model' is implemented in enterprise edition |
| instance_name | instance1                                        |
| model_names   | [xxx]                                            |
| provider_id   | zhipu-ai                                         |
| user_id       | admin                                            |
+---------------+--------------------------------------------------+

RAGFlow(admin)> delete provider 'zhipu-ai' instance 'test' model 'xxx';
+---------------+------------------------------------------------------+
| field         | value                                                |
+---------------+------------------------------------------------------+
| command       | delete_model_provider                                |
| error         | 'delete models' is implemented in enterprise edition |
| instance_name | test                                                 |
| models        | [xxx]                                                |
| provider_id   | zhipu-ai                                             |
| user_id       | admin                                                |
+---------------+------------------------------------------------------+

```

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-23 10:26:31 +08:00
Zhichang Yu
06ededb26a test(go): ensure go unit tests pass (#16241)
## Summary

Stabilizes the Go unit-test surface so the test suite can run reliably
in CI and locally via \`bash build.sh --test\`.

## Verification

\`\`\`bash
bash build.sh --test -- -count=10 -run TestWithCancel_SequentialAgent
./internal/harness/core/
bash build.sh --test -- -count=5 -run TestSiliconflowChatExtracts
./internal/entity/models/
bash build.sh --test # full suite
\`\`\`

All previously failing packages (\`admin\`, \`cli\`, \`handler\`,
\`parser\`,
\`router\`, \`service\`, \`service/chunk\`) now build and test
successfully.
\`TestWithCancel_SequentialAgent\` passes 10/10 (was flaky). SiliconFlow
reasoning test passes after switching the assertion to the SiliconFlow
wire
format.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-22 20:43:29 +08:00
VincentLambert
a4fcc988e7 i18n(fr): add missing French translations for chat channels, username validation and model editing (#16217)
## Summary

Several keys added in recent releases were missing from the French
(`fr.ts`) locale file.

- **`top`** — missing in both the common section and the dataset section
- **Chat channels** — all UI strings for the new chat channels feature
(`chatChannels`, `chatChannelDesc.*`, `connectDialog`, `notConnected`,
etc.)
- **Username validation** — `usernameMaxLength`,
`usernameInvalidCharacters`
- **Model editing** — `editCustomModelTitle`

## Changes

- `web/src/locales/fr.ts` — 47 lines added, no other files touched


🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 20:09:59 +08:00
balibabu
c849c76f8a Feat: Add a prefix to the name of the FormField associated with the chat. (#16178)
Fix: Add a prefix to the `name` of the `FormField` associated with the chat.
2026-06-22 19:18:11 +08:00
Jin Hai
0e6b28a7fe Add show / set role default models (#16240)
### What problem does this PR solve?

```
RAGFlow(admin)> show role 'user' default models;
+--------------------------+-----------------------------------------------------------------+-----------+
| command                  | error                                                           | role_name |
+--------------------------+-----------------------------------------------------------------+-----------+
| show_role_default_models | 'show role default models' is implemented in enterprise edition | user      |
+--------------------------+-----------------------------------------------------------------+-----------+

RAGFlow(admin)> set role 'user' default chat 'glm4.5@test@zhipu-ai';
+------------+---------------------------------------------------------------+
| field      | value                                                         |
+------------+---------------------------------------------------------------+
| model_id   |                                                               |
| model_type | chat                                                          |
| role_name  | user                                                          |
| command    | set_role_default_model                                        |
| error      | 'set role default model' is implemented in enterprise edition |
+------------+---------------------------------------------------------------+

RAGFlow(admin)> reset role 'user' default chat;
+------------+-----------------------------------------------------------------+
| field      | value                                                           |
+------------+-----------------------------------------------------------------+
| command    | reset_role_default_model                                        |
| error      | 'reset role default model' is implemented in enterprise edition |
| model_type | chat                                                            |
| role_name  | user                                                            |
+------------+-----------------------------------------------------------------+

```

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-22 19:03:36 +08:00
Hz_
9eb7cee473 feat(go-api): migrate searchbot share detail endpoint to go (#16124)
## Summary

- add public Go route for `/api/v1/searchbots/detail`
- implement beta-token auth flow for shared search access
- add tenant-based access check for shared search apps
- add joined search detail query for the share response
- align Go response shape with the current Python runtime behavior
- add DAO / service / handler tests for the new endpoint
2026-06-22 18:17:37 +08:00
Hz_
2856cde2d1 feat(go-api): Implement BulkDeleteChats Go API and fix ListChats (#16157)
### Description
- **Bulk Delete Chats**: Implemented Go endpoint `DELETE /api/v1/chats`
supporting bulk delete by `ids`, `delete_all` flag, and
backward-compatible `chat_id` body payload (with tenant-ownership
security checks).
- **Bug Fix**: Fixed a parameter swap in Go `ListChats` handler to
properly exclude soft-deleted chats.
2026-06-22 18:16:52 +08:00
Hz_
4e0db3053d feat(go-api): complete chat channel API migration with tests (#16139)
close #16132

## Summary

This PR completes the Go-side merge and cleanup for chat channel APIs,
including handler/service wiring, route registration, and test coverage.

Implemented and aligned 5 chat channel APIs:

```
- POST `/api/v1/chat-channels`
- GET `/api/v1/chat-channels`
- GET `/api/v1/chat-channels/:channel_id`
- PATCH `/api/v1/chat-channels/:channel_id`
- DELETE `/api/v1/chat-channels/:channel_id`
```


Co-authored-by: Haruko386 <tryeverypossible@163.com>
2026-06-22 18:16:15 +08:00
Haruko386
02cc1d6438 fix: unable to chat after set model (#16195)
### What problem does this PR solve?

```
fixed:

RAGFlow(api/default)> use model 'minimax-m2.5@test@minimax'
SUCCESS

RAGFlow(api/default)> chat message 'who r u'
Answer: Hey! I'm MiniMax-M2.5, an AI assistant here to help you with questions, tasks, or whatever you need. What can I do for you?
Time: 1.727263

```

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-22 18:14:58 +08:00
Haruko386
b777e50291 feat[Go]: implement api /api/v1/datasets/<dataset_id>/chunks POST (#16067)
### What problem does this PR solve?

As title

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-06-22 18:14:01 +08:00
Haruko386
c6b3618f9a fix: fix release workflow for Windows and MAC builds (#16235)
### What problem does this PR solve?

Removed references to 'simde' from the package lists and updated paths
for compiler detection and CMake configuration to ensure proper handling
of Windows executables.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-22 18:13:12 +08:00
Jin Hai
05e758e4fe Go CLI: Fix alter role (#16226)
### What problem does this PR solve?

As title.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-22 17:33:47 +08:00
Nick M
329e09f16a Fix: metadata add modal sends empty value due to stale closure (#15229)
Closes #15139.

The "+ Add" flow in the Set/Edit Metadata modal posted updates with an
empty value, so backend saves were silent no-ops and the document's "X
fields" count stayed at 0 despite a "Success" toast.

The value `<Input>` updates `tempValues` synchronously per keystroke but
only writes through to `metaData.values` on blur (via
`handleValueBlur`). When the user clicks the nested modal's Confirm
button without first blurring, the click handler races the blur and
`handleSave` closes over the pre-blur `metaData.values` — still the
initial `['']`. `addUpdateValue` then queues an empty-string update; the
auto-fire save sends it, and after `resetOperations()` the outer Save
button posts `updates: []`.

Read from `tempValues` instead so the queued update carries the typed
value.

Regression test in `tests/use-manage-values-modal.test.ts` asserts that
`handleSave` passes the typed value (not the pre-blur empty string) to
`addUpdateValue` in the add-new code path.
2026-06-22 16:30:42 +08:00
Haruko386
b337534a6c fix: Enhance Windows build configuration in release.yml (#16227)
### What problem does this PR solve?

Updated rust_target and added simde support for Windows builds. Modified
CMake commands to include simde and adjusted paths for compilers.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-22 15:29:05 +08:00
Zhichang Yu
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>
2026-06-22 11:58:29 +08:00
Haruko386
dfe841a3e3 fix: Enhance Windows build process for office_oxide and rag tokenizer (#16223)
### What problem does this PR solve?

Updated MSYS2 package list for Windows builds and added Rust target
specifications. Modified build steps for office_oxide and rag tokenizer
libraries to improve compatibility and streamline the build process.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-22 10:17:40 +08:00
Manan Bansal
70c0121b78 Fix: preserve tables when parsing DOCX with the laws parser (#16008) (#16155)
## What

Fixes #16008 — tables contained in a DOCX are silently dropped when the
document is parsed with the **laws** chunking method.

## Root cause

`Docx.__call__` in `rag/app/laws.py` iterated `self.doc.paragraphs`,
which only yields paragraph elements. Tables are separate `tbl` blocks
in the document body, so they were never visited and were lost from the
output. (The `naive` parser already handles tables by iterating the
document body.)

## Changes

- Iterate `self.doc._element.body` so tables are visited in document
order alongside paragraphs.
- Add a `__table_to_html` helper that renders each table to HTML,
including merged-cell `colspan` detection (mirrors the `naive` parser's
logic).
- Inject each table into the section tree with a sentinel level deeper
than any heading, so `Node.build_tree` merges it into its **enclosing
section** — keeping the chapter/article title path as retrieval context
rather than producing an orphaned chunk.
- Guard the `h2_level` computation against an empty heading set, so a
tables-only or empty DOCX no longer raises `IndexError`.

This keeps the laws parser's hierarchical chunking **and** adds table
extraction, so users no longer have to choose between losing structure
(naive) or losing tables (laws).

## Tests

Adds `test/unit_test/rag/test_laws_docx_tables.py` covering:
- table content is preserved and carries its section title path,
- merged adjacent cells collapse to `colspan`,
- tables-only document does not crash,
- empty document returns `[]`.

All four pass; `ruff check` / `ruff format` are clean.
2026-06-22 09:46:44 +08:00
Jin Hai
760229d917 Go CLI: admin list configs (#16221)
### What problem does this PR solve?

- list configs;

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-22 08:19:23 +08:00
Jin Hai
5039f46999 Go CLI: refactor commands (#16213)
### What problem does this PR solve?

As title.

### Type of change

- [x] Refactoring

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-21 16:50:02 +08:00
Jin Hai
1b712be599 Go CLI: refactor some commands (#16204)
### What problem does this PR solve?

- list resources

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
- [x] Refactoring

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-20 02:31:07 +08:00
Jin Hai
11499f7bb3 Go CLI: add list user commands framework (#16201) 2026-06-19 15:09:54 +08:00
Jin Hai
7214a23614 Go: fix duplicate models (#16197)
### What problem does this PR solve?

1. Remove unused file
2. Remove duplicate models
3. Resort the function order

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-19 09:57:58 +08:00
buua436
b409cfc3d5 feat: add dingtalk chat channel (#16183)
### What does this PR do?
This PR adds a new DingTalk chat channel integration and hardens the
inbound callback path.

### Summary
- Adds DingTalk as a selectable chat channel in the UI and backend
channel registry.
- Adds the DingTalk chat channel icon asset.
- Acknowledges DingTalk Stream callbacks and deduplicates repeated
inbound messages to avoid duplicate replies.
2026-06-18 20:06:00 +08:00
Wang Qi
5ca1686ac7 Fix that agent cannot be the same name (#16192)
Fix that agent cannot be the same name
2026-06-18 19:10:21 +08:00
Haruko386
eb5fcce1ca fix: hard-coded paths for Windows C compiler (#16193)
### What problem does this PR solve?

As title

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-18 18:55:02 +08:00
qinling0210
563d855780 Implement OpenAI chat completions in GO (#16177)
### What problem does this PR solve?

Implement OpenAI chat completions in GO

POST /api/v1/openai/<chat_id>/chat/completions

OpenAI chat cli: internal/development.md

### Type of change

- [x] Refactoring
2026-06-18 18:07:27 +08:00
Haruko386
b53b5bf12c Json add paddleOCR models (#16156)
close #15853

### What problem does this PR solve?

As title

### Type of change

- [x] Other (add models):
2026-06-18 17:57:41 +08:00
Haruko386
217c2a94c2 feat[Go]: implement datasets/<dataset_id>/index P/G (#16153)
### What problem does this PR solve?

```
POST: http://localhost:9384/api/v1/datasets/433b390c630411f1a13eab5f89540b2a/index?type=graph

Output: {
    "code": 0,
    "data": {
        "task_id": "ff5a3546bafa49d794a9a050d99c4a52"
    },
    "message": "success"
}
```

---

```
GET: http://localhost:9384/api/v1/datasets/433b390c630411f1a13eab5f89540b2a/index?type=graph

Output: {
    "code": 0,
    "data": {
        "id": "ff5a3546bafa49d794a9a050d99c4a52",
        "doc_id": "graph_raptor_x",
        "from_page": 100000000,
        "to_page": 100000000,
        "task_type": "graphrag",
        "priority": 0,
        "begin_at": "2026-06-17T18:07:45+08:00",
        "process_duration": 4.108135,
        "progress": -1,
        "progress_msg": "18:07:45 created task graphrag\n18:07:47 Task has been received.\n18:07:49 [ERROR][Exception]: Model config not found: Qwen/Qwen3-235B-A22B@test@SILICONFLOW",
        "retry_count": 1,
        "digest": "f16fd067d5c92cec",
        "create_time": 1781690865552,
        "create_date": "2026-06-17T18:07:45+08:00",
        "update_time": 1781690869108,
        "update_date": "2026-06-17T18:07:49+08:00"
    },
    "message": "success"
}

```

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-06-18 17:57:24 +08:00
Haruko386
5f6ebc97c6 feat[go]: implement /api/v1/datasets/<dataset_id> PUT (#16122)
### What problem does this PR solve?

As pic shows

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-06-18 17:57:07 +08:00
Haruko386
6beae949d8 feat[Go]: add modelID for delete_model and update_status (#16025)
### What problem does this PR solve?

1. add modelID for delete_model and update_status
2. fix the bug when update-status delete model

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
2026-06-18 17:56:51 +08:00
Jin Hai
3eb49ca7f8 Go: add command, list, remove, stop tasks (#16190)
### What problem does this PR solve?

```
RAGFlow(admin)> stop user 'abc' ingestion tasks;
+-----------------------------------+-------+--------------------------------------------------------------------------+-------+
| command                           | email | error                                                                    | tasks |
+-----------------------------------+-------+--------------------------------------------------------------------------+-------+
| stop_ingestion_tasks_by_condition | abc   | 'Stop ingestion tasks by condition' is implemented in enterprise edition |       |
+-----------------------------------+-------+--------------------------------------------------------------------------+-------+
RAGFlow(admin)> stop user 'abc' ingestion tasks 'created;
+-----------------------------------+-------+--------------------------------------------------------------------------+----------+-------+
| command                           | email | error                                                                    | status   | tasks |
+-----------------------------------+-------+--------------------------------------------------------------------------+----------+-------+
| stop_ingestion_tasks_by_condition | abc   | 'Stop ingestion tasks by condition' is implemented in enterprise edition | created; |       |
+-----------------------------------+-------+--------------------------------------------------------------------------+----------+-------+
RAGFlow(admin)> stop user 'abc' ingestion tasks 'create';
+-----------------------------------+-------+--------------------------------------------------------------------------+--------+-------+
| command                           | email | error                                                                    | status | tasks |
+-----------------------------------+-------+--------------------------------------------------------------------------+--------+-------+
| stop_ingestion_tasks_by_condition | abc   | 'Stop ingestion tasks by condition' is implemented in enterprise edition | create |       |
+-----------------------------------+-------+--------------------------------------------------------------------------+--------+-------+
RAGFlow(admin)> remove user 'abc' ingestion tasks 'create';
+-------------------------------------+-------+----------------------------------------------------------------------------+--------+-------+
| command                             | email | error                                                                      | status | tasks |
+-------------------------------------+-------+----------------------------------------------------------------------------+--------+-------+
| remove_ingestion_tasks_by_condition | abc   | 'Remove ingestion tasks by condition' is implemented in enterprise edition | create |       |
+-------------------------------------+-------+----------------------------------------------------------------------------+--------+-------+
RAGFlow(admin)> remove user 'abc' ingestion tasks;
+-------------------------------------+-------+----------------------------------------------------------------------------+-------+
| command                             | email | error                                                                      | tasks |
+-------------------------------------+-------+----------------------------------------------------------------------------+-------+
| remove_ingestion_tasks_by_condition | abc   | 'Remove ingestion tasks by condition' is implemented in enterprise edition |       |
+-------------------------------------+-------+----------------------------------------------------------------------------+-------+
```

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-18 17:50:21 +08:00
Haruko386
1a8ee8ba61 fix: wrong clang/toolchain for windows (#16191)
### What problem does this PR solve?

As title

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-18 17:49:55 +08:00
buua436
a2de7d0060 fix: chat channel defaults and feishu shutdown (#16176)
This PR keeps the chat-channel default values and Feishu shutdown behavior consistent after the rebase.
2026-06-18 17:44:48 +08:00
Jin Hai
5eedd13d49 Go: add command, show tasks summary (#16187)
### What problem does this PR solve?

RAGFlow(admin)> show tasks summary;

+---------+-----------------------------------------------------------------+
| field | value |

+---------+-----------------------------------------------------------------+
| command | show_users_quota_summary |
| error | 'Show users quota summary' is implemented in enterprise
edition |

+---------+-----------------------------------------------------------------+

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-18 17:09:20 +08:00
Haruko386
3762f1e573 fix: add nightly tags (#16189) 2026-06-18 16:42:01 +08:00
Lynn
47bd9dd049 Fix: replace tenant_llm apis (#16131)
Replace tenant_llm apis with provider-instance apis.
2026-06-18 16:38:32 +08:00
euvre
72db9044e2 fix: use RESTful pipeline detail API with knowledgeId and logId (#16182)
The pipeline file log detail hook (`useFetchPipelineFileLogDetail`) was
calling the legacy `kbService.getPipelineDetail({ log_id })` endpoint,
which does not match the current RESTful API contract. The backend now
expects both `datasetId` and `logId` to construct the correct URL (`GET
/api/v1/datasets/{datasetId}/ingestions/{logId}`).
2026-06-18 16:24:35 +08:00
Haruko386
fc1555a58f Go CLI: add support for windows, linux, macos (#16184)
### What problem does this PR solve?

As title

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-06-18 16:10:27 +08:00
Wang Qi
b47af3b5de Fix search rename error with multiple error message (#664) (#16186) 2026-06-18 15:51:41 +08:00
Jin Hai
20d11648a4 Go: add statistics command (#16119)
### What problem does this PR solve?

Prepare for enterprise command

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-18 15:21:44 +08:00
Haruko386
351b61a243 Go CLI: add support for windows, linux, macos (#16082)
### What problem does this PR solve?

As title

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
2026-06-18 15:20:00 +08:00
jaso0n0818
a70c7e8cc7 fix(deepdoc): attach lone header lines to the following section when delimiter is set (#16109)
## Summary
Fixes #15487 — lone markdown headers are no longer isolated as empty
chunks when a custom `delimiter` is set.

- Merge consecutive lone headers before attaching to the following prose
body
- Skip code fences, tables, lists, and blockquotes via
`_is_attachable_body()`
- Unit tests include the `# Title / ## Intro / Body` regression from
CodeRabbit review

## Validation
- `pytest test/unit_test/deepdoc/parser/test_markdown_parser.py` (11
passed locally)

Closes #15487
2026-06-18 14:24:09 +08:00
Haruko386
27d723e13a fix: fix some bugs in check_conn and drop_inst (#16180)
### What problem does this PR solve?

As title:

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-18 14:19:46 +08:00
balibabu
a9021528c3 Fix: Lint error. (#16172)
### What problem does this PR solve?

Fix: Lint error.
### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-18 13:14:18 +08:00
buua436
ea70663f09 feat: support wecom websocket channel (#16175)
Added WeCom chat channel websocket mode alongside the existing webhook mode, plus frontend support for selecting the connection type.
2026-06-18 13:10:09 +08:00
Hz_
69dbc44983 feat(go-api): migrate MCP server detail and download API to Go (#16113)
### What problem does this PR solve?

- Migrated MCP server detail and export (download) API from Python to
Go.
- Registered route: `GET /api/v1/mcp/servers/:mcp_id` (supporting
`?mode=download` query parameter).
2026-06-18 11:09:22 +08:00
Hz_
f59332bc37 feat(go-api): implement Go-side document PATCH API & align parsing/metadata sync behavior (#15975)
### What problem does this PR solve?

This PR implements the Go backend counterpart for the document partial
update API:
`PATCH /api/v1/datasets/:dataset_id/documents/:document_id`

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
- [x] Refactoring
2026-06-18 11:08:47 +08:00
Idriss Sbaaoui
8ff6a21af9 Fix: cli points to the wrong api endpoints (#16171)
### What problem does this PR solve?

fix the cli endpoints

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-18 10:54:33 +08:00
xu haiLong
a9ddcae0b3 Fix: MCP dataset discovery fails due to REST API max page size limit … (#16148)
Fix #16146
2026-06-18 09:39:37 +08:00
Wang Qi
99a25dca34 Fix Chat/Search/Agent bot show image (#16152)
Fix Chat/Search/Agent bot show image
2026-06-18 09:38:31 +08:00
Hz_
065797b047 Refactor(go-cli): improve variable and label naming in CLI parseAddModel (#16145)
### What problem does this PR solve?

This PR improves code readability in the CLI parser by renaming the loop
index `i` to `modelIndex`. It also renames the loop label `A` to
`optionsLoop` to align with standard Go naming conventions.

### Type of change

- [x] Refactoring
2026-06-17 20:21:42 +08:00
Wang Qi
27a05be643 Fix the launch script (#16159)
Fix the launch script
2026-06-17 20:20:37 +08:00
Haruko386
a3e3bdd386 fix back release.yml to old version (#16160)
### What problem does this PR solve?

As title

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-17 20:02:42 +08:00
dependabot[bot]
c1c79c2e55 build(deps): bump python-multipart from 0.0.21 to 0.0.31 (#16088) 2026-06-17 19:39:42 +08:00
Liu An
4379269374 Docs: Update version references to v0.26.1 in READMEs and docs (#16158)
### What problem does this PR solve?

- Update version tags in README files (including translations) from
v0.26.0 to v0.26.1
- Modify Docker image references and documentation to reflect new
version
- Update version badges and image descriptions
- Maintain consistency across all language variants of README files

### Type of change

- [x] Documentation Update
2026-06-17 19:35:32 +08:00
Idriss Sbaaoui
7d3928e501 Enhancement: update ci for parallel test execution (#16133)
### What problem does this PR solve?

split ci into multiple jobs

### Type of change

- [x] Performance Improvement
2026-06-17 19:22:24 +08:00
BitToby
2ab9256e8a fix(go): correct OpenRouter streaming URL routing and reasoning parameter (#16111)
### What problem does this PR solve?

Fixes two bugs in the OpenRouter streaming chat request builder
(`internal/entity/models/openrouter.go`, `ChatStreamlyWithSender`):

1. **qwen/glm models streamed to a broken URL.** The code routed any
`qwen`/`glm` model to
`URLSuffix.AsyncChat`, but `conf/models/openrouter.json` defines no
`async_chat` suffix
(empty), so the request was POSTed to `<base>/` instead of
`<base>/chat/completions` —
breaking streaming for every qwen/glm model. The non-stream path has no
such branch.
Fix: all models use the standard `Chat` suffix, consistent with the
non-stream path.

2. **Streaming reasoning was never enabled.** The request set reasoning
via a non-standard
`thinking` key, which OpenRouter ignores. OpenRouter's API — and this
provider's own
non-stream request (line ~110) and its streamed `delta.reasoning` parser
(line ~311) —
use the `reasoning` object. Fix: send `reasoning: {"enabled":
<thinking>}` (and
`{"effort": ...}` when set, taking precedence as in the non-stream
path).

Closes #16110

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-17 19:14:13 +08:00
balibabu
cf7b06c0f3 Fix: A pipeline created from a template fails immediately upon execution with a "hierarchy does not exist" error. (#16151)
### What problem does this PR solve?

Fix: A pipeline created from a template fails immediately upon execution
with a "hierarchy does not exist" error.
2026-06-17 19:07:04 +08:00
Lynn
a5cce29f22 Fix: add mimo (#16136)
### What problem does this PR solve?

Add chat model factory for Xiaomi model.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-17 19:02:33 +08:00
writinwaters
cb2e061120 Docs: Updated v0.26.1 release date. (#16154)
### What problem does this PR solve?

Updated v0.26.1 release date.

### Type of change


- [x] Documentation Update
2026-06-17 18:53:06 +08:00
buua436
43d121ad38 feat: add qqbot chat channel (#16140)
### What problem does this PR solve?
Adds qqbot as a built-in chat channel so it can be discovered and
started by the channel bootstrapper and shown in the chat channel
settings UI.

### Type of change
- [x] New Feature (non-breaking change which adds functionality)
2026-06-17 18:49:38 +08:00
Hunnyboy1217
e178c81bb4 refactor(go-models): harden Ollama ListModels and route through ParseListModel (#15853) (#15955)
### What problem does this PR solve?

Part of #15853 (provider model-list refactor).

Refactors **Ollama** `ListModels` onto the shared `ParseListModel`
pattern and fixes two correctness issues:

- **Endpoint:** switch the models suffix from `api/ps` (only
currently-running models) to `api/tags` (all installed models) — the
latter is what a model picker should show.
- **Parsing:** Ollama returns `{"models":[{"name","model"}]}`, a
non-OpenAI shape. Decode it into a typed struct, map the names into
`ModelList`, then enrich through `ParseListModel`. This removes the
previous unchecked type assertions (`result["models"].([]interface{})` /
`.(map[string]interface{})` / `.(string)`) that **panicked** when the
body was missing the `models` array or any field, and adds a fallback to
the `model` field when `name` is blank.
- Drops the no-op GET request body and a dead base-URL reassignment.

#### Drive-by fix
Shared gitee_test.go `DSModelList` -> `ModelList` compile fix (renamed
in #15900) so the models test package builds; auto-resolves against the
sibling #15853 PRs.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
2026-06-17 18:47:27 +08:00
balibabu
70f319c536 Fix: The pipeline created from the template fails immediately upon execution. (#16149)
### What problem does this PR solve?

Fix: The pipeline created from the template fails immediately upon
execution.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-17 17:03:17 +08:00
chanx
9302233b95 fix: misc frontend fixes for agent log, login, search settings (#16137)
### What problem does this PR solve?

fix: misc frontend fixes for agent log, login, search settings
- agent-log: restore server-side pagination on export and search;
replace hardcoded labels with i18n keys; switch container to
text-text-primary
- login: validate register nickname against NICKNAME_PATTERN with
reusable setting i18n
- next-search: align llm_setting schema with chat (LlmSettingFieldSchema
+ LLMIdFormField nested, LlmSettingEnabledSchema at form
root) so the slider Switch reads the correct path; strip *Enabled flags
before submit to avoid backend "Unrecognized field name"
  errors
  - locales: add common.reset (zh/en)
  - skills/go-naming: fix relative link to rules/named.md

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-17 16:20:26 +08:00
balibabu
3247e353c7 Fix: The .docx file is not displaying fully; the hierarchy of the pipeline created from the template is missing. (#16134)
### What problem does this PR solve?

Fix: The .docx file is not displaying fully; the hierarchy of the
pipeline created from the template is missing.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-17 16:18:47 +08:00
Wang Qi
fcb4f78d97 Dev: add go starter (#16138)
Dev: add go starter
2026-06-17 16:09:53 +08:00
Wang Qi
e08bcd4d0d Update doc rerank_id from int to string (#16142)
Update doc rerank_id from int to string
2026-06-17 16:09:33 +08:00
buua436
be869f5d96 fix: chat channel runtime (#16129)
### What problem does this PR solve?
Fix chat channel message routing to use the connected `chat_id`, and
make the Feishu websocket client bind to the thread-local event loop.

### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-17 15:52:13 +08:00
Idriss Sbaaoui
44164e18d8 Enhancement: optimize ci (#16130)
### What problem does this PR solve?

optimize ci by fixing flaky clean-ups and rendundant tasks

### Type of change

- [x] Performance Improvement
2026-06-17 15:16:11 +08:00
Wang Qi
b3ac03b96c Set default Paddle OCR URL (#16128)
Set default Paddle OCR URL
2026-06-17 14:29:20 +08:00
buua436
486b28c409 fix: show telegram chat channel (#16125)
### What problem does this PR solve?
Show Telegram in the chat channel picker alongside the existing Discord
and Feishu entries.

### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-17 14:18:16 +08:00
buua436
78b4906f7a fix: tighten embedding truncation threshold (#16123)
### What problem does this PR solve?
Use a 95% max_length threshold before truncating embedding inputs, which
reduces the chance of provider-side invalid-parameter errors on
near-limit chunks.

### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-17 14:18:02 +08:00
Zhichang Yu
e45659868a feat(agent): ship the Go agent canvas port — eino interrupt/resume + Redis check-pointing (#16035)
Replaces the Python agent canvas runtime with a Go implementation that
runs inside `cmd/server_main`.

The canvas compiles into an eino Workflow that pauses on wait-for-user
via native Interrupt/Resume (no sentinel flag) and resumes from a
Redis-backed CheckPointStore.

All 21 Python agent components and ~35 tools are ported with functional
parity.

Sandbox providers now read their JSON config from the admin-panel
system_settings table with env fallback.

234 files / +35,413 / -6,111. All Go files are gofmt-clean (CI gate
added); drops the v2 DSL E2E step and the gap-analysis plan (both
redundant after the port ships).

## Type of change

- [x] Refactoring
- [x] New feature
- [x] Bug fix

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-17 13:24:03 +08:00
Wang Qi
2290bb0023 Fix MinerU table option sanitization (#16118)
Follow on issue: #14831 and PR: #14920 to fix the table options, with
table recognition enabled, do not sanitize html tags.
2026-06-17 13:06:07 +08:00
euvre
9bd53ce675 fix: return full record in get_ingestion_log (#16120)
### What problem does this PR solve?

The `get_ingestion_log` endpoint (both Python
`dataset_api_service.get_ingestion_log` and Go
`DatasetService.GetIngestionLog`) was returning only the
**dataset-level** field set, which omits critical fields such as `dsl`,
`document_id`, `parser_id`, `document_name`, `pipeline_id`, etc.

This caused the front-end **dataflow-result page** to be unable to
render the pipeline timeline and chunks when viewing a single ingestion
log, regardless of whether the log was a dataset-level operation
(graph/raptor/mindmap) or a per-file parse.

### Background

`PipelineOperationLogService` provides two field sets:

| Method | Fields |
|---|---|
| `get_dataset_logs_fields` | Minimal set (progress, status, timestamps,
etc.) |
| `get_file_logs_fields` | Superset — includes `document_id`, `dsl`,
`parser_id`, `document_name`, `pipeline_id`, … |

When listing logs, the API correctly distinguishes dataset-level vs
file-level logs and uses the appropriate converter. However, when
**fetching a single log by ID**, both the Python and Go implementations
were hardcoded to the dataset-level set, dropping the extra fields that
the front-end needs.
2026-06-17 13:03:51 +08:00
Hunnyboy1217
fd196f694e feat(go-models): harden ListModels for FishAudio (#15853) (#15957)
### What problem does this PR solve?

Part of #15853 (provider model-list refactor). Final two providers.

- **voyage:** Voyage AI exposes no live model-list endpoint — its public
API only has `/v1/embeddings` and `/v1/rerank` — so the previous
`ListModels` was a `no such method` stub. Replace it with a
static-catalog listing sourced from the loaded provider definition,
carrying each model's `max_tokens`, `model_types`, and embedding
`dimensions`. `list models from voyage` now returns the 13-model catalog
instead of erroring.
- **fishaudio:** route the existing `/model` voice listing through the
shared `ParseListModel` helper for consistency; keep the human-readable
`title` as the model name and fall back to `_id` when a title is blank.

#### Drive-by fix
Shared gitee_test.go `DSModelList` -> `ModelList` compile fix (renamed
in #15900); auto-resolves against the sibling #15853 PRs.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
- [x] Refactoring

Co-authored-by: Haruko386 <tryeverypossible@163.com>
2026-06-17 11:56:20 +08:00
writinwaters
0aaba0033f Docs: Updated Converse with chat assistant (#16117)
### What problem does this PR solve?

Miscellaneous editorial updates to the API reference.

### Type of change


- [x] Documentation Update
2026-06-17 11:50:14 +08:00
Wang Qi
02ccd35241 Fix RAGFlow cannot start (#16116)
# Summary
- The culprit is commit b4c8711d5 / PR #15415 (fix: upgrade crawl4ai to
0.8.0).
- That upgrade brought in unclecode-litellm, which installs the same
top-level litellm namespace as upstream litellm.
- The crash happens when files from one LiteLLM distribution are mixed
with files from the other: custom_guardrail.py expects
GuardrailTracingDetail, but types/utils.py can come from the older
conflicting package.
2026-06-17 11:27:31 +08:00
Hz_
b48f03d0f5 feat(go/dao): migrate chat channel database entity and DAO to Go (#16055)
## Changes
1. **Entity (`internal/entity/chat_channel.go`)**:
- Implemented `ChatChannel` struct mapping the `chat_channel` database
table.
- Declared `ChatChannelListResponse` as a DTO to filter out sensitive
credentials (`config` field) and fetch the associated `dialog_name` via
left join.
2. **GORM Migration (`internal/dao/database.go`)**:
- Registered `&entity.ChatChannel{}` in the `dataModels` array inside
`InitDB()` to enable safe GORM schema synchronization.
3. **DAO (`internal/dao/chat_channel.go`)**:
- Implemented `ChatChannelDAO` wrapping GORM CRUD methods (`Create`,
`GetByID`, `UpdateByID`, `DeleteByID`).
- Implemented `ListByTenantID` performing a `LEFT JOIN` on the `dialog`
table to retrieve `dialog_name` while excluding `config` values to avoid
credential leaks.
4. **Test (`internal/dao/chat_channel_test.go`)**:
- Added integration unit tests testing the full CRUD lifecycle and GORM
left-join mapping list querying.
2026-06-17 11:26:13 +08:00
balibabu
5de00bdf50 Fix: Importing the MCP dialog causes duplicate submissions. (#16037)
### What problem does this PR solve?

Fix: Importing the MCP dialog causes duplicate submissions.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-17 09:49:51 +08:00
euvre
fe46244d30 fix: paginate non-DeepDOC PDF parsing tasks to prevent OOM (#16106)
The parser pods suffer from OOM kills when processing large PDF
documents. The root cause is in api/db/services/task_service.py: when
layout_recognize is not DeepDOC (e.g. Plain Text), page_size was set to
MAXIMUM_TASK_PAGE_NUMBER (100 million), causing the entire PDF to be
processed as a single task with all pages loaded into memory
simultaneously.

This PR fixes the issue by paginating non-DeepDOC PDF parsing tasks the
same way DeepDOC already does.
2026-06-17 09:33:53 +08:00
Jin Hai
6865039a22 Go: add more start server parameters (#16093)
### What problem does this PR solve?

```
$ ./bin/ragflow_server --version 
RAGFlow version: v0.26.0-65-g549f6109c

$ ./bin/ragflow_server --debug # start server with debug log level

$ ./bin/admin_server --version 
RAGFlow version: v0.26.0-65-g549f6109c

$ ./bin/admin_server --debug # start server with debug log level

$ ./bin/admin_server --init-superuser # init default superuser

$ ./bin/ingestor --version
RAGFlow version: v0.26.0-68-g6f6c39706

$ ./bin/ingestor --debug
```


### Type of change

- [x] New Feature (non-breaking change which adds functionality)

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-16 20:27:37 +08:00
Wang Qi
17e3aad7ae Revert "fix: paginate non-DeepDOC PDF parsing tasks to prevent OOM" (#16104)
Reverts infiniflow/ragflow#15951
2026-06-16 20:11:45 +08:00
buua436
1e4796da9d Docs: update chat completions docs (#16100)
### What problem does this PR solve?
Syncs the /api/v1/chat/completions docs with the current behavior,
including the new legacy streaming mode.
### Type of change
- [x]  Documentation Update
2026-06-16 20:08:23 +08:00
dependabot[bot]
b732636546 build(deps): bump aiohttp from 3.13.3 to 3.14.1 (#16090) 2026-06-16 20:07:32 +08:00
euvre
d2a18d5c46 fix: paginate non-DeepDOC PDF parsing tasks to prevent OOM (#15951)
### What problem does this PR solve?

The parser pods suffer from OOM kills when processing large PDF
documents. The root cause is in api/db/services/task_service.py: when
layout_recognize is not DeepDOC (e.g. Plain Text), page_size was set to
MAXIMUM_TASK_PAGE_NUMBER (100 million), causing the entire PDF to be
processed as a single task with all pages loaded into memory
simultaneously.

This PR fixes the issue by paginating non-DeepDOC PDF parsing tasks the
same way DeepDOC already does.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [ ] New Feature (non-breaking change which adds functionality)
- [ ] Documentation Update
- [ ] Refactoring
- [x] Performance Improvement
- [ ] Other (please describe):
2026-06-16 20:07:19 +08:00
Rander
62698725ca feat(paddleocr): add image parsing support with async Job API (#16086)
## Summary

Add image parsing capability to PaddleOCR integration, building on top
of #15967 (async Job API migration).

## Changes

### `deepdoc/parser/paddleocr_parser.py`
- Add `parse_image()` method that uses the same async Job API flow as
`parse_pdf()`
- Extracts text from `layoutParsingResults` → `prunedResult` →
`parsing_res_list`
- Returns concatenated block content as a single string

### `rag/llm/ocr_model.py`
- Add `parse_image()` wrapper to `PaddleOCROcrModel` with availability
check and logging

## Relationship to other PRs

- **Depends on**: #15967 (async Job API migration) — this PR is based on
that branch
- **Replaces**: #14826 (original image processing PR based on old sync
API)

## Notes

This PR uses `base_url` and the async Job API (submit → poll → fetch)
consistent with #15967, rather than the old `api_url` + sync POST
pattern from #14826.
2026-06-16 19:34:38 +08:00
Rander
1235da7093 refactor(paddleocr): migrate from sync API to async Job API (#15967)
## Summary

Migrate PaddleOCR integration from the deprecated synchronous HTTP API
to the new asynchronous Job API (`submit → poll → fetch`), aligning with
PaddleOCR 3.6.0+ architecture.

## Changes

### Python (`deepdoc/parser/paddleocr_parser.py`)
- Replace synchronous `requests.post()` with async Job API flow (submit
→ poll → fetch)
- Authentication: `token {token}` → `Bearer {token}`
- File transfer: base64 JSON body → multipart file upload
- Polling: exponential backoff (initial 3s, ×1.5, max 15s, timeout
controlled by `request_timeout`)
- Result: fetch full JSONL from result URL, preserving `prunedResult`
with bbox info for crop functionality
- Rename `api_url` → `base_url` (backward compatible: `api_url` still
accepted as fallback)

### Python (`rag/llm/ocr_model.py`)
- Prefer `paddleocr_base_url` / `PADDLEOCR_BASE_URL`, fallback to
`paddleocr_api_url` / `PADDLEOCR_API_URL`

### Go (`internal/entity/models/paddleocr.go`)
- Add `Client-Platform: ragflow` header to submit and poll requests
- Change polling from fixed 3s to exponential backoff (initial 3s, ×1.5,
max 15s)

### Python (`common/constants.py`)
- Add `PADDLEOCR_BASE_URL` to env keys and default config

## Backward Compatibility

- Old env var `PADDLEOCR_API_URL` still works (used as fallback)
- Frontend field `paddleocr_api_url` still works (backend reads it as
fallback)
- No user-facing configuration changes required for existing setups

## Why not use the `paddleocr` SDK package directly?

RAGFlow's `_transfer_to_sections()` relies on `prunedResult` (containing
`block_bbox`, `block_label`, `parsing_res_list`) from the raw API
response for PDF crop functionality. The SDK's public `parse_document()`
API only returns `DocParsingResult` with `markdown_text`, discarding the
bbox data. Therefore we implement the async Job API flow directly via
HTTP, following the same logic as the SDK internally.
2026-06-16 19:34:21 +08:00
Jin Hai
3d8bc76e27 Go refactor: merge similar functions (#16098)
### What problem does this PR solve?

Merge password related functions

### Type of change

- [x] Refactoring

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-16 19:26:42 +08:00
dependabot[bot]
59aedad5e1 build(deps): bump starlette from 0.51.0 to 1.3.1 (#16089)
Bumps [starlette](https://github.com/Kludex/starlette) from 0.51.0 to
1.3.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/Kludex/starlette/releases">starlette's
releases</a>.</em></p>
<blockquote>
<h2>Version 1.3.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Use <code>StarletteDeprecationWarning</code> instead of
<code>DeprecationWarning</code> by <a
href="https://github.com/Kludex"><code>@​Kludex</code></a> in <a
href="https://redirect.github.com/Kludex/starlette/pull/3119">Kludex/starlette#3119</a></li>
<li>Enforce <code>max_fields</code> and <code>max_part_size</code> in
<code>FormParser</code> by <a
href="https://github.com/Kludex"><code>@​Kludex</code></a> in <a
href="https://redirect.github.com/Kludex/starlette/pull/3329">Kludex/starlette#3329</a></li>
<li>Enforce <code>FormParser</code> limits in parser callbacks by <a
href="https://github.com/Kludex"><code>@​Kludex</code></a> in <a
href="https://redirect.github.com/Kludex/starlette/pull/3331">Kludex/starlette#3331</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/Kludex/starlette/compare/1.3.0...1.3.1">https://github.com/Kludex/starlette/compare/1.3.0...1.3.1</a></p>
<h2>Version 1.3.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Clamp oversized suffix ranges in <code>FileResponse</code> by <a
href="https://github.com/jiyujie2006"><code>@​jiyujie2006</code></a> in
<a
href="https://redirect.github.com/Kludex/starlette/pull/3307">Kludex/starlette#3307</a></li>
<li>Catch <code>OSError</code> alongside <code>MultiPartException</code>
when closing temp files by <a
href="https://github.com/N3XT3R1337"><code>@​N3XT3R1337</code></a> in <a
href="https://redirect.github.com/Kludex/starlette/pull/3191">Kludex/starlette#3191</a></li>
<li>Add <code>httpx2</code> to the <code>full</code> extra by <a
href="https://github.com/Kludex"><code>@​Kludex</code></a> in <a
href="https://redirect.github.com/Kludex/starlette/pull/3323">Kludex/starlette#3323</a></li>
<li>Adjust testclient typing and warnings by <a
href="https://github.com/waketzheng"><code>@​waketzheng</code></a> in <a
href="https://redirect.github.com/Kludex/starlette/pull/3322">Kludex/starlette#3322</a></li>
<li>Fix IndexError in URL.replace() on a URL with no authority by <a
href="https://github.com/LeSingh1"><code>@​LeSingh1</code></a> in <a
href="https://redirect.github.com/Kludex/starlette/pull/3317">Kludex/starlette#3317</a></li>
<li>Annotate URLPath protocol parameter with Literal by <a
href="https://github.com/Chang-LeHung"><code>@​Chang-LeHung</code></a>
in <a
href="https://redirect.github.com/Kludex/starlette/pull/3285">Kludex/starlette#3285</a></li>
<li>avoid collapsing exception groups from user code by <a
href="https://github.com/graingert"><code>@​graingert</code></a> in <a
href="https://redirect.github.com/Kludex/starlette/pull/2830">Kludex/starlette#2830</a></li>
<li>Use <code>removeprefix</code> to strip weak ETag indicator in
<code>is_not_modified</code> by <a
href="https://github.com/gnosyslambda"><code>@​gnosyslambda</code></a>
in <a
href="https://redirect.github.com/Kludex/starlette/pull/3193">Kludex/starlette#3193</a></li>
<li>Build <code>request.url</code> from structured components by <a
href="https://github.com/Kludex"><code>@​Kludex</code></a> in <a
href="https://redirect.github.com/Kludex/starlette/pull/3326">Kludex/starlette#3326</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/jiyujie2006"><code>@​jiyujie2006</code></a>
made their first contribution in <a
href="https://redirect.github.com/Kludex/starlette/pull/3307">Kludex/starlette#3307</a></li>
<li><a
href="https://github.com/N3XT3R1337"><code>@​N3XT3R1337</code></a> made
their first contribution in <a
href="https://redirect.github.com/Kludex/starlette/pull/3191">Kludex/starlette#3191</a></li>
<li><a
href="https://github.com/leestana01"><code>@​leestana01</code></a> made
their first contribution in <a
href="https://redirect.github.com/Kludex/starlette/pull/3319">Kludex/starlette#3319</a></li>
<li><a href="https://github.com/LeSingh1"><code>@​LeSingh1</code></a>
made their first contribution in <a
href="https://redirect.github.com/Kludex/starlette/pull/3317">Kludex/starlette#3317</a></li>
<li><a
href="https://github.com/EmmanuelNiyonshuti"><code>@​EmmanuelNiyonshuti</code></a>
made their first contribution in <a
href="https://redirect.github.com/Kludex/starlette/pull/3204">Kludex/starlette#3204</a></li>
<li><a
href="https://github.com/Chang-LeHung"><code>@​Chang-LeHung</code></a>
made their first contribution in <a
href="https://redirect.github.com/Kludex/starlette/pull/3285">Kludex/starlette#3285</a></li>
<li><a
href="https://github.com/gnosyslambda"><code>@​gnosyslambda</code></a>
made their first contribution in <a
href="https://redirect.github.com/Kludex/starlette/pull/3193">Kludex/starlette#3193</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/Kludex/starlette/compare/1.2.1...1.3.0">https://github.com/Kludex/starlette/compare/1.2.1...1.3.0</a></p>
<h2>Version 1.2.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Use <code>httpx2</code> for type checking in the
<code>testclient</code> module by <a
href="https://github.com/leifwar"><code>@​leifwar</code></a> in <a
href="https://redirect.github.com/Kludex/starlette/pull/3304">Kludex/starlette#3304</a></li>
<li>Add assert error for requires() when request param is not Request
type by <a
href="https://github.com/KeeganOP"><code>@​KeeganOP</code></a> in <a
href="https://redirect.github.com/Kludex/starlette/pull/3298">Kludex/starlette#3298</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/leifwar"><code>@​leifwar</code></a> made
their first contribution in <a
href="https://redirect.github.com/Kludex/starlette/pull/3304">Kludex/starlette#3304</a></li>
<li><a href="https://github.com/diskeu"><code>@​diskeu</code></a> made
their first contribution in <a
href="https://redirect.github.com/Kludex/starlette/pull/3243">Kludex/starlette#3243</a></li>
<li><a href="https://github.com/KeeganOP"><code>@​KeeganOP</code></a>
made their first contribution in <a
href="https://redirect.github.com/Kludex/starlette/pull/3298">Kludex/starlette#3298</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/Kludex/starlette/compare/1.2.0...1.2.1">https://github.com/Kludex/starlette/compare/1.2.0...1.2.1</a></p>
<h2>Version 1.2.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Support httpx2 in the test client by <a
href="https://github.com/Kludex"><code>@​Kludex</code></a> in <a
href="https://redirect.github.com/Kludex/starlette/pull/3291">Kludex/starlette#3291</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/Kludex/starlette/compare/1.1.0...1.2.0">https://github.com/Kludex/starlette/compare/1.1.0...1.2.0</a></p>
<h2>Version 1.1.0</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/Kludex/starlette/blob/main/docs/release-notes.md">starlette's
changelog</a>.</em></p>
<blockquote>
<h2>1.3.1 (June 12, 2026)</h2>
<h4>Fixed</h4>
<ul>
<li>Enforce <code>max_fields</code> and <code>max_part_size</code> in
<code>FormParser</code> <a
href="https://redirect.github.com/encode/starlette/pull/3329">#3329</a>.</li>
<li>Enforce <code>FormParser</code> limits in parser callbacks <a
href="https://redirect.github.com/encode/starlette/pull/3331">#3331</a>.</li>
</ul>
<h2>1.3.0 (June 11, 2026)</h2>
<h4>Added</h4>
<ul>
<li>Add <code>httpx2</code> to the <code>full</code> extra <a
href="https://redirect.github.com/encode/starlette/pull/3323">#3323</a>.</li>
<li>Annotate the <code>URLPath</code> <code>protocol</code> parameter
with <code>Literal</code> <a
href="https://redirect.github.com/encode/starlette/pull/3285">#3285</a>.</li>
</ul>
<h4>Fixed</h4>
<ul>
<li>Build <code>request.url</code> from structured components <a
href="https://redirect.github.com/encode/starlette/pull/3326">#3326</a>.</li>
<li>Clamp oversized suffix ranges in <code>FileResponse</code> <a
href="https://redirect.github.com/encode/starlette/pull/3307">#3307</a>.</li>
<li>Catch <code>OSError</code> alongside <code>MultiPartException</code>
when closing temp files <a
href="https://redirect.github.com/encode/starlette/pull/3191">#3191</a>.</li>
<li>Avoid collapsing exception groups raised from user code <a
href="https://redirect.github.com/encode/starlette/pull/2830">#2830</a>.</li>
<li>Use <code>removeprefix</code> to strip the weak <code>ETag</code>
indicator in <code>is_not_modified</code> <a
href="https://redirect.github.com/encode/starlette/pull/3193">#3193</a>.</li>
<li>Fix <code>IndexError</code> in <code>URL.replace()</code> on a URL
with no authority <a
href="https://redirect.github.com/encode/starlette/pull/3317">#3317</a>.</li>
<li>Adjust <code>testclient</code> typing and warnings <a
href="https://redirect.github.com/encode/starlette/pull/3322">#3322</a>.</li>
</ul>
<h2>1.2.1 (May 31, 2026)</h2>
<h4>Fixed</h4>
<ul>
<li>Use <code>httpx2</code> for type checking in the
<code>testclient</code> module <a
href="https://redirect.github.com/encode/starlette/pull/3304">#3304</a>.</li>
<li>Add assert error for <code>requires()</code> when the request
parameter is not a <code>Request</code> type <a
href="https://redirect.github.com/encode/starlette/pull/3298">#3298</a>.</li>
</ul>
<h2>1.2.0 (May 28, 2026)</h2>
<h4>Added</h4>
<ul>
<li>Support httpx2 in the test client <a
href="https://redirect.github.com/encode/starlette/pull/3291">#3291</a>.</li>
</ul>
<h2>1.1.0 (May 23, 2026)</h2>
<h4>Added</h4>
<ul>
<li>Use <code>&quot;application/octet-stream&quot;</code> as the
<code>FileResponse</code> media type fallback <a
href="https://redirect.github.com/encode/starlette/pull/3283">#3283</a>.</li>
</ul>
<h4>Fixed</h4>
<ul>
<li>Only dispatch standard HTTP verbs in <code>HTTPEndpoint</code> <a
href="https://redirect.github.com/encode/starlette/pull/3286">#3286</a>.</li>
<li>Reject absolute paths in <code>StaticFiles.lookup_path</code> <a
href="https://redirect.github.com/encode/starlette/pull/3287">#3287</a>.</li>
</ul>
<h2>1.0.1 (May 21, 2026)</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="8ebffd0678"><code>8ebffd0</code></a>
Version 1.3.1 (<a
href="https://redirect.github.com/Kludex/starlette/issues/3330">#3330</a>)</li>
<li><a
href="25b8e179d8"><code>25b8e17</code></a>
Enforce <code>FormParser</code> limits in parser callbacks (<a
href="https://redirect.github.com/Kludex/starlette/issues/3331">#3331</a>)</li>
<li><a
href="dba1c4babc"><code>dba1c4b</code></a>
Enforce <code>max_fields</code> and <code>max_part_size</code> in
<code>FormParser</code> (<a
href="https://redirect.github.com/Kludex/starlette/issues/3329">#3329</a>)</li>
<li><a
href="45e51dcf99"><code>45e51dc</code></a>
Use <code>StarletteDeprecationWarning</code> instead of
<code>DeprecationWarning</code> (<a
href="https://redirect.github.com/Kludex/starlette/issues/3119">#3119</a>)</li>
<li><a
href="5f8610c386"><code>5f8610c</code></a>
Version 1.3.0 (<a
href="https://redirect.github.com/Kludex/starlette/issues/3327">#3327</a>)</li>
<li><a
href="167b5850e8"><code>167b585</code></a>
Build <code>request.url</code> from structured components (<a
href="https://redirect.github.com/Kludex/starlette/issues/3326">#3326</a>)</li>
<li><a
href="37309255b4"><code>3730925</code></a>
Use <code>removeprefix</code> to strip weak ETag indicator in
<code>is_not_modified</code> (<a
href="https://redirect.github.com/Kludex/starlette/issues/3193">#3193</a>)</li>
<li><a
href="e6f7ad1ab8"><code>e6f7ad1</code></a>
avoid collapsing exception groups from user code (<a
href="https://redirect.github.com/Kludex/starlette/issues/2830">#2830</a>)</li>
<li><a
href="115228fcdc"><code>115228f</code></a>
Annotate URLPath protocol parameter with Literal (<a
href="https://redirect.github.com/Kludex/starlette/issues/3285">#3285</a>)</li>
<li><a
href="113f193a34"><code>113f193</code></a>
docs: replace inline ASGI server list with link to canonical implemen…
(<a
href="https://redirect.github.com/Kludex/starlette/issues/3204">#3204</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/Kludex/starlette/compare/0.51.0...1.3.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=starlette&package-manager=uv&previous-version=0.51.0&new-version=1.3.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/infiniflow/ragflow/network/alerts).

</details>

Co-authored-by: Yingfeng Zhang <yingfeng.zhang@gmail.com>
2026-06-16 19:24:45 +08:00
Wang Qi
8067e97f0d Refactor: rename /chat_channels to /chat-channels (#16099) 2026-06-16 19:15:43 +08:00
Kevin Hu
15f50e5cb2 fix: rename dialog_id to chat_id in chat_channel (backend + frontend) (#16096)
## Summary

- The `ChatChannel` DB column was renamed from `dialog_id` to `chat_id`
via a migration (added in a prior commit).
- Aligns the REST API layer (`chat_channel_api.py`,
`chat_channel_service.py`) to use `chat_id` consistently.
- Updates the frontend (`interface.ts`, `hooks.ts`,
`connect-dialog-modal.tsx`, `added-channel-card.tsx`) to read/write
`chat_id` instead of `dialog_id`.
- The joined `dialog_name` alias in the list query is unchanged (backend
still returns it under that name).

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 19:02:20 +08:00
galuis116
6bfaa3f21e Fix: SSRF in markdown parser remote image fetch (#15438)
### What problem does this PR solve?

`rag/app/naive.py` `Markdown.load_images_from_urls` fetched image URLs
parsed
straight out of an untrusted uploaded markdown document via a raw
`requests.get`,
with no SSRF validation. Markdown chunking always reaches this path
(`return_section_images=True`), so any authenticated user who uploads a
`.md`/`.markdown`/`.mdx` file to a knowledge base could make the server
issue
requests to internal services or cloud-metadata endpoints, e.g.
`![x](http://169.254.169.254/latest/meta-data/...)`. The `image/`
Content-Type
check only gates decoding — the outbound request (the SSRF) always
fires.

This was the one user-controlled fetch site missed by the project's
existing
SSRF-hardening (`common/ssrf_guard.py`, already applied to the crawler,
SearXNG,
RSS connector, MCP/document APIs, and OAuth avatar download).

The fix validates and DNS-pins every hop with
`common.ssrf_guard.assert_url_is_safe`
before connecting, and follows redirects manually so each redirect
target is
re-validated (closing the DNS-rebinding / redirect-bypass window),
mirroring
`common/data_source/rss_connector.py`. Blocked URLs are skipped and
logged like
any other unreachable image, so legitimate public images are unaffected.
Adds a
regression test at `test/unit_test/rag/app/test_markdown_image_ssrf.py`.

Closes #15437 

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

---------

Co-authored-by: Ubuntu <ubuntu@ubuntu-2204.linuxvmimages.local>
Co-authored-by: galuis116 <galuis116@users.noreply.github.com>
2026-06-16 18:54:55 +08:00
writinwaters
abca767103 Docs: Added v0.26.1 release notes (#16087)
### What problem does this PR solve?

Initial draft for v0.26.1 release notes.

### Type of change


- [x] Documentation Update
2026-06-16 17:55:29 +08:00
chanx
cac87d7f77 fix: remove unnecessary 'asChild' prop from FilterButton component (#16094)
### What problem does this PR solve?

fix: remove unnecessary 'asChild' prop from FilterButton component

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-16 17:55:04 +08:00
maoyifeng
1feb1c4785 fix workflow ss not found (#16085)
### What problem does this PR solve?
fix workflow ss not found
2026-06-16 17:46:07 +08:00
chanx
ff2e76e77c fix: remove unnecessary div in profile page layout (#16091)
### What problem does this PR solve?

fix: remove unnecessary div in profile page layout

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-16 17:42:29 +08:00
chanx
a15e667b3c fix: update channelTemplates to filter for Discord and Lark only (#16065)
### What problem does this PR solve?

fix: update channelTemplates to filter for Discord and Lark only
- Fixed a display issue with chunks during pipeline parsing.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-16 17:09:24 +08:00
chanx
ba8796b8da fix: update localization keys for image2text and add ocr option (#16076)
### What problem does this PR solve?

fix: update localization keys for image2text and add ocr option

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-16 17:09:08 +08:00
Jin Hai
509e5b0fed Fix auto migration issue (#16081)
### What problem does this PR solve?

Fix DB migration issue.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-16 17:02:35 +08:00
Haruko386
5bd0ed0517 fix: resolve the error caused by office_oxide (#16078)
### What problem does this PR solve?

As title

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-16 16:52:02 +08:00
Lynn
70792de899 Fix: v0.26.1 model provider (#16073)
### What problem does this PR solve?

Fix:
- Pass session_id to langfuse.
- Get correct status for add model_type.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-16 16:21:43 +08:00
Hz_
8047857de0 fix(go): all_models.json (#16075)
### What problem does this PR solve?

This PR fixes Go admin server startup failure caused by duplicate model
aliases in conf/all_models.json.

The model provider loader builds a global lookup table from both model
name and alias values. Some aliases duplicated another model's name or
another
alias, for example amazon.titan-embed-text-v1, which caused startup to
fail with a duplicate alias error. This PR removes conflicting duplicate
aliases
  while keeping all model definitions intact.
2026-06-16 15:31:17 +08:00
Haruko386
911bb20209 Document: github release for RAGFlow Go CLI (#16036)
### What problem does this PR solve?

As title

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
- [x] Documentation Update
2026-06-16 14:53:00 +08:00
Jin Hai
fad82fd1c0 Go: fix register user (#16058)
### What problem does this PR solve?

Fix register user

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-16 14:03:53 +08:00
buua436
5751a22444 fix: add toc field to extractor output (#16059)
### What problem does this PR solve?
TOC chunks now include a toc field so the agent pipeline logs expose the
data the frontend expects.

### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-16 13:27:45 +08:00
Lynn
b4a161b50e Fix: filter unsupported model_type (#16062)
### What problem does this PR solve?

As title.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-16 13:15:42 +08:00
BasilFoubert
3d55a0334a fix: improve remember me checkbox UX in login form (#16051)
### What problem does this PR solve?

## What
- Add `group` class to wrapper div to enable hover state coordination
- Apply hover styles to checkbox via group-hover
- Make FormLabel clickable via onClick toggle + cursor-pointer
- Fix label color logic: disabled vs primary state

## Why
The "Remember me" label was not clickable and had no hover feedback,
making the UX inconsistent with standard checkbox behavior.

## How to test
0. Go to the demo video before/after attached below
1. Go to the login page
2. Click directly on the "Remember me" label → should toggle the
checkbox
3. Hover over the checkbox area → should show hover styles

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

## Before


https://github.com/user-attachments/assets/bd47d45c-09ea-437f-bd98-3397ce040c1e

## After


https://github.com/user-attachments/assets/45c65d1a-bec7-4ad6-8f1c-d149f7296f8f
2026-06-16 12:57:56 +08:00
Hz_
4a33455a20 feat(go-models): add more providers (#16017)
### What problem does this PR solve?

add more providers.
2026-06-16 12:54:19 +08:00
Hz_
0c92a38055 feat(go-cli): support add models with embedding type (#16020)
### What problem does this PR solve?

This PR enhances the CLI parser to support dimension configurations for
custom embedding models. Users can now specify the maximum dimension and
other supported dimensions directly after the embedding keyword.

```
add model 'x1 x2 x3 x4 x5' to provider 'vllm' instance 'test' with 
tokens 1024 chat think vision, 
token 2048 chat, 
token 1024 think vision,
token 0 embedding 2048 64 1024 2048,
token 0 embedding 2048;
```
- The first integer following embedding represents the max_dimension.
- Any subsequent integers represent specific alternative dimensions.
- If no subsequent integers are provided, dimensions defaults to empty,
indicating all sizes under max_dimension are supported.
2026-06-16 12:53:43 +08:00
Hz_
3d7b45bbd7 feat(go-api): support setting tenant default models by model_id (#16030)
### Description
Currently, when setting tenant default models (e.g., chat, embedding,
rerank), the API only accepts the composite name
(`model_name@model_instance@model_provider`). However, some integrations
and front-end features prefer using the database `model_id` (UUID)
directly.

This PR adds support for `model_id` in default model configuration:
1. **Request Binding**: Added `model_id` (optional field) to the request
body schema in the handler.
2. **Database Lookup**: If `model_id` is supplied, the service queries
the database to resolve the respective provider, instance, and model
names.
3. **Security Validation**: Verified that the provider associated with
the resolved `model_id` belongs to the requesting tenant.
4. **Unit Tests**: Added `TestSetTenantDefaultModels_WithModelID` to
verify DB ID resolution and tenant mapping.
2026-06-16 12:53:03 +08:00
Kevin Hu
5a817762fa Refactor: Change table chat_channel status data type. (#16061)
### What problem does this PR solve?

As title.

### Type of change

- [x] Refactoring
2026-06-16 12:02:12 +08:00
Wang Qi
a6f71e0e12 Dev: consoldiate dev script (#16066)
Dev: consoldiate dev script
2026-06-16 11:53:13 +08:00
Yingfeng
956357b997 Feat: add harness-go framework —— agent core (#16045)
### What problem does this PR solve?

core module for agent layer built on top of graph engine #16039

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-06-16 11:39:48 +08:00
buua436
ee1c503471 fix: sandbox config api method mismatch (#16031)
### What problem does this PR solve?
Fixes the sandbox config API method mismatch so the frontend and backend
use the same HTTP verb.

### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-16 10:34:18 +08:00
buua436
8e235b7b95 fix: add legacy chat/completions mode (#16014)
### What problem does this PR solve?
Adds a legacy mode for /chat/completions that restores v0.23.0-style
output by converting start_to_think/end_to_think back into raw
<think></think> markers and streaming cumulative answer text.

### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-16 10:34:06 +08:00
Haruko386
efdd58df66 feat[Go] add max_dimension and dimensions for ModelRequest (#16019)
### What problem does this PR solve?

As title

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-06-16 10:31:27 +08:00
Yingfeng
e7c068747e Feat: add harness-go framework —— graph engine (#16039)
### What problem does this PR solve?

go-version of Pregel-based BSP engine

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-06-15 21:36:39 +08:00
chanx
7d94b0818e Feat: Add edit model type function (#16029)
### What problem does this PR solve?

Feat: Add edit model type function

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-15 19:11:05 +08:00
Lynn
47495c1f6a Feat: model provider (#16028)
### What problem does this PR solve?

Feat:
- Allow upsert model_type for instance model

Fix:
- Allow create instance with duplicate api_key

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
2026-06-15 19:10:33 +08:00
balibabu
ba93ac3bd7 Feat: Move less important chat settings into a collapsible panel. (#16024)
### What problem does this PR solve?

Feat: Move less important chat settings into a collapsible panel.

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2026-06-15 19:09:19 +08:00
Wang Qi
f6a2075ad0 Fix one data source can be synced to multiple dataset (#16023)
Fix one data source can be synced to multiple dataset
Test add/delete - worked.
2026-06-15 16:54:25 +08:00
balibabu
fa6d29603a Fix: Adjust chat line height. (#16021)
### What problem does this PR solve?

Fix: Adjust chat line height.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-15 16:53:45 +08:00
Jin Hai
417f805bd9 Go: add API mode check in file system command (#16022)
### What problem does this PR solve?

As title.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-15 16:37:47 +08:00
Jin Hai
e3cb86d540 Go: parse HTML file (#16018)
### What problem does this PR solve?

```
RAGFlow(api/default)> parse file 'test.html';
Parsing HTML file: test.html
  <html>
......
```

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-15 15:49:17 +08:00
dripsmvcp
53d4d9b3bd fix(api): return 4xx not 500 when attachment blob is missing (#15509)
Guard the agent-attachment download against a missing or empty storage blob so the caller gets a structured 4xx (`Document not found!`) instead of an HTTP 500. Same bug class as #15365 on document preview.
Resolve #15502
2026-06-15 15:41:49 +08:00
Haruko386
0480dee83f fix: output 2 lines when list-supported models (#16015)
### What problem does this PR solve?

```
RAGFlow(api/default)> list supported models from 'longcat' 'test'
+-----------+------------+---------------+------------+-------------+-----------------------------+----------+
| dimension | dimensions | max_dimension | max_tokens | model_types | name                        | thinking |
+-----------+------------+---------------+------------+-------------+-----------------------------+----------+
|           |            |               |            |             | LongCat-2.0-Preview@LongCat |          |
|           |            |               |            |             | LongCat-2.0-Preview@LongCat |          |
+-----------+------------+---------------+------------+-------------+-----------------------------+----------+

# Fixed:

RAGFlow(api/default)> list supported models from 'longcat' 'test'
+------------+---------------+------------+-------------+-----------------------------+----------+
| dimensions | max_dimension | max_tokens | model_types | name                        | thinking |
+------------+---------------+------------+-------------+-----------------------------+----------+
|            |               |            |             | LongCat-2.0-Preview@LongCat |          |
+------------+---------------+------------+-------------+-----------------------------+----------+
```

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-15 15:26:35 +08:00
Haruko386
cafd8a1125 Json: add many models to all_models.json (#16013)
### What problem does this PR solve?

As title

### Type of change

- [x] Other (please describe): add some models
2026-06-15 15:25:49 +08:00
Jin Hai
2846216674 Go: add Markdown parser (#16016)
### What problem does this PR solve?

```
RAGFlow(api/default)> parse file 'README.md';
Parsing Markdown file: README.md
--- AST tree:
HTMLBlock '<div align="center">\n<a href="https:…'
```

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-15 15:07:29 +08:00
Jin Hai
fcebcebe1e Move REDIS to engine dir (#16006)
### What problem does this PR solve?

as title.

### Type of change

- [x] Refactoring

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-15 14:44:16 +08:00
Hz_
bc963f8cf2 refactor(go): replace GenerateUUID1 with GenerateToken for entity IDs (#16010)
### Description
- **Refactor**: Replaced `utility.GenerateUUID1` (UUID v1) with
`utility.GenerateToken` (UUID v4) for generating entity IDs (`userID`,
`kbID`, `modelID`, etc.).

- **Cleanup**: Removed the unused `GenerateUUID1` function from
`utility` package.

- **Improvement**: Simplified ID generation logic and eliminated
unnecessary error handling boilerplate since `GenerateToken` cannot
fail.
2026-06-15 14:06:07 +08:00
buua436
400dfd50d8 feat: add custom value support for s3 region (#15968)
### What problem does this PR solve?
Allow S3-compatible data source region fields to accept custom values
while preserving search-and-select behavior.

### Type of change
- [x] New Feature (non-breaking change which adds functionality)
2026-06-15 11:40:28 +08:00
Hz_
eb6ea284a8 feat(go-models): Add google models to all_models.json (#16007)
### What problem does this PR solve?

Add google models to all_models.json
2026-06-15 11:37:56 +08:00
Yingfeng
b5bea72e4b Add git-like file commit API (#15978)
### What problem does this PR solve?

| # | Method | Endpoint | Description | Git Equivalent |
|---|--------|----------|-------------|----------------|
| 1 | `POST` | `/api/v1/{prefix}/{folder_id}/commits` | Create a
snapshot commit with file changes (add/modify/delete/rename) | `git add`
+ `git commit` |
| 2 | `GET` | `/api/v1/{prefix}/{folder_id}/commits` | List commit
history (paginated) | `git log` |
| 3 | `GET` | `/api/v1/{prefix}/{folder_id}/commits/{commit_id}` | Get
commit detail with file changes | `git show` |
| 4 | `GET` | `/api/v1/{prefix}/{folder_id}/commits/{commit_id}/files` |
List file changes in a commit | `git show --name-status` |
| 5 | `GET` |
`/api/v1/{prefix}/{folder_id}/commits/diff?from=...&to=...` | Compare
two commits and return differences | `git diff` |
| 6 | `GET` | `/api/v1/{prefix}/{folder_id}/changes` | Get uncommitted
changes (add/modify/delete) | `git status` |
| 7 | `GET` | `/api/v1/{prefix}/{folder_id}/commits/{commit_id}/tree` |
Get the folder tree snapshot at commit time | `git ls-tree` |
| 8 | `GET` |
`/api/v1/{prefix}/{folder_id}/commits/{commit_id}/files/{file_id}/content`
| Get a file's content as it existed in a specific commit | `git show
HEAD:file` |
| 9 | `GET` | `/api/v1/{prefix}/{file_id}/versions` | Get version
history for a specific file across all commits | `git log -- file` |

Where `{prefix}/{id}` can be:
- `folders/{folder_id}` — direct folder access
- `workspaces/{workspace_id}` — alias of `folders/{folder_id}`
- `datasets/{dataset_id}` — resolves to the dataset's folder
- `memories/{memory_id}` — resolves to the memory's folder
- `skills/{skill_id}` — resolves to the skill's folder

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
- [x] Documentation Update
2026-06-15 11:19:56 +08:00
zaviermeekz-cpu
83e2180e80 fix: use /api/tags endpoint for Ollama model listing (#16000) (#16003)
After upgrading to v0.26.0, the Ollama provider returns an empty model
list because the Go rewrite uses `/api/ps` (only running models) instead
of `/api/tags` (all installed models). This PR changes the endpoint to
`/api/tags`, restoring the ability to list and add Ollama models.

Closes #16000
2026-06-15 10:20:15 +08:00
Jin Hai
32d5c0039b Go: refactor model API to accept model id (#15999)
### What problem does this PR solve?

Not not only model_name@instance_name@provider_name is acceptable, but
also model_id is acceptable.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-15 10:10:14 +08:00
Wang Qi
59d4203947 Fix last login time (#16004)
Fix last login time
2026-06-15 10:06:24 +08:00
Jin Hai
e89afbae21 Go: file parser config (#15989)
### What problem does this PR solve?

Add parser config

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-13 19:40:43 +08:00
VincentLambert
f671e7cb34 i18n(fr): add ~70 missing French translation keys (#15983)
## Summary

Adds missing French (`fr.ts`) translations that were present in `en.ts`
but absent in the French locale file.

### LLM provider settings
- `openaiBaseUrlPlaceholder`, `anthropicBaseUrlPlaceholder`,
`siliconflowBaseUrlPlaceholder`
- `groupId`, `providerOrder`

### PaddleOCR (settings section)
- Validation messages: `paddleocrApiUrlMessage`,
`paddleocrAccessTokenMessage`, `paddleocrAlgorithmMessage`
- Labels/placeholders duplicated in the settings context

### MinerU configuration
- `mineruApiserver*`, `mineruOutputDir*`, `mineruBackend*`,
`mineruServerUrl*`, `mineruDeleteOutput*`, `mineruSelectBackend`

### OpenDataLoader
- `opendataloaderApiserver*` (3 keys)

### Model management UI
- `listModels`, `allModels`, `listModelsSearchPlaceholder`,
`listModelsEmpty`, `listModelsLoading`
- `selectModelBeforeVerify`, `addCustomModel`, `addCustomModelTitle`
- `modelMaxTokens`, `modelFeatures`, `modelFeatureToolCall`,
`modelFeatureFunctionCall`
- `modelNameRequired`, `modelNameDuplicate`, `modelTypeRequired`,
`modelMaxTokensMessage`, `modelMaxTokensMinMessage`

### Data source connector tips
- **Microsoft Teams**: `teamsTenantIdTip`
- **Slack**: `slackBotTokenTip`, `slackChannelsTip`
- **SharePoint**: `sharepointSiteUrlTip`
- **OneDrive**: `onedriveTenantIdTip`, `onedriveClientIdTip`,
`onedriveClientSecretTip`, `onedriveFolderPathTip`
- **Outlook**: `outlookTenantIdTip`, `outlookClientIdTip`,
`outlookClientSecretTip`, `outlookFolderTip`, `outlookUserIdsTip`
- **Salesforce**: `salesforceInstanceUrlTip`, `salesforceClientIdTip`,
`salesforceClientSecretTip`, `salesforceObjectsTip`,
`salesforceApiVersionTip`
- **Azure Blob Storage**: `azureBlobAuthModeTip`,
`azureBlobAccountNameTip`, `azureBlobAccountKeyTip`,
`azureBlobConnectionStringTip`, `azureBlobContainerUrlTip`,
`azureBlobSasTokenTip`, `azureBlobContainerNameTip`,
`azureBlobPrefixTip`

## Test plan
- [ ] Verify the French locale displays correctly in the RAGFlow UI with
language set to French
- [ ] Check that all new keys render without `[missing translation]`
placeholders
- [ ] TypeScript build passes (`npx tsc --noEmit` — no errors in
`fr.ts`)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-13 11:01:03 +08:00
Jin Hai
d32e05d560 Go: add more file parser (#15979)
### What problem does this PR solve?

Now we can parse 'pptx', 'ppt', 'doc', 'xls', 'xlsx'

```
RAGFlow(api/default)> parse file 'test.pptx';
Parsing PPTX file: test.pptx
Document format: pptx
```

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-12 23:28:14 +08:00
Zhichang Yu
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
2026-06-12 22:58:28 +08:00
bitloi
cafa0f2e4f fix: SSE write timeout (#15852)
### What problem does this PR solve?

Fixes #15840.

The Go HTTP server sets `WriteTimeout: 120s`, which also applies to
long-lived SSE responses. Existing Go streaming handlers did not clear
the per-response write deadline, so streams that run longer than the
server timeout can be terminated mid-response.

This PR adds a small handler helper that clears the response write
deadline for SSE requests and calls it only in existing Go streaming
branches:

- conversation completion streaming
- provider chat streaming
- provider transcription streaming
- provider speech streaming

The global server `WriteTimeout` remains unchanged for non-streaming
requests.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

### Test plan

- `/root/go/bin/go test ./internal/handler -run
TestDisableWriteDeadlineForSSEAllowsLongLivedStream -count=1`
- `/root/go/bin/go test ./internal/handler -count=1`
2026-06-12 20:49:34 +08:00
Jin Hai
234f1b7cff Go: add office_oxide and parse docx file. (#15976)
### What problem does this PR solve?

As title.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-12 20:28:15 +08:00
Haruko386
4115282c5f Json[model-provider] add nvidia, moonshot, minimax, claude, GPT models (#15970)
### What problem does this PR solve?

As title

### Type of change

- [x] Other (please describe): add models
2026-06-12 19:16:10 +08:00
Haruko386
547139da29 fix(Go-models): preserve model name lookup when aliases exist (#15969)
### What problem does this PR solve?

As title

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Documentation Update
2026-06-12 19:15:28 +08:00
Kevin Hu
b5a426e6e0 Feat: chat channels — connect assistants to external messaging bots (#15850)
### What problem does this PR solve?

#15844

Adds a **Chat channels** capability so a RAGFlow assistant (Dialog) can
be exposed as a bot on external messaging platforms (Feishu/Lark,
Discord, Telegram, Slack, WeCom, LINE, etc.). An admin configures a bot
in the UI, connects it to an assistant, and inbound messages are
answered from that assistant's knowledge base — replies are delivered
back on the channel.

**Feishu/Lark is implemented and tested end-to-end.** Discord, Telegram,
LINE, and WeCom are scaffolded against the same interface; the remaining
listed channels are tracked as follow-ups.

### Design

**Backend**
- New `chat_channel` table (`tenant_id`, `name`, `channel`, `config`
JSON holding `{credential: {...}}`, `dialog_id`, `status`) +
`ChatChannelService` and RESTful CRUD under `/api/v1/chat_channels`.
- Channel framework under `api/channels/`: a `core` registry +
per-channel packages that self-register a builder and implement a common
`Channel` interface (`start`/`stop`/`send` + inbound normalization) over
`IncomingMessage`/`OutgoingMessage`.
- Embedded **reconcile loop** in `ragflow_server`
(`api/channels/bootstrap.py`): loads enabled bots, and
starts/stops/restarts them as rows change (no server restart needed).
Inbound messages run the connected dialog via the non-streaming
completion path, keeping per-end-user conversation history.
- Missing optional channel SDKs degrade gracefully (channel skipped with
a warning; others unaffected). Channel-level errors are logged, not
crashed.
- Feishu's WebSocket client runs in a dedicated thread with its own
event loop to avoid cross-loop/contextvars conflicts with the channel
runtime.

**Frontend**
- **Settings → Chat channels** panel: available-channels grid +
configured-bots list with add/edit/delete and a **Connect assistant**
popup that binds a bot to a dialog.
- Brand icons via simple-icons / reused shared data-source assets, with
colored fallbacks for brands not available.
- Route, sidebar entry, i18n (en/zh), and a top-nav segment-boundary fix
so the settings page no longer highlights the Chat tab.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

### Notes
- DB: new `chat_channel` table is auto-created; `chat_channel.dialog_id`
is also covered by a `migrate_db` `alter_db_add_column` for existing
installs.
- Channel SDKs (`lark-oapi`, `discord.py`, `python-telegram-bot`,
`line-bot-sdk`, `wechatpy`, `aiohttp`) added to dependencies.
- Screenshots / per-channel credential docs to follow.

<img width="1338" height="1290" alt="Image"
src="https://github.com/user-attachments/assets/042cb2f9-0dad-4e6a-bcf7-43ced4bbd704"
/>

<img width="1344" height="738" alt="Image"
src="https://github.com/user-attachments/assets/373cd08e-ec40-4c67-9c51-4d948b1ba617"
/>

<img width="672" height="887" alt="Image"
src="https://github.com/user-attachments/assets/5a34953f-a9a3-4c1e-869e-5eff0dc64c84"
/>

---------
2026-06-12 18:21:30 +08:00
Yingfeng
5a7d7771a3 Decouple skill space from Python API (#15971)
### What problem does this PR solve?

Make skill space independent of Python filesystem API

### Type of change

- [x] Refactoring
2026-06-12 18:18:55 +08:00
Jin Hai
115b730d07 Go: parse ingestion DSL (#15938)
PR #15938

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-12 17:58:36 +08:00
balibabu
89aac82663 Fix: chat/agent -- Default avatar is not displaying correctly. (#15948)
### What problem does this PR solve?

Fix: chat/agent -- Default avatar is not displaying correctly.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-12 17:54:36 +08:00
bitloi
22a058f56c fix(go): redact internal handler errors (#15746)
### What problem does this PR solve?

Refs #15743

Some Go API handlers return raw `err.Error()` strings in
`CodeServerError` responses. Those errors can include internal backend
details such as database, storage, search engine, or host information.

This PR adds a small shared `jsonInternalError` helper for handler-level
internal failures. The helper logs the raw error server-side with
request method/path context, then returns the existing generic
`common.CodeServerError.Message()` to API clients.

This first slice migrates the existing `jsonError(c,
common.CodeServerError, err.Error())` production call sites in agent,
dataset graph, file, and system handlers. It intentionally does not
close the full issue because direct `c.JSON` error responses in other
handlers remain for follow-up PRs.

### 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):

### Tests

- `/root/go/bin/go test ./internal/handler -count=1`

---------

Co-authored-by: Yingfeng <yingfeng.zhang@gmail.com>
2026-06-12 16:09:10 +08:00
Jin Hai
e96bc37d06 Go: use NATS as the message queue (#15327)
### What problem does this PR solve?

```
RAGFlow(admin)> mq publish 'msg2';
SUCCESS
RAGFlow(admin)> mq publish 'msg3';
SUCCESS
RAGFlow(admin)> mq list;
+---------+---------------+
| message | subject       |
+---------+---------------+
| msg1    | tasks.RAGFLOW |
| msg2    | tasks.RAGFLOW |
| msg3    | tasks.RAGFLOW |
+---------+---------------+
RAGFlow(admin)> mq pull 2;
+---------+---------------+
| message | subject       |
+---------+---------------+
| msg1    | tasks.RAGFLOW |
| msg2    | tasks.RAGFLOW |
+---------+---------------+
RAGFlow(admin)> mq pull noack;
+---------+---------------+
| message | subject       |
+---------+---------------+
| abc     | tasks.RAGFLOW |
+---------+---------------+
RAGFlow(admin)> mq show
+-------------------+----------------+--------+---------------+---------------+-------------------+---------------+
| ack_pending_count | consumer_count | memory | message_count | pending_count | redelivered_count | waiting_count |
+-------------------+----------------+--------+---------------+---------------+-------------------+---------------+
| 2                 | 1              | 0      | 2             | 0             | 1                 | 0             |
+-------------------+----------------+--------+---------------+---------------+-------------------+---------------+

RAGFlow(admin)> list ingestors;
+--------------+-------------------------------------------+--------+
| host         | name                                      | status |
+--------------+-------------------------------------------+--------+
| 192.168.1.38 | ingestor-8f0e4bd5650a4ac58b0151969fbf6935 | alive  |
+--------------+-------------------------------------------+--------+

RAGFlow(admin)> list ingestion tasks;
+----------------------------------+----------------------------------+-----------+------+-------------+----------------------------------+
| document_id                      | id                               | status    | step | user        | user_id                          |
+----------------------------------+----------------------------------+-----------+------+-------------+----------------------------------+
| ffe64fae423411f1a2d938a74640adcc | 90d3d0f6528941c1ac8eb0360effccc4 | COMPLETED | 5    | aaa@aaa.com | 2ba4881420fa11f19e9c38a74640adcc |
+----------------------------------+----------------------------------+-----------+------+-------------+----------------------------------+

RAGFlow(admin)> remove ingestion tasks '90d3d0f6528941c1ac8eb0360effccc4';
+---------+----------------------------------+
| delete  | task_id                          |
+---------+----------------------------------+
| success | 90d3d0f6528941c1ac8eb0360effccc4 |
+---------+----------------------------------+

RAGFlow(admin)> stop ingestion tasks 'e89e20d9a25848a1b79bd9345ddbfe1d';
+----------+----------------------------------+
| status   | task_id                          |
+----------+----------------------------------+
| STOPPING | e89e20d9a25848a1b79bd9345ddbfe1d |
+----------+----------------------------------+

# Publish a message
RAGFlow(admin)> mq publish 'cdd';
SUCCESS

# List current tasks in the message queue
RAGFlow(admin)> mq list
+----------------------------------+---------------+
| message                          | subject       |
+----------------------------------+---------------+
| 7ce392a3c1624cd2be4b5276e8825059 | tasks.RAGFLOW |
+----------------------------------+---------------+

# Consume a task from the message queue
RAGFlow(admin)> mq pull
+------+-----+----------------+
| ack  | id  | type           |
+------+-----+----------------+
| true | cdd | ingestion_test |
+------+-----+----------------+

# User mode
# List ingestion tasks, followed by dataset id
RAGFlow(user)> list ingestion tasks from '0abe79f9423311f1ad8d38a74640adcc';
+---------------------------+---------------+----------------------------------+----------------------------------+----------------------------------+--------+-----------+---------------------------+---------------+----------------------------------+
| create_date               | create_time   | dataset_id                       | document_id                      | id                               | schema | status    | update_date               | update_time   | user_id                          |
+---------------------------+---------------+----------------------------------+----------------------------------+----------------------------------+--------+-----------+---------------------------+---------------+----------------------------------+
| 2026-05-30T20:21:06+08:00 | 1780143666289 | 0abe79f9423311f1ad8d38a74640adcc | ffe64fae423411f1a2d938a74640adcc | 8d758cd14a8b4ba8ab505003fb52017d |        | COMPLETED | 2026-05-30T20:21:26+08:00 | 1780143686431 | 2ba4881420fa11f19e9c38a74640adcc |
+---------------------------+---------------+----------------------------------+----------------------------------+----------------------------------+--------+-----------+---------------------------+---------------+----------------------------------+

RAGFlow(user)> list ingestion tasks;
+---------------------------+---------------+----------------------------------+----------------------------------+----------------------------------+--------+-----------+---------------------------+---------------+----------------------------------+
| create_date               | create_time   | dataset_id                       | document_id                      | id                               | schema | status    | update_date               | update_time   | user_id                          |
+---------------------------+---------------+----------------------------------+----------------------------------+----------------------------------+--------+-----------+---------------------------+---------------+----------------------------------+
| 2026-06-02T19:02:31+08:00 | 1780398151417 | 0abe79f9423311f1ad8d38a74640adcc | ffe64fae423411f1a2d938a74640adcc | e89e20d9a25848a1b79bd9345ddbfe1d |        | COMPLETED | 2026-06-02T19:02:52+08:00 | 1780398172208 | 2ba4881420fa11f19e9c38a74640adcc |
+---------------------------+---------------+----------------------------------+----------------------------------+----------------------------------+--------+-----------+---------------------------+---------------+----------------------------------+

# Create an ingestion task
# First argument is document id, second argument is dataset id
RAGFlow(user)> start ingestion 'ffe64fae423411f1a2d938a74640adcc' from '0abe79f9423311f1ad8d38a74640adcc';
+----------------------------------+-------------------------------------------+
| document_id                      | result                                    |
+----------------------------------+-------------------------------------------+
| ffe64fae423411f1a2d938a74640adcc | task_id: 8d758cd14a8b4ba8ab505003fb52017d |
+----------------------------------+-------------------------------------------+

# Pause an ingestion task, first argument is ingestion id
RAGFlow(user)> stop ingestion '8d758cd14a8b4ba8ab505003fb52017d';
+---------------------------+---------------+----------------------------------+----------------------------------+----------------------------------+--------+-----------+---------------------------+---------------+----------------------------------+
| create_date               | create_time   | dataset_id                       | document_id                      | id                               | schema | status    | update_date               | update_time   | user_id                          |
+---------------------------+---------------+----------------------------------+----------------------------------+----------------------------------+--------+-----------+---------------------------+---------------+----------------------------------+
| 2026-05-30T20:21:06+08:00 | 1780143666289 | 0abe79f9423311f1ad8d38a74640adcc | ffe64fae423411f1a2d938a74640adcc | 8d758cd14a8b4ba8ab505003fb52017d |        | COMPLETED | 2026-05-30T20:21:26+08:00 | 1780143686431 | 2ba4881420fa11f19e9c38a74640adcc |
+---------------------------+---------------+----------------------------------+----------------------------------+----------------------------------+--------+-----------+---------------------------+---------------+----------------------------------+

# Delete an ingestion task
RAGFlow(api/default)> remove ingestion tasks 'f366450a27d54677aec1c7090add30f0';
+---------+----------------------------------+
| remove  | task_id                          |
+---------+----------------------------------+
| success | f366450a27d54677aec1c7090add30f0 |
+---------+----------------------------------+

```

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-12 14:56:44 +08:00
Hz_
30724140d2 feat(go): Add Z.ai model entries to all_models.json Add missing Qwen commercial models and provider aliases (#15929)
### What problem does this PR solve?

- Add Z.ai model definitions to `conf/all_models.json`.
- Add missing Qwen / DashScope commercial API-only models, including:
    - Qwen3.7 / Qwen3.6 / Qwen3.5 Max, Plus, Flash families
    - Qwen Coder and Math commercial models
- Qwen VL, OCR, Omni, ASR, TTS, translation, image generation, and image
editing models
- Add verified provider-specific aliases for supported Qwen models:
  - DashScope / Alibaba Cloud Model Studio model IDs
  - OpenRouter `qwen/...` aliases
  - Amazon Bedrock `qwen.qwen3-*` model IDs
- Add `thinking` metadata for Qwen models that officially support
thinking mode.
- Remove aliases that exactly duplicate their own canonical `name`.
2026-06-12 14:33:01 +08:00
Haruko386
e3be39d0de Json: add some models (#15947)
### What problem does this PR solve?

As title

### Type of change

- [x] Other (please describe): add models
2026-06-12 14:32:21 +08:00
Carl Harris
a2de880b6d fix(profile): enforce profile name validation and input constraints (#15694)
### What problem does this PR solve?

The Profile **Name** field currently lacks application-level validation
and allows users to save excessively long names and unsupported special
characters.

While the database enforces a maximum length of 100 characters, neither
the frontend nor backend validates nickname format before persistence.
This can result in inconsistent user data, poor user experience, and UI
layout issues when long names wrap across multiple lines.

This PR introduces consistent frontend and backend validation for
profile names, enforces length and character constraints, provides clear
validation feedback, and prevents invalid values from being saved.

Fixes #15693

### Type of change

* [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-12 11:13:18 +08:00
Jonathan Chang
de06c9a60b feat: Langfuse session grouping for multi-turn chat traces (#15679)
## Summary

This PR passes `session_id` into Langfuse trace observations so
multi-turn chat messages can be grouped under the same session in
Langfuse.

Changes include:
- Propagate `session_id` from chat/session APIs into
`dialog_service.async_chat`.
- Pass `session_id` into Langfuse `start_observation(...)`.
- Share Langfuse `trace_context` with chat, embedding, rerank, and TTS
model bundles where applicable.
- Add unit coverage to verify Langfuse observations receive
`session_id`.
- Update affected test stubs for the new optional Langfuse context
arguments.

## Related Issue
Closes: #15636 

## Change Type
- [x] Feature
- [x] Bug fix
- [x] Test
- [ ] Refactor
- [ ] Documentation
- [ ] Breaking change

## Real Behavior Proof

Before this change:

- Langfuse observations were created without `session_id`.
- Multi-turn chat traces could not be grouped by session in Langfuse.

After this change:

- Chat/session flows pass `session_id` into `async_chat`.
- Langfuse observations include `session_id`.
- Related model bundles receive shared trace context and session
metadata.

Validation result:

```bash
uv run python -m py_compile \
  api/db/services/tenant_llm_service.py \
  api/db/services/llm_service.py \
  api/db/services/dialog_service.py \
  api/db/services/conversation_service.py \
  api/apps/restful_apis/chat_api.py \
  test/unit_test/api/db/services/test_dialog_service_final_answer.py \
  test/unit_test/api/db/services/test_dialog_service_use_sql_source_columns.py
```
Passed.

```bash
uv run pytest \
  test/unit_test/api/db/services/test_dialog_service_final_answer.py \
  test/unit_test/api/db/services/test_dialog_service_use_sql_source_columns.py -q
```
Result:

```text
11 passed in 16.89s
```

```bash
git diff --check
```
Passed.
## Checklist

- [x] Analyzed the issue requirement.
- [x] Checked existing Langfuse trace integration.
- [x] Implemented only the requested session grouping behavior.
- [x] Added/updated unit tests.
- [x] Ran focused tests successfully.
- [x] Ran Python compile validation.
- [x] Ran whitespace diff validation.
2026-06-12 10:18:06 +08:00
Yufeng He
0d836afd34 fix: keep max pagerank for repeated n-hop edges (#15696)
## Summary

Fixes #15695.

The Python GraphRAG path already accumulates similarity when several
N-hop paths produce the same edge, but PageRank was overwritten by the
last path. That makes ranking depend on path order for repeated edges.

This keeps the strongest PageRank seen for a repeated edge in the Python
implementation:

- `rag/graphrag/search.py`

The similarity score still accumulates exactly as before.

## To verify

- `python -m py_compile rag\graphrag\search.py`
- `git diff --check`
- `git diff --stat upstream/main` -> only `rag/graphrag/search.py`

I originally included the Go implementation too, but removed it after
maintainer feedback because the Go version is still under development
and not released yet.
2026-06-11 20:53:11 +08:00
Yingfeng
bae8c6f109 Improve docx preview (#15907) 2026-06-11 20:43:58 +08:00
Dexterity
bde2b1fc6d fix(llm): correct error handling, token accounting, and truncation in embedding providers (#15424)
### Summary

Closes #15423

`rag/llm/embedding_model.py` hosts about 40 embedding providers that
shared several defects affecting indexing reliability, cost accounting,
and error visibility. This PR fixes four concrete bugs.

**Masked, inconsistent errors (27 sites).** Nearly every provider ran
`log_exception(_e, res)` followed by `raise Exception(f"Error: {res}")`.
Because `log_exception` always raises, the second line was dead code,
and the surfaced exception varied with whether the SDK response exposed
a `.text` attribute. Every failure path now raises a single
`EmbeddingError` that includes the underlying response detail, so the
cause of a failed embedding is consistent and visible.

**Fabricated token counts.** `LocalAIEmbed` returned a hardcoded `1024`
and `OllamaEmbed` added `128` per text. These values feed `used_tokens`
and therefore billing and usage tracking. Both now report the real count
from the API (Ollama `prompt_eval_count`, LocalAI `usage`) and fall back
to a local token count only when the server omits it.

**Truncation overshoot.** The `8196` limit used by Mistral and Bedrock
exceeded the standard `8192` ceiling and could push boundary sized
inputs past the model limit. Limits are corrected to `8192` and made
intentional per provider, and providers that rely on server side
truncation now request it explicitly (Ollama `truncate=True`, Cohere
`truncate="END"`).

**Missing batching on Zhipu and Ollama.** Both issued one request per
text. They now batch like the other OpenAI compatible providers, turning
N round trips into `ceil(N / batch_size)`. Batched results are realigned
by response `index` so a chunk always keeps its own vector.

A shared `Base._batched_encode` helper owns the batch loop, optional
truncation, result accumulation, and the single error path. It is the
mechanism that lets these fixes live in one place instead of across 27
duplicated sites. The public `encode()` and `encode_queries()` contract
stays the same, so existing callers are unaffected.

Tests covering all four fixes are added under
`test/unit_test/rag/llm/test_embedding_model.py`.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-11 19:29:46 +08:00
Carl Harris
ec89fc036d fix(user-settings): collapse sidebar to icon-only rail on mobile (#15678)
## Summary

Improves the responsiveness of the User Settings layout by converting
the left navigation sidebar into a compact icon-only rail on mobile
devices.

Previously, the sidebar retained its full desktop width on narrow
viewports, reducing the available space for settings content and making
pages such as **Data Sources** difficult to use on phones and smaller
tablets.

With this change:

- Desktop layouts retain the existing full sidebar experience
- Mobile layouts (<768px) display a compact 64px icon-only navigation
rail
- Main content receives significantly more horizontal space
- Navigation and logout actions remain fully accessible on mobile

## Type of Change

- [x] Bug fix
## Screenshots

| Before | After |
|---------|---------|
| <img width="557" height="760" alt="image"
src="https://github.com/user-attachments/assets/fb0d6a90-2d57-464c-90c6-9097418c7c13"
/> | <img width="557" height="760" alt="image"
src="https://github.com/user-attachments/assets/8db36d0f-7070-41e1-b7b2-0fe9d0cceefb"
/> |

## What Changed

### Mobile Sidebar Optimization

- Added responsive mobile behavior using `useIsMobile()`
- Displays avatar and navigation icons only on mobile
- Hides user email, navigation labels, version information, theme
switcher, and logout text on smaller screens
- Preserves navigation and logout functionality through icon actions

### Layout Improvements

- Updated settings page grid layout to use fixed sidebar widths:
  - Mobile: `4rem` (64px)
  - Desktop: `303px`
- Uses `minmax(0, 1fr)` for the content panel to prevent overflow and
allow proper shrinking
- Prevents sidebar width from expanding based on content

## Impact

- Improves usability of User Settings pages on phones and small tablets
- Increases available space for settings content
- Reduces horizontal crowding and overflow issues
- Maintains the existing desktop experience

## Test Plan

### Desktop (≥768px)

- Verify the full sidebar is displayed
- Confirm email, navigation labels, version information, theme switch,
and logout text are visible
- Ensure all navigation items function correctly

### Mobile (<768px)

- Verify the sidebar collapses to a 64px icon-only rail
- Confirm main content remains readable without horizontal crowding
- Verify navigation icons route correctly:
  - Data Sources
  - Model Providers
  - MCP
  - Team
  - Profile
  - API
- Confirm logout works from the icon button

### Verification

- Run `npm run build`
- Hard refresh when testing production or Docker deployments
- Verify responsive behavior using browser device emulation
2026-06-11 19:28:44 +08:00
JPette1783
daa3811165 feat(models): add shared HTTP client, SSE parser, and stub helpers for Go model drivers (#15821)
### What problem does this PR solve?

The Go model-driver layer () has ~38,700 lines across 109 files. Roughly
74% of that is boilerplate duplicated into every driver: identical HTTP
client setup, the same 65-line SSE scanner loop, and 10-11 one-line "not
supported" stub methods per driver. Any fix must be manually propagated
to every file. Closes #15820.

This PR establishes the three shared utility files that form the
foundation for incremental driver migration:

---

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
- [x] Refactoring

---------

Co-authored-by: Haruko386 <tryeverypossible@163.com>
2026-06-11 19:20:12 +08:00
Haruko386
9c30557ef7 Go: add dimensions for list models and fix some embed-bug in providers (#15940)
### What problem does this PR solve?

As title

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
- [x] Refactoring
2026-06-11 19:18:49 +08:00
Liu An
92c4b7688b Docs: Update version references to v0.26.0 in READMEs and docs (#15941)
### What problem does this PR solve?

- Update version tags in README files (including translations) from
v0.25.6 to v0.26.0
- Modify Docker image references and documentation to reflect new
version
- Update version badges and image descriptions
- Maintain consistency across all language variants of README files

### Type of change

- [x] Documentation Update
2026-06-11 18:34:26 +08:00
writinwaters
7efa481d61 Docs: Added initial draft for v0.26.0 release notes. (#15603)
### What problem does this PR solve?

Initial draft for v0.26.0 release notes.

### Type of change


- [x] Documentation Update
2026-06-11 18:24:49 +08:00
Wang Qi
290432d172 Fix: Search mindmap not working (#15949)
Fix: Search mindmap not working
2026-06-11 17:57:27 +08:00
Hz_
312514c032 feat(go): Add embedding dimension metadata and validation (#15939)
### What problem does this PR solve?

- Replace embedding model `dimension` metadata with `max_dimension`.
- Add optional `dimensions` metadata for models with fixed selectable
output dimensions.
- Include `max_dimension` and `dimensions` in model list responses.
- Validate requested embedding dimensions before calling provider
embedding APIs.
- Forward SiliconFlow embedding dimensions with the correct `dimensions`
request field.
- Add unit coverage for embedding dimension validation rules.
2026-06-11 17:55:13 +08:00
Lynn
9d5950963b Fix: get is_tools from model record (#15946)
### What problem does this PR solve?

As title.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-11 17:29:28 +08:00
少卿
9614605bf9 fix: propagate max_tokens from model config to downstream consumers (#15945)
## Summary

`get_model_config_from_provider_instance()` was not including
`max_tokens` in its returned dict, causing all downstream consumers
(dialog truncation, message fitting, knowledge base trimming, embedding,
graphrag, RAPTOR) to fall back to the hardcoded default of **8192
tokens** regardless of the actual model context window size (e.g.,
GPT-4o 128K, Claude 200K).

Closes #15944

## Root Cause

The function builds `model_config` with only: `llm_factory`, `api_key`,
`llm_name`, `api_base`, `model_type`, `is_tools`. `max_tokens` is never
included.

Yet the data exists in four independent sources:
1. `TenantModel.extra` JSON field — written by
`provider_api_service.py:659`
2. `conf/llm_factories.json` — every model entry has `max_tokens`
3. `rag/llm/model_meta.py` — 9 provider classes fetch real context
windows from APIs
4. `TenantLLM.max_tokens` database column

None of them are read by this function.

## Fix

Two lines added, one per return path:

- **Path B** (model_obj exists → provider-instance model): reads
`max_tokens` from `model_obj.extra` JSON
- **Path C** (fallback → factory config): reads `max_tokens` from
`llm_info` (sourced from `llm_factories.json`)

Both fall back to 8192 when the value is absent, preserving backward
compatibility.

## Impact

This single 5-line change fixes the context window budget for all **78+
call sites** across **20 files** that construct `LLMBundle` or read
`max_tokens` from the config dict, including:

| Consumer | File | Effect |
|---|---|---|
| Dialog chat truncation | `dialog_service.py:562` |
`message_fit_in(msg, max_tokens * 0.95)` now uses real context window |
| Knowledge base trimming | `dialog_service.py:752` |
`kb_prompt(kbinfos, max_tokens)` now fits more retrieved content |
| Agent message fitting | `agent/component/llm.py:322` | Agent prompts
no longer truncated at 7946 tokens |
| Embedding truncation | `task_executor.py:704` | Embedding input uses
actual model limit |
| GraphRAG extraction | `graphrag/*/extractor.py` | Entity extraction
gets full context budget |
| LLM4Tenant.max_length | `tenant_llm_service.py:513` | Chat model
wrapper exposes real context window |
2026-06-11 17:24:58 +08:00
Yoorim Choi
49ef959991 i18n(ko): add Korean (한국어) translation (#15863)
### What problem does this PR solve?

- Add `web/src/locales/ko.ts` with full Korean translation (~3100 keys)
- Register `Ko = 'ko'` in `LanguageAbbreviation` enum (`common.ts`)
- Add `[LanguageAbbreviation.Ko]: '한국어'` to `LanguageAbbreviationMap`
- Add lazy-load entry in `web/src/locales/config.ts`
- Add `korean` key to all existing locale files (`ja`, `id`, `es`,
`pt-br`, `vi`, `zh-traditional`)
- Fix duplicate enum value `FileMimeType.Mdx` (`'text/markdown'` →
`'text/mdx'`)

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
- [x] Other (please describe): Korean (한국어) i18n translation + fix
duplicate FileMimeType.Mdx enum value
2026-06-11 16:55:40 +08:00
balibabu
70ae25fc7b Fix: Remove the pagination from the search and retrieval pages. (#15942)
### What problem does this PR solve?

Fix: Remove the pagination from the search and retrieval pages.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-11 16:36:05 +08:00
jaso0n0818
2971849783 fix: guard docStoreConn.delete with index_exist in parse and stop_parsing (#15876)
## What problem does this PR solve?

Closes #15874

Both the `POST /api/v1/datasets/<dataset_id>/chunks` (re-parse) and
`DELETE /api/v1/datasets/<dataset_id>/chunks` (stop-parsing) handlers
called `settings.docStoreConn.delete` unconditionally. When the
tenant/dataset index has not been created yet — fresh dataset, first
parse interrupted before any chunks were indexed, or index manually
removed — the delete call throws and the handler returns HTTP 500
**after** the document state was already mutated (RUNNING with zeroed
counters for the parse path; CANCEL with zeroed counters for the stop
path), leaving the document in an inconsistent state.

The newer `parse_documents` path in `document_api.py` already uses
`index_exist` before deleting:



## How to fix?

Apply the same `index_exist` guard to both call sites in `chunk_api.py`:

- **`parse`** (POST path, line ~192): guard the delete before
`TaskService.filter_delete`.
- **`stop_parsing`** (DELETE path, line ~242): guard the delete after
`DocumentService.update_by_id`.

Both sites already have the correct `search.index_name(tenant_id)` and
`dataset_id` parameters; the guard is a one-line addition at each site.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Wang Qi <wangq8@outlook.com>
2026-06-11 16:30:03 +08:00
jaso0n0818
d4fbc013b9 fix: tolerate raw api_key string in AzureEmbed and AzureGptV4 __init__ (#15877)
Fixes #15587

## Problem

`AzureEmbed.__init__` in `rag/llm/embedding_model.py` and
`AzureGptV4.__init__` in `rag/llm/cv_model.py` both call
`json.loads(key)` unconditionally:

```python
api_key = json.loads(key).get("api_key", "")
api_version = json.loads(key).get("api_version", "2024-02-01")
```

When a user stores a plain API key string (not a JSON object) in the
model configuration — which is a valid and common way to configure Azure
OpenAI — `json.loads` raises `JSONDecodeError`. This makes the model
fail to initialize and causes document parsing/embedding to return a 500
error.

## Fix

Wrap `json.loads` in `try/except (json.JSONDecodeError, TypeError)` and
fall back to using the raw string as the `api_key` with the default
`api_version`. This is the same pattern already applied to the Azure
chat model in PR #15604.

## Files changed

- `rag/llm/embedding_model.py` — `AzureEmbed.__init__`
- `rag/llm/cv_model.py` — `AzureGptV4.__init__`

Fixes #15857

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Wang Qi <wangq8@outlook.com>
2026-06-11 16:28:29 +08:00
bohdansolovie
381091df71 fix(dialog): guard async_ask() against empty or invalid kb_ids (#15530)
Fixes #15529 .

### Problem

`async_ask()` accessed `kbs[0]` without verifying that
`KnowledgebaseService.get_by_ids()` returned any knowledge bases. Empty
or stale `kb_ids` raised `IndexError`, which surfaced as HTTP 500 on
search/bot SSE endpoints.

### Fix

- Add an early guard when `kbs` is empty, yielding a final SSE error
event (consistent with `gen_mindmap()` in the same module).
- Add regression tests for empty `kb_ids` and deleted/invalid KB IDs.

### Test plan

- [ ] `pytest
test/unit_test/api/db/services/test_dialog_service_final_answer.py -k
"async_ask_empty or async_ask_stale"`
- [ ] Manual: `POST /api/v1/searchbots/ask` with invalid `kb_ids`
returns SSE error, not HTTP 500

---------

Co-authored-by: Wang Qi <wangq8@outlook.com>
2026-06-11 15:52:59 +08:00
kpdev
de18313f97 fix(api): POST /documents/stop removes partial chunks and resets counters (#15789)
### What problem does this PR solve?

`POST /api/v1/datasets/{dataset_id}/documents/stop`
(`stop_parse_documents`) cancels parsing tasks and sets `run` to
`CANCEL`, but it does **not** remove chunks already indexed in the doc
store or reset `progress` / `chunk_num`. REST callers can end up with a
“cancelled” document that still returns partial chunks in `GET
.../chunks` and in retrieval.

Legacy `DELETE /api/v1/datasets/{dataset_id}/chunks` (`stop_parsing`)
already performs full cleanup: it resets counters and calls
`docStoreConn.delete`. This PR aligns the newer stop endpoint with that
behavior so both paths leave the dataset consistent.

Fixes [#15788](https://github.com/infiniflow/ragflow/issues/15788).

### 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):

### Changes

- Update `stop_parse_documents` in `document_api.py` to reset `progress`
and `chunk_num` to `0` and delete partial chunks via
`docStoreConn.delete` after `cancel_all_task_of`.
- Add unit test `test_stop_parse_documents_cleans_partial_chunks` to
assert counters reset and doc store delete is invoked.

### Test plan

- [x] Unit test: `pytest
test/testcases/test_http_api/test_file_management_within_dataset/test_doc_sdk_routes_unit.py::TestDocRoutesUnit::test_stop_parse_documents_cleans_partial_chunks
-v`
- [ ] Manual: upload a slow document, start parse, call `POST
.../documents/stop` while `RUNNING`, verify `GET .../chunks` returns
zero chunks and UI `chunk_count` is 0
- [ ] Control: legacy `DELETE .../chunks` behavior unchanged

---------

Co-authored-by: Wang Qi <wangq8@outlook.com>
2026-06-11 15:51:32 +08:00
oktofeesh
c15b2b3f66 fix(connectors): enforce WebDAV numeric string size limits (#15731)
## Summary
- Normalize WebDAV file-size metadata before applying the sync size
threshold.
- Enforce the same threshold for numeric string sizes in both document
sync and slim snapshot paths.
- Add focused WebDAV unit coverage for size parsing and over-threshold
skips.

## Why
Some WebDAV servers return file sizes from PROPFIND metadata as strings.
The previous threshold check only handled integer values, so oversized
files could still be downloaded and sent into the chunking pipeline.

Closes #15724.

## Validation
- `uv run --no-project --with pytest --with pytest-asyncio pytest
test/unit_test/data_source/test_webdav_connector_unit.py -q`
- `uvx ruff check common/data_source/webdav_connector.py
test/unit_test/data_source/test_webdav_connector_unit.py`
- `python -m compileall -q common/data_source/webdav_connector.py
test/unit_test/data_source/test_webdav_connector_unit.py`
- `git diff --check`

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 15:47:54 +08:00
Rene Arredondo
b978e26208 fix(db): drop Peewee-auto-named unique index on tenant_model_instance (#15699) (#15879)
## Summary

Fixes #15699.

User upgrades to v0.25.6 against an existing MySQL database, tries to
add an Ollama provider instance, and gets:

```
MySQL IntegrityError: Duplicate entry 'dbaafbfe608a11f1a5516d6066988224'
for key 'tenant_model_instance.tenantmodelinstance_api_key_provider_id'
```

The route at
[api/apps/restful_apis/provider_api.py:354](api/apps/restful_apis/provider_api.py#L354)
catches it and returns `get_error_data_result(message="Internal server
error")` — which by RAGFlow's convention is HTTP 200 with an error
`code` on the body — hence the reporter's "200 status code but the
database errored" complaint.

### Root cause

The provider-instance refactor in [PR
#15460](https://github.com/infiniflow/ragflow/pull/15460) dropped the
unique-compound-index tuple from `TenantModelInstance`:

```python
# Removed in #15460
class Meta:
    db_table = "tenant_model_instance"
    indexes = (
        (("api_key", "provider_id"), True),   # unique
    )
```

and added a one-shot drop in `migrate_db()` for existing databases. But
the drop targets the wrong index name:

```python
# Before this PR — wrong name
for table_name, index_name in [
    ("tenant_model_instance", "idx_api_key_provider_id"),       # ← doesn't exist
    ("tenant_model",          "idx_provider_model_instance"),
]:
```

Peewee's auto-derived index name is `<lowercase
classname>_<col1>_<col2>` →
**`tenantmodelinstance_api_key_provider_id`**, which matches the user's
error verbatim. The drop raises `OperationalError: 1091 (HY000): Can't
DROP …`, the surrounding `except` clause at
[db_models.py:1736](api/db/db_models.py#L1736) swallows it as
expected-on-fresh-installs, and the legacy unique index lives on
indefinitely.

### Why Ollama hits it specifically

Ollama doesn't require an API key. The form posts `api_key: ""`. The
app-layer dedupe at
[provider_api_service.py:288-292](api/apps/services/provider_api_service.py#L288-L292):

```python
api_key_str = ""
if api_key:                                                     # ← skipped for ""
    ...
    same_key_instance = TenantModelInstanceService.get_by_provider_id_and_api_key(...)
    if same_key_instance:
        return False, f"Already exist instance: ... with api_key {api_key}"
```

falls through for empty keys. Control reaches
`TenantModelInstanceService.create_instance(..., api_key="")` which
inserts a row whose `(api_key, provider_id) = ("", <provider_uuid>)`
collides with any prior Ollama row that already shipped that same pair →
the still-present unique index throws.

(`dbaafbfe608a11f1a5516d6066988224` in the user's error is the
duplicated `provider_id` UUID, paired with the empty `api_key`.)

### Fix

Add the Peewee auto-name alongside the existing `idx_*` entry so the
migration finally drops the obsolete index on next restart:

```python
legacy_indexes = [
    ("tenant_model_instance", "idx_api_key_provider_id"),
    ("tenant_model_instance", "tenantmodelinstance_api_key_provider_id"),  # ← added
    ("tenant_model",          "idx_provider_model_instance"),
]
```

The surrounding `try/except (OperationalError, ProgrammingError)`
matches `1091` / `can't DROP` / `does not exist` and treats them as
success, so every state is idempotent (see Test plan).

### Idempotency matrix

| Database state | First entry (`idx_api_key_provider_id`) | New entry
(`tenantmodelinstance_api_key_provider_id`) |
| --- | --- | --- |
| Fresh install (≥ #15460) — neither index exists | `1091` → swallowed |
`1091` → swallowed |
| Upgraded from before dc4b82523 (the user's case) — auto-name present |
`1091` → swallowed | **drops the index** |
| Upgraded after a manual rename to `idx_*` | drops the index | `1091` →
swallowed |
| Re-run of `migrate_db()` after either of the above | `1091` →
swallowed | `1091` → swallowed |

No rollback hazard: nothing depends on this unique constraint anymore
(`create_instance` dedupes by `instance_name` via `duplicate_name`, see
[tenant_model_instance_service.py:27](api/db/services/tenant_model_instance_service.py#L27)).

### What this PR does NOT change

- **`provider_api_service.create_provider_instance`** — its `if
api_key:` gate is correct *for the post-migration world*: multiple
Ollama instances with empty keys under one provider are legitimate, so
we shouldn't tighten the app-layer check.
- **`TenantModelInstance` Peewee model** — the `indexes` tuple was
already removed in #15460. New databases never get the constraint in the
first place.
- **The `except → get_error_data_result` → HTTP 200 pattern at
`provider_api.py:354`** — that's a project-wide convention; changing one
route to HTTP 500 would be inconsistent and out of scope.

## Test plan

- [ ] **Reproducer (pre-fix):** on a database originally created before
#15460, configure an Ollama provider with an empty `api_key`, then try
to create a *second* instance under the same provider — confirm the
`Duplicate entry … 'tenantmodelinstance_api_key_provider_id'` error in
the server log.
- [ ] **Verify the index is present pre-restart:** `SHOW INDEX FROM
tenant_model_instance WHERE Key_name =
'tenantmodelinstance_api_key_provider_id';` — non-empty result.
- [ ] **Restart with the fix applied:** server starts cleanly,
`migrate_db()` runs, no `Failed to drop index` in critical logs.
- [ ] **Verify the index is gone post-restart:** same `SHOW INDEX` query
— empty result.
- [ ] **Re-run the reproducer:** two Ollama instances under the same
provider, both `api_key=""`, both succeed.
- [ ] **Restart a second time** — no new errors; the matching `1091`
swallow keeps the migration idempotent.
- [ ] **Fresh install smoke test:** drop the DB volume, start clean — no
`1091` noise (the new index never existed), no functional regression.

## Files changed

- [api/db/db_models.py](api/db/db_models.py) — extend the legacy-index
drop list with `tenantmodelinstance_api_key_provider_id`; refactor the
inline list to a named `legacy_indexes` local with a comment pointing at
#15460 and #15699.

### 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: Wang Qi <wangq8@outlook.com>
2026-06-11 15:47:12 +08:00
monsterDavid
a851228ded fix(preview): authenticate markdown document preview requests (#15589)
## Summary

Fixes [#15585](https://github.com/infiniflow/ragflow/issues/15585).

- Route markdown preview through the shared `request` client (same as
txt/image previewers) so `Authorization` headers and interceptors are
applied consistently.
- Add a unit test covering `AUTH_BETA` token loading for embedded search
auth.

## Root cause

Search result preview for `.md`/`.mdx` used raw `fetch`, which did not
apply the same auth path as other preview types. That led to `401` on
`GET /api/v1/documents/{id}/preview` even when the user was logged in or
using an embedded search `auth` query param.

## Test plan

- [ ] Log in, run a search, open a markdown citation link — preview
loads (no 401).
- [ ] Open an embedded shared search URL with `auth` query param,
preview a markdown file — preview loads.
- [ ] Confirm PDF/txt preview still works in the same search UI.

---------

Co-authored-by: MkDev11 <89318445+bitloi@users.noreply.github.com>
Co-authored-by: Wang Qi <wangq8@outlook.com>
2026-06-11 15:46:20 +08:00
bohdansolovie
47fb462e46 fix(api): guard dataset delete when File2Document row is missing (#15533)
## Summary
Fixes #15532 — `delete_datasets()` crashes with `IndexError` when a
document has no `File2Document` row.
`delete_datasets()` in `dataset_api_service.py` called
`File2DocumentService.get_by_document_id()` and immediately accessed
`f2d[0].file_id` without checking whether the lookup returned any rows.
Documents created via API ingestion or connector sync may exist without
a linked file record, causing dataset deletion to abort with HTTP 500.
This PR mirrors the existing guard already used in `file_service.py` and
`document_api_service.py`.
2026-06-11 15:18:08 +08:00
Idriss Sbaaoui
9871a7e0b6 fix: replicate model provider (#15933)
### What problem does this PR solve?

FIx replicate model provider failing with valid api key 

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

---------

Co-authored-by: Wang Qi <wangq8@outlook.com>
2026-06-11 15:08:33 +08:00
Rene Arredondo
3f929e3904 fix(es): downgrade LLM-generated invalid SQL to WARNING in ES sql() (#15409) (#15709)
## Summary

Fixes #15409.

Reporter sees scary ERROR-level stack traces in `ragflow_server.log` on
every chat turn against a knowledge base whose spreadsheet has many
columns with embedded IDs (e.g. `id-wstc-bios fvt-322-wstc-bios
fvt-323`). Simple queries work; complex ones return "No answer" with
logs that look like a hard crash.

### What's actually happening

1. The user uploads a wide Excel/CSV.
[rag/app/table.py:477-493](rag/app/table.py#L477-L493) turns each header
into an ES field with a type suffix, e.g. `id-wstc-bios
fvt-322-wstc-bios fvt-323_tks`. This is correct — the parser faithfully
encodes the user's column names.
2. The user asks about test case `fvt-085`. The SQL chat path in
[api/db/services/dialog_service.py:914
use_sql](api/db/services/dialog_service.py#L914) asks the LLM to write
SQL using the field list. The LLM sees the `id-wstc-bios
fvt-NNN-wstc-bios fvt-MMM_tks` pattern and pattern-completes a
plausible-but-nonexistent column.
3. Elasticsearch rejects with `BadRequestError(400,
'verification_exception')`: `Unknown column [id-wstc-bios
fvt-085-wstc-bios fvt-086_tks]` and suggests the closest valid column.
4. **The recovery path already exists**: `use_sql` catches the
exception, re-prompts the LLM with the error text (which contains ES's
"did you mean" hint), and on second failure the caller at
[api/db/services/dialog_service.py:626](api/db/services/dialog_service.py#L626)
falls back to vector search. The chat does produce an answer — it's just
generated from the vector hits instead of SQL.

The only real bug is logging:

-
[common/doc_store/es_conn_base.py:399](common/doc_store/es_conn_base.py#L399)
catches every exception with `self.logger.exception(...)`, which writes
a full traceback at **ERROR** level.
- For LLM-generated SQL this is the hot path, not an exceptional
condition — it can fire twice per turn before the fallback runs.

### Fix

Catch `elasticsearch.BadRequestError` (the parent class of
`verification_exception` / `parsing_exception` / similar SQL-validity
errors) separately and log it at **WARNING** with the SQL plus ES error
message. The message still carries the unknown column name and ES's
suggested alternative, so it's actionable for anyone investigating "why
is my LLM producing bad SQL?" — just without the misleading stack trace.

Other exception types (`ConnectionTimeout`, generic `Exception`) keep
their original `ERROR`-level traceback treatment; those represent real
connectivity / library bugs.

This is a one-file, two-line-net change. The retry loop in `use_sql`,
the `add_kb_filter` injection, and the vector-search fallback are all
unchanged.

### What this PR does NOT change

- **The LLM prompts in `use_sql`** — they already specify `Use EXACT
field names from the schema` and pass the field list explicitly.
Strengthening them risks regressing well-behaved cases and is out of
scope for #15409.
- **The single-retry policy** — extending it to multi-retry with
extracted ES suggestions is a separate enhancement.
- **The parser at `rag/app/table.py`** — the field names match the
user's actual column headers; the parser is doing its job.

## Files changed

- [common/doc_store/es_conn_base.py](common/doc_store/es_conn_base.py)
  - Add `BadRequestError` to the `elasticsearch` import.
- In `ESConnectionBase.sql()`, add an `except BadRequestError` arm above
the generic `except Exception` that logs at WARNING and re-raises (so
`use_sql` retry/fallback still triggers).
2026-06-11 15:04:52 +08:00
zaviermeekz-cpu
a1dc2da7b4 fix: add model_name to embed completion request (#15883) (#15888)
### What problem does this PR solve?

When embedding a chatbot, the API returned `"Model Name is required"`.
The embed widget now includes the assistant's `llm_id` as `model_name`
in the completion request.

### Type of change

- [x] Bug Fix

### How has this been tested?

- Created a chatbot with a default model.
- Embedded it and sent a message – the error is gone and the assistant
replies correctly.

### Related Issue

Closes #15883

Co-authored-by: RAGFlow Dev <dev@ragflow.local>
Co-authored-by: Wang Qi <wangq8@outlook.com>
2026-06-11 14:38:37 +08:00
balibabu
5d3f8bbf32 Fix: The regular expression configuration for pipeline header-based chunking will be reset. (#15935)
### What problem does this PR solve?

Fix: The regular expression configuration for pipeline header-based
chunking will be reset.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-11 14:12:24 +08:00
Wang Qi
906618fb30 Fix Agent chat Minimax content in thinking (#15937)
Fix Agent chat Minimax content in thinking
2026-06-11 14:09:57 +08:00
Jin Hai
ca00d23aac Go: add parse and chunk command (#15936)
### What problem does this PR solve?

Two commands are used for ingestion file testing
```
RAGFlow(api/default)> chunk 'file' with 'dsl';
Chunk file: file, DSL: dsl
SUCCESS
RAGFlow(api/default)> parse file 'filename' chat 'xxx';
Success to parse local file "filename", vision: , chat: xxx, asr: , ocr: , embedding: , doc_parse: 
SUCCESS
```

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-11 13:33:26 +08:00
Haruko386
84edf539e7 Go: Refactor list-models func (#15900)
### What problem does this PR solve?

As title
Issue: #15853 

### Type of change

- [x] Refactoring
2026-06-11 13:32:50 +08:00
JPette1783
4b10c0b885 fix(go-models): guard nil pointers in DeepSeek and VolcEngine streaming (#15817)
### What problem does this PR solve?

`ChatStreamlyWithSender` in two Go model drivers could panic on nil
pointer dereferences when a caller passes a nil model config or omits
the reasoning `Effort`:

- **deepseek.go** - `switch *chatModelConfig.Effort` dereferenced
`Effort` without a nil check. It now defaults to `"high"` when nil.
- **volcengine.go** - the `modelConfig` pointer itself was dereferenced
(`Stream`, `MaxTokens`, `Temperature`, .) with no guard, and `Effort`
was dereferenced unchecked. `modelConfig` now defaults to an empty
`&ChatConfig{}` when nil so the optional-field accesses are safe, and
`Effort` defaults to `"medium"` when nil.

Addresses the CodeRabbit review on `volcengine.go`
`ChatStreamlyWithSender`. Per maintainer feedback ("one PR do one
thing"), the unrelated `handler/auth.go` and
`service/heartbeat_sender.go` changes were removed so this PR is scoped
to the model-provider fixes.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-11 13:32:24 +08:00
Rene Arredondo
19104168a6 fix(sync): tolerate list inputs for Discord server_ids / channels (#15790) (#15809)
## Summary

Fixes #15790.

Every Discord sync launched from the current Web UI crashes immediately
with:

```
'list' object has no attribute 'split'
```

The error is raised in
[rag/svr/sync_data_source.py:650-651](rag/svr/sync_data_source.py#L650-L651):

```python
server_ids=server_ids.split(",") if server_ids else [],
channel_names=channel_names.split(",") if channel_names else [],
```

### Root cause

Three independent bugs stack here, all in the Discord branch of
`sync_data_source.py`:

1. **Type mismatch (the user's exact error).** The current form at
[web/src/pages/user-setting/data-source/constant/index.tsx:833-843](web/src/pages/user-setting/data-source/constant/index.tsx#L833-L843)
uses `FormFieldType.Tag` for both **Server IDs** and **Channels**:

    ```tsx
{ label: 'Server IDs', name: 'config.server_ids', type:
FormFieldType.Tag, required: false },
{ label: 'Channels', name: 'config.channels', type: FormFieldType.Tag,
required: false },
    ```

Tag inputs serialise to **lists**, not comma-separated strings. The
backend `.split(",")` then explodes on the very first sync.

2. **Field-name mismatch.** The form writes `config.channels`. The
backend reads `self.conf.get("channel_names", None)`. Even if
`.split(",")` were fixed, channels would silently be empty for every
UI-created source.

3. **Int conversion missing.**
[common/data_source/discord_connector.py:82](common/data_source/discord_connector.py#L82)
types `server_ids` as `list[int]` (Discord guild IDs are integers); the
previous `.split(",")` produced strings, so the `channel.guild.id not in
server_ids` filter at
[discord_connector.py:92](common/data_source/discord_connector.py#L92)
silently never matched.

So even the configurations that didn't crash were also broken — there is
no path through the current code that actually filtered by server id
from a UI-created source.

### Fix

A 39-line patch in one function:

- New `Discord._coerce_str_list` static method: accepts `None` / `""` /
`list` / `tuple` / `set` / scalar / comma-separated str, returns a clean
`list[str]` with whitespace trimmed and empty entries dropped.
Smoke-tested against the 10 input shapes that can hit it (see Test
plan).
- `_generate` reads `config.channels` first (the form's actual key) and
falls back to `config.channel_names`, so SDK callers and legacy configs
that already shipped with the old key keep working.
- `server_ids` is coerced to `list[int]`. Non-integer entries are logged
and dropped instead of crashing the sync, so a single malformed tag from
the form doesn't tank the rest of the run.

### What this PR does NOT change

- **Web form key (`config.channels`)** — kept as-is. Renaming it to
`channel_names` would force a UI migration and break in-flight configs;
the backend fallback solves the same problem more safely.
- **`common/data_source/discord_connector.py`** — its signature was
already correct.
- **Other connectors (Slack, Gmail, Confluence, etc.)** — they don't
crash today and were not in the issue's scope.

## Test plan

`Discord._coerce_str_list` has been exercised against all ten realistic
input shapes — list, tuple, set, comma-separated string, str with extra
whitespace, empty entries, integers from a Tag input, None, empty list,
single trailing comma. All pass.
2026-06-11 13:27:42 +08:00
zaviermeekz-cpu
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>
2026-06-11 13:24:48 +08:00
Rene Arredondo
a079c08594 fix(deps): exclude litellm 1.82.6 (internal ImportError) — #15916 (#15920)
## Summary

Fixes #15916.

A fresh `docker compose -f docker-compose-macos.yml up -d` against
v0.25.6 errors out on container start with
2026-06-11 11:40:07 +08:00
Wang Qi
238a01d9e3 Fix multiple tags (#15931)
Fix multiple tags
2026-06-11 10:55:28 +08:00
Lynn
32559d2dfc Fix: model list (#15914)
### What problem does this PR solve?

Display OCR tag for model providers.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-11 09:40:45 +08:00
Rene Arredondo
bf59eb77cc feat(go-api): port forgot-password flow to Go (#15282) (#15290)
## Summary

Implements **chunk 1** of #15282 — the four `/api/v1/auth/password/...`
endpoints from the login-page Go port. **Chunk 2 (OAuth/OIDC) is
deferred** to its own subtask, matching the issue author's own
confidence-low recommendation ("multi-provider, stateful redirect flow
with external dependencies; recommend its own subtask").

New endpoints, all registered under `apiNoAuth` (forgot-password users
are unauthenticated by definition):

| Method | Path | Status |
|--------|------|--------|
| `POST` | `/api/v1/auth/password/forgot/captcha` | new |
| `POST` | `/api/v1/auth/password/forgot/otp` | new |
| `POST` | `/api/v1/auth/password/forgot/otp/verify` | new |
| `POST` | `/api/v1/auth/password/reset` | new |

## Wire compatibility with the Python backend

The two backends share state through Redis, so the Go port had to use
identical keys, encodings, and constants. Either backend can now
validate a code the other minted.

- **Redis keys**: `captcha:<email>`, `otp:<email>`,
`otp_attempts:<email>`, `otp_last_sent:<email>`, `otp_lock:<email>`,
`otp:verified:<email>` — same as `api/utils/web_utils.py`.
- **Stored OTP value**: `"<hex_hash>:<hex_salt>"` — same as Python.
- **Hash**: HMAC-SHA256 with a `crypto/rand` 16-byte salt — same as
`hash_code()`.
- **Constants**: `OTP_LENGTH=4`, `OTP_TTL=5min`, `ATTEMPT_LIMIT=5`,
`ATTEMPT_LOCK_SECONDS=30min`, `RESEND_COOLDOWN_SECONDS=60s` — all match
`api/utils/web_utils.py`.
- **Email body**: matches `RESET_CODE_EMAIL_TMPL` byte-for-byte.

## Files

### New

| File | Purpose |
|---|---|
| `internal/utility/otp.go` | OTP/captcha constants, Redis key builders
(`CaptchaRedisKey`, `OTPRedisKeys`, `OTPVerifiedRedisKey`),
`HashOTPCode`, `GenerateOTPCode` / `GenerateCaptchaCode` /
`GenerateOTPSalt` via `crypto/rand`, and `EncodeOTPStorageValue` /
`DecodeOTPStorageValue` matching Python's storage shape. |
| `internal/utility/smtp.go` | Minimal stdlib `net/smtp` sender.
`SendResetCodeEmail(to, otp, ttlMin)` builds an RFC 5322 plain-text
message and dispatches via implicit TLS / STARTTLS / plain — same
selectors as Python `aiosmtplib`. Returns `SMTPNotConfiguredError` if
the config block is empty. |

### Modified

| File | Change |
|---|---|
| `internal/server/config.go` | New `SMTPConfig` struct + `Config.SMTP`
field. Field names mirror the `smtp:` keys in `common/settings.py`
(`mail_server`, `mail_port`, `mail_use_ssl`, `mail_use_tls`,
`mail_username`, `mail_password`, `mail_from_name`, `mail_from_address`,
`mail_frontend_url`) so a single `conf/service_conf.yaml` powers both
backends. |
| `internal/service/user.go` | Four methods — `ForgotIssueCaptcha`,
`ForgotSendOTP`, `ForgotVerifyOTP`, `ForgotResetPassword`. Reuses the
existing `decryptPassword`, `HashPassword`, `userDAO.Update`, and
`utility.GenerateToken` so the reset+auto-login path is identical to
`LoginByEmail`. |
| `internal/handler/user.go` | Four handlers in the same `c.JSON` shape
as `LoginByEmail`. The reset handler rotates the access token and emits
an `Authorization` header for auto-login (matches Python
`construct_response(auth=user.get_id())`). |
| `internal/router/router.go` | Routes registered under `apiNoAuth`,
with an explanatory comment on why they sit outside the auth middleware.
|

## Known divergence — captcha rendering

The Python endpoint returns a rendered `image/JPEG` from the
`python-captcha` library. The Go side has **no image-captcha dependency
vendored** in `go.mod`, and hand-rolling a raster generator was out of
scope for this PR.

This commit returns JSON `{captcha: "<text>"}` instead. Implications:

- **Backend gate is identical** — the OTP step still verifies the
user-submitted captcha string against the Redis value, so the security
model is unchanged.
- **Frontend impact**: the password-reset page rendering needs a small
tweak (text display instead of `<img>`) until a Go captcha library is
wired in.
- The handler comments call this out explicitly so the next PR knows
what to swap.

Possible follow-ups (any one closes the gap):
1. Add `github.com/mojocn/base64Captcha` or `github.com/dchest/captcha`
to `go.mod` and replace the JSON response with an `image/JPEG`.
2. Hand-roll a 5x7 bitmap font + `image/png` writer using only the
stdlib.
3. Render a server-side SVG (cheap, but trivially OCR-able — only useful
as a UI shim).

## Test plan

- [ ] **Captcha**: `POST
/api/v1/auth/password/forgot/captcha?email=<existing>` returns `{code:
0, data: {captcha: "ABCD"}}`. Redis shows `captcha:<email>` with that
value and ~60s TTL. Unknown email returns `code: CodeDataError`.
- [ ] **OTP send**: `POST /api/v1/auth/password/forgot/otp` with the
right captcha mints an OTP, stores `<hash>:<salt>` under `otp:<email>`
for 5 min, sends an email, returns success. With a wrong captcha returns
`CodeAuthenticationError`. Hitting it again within 60s returns "you
still have to wait …" with `CodeNotEffective`.
- [ ] **OTP verify**: correct OTP → `code: 0`, OTP keys cleared,
`otp:verified:<email>` = `"1"`. Wrong OTP → `code:
CodeAuthenticationError`, attempt counter bumped; after 5 wrong tries
`otp_lock:<email>` is set and further attempts hit `CodeNotEffective`.
- [ ] **Reset**: with the verified flag set, supply a new password
(RSA-encrypted+base64, same as `LoginByEmail`). Returns `code: 0`,
`Authorization` header set, verified flag deleted. Without the verified
flag returns `CodeAuthenticationError`.
- [ ] **Wire-compat smoke**: mint an OTP from the Python backend, verify
it via the Go endpoint, and vice versa. Should both succeed.
- [ ] **SMTP misconfigured**: drop `smtp.mail_server` from
`conf/service_conf.yaml`. The OTP-send endpoint should now return
"failed to send email" without panicking; check the log for the
`SMTPNotConfiguredError` warning.
- [ ] **End-to-end FE**: hit the password-reset flow from
`web/src/pages/login-next/`. Confirm the text-captcha shim works after
the FE tweak.
- [ ] `go build ./...` and `go vet ./...` — I could not run these in the
sandbox; please confirm a clean build before merging.
- [ ] `uv run pytest` to confirm no Python regressions (shared Redis
schema).

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-06-10 21:27:56 +08:00
Jonathan Chang
dfcf226ba3 feat: Implement API of ragflow server in Go (#15256)
## Summary
- Implemented the Go API endpoint for Memory message forgetting:
  - `DELETE /api/v1/messages/{memory_id}:{message_id}`
- Added route registration for the Memory message DELETE endpoint only.
- Added request path validation for `memory_id:message_id`.
- Added service logic to mark a message as forgotten by setting
`forget_at`.
- Preserved Python-compatible response behavior:
  - Success returns `code: 0`, `message: true`, `data: null`.
- Added focused unit tests for message path parsing and invalid message
ID handling.
- Fixed Linux cgo linker config to use the installed shared PCRE2
library so Go tests/builds can run in this environment.
## Related Issue
Closes: #15240 
## Change Type
- [x] Feature
- [x] Test
- [x] Build / CI compatibility

## Implemented API
- `DELETE /api/v1/messages/{memory_id}:{message_id}`
## Real Behavior Proof
Validated with targeted Go tests:
```bash
/tmp/go1.25.0/bin/go test ./internal/handler ./internal/router
```
Result:
```text
ok  	ragflow/internal/handler
?   	ragflow/internal/router	[no test files]
```
Validated server entrypoint build:
```bash
/tmp/go1.25.0/bin/go build -o /tmp/ragflow-server-main ./cmd/server_main.go
```

Result:
```text
build succeeded
```
Validated patch formatting:
```bash
git diff --check
```

Result:

```text
no whitespace errors
```
## Checklist
- [x] Implemented only `DELETE
/api/v1/messages/{memory_id}:{message_id}`.
- [x] Did not implement unrelated Memory message APIs.
- [x] Added route registration.
- [x] Added handler validation.
- [x] Added service-level memory access check.
- [x] Added tests.
- [x] Ran targeted Go tests.
- [x] Ran server build validation.
- [x] Ran `git diff --check`.
2026-06-10 21:27:35 +08:00
Jin Hai
3e4fb8cf1c Go: fix test and remove unused code (#15909)
### What problem does this PR solve?

1. Fix go test, some cases still failed.
2. Remove unused code.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-10 20:38:43 +08:00
Hz_
e132173d1a feat(go): Update Qwen models in all_models.json (#15910)
## Summary

- Add official Qwen models to `conf/all_models.json` with `qwen/`
canonical names
- Include verified aliases from official Qwen/Hugging Face model IDs and
common provider naming
- Add metadata for context length, model types, thinking support, and
embedding dimensions

  ## Details

- Added Qwen model families from the official Hugging Face Qwen
organization
  - Normalized canonical model names to the `qwen/...` format
- Preserved official HF IDs and lowercase/common aliases for lookup
compatibility
  - Added `dimension` for Qwen embedding models
  - Added or corrected `max_tokens` for Qwen model families, including:
    - Qwen2.5 Instruct variants
- Qwen3 original, 2507, VL, Coder, Coder-Next, Next, Embedding, and
Reranker models
    - Qwen3.5 and Qwen3.6 models
    - QwQ models
  - Added verified `thinking` metadata where officially supported
- Corrected `model_types` for Qwen Image, Omni, Audio, VL, embedding,
reranker, benchmark, and tokenizer entries
2026-06-10 20:37:01 +08:00
Hz_
515acf4f60 fix(go): Fix case-insensitive model alias lookup (#15911)
## Summary

- Normalize model alias index keys to lowercase
- Detect lowercase alias collisions during provider manager
initialization
- Fix ListModels metadata mapping for mixed-case provider aliases
2026-06-10 20:36:43 +08:00
chanx
dfa4c5a795 Fix: add image2text/speech2text/ocr support (#15915)
### What problem does this PR solve?

Fix:  add image2text/speech2text/ocr support

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-10 20:28:25 +08:00
Wang Qi
acaeb416ca Fix cannot add fish audio (#15913)
Fix cannot add fish audio
2026-06-10 20:27:43 +08:00
chanx
1fd9e1df8e Fix: add thin scrollbar styling for x-spreadsheet component (#15912)
### What problem does this PR solve?

Fix: add thin scrollbar styling for x-spreadsheet component

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-10 19:39:00 +08:00
balibabu
aafe6c5534 Fix: The dataset retrieval test returned an incorrect total number. (#15901)
### What problem does this PR solve?

Fix: The dataset retrieval test returned an incorrect total number.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

---------

Co-authored-by: balibabu <assassin_cike@163.com>
2026-06-10 19:11:31 +08:00
buua436
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)
2026-06-10 19:09:18 +08:00
Jack
0d3e410826 fix: strip Ollama-style tag suffix from LocalAI model names (#15908)
## Summary

LocalAI exposes two API surfaces with conflicting naming conventions:
- `GET /api/tags` returns model names with `:latest` suffix (Ollama
format)
- `POST /v1/chat/completions` expects names without `:latest` (OpenAI
format)

RAGFlow discovered models via `/api/tags` and stored the tagged name,
then used it with `/v1/chat/completions`, causing a 404 error because
LocalAI didn't recognize `model:latest`.

## Fix

In `LocalAI.get_model_list()`, strip the tag suffix from model names
using `model["name"].rsplit(":", 1)[0]`, so stored names match what the
OpenAI-compatible endpoints expect.
2026-06-10 19:05:05 +08:00
Lynn
7355db183f Fix: model list (#15905)
### What problem does this PR solve?

Set OpenDataLoader and call in parser and naive

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-10 17:44:50 +08:00
Wang Qi
ad63877f04 Fix cannot add bedrock (#15904)
Fix cannot add bedrock
2026-06-10 17:08:15 +08:00
少卿
8e17a12990 fix: remove think text buffering for real-time reasoning stream (#15891)
Fix: remove think text buffering for real-time reasoning stream
2026-06-10 16:55:57 +08:00
Wang Qi
3091d91cf7 Fix no need to put inactive models to bottom (#15903)
Fix no need to put inactive models to bottom
2026-06-10 16:55:02 +08:00
Hunnyboy1217
16d5b4fa02 feat[Go]: implement POST /api/v1/files/link-to-datasets (#15674)
### What problem does this PR solve?

Closes #15673 — ports the Python `file2document_api.py` `convert()`
endpoint to Go.

| Method | Path | Handler |
|--------|------|---------|
| POST | `/api/v1/files/link-to-datasets` | `FileHandler.LinkToDatasets`
|

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

---

#### Implementation notes

**Files changed:**

```
internal/service/file2document.go  – new service (File2DocumentService)
internal/dao/file2document.go      – added Create method
internal/handler/file.go           – FileHandler gains file2DocumentService;
                                     LinkToDatasets HTTP handler
internal/router/router.go          – route registered
```

**Functional parity table:**

| Concern | Go behaviour |
|---------|-------------|
| Required fields | `file_ids` and `kb_ids` both required; missing
either → `CodeDataError` mirroring Python `@validate_request` |
| File existence | `fileDAO.GetByIDs(fileIDs)` builds a set; any missing
ID → `"File not found!"` |
| KB existence | `kbDAO.GetByID(kbID)` per KB; missing → `"Can't find
this dataset!"` |
| Folder expansion | `getAllInnermostFileIDs` recursively calls
`fileDAO.ListByParentID` — mirrors
`FileService.get_all_innermost_file_ids` |
| File permissions | `checkFileTeamPermission`: `file.TenantID ==
userID` OR user in tenant's team — mirrors `check_file_team_permission`
|
| KB permissions | `checkKBTeamPermission`: `kb.TenantID == userID` OR
user in tenant's team — mirrors `check_kb_team_permission` |
| Fire-and-forget | `go convertFiles(...)` goroutine after all
validation passes — mirrors `loop.run_in_executor(None, _convert_files,
…)` |
| Conversion | `convertFiles`: for each file → delete existing mappings
+ hard-delete old documents → create new `Document` in each target KB →
create `File2Document` mapping — mirrors Python `_convert_files` |
| `getParser` | Extension-based lookup with fallback to `kb.ParserID` —
mirrors `FileService.get_parser` |
| Immediate return | `true` returned to caller as soon as goroutine is
scheduled |

---------

Co-authored-by: Yingfeng <yingfeng.zhang@gmail.com>
2026-06-10 16:46:55 +08:00
Hz_
3796835c4d feat(go-api): migrate agent file download handler to Go with strict P… (#15769)
## What does this PR do?

This PR migrates the Agent Temporary File Download endpoint (`GET
/api/v1/agents/download`) from the Python backend to the Go backend,
optimizing the data retrieval flow and maintaining strict functional
parity. It also fixes a persistent parsing error in the Sandbox code
execution node.

## Checklist
- [x] Code logic matches Python implementation
- [x] All local unit tests passed
- [x] No breaking changes to existing router interfaces

Co-authored-by: Yingfeng <yingfeng.zhang@gmail.com>
2026-06-10 16:09:36 +08:00
Jin Hai
139f4515e8 Go: refactor CLI (#15898)
### What problem does this PR solve?

1. remove unused code
2. fix login issue

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-10 16:06:30 +08:00
Idriss Sbaaoui
357cb84cd4 Fix: cohere call failing (#15899)
### What problem does this PR solve?

cohere api call failing because of missing prefix

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-10 15:57:10 +08:00
buua436
dcf623d60d feat: support multi-type factory models (#15893)
### What problem does this PR solve?
Support factory models with multiple model types, so visual chat models
can be exposed as both image2text and chat while preserving the database
model-type-per-record design.

This also updates the SILICONFLOW model list and adds a helper script to
refresh SiliconFlow models from the provider API.

### Type of change
- [x] New Feature (non-breaking change which adds functionality)
2026-06-10 15:35:21 +08:00
Lynn
478c9846a1 Fix: model list (#15860)
### What problem does this PR solve?

Remove tenant_llm call in rag.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-10 14:59:57 +08:00
Wang Qi
899f76af6b Fix add OpenRouter base_url, UI need to select at least one model to verify (#15894)
Fix add OpenRouter base_url, UI need to select at least one model to verify
2026-06-10 14:59:27 +08:00
chanx
6822307436 fix: rename ark_api_key to api_key for volcengine provider config (#15896)
### What problem does this PR solve?

fix: rename ark_api_key to api_key for volcengine provider config

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-10 14:56:38 +08:00
Lynn
f632bb4a85 Fix: tenant_model migrate (#15886)
### What problem does this PR solve?

Find instance for models.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-10 14:06:23 +08:00
chanx
c23809a4bd Fix: Fix some model provider-related UI issues (#15884)
### What problem does this PR solve?

Fix: Fix some model provider-related UI issues

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-10 14:05:57 +08:00
Hz_
38755c705a feat(go): Add DeepSeek models and Gitee alias metadata tests (#15885)
This PR expands conf/all_models.json with DeepSeek model entries and
provider aliases.

Changes:

- Added DeepSeek model entries across `V4`, `V3.2`, `V3.1`, `V3`, `R1`,
`Coder`, `Math`, `VL`, `OCR`, `Prover`, `MoE`, and `LLM` series.
- Normalized model name values to lowercase canonical IDs.
- Added alias values for official DeepSeek/Hugging Face names and
provider-specific names from OpenRouter, VolcEngine, SiliconFlow,
HuaweiCloud, and QiniuCloud.
- Preserved model metadata such as max_tokens, model_types, and thinking
where applicable.
- Added Gitee ListModels tests to verify DeepSeek aliases map back to
model metadata from all_models.json.
- Added an optional Gitee integration test gated by
GITEE_LIST_MODELS_INTEGRATION=1.

Test:

/usr/local/go/bin/go clean -cache
/usr/local/go/bin/go test ./internal/entity/models -run
'TestGiteeListModels(MapsAllDeepSeekAliasesToModelMetadata|KeepsOwnedBySuffixAfterAliasMetadataLookup|
Integration)'
2026-06-10 13:59:23 +08:00
buua436
093eec3105 fix: handle qwen rerank error response (#15881)
### What problem does this PR solve?
Fix QWen rerank error handling so DashScope error responses without a
text attribute do not raise a secondary KeyError and hide the real
provider error.

### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-10 13:05:24 +08:00
Wang Qi
9aa81e7cad Fix paddle ocr / minerU cannot add (#15858)
Fix paddle ocr / minerU cannot add
2026-06-10 13:04:13 +08:00
Idriss Sbaaoui
7f4bf69f05 Enhancement: slim Docker image, add .dockerignore, fix Go binary shipping (#15880)
### What problem does this PR solve?

The RAGFlow Docker image was 9.06 GB with build-only compiler packages
leaking into the runtime, duplicate frontend source shipped alongside
compiled assets, and no .dockerignore causing ~6 GB of unnecessary
context transfer per build.

### Type of change

- [x] Performance Improvement
2026-06-10 11:44:22 +08:00
oktofeesh
bbc1f2ecec feat(go-api): add RAG retrieval to chat completions (#15739)
## Summary
- Add knowledge-base retrieval support to Go chat completions.

## What changed
- Routes KB-backed chat sessions through the Go retrieval service
instead of falling back to solo chat.
- Resolves embedding and rerank models, validates accessible knowledge
bases, and preserves tenant-aware retrieval.
- Rejects mixed embedding models across selected knowledge bases before
retrieval to avoid incompatible vector dimensions.
- Threads the HTTP request context into streaming retrieval so cancelled
requests can stop downstream retrieval work.
- Applies metadata filters and message-level `doc_ids` before retrieval.
- Expands parent/child chunks before building references and prompt
context.
- Injects retrieved knowledge through a copied dialog prompt config so
the caller's original dialog is not mutated.
- Honors configured empty responses when no chunks are found.
- Names the metadata no-match sentinel and reuses it across
retrieval/handler paths.
- Adds a defensive content cast while appending streamed answers.
- Adds focused unit coverage for retrieval, metadata filtering,
authorization, multimodal messages, references, empty-response behavior,
prompt immutability, and mixed embedding models.

---------

Co-authored-by: Yingfeng <yingfeng.zhang@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 11:07:45 +08:00
Jin Hai
7c1bd9a5a5 Go CLI: switch to admin/api server (#15861)
### What problem does this PR solve?

```
RAGFlow(api/default)> use admin
SUCCESS
RAGFlow(api/default)> use api 'abc';
SUCCESS
```

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-10 10:57:00 +08:00
writinwaters
9d9c2dc92c Docs: Supported model providers and URLs updated (#15866)
### What problem does this PR solve?

Updated supported model providers and the corresponding URLs.

~~Synced supported model providers and base URLs with
**llm_factories.json**, while keeping the AI Badgr configuration example
via the OpenAI-API-Compatible provider.~~


### Type of change


- [x] Documentation Update
2026-06-10 10:18:14 +08:00
Haruko386
d56aeb2f5d feat[Go]: api datasets/<dataset_id>/documents/<document_id>/metadata/… (#15846)
### What problem does this PR solve?

As title

```
/api/v1/datasets/<dataset_id>/documents/<document_id>/metadata/config PUT
```

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

Co-authored-by: Yingfeng <yingfeng.zhang@gmail.com>
2026-06-10 09:57:11 +08:00
Haruko386
a396b1ace2 feat[Go]: implement /api/v1/agents/<agent_id> and test_db_connection (#15771)
### What problem does this PR solve?

Add two API in go
```
/api/v1/agents/test_db_connection POST

/api/v1/agents/<agent_id>/sessions DELETE
```

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

---------

Co-authored-by: Yingfeng <yingfeng.zhang@gmail.com>
2026-06-10 09:54:07 +08:00
Jack
87b8062df4 feat: implement POST /api/v1/searchbots/ask — streaming RAG with citations and think-tag processing (#15825)
Implements POST /api/v1/searchbots/ask in Go with streaming SSE,
citations, and think-tag processing. 23 files, 90+ unit tests.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 22:48:50 +08:00
Yingfeng
cf5cca5cbb Fix wrong unit test path (#15864) 2026-06-09 22:48:33 +08:00
Jack
2f99d52fb5 fix(ci): re-enable Go tests and fix compilation errors after ListModels signature change (#15862)
## Summary

This PR re-enables the Go test steps in CI that were previously
commented out, and fixes all compilation errors that have accumulated in
`internal/entity/models/` since the `ListModels` return type was changed
from `[]string` to `[]ListModelResponse`.

## Changes

### CI (`.github/workflows/tests.yml`)
- Re-enable **Prepare test resources** step (clones resource repo with
WordNet data)
- Re-enable **Test Go packages** step (runs `go test ./internal/...`)
- Fix resource path race condition by using
`/tmp/resource-${GITHUB_RUN_ID}` instead of `/tmp/resource`
- Exclude `/cli` package from Go tests (contains `main` redeclarations)

### Test fixes (16 model provider test files)
All errors were caused by the upstream change from `[]string` to
`[]ListModelResponse` in the `ListModels` interface:

- Add `joinModelNames` test helper to extract `.Name` from
`[]ListModelResponse` slices
- `strings.Join(models, ",")` → `joinModelNames(models, ",")` (11 files)
- `ids[i] != "..."` → `ids[i].Name != "..."` (cometapi, mistral)
- `got[i] != want[i]` → `got[i].Name != want[i]` (bedrock)
- `[]string` return types → `[]ListModelResponse` (google)

### Pre-existing bugs in model_test.go
Bugs introduced by the upstream `entity/` → `entity/models/` directory
rename:

- Add missing `pm := GetProviderManager()` calls in 3 test functions
- Fix `InitProviderManager` signature (`_, err :=` → `err :=`)
- Fix `MaxTokens` `*int` dereference (6 comparisons)
- Fix `readProviderConfig` relative path (3 levels up instead of 2)

### model.go
- Add `findRepoRoot()` to make `conf/all_models.json` resolution work
from any CWD, fixing `TestSiliconFlowProviderConfigLoadsLatestProModels`

### Test validation

```bash
go build ./internal/...      # 
go test ./internal/entity/models/... -count=1  #  all pass
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 21:12:15 +08:00
cleanjunc
88e4d6bddb Fix: restore GraphRAG entity ranking by indexing pagerank and n-hop paths (#15797)
### Summary

Closes #15795 

Knowledge-graph queries rank entities by `pagerank * sim` in `KGSearch`,
but the entity chunks written at index time stopped carrying the values
that ranking depends on. `graph_node_to_chunk` only stored
`entity_type`, `description`, and `source_id`, dropping the node
`pagerank` and the n-hop neighbour paths, while `search.py` still read
them back as `rank_flt` and `n_hop_with_weight`.

The producer of these fields, `update_nodes_pagerank_nhop_neighbour`,
was removed in #6513, but the read side in `KGSearch` was never updated.
The result is that on every knowledge-graph query:

- `pagerank` resolves to `0`, so the `pagerank * sim` sort key is `0`
for every entity and selection falls back to arbitrary order.
- Every displayed entity score is `0.00`.
- The n-hop relation-enrichment block is dead code because `n_hop_ents`
is always empty, leaving `merge_tuples` and `is_continuous_subsequence`
orphaned.

This PR restores the missing index-time fields so the documented `P(E|Q)
= pagerank * sim` ranking and the n-hop enrichment work again.

What changed:

- `graph_node_to_chunk` now writes `rank_flt` from the node pagerank and
`n_hop_with_weight` from the recomputed n-hop neighbour paths.
- Reintroduced the n-hop path computation (`n_neighbor`) in
`rag/graphrag/utils.py`, reusing the previously orphaned `merge_tuples`
/ `is_continuous_subsequence` helpers, with a direction-agnostic
edge-weight lookup for undirected graphs. `set_graph` computes the paths
per added or updated node and passes them through.
- `KGSearch` now selects `n_hop_with_weight` in the entity keyword
search so Infinity and OceanBase return it (Elasticsearch and OpenSearch
already read it from `_source`), and the read is hardened against
missing keys or empty strings before `json.loads`.
- Added the `n_hop_with_weight` column to OceanBase, including the
`EXTRA_COLUMNS` migration entry so existing tables get it. The other
engines already map both fields via dynamic templates or the Infinity
mapping.

Scope note: pagerank and n-hop are re-indexed for the added or updated
nodes in each pass, consistent with the existing incremental indexing
design.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

### Testing

Added unit tests in
`test/unit_test/rag/graphrag/test_graphrag_utils.py`:

- `n_neighbor`: path and weight shape, one-hop vs two-hop, isolated
nodes, missing weights, and direction-agnostic lookup.
- `graph_node_to_chunk`: `rank_flt` populated from pagerank and
defaulting to `0`, `n_hop_with_weight` serialized and defaulting to an
empty list.

```
uv run pytest test/unit_test/rag/graphrag/   # 106 passed
uv run ruff check rag/graphrag/ rag/utils/ob_conn.py
```
2026-06-09 20:50:45 +08:00
ghost
64b860f771 fix(elasticsearch): complete Go result functions (#15148)
## Summary
- Complete the Go Elasticsearch result functions that remained stubbed
after #15160.
- Add focused unit coverage for field mapping, aggregation, IDs, and
highlighting behavior.
- Update a stale query-builder test type import discovered during
validation.

## What changed
- Keep the Elasticsearch Go implementation merged in #15160 and fill in
`GetFields`, `GetAggregation`, `GetHighlight`, and `GetDocIDs` in
`internal/engine/elasticsearch/chunk.go`.
- Add regression and invariant coverage in
`internal/engine/elasticsearch/chunk_helpers_test.go`.
- Update `internal/service/nlp/query_builder_test.go` to use the current
`types.MatchTextExpr` type.

## Why
- #15160 implemented the main Go Elasticsearch surface, but
retrieval/tag flows still call result functions that returned stubs.
- Completing these functions keeps Elasticsearch result processing
aligned with the expected document-engine behavior for field extraction,
tag aggregation, doc ID extraction, and snippet highlighting.

## Validation
- `go test ./internal/engine/elasticsearch`
- `GOARCH=arm64 CGO_ENABLED=1 go test ./internal/service/nlp -run
TestQueryBuilder`
- `git diff --check`
- CodeRabbit review reported 0 issues after follow-up fixes.
- Codex Security diff scan found no reportable issues.

## Notes
- This PR is now a follow-up to #15160 rather than a competing
implementation.
- A full local `go test ./internal/service/nlp` run is blocked by local
WordNet resource prerequisites; the query-builder tests touched by this
PR pass with the arm64 CGO path.
2026-06-09 20:10:11 +08:00
balibabu
10bbe6b5d4 Fix: The variables in the Visual Input File of the agent operator are not displayed. (#15856)
### What problem does this PR solve?

Fix: The variables in the Visual Input File of the agent operator are
not displayed.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-09 19:41:22 +08:00
JPette1783
acae932938 fix(go): guard four nil-pointer dereferences causing runtime panics (#15815)
### What problem does this PR solve?

Fixes four Go paths that dereference a pointer with no prior nil check,
each
causing a **runtime panic**. Closes #15814.

| # | File | Bug | Fix |
|---|------|-----|-----|
| 1 | `internal/entity/models/deepseek.go` | streaming path runs `switch
*chatModelConfig.Effort` inside `if *Thinking`; panics when
`Thinking=true` and `Effort==nil` | nil-check with default `"high"`,
matching the non-streaming path in the same file |
| 2 | `internal/entity/models/volcengine.go` | identical oversight:
`switch *modelConfig.Effort` with no guard | nil-check with default
`"medium"`, matching its non-streaming path |
| 3 | `internal/handler/auth.go` | `AuthMiddleware` does `if
*user.IsSuperuser`; panics on every authenticated request when the DB
column is `NULL` | guard with `user.IsSuperuser != nil &&`, matching
every other call site (`admin/handler.go`, `admin/service.go`,
`user.go`) |
| 4 | `internal/service/heartbeat_sender.go` |
`responseBody["code"].(float64)` panics on any non-200 response lacking
a numeric `code`; the upstream `recover()` calls `Fatal()` →
`os.Exit(1)`, taking down the whole server | comma-ok assertion (`code,
ok := ...`); return an error instead of panicking |

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-09 19:29:25 +08:00
Hz_
d4fe3bb148 feat(go-api): Add GET dataset metadata summary API (#15843)
## What

Adds the RESTful dataset metadata summary endpoint:

`GET /api/v1/datasets/{dataset_id}/metadata/summary`

The endpoint supports optional document filtering through:

`?doc_ids=doc_id_1,doc_id_2`
2026-06-09 19:27:47 +08:00
JPette1783
e050f1816e fix(models): guard unsafe index access in Google and Ollama drivers (#15819)
### What problem does this PR solve?

Fixes four panic / spurious-error paths in the Go model layer. Closes
#15818.

| # | File | Bug | Fix |
|---|------|-----|-----|
| 1 | | Thinking-mode streaming path: accessed unconditionally; Gemini
emits usage-only chunks with an empty slice, causing a runtime panic |
Guard each step: , , before indexing |
| 2 | | is a plain for ordinary requests; the cast to silently returns ,
then panics immediately | Switch on concrete type; handle both and |
| 3 | | Identical panic on the streaming path | Same switch-on-type fix
|
| 4 | | The field is optional (absent for non-thinking models) but the
code returned an error when it was missing, breaking every ordinary
Ollama completion | Change to a silent comma-ok assertion; is empty
string when the field is absent |

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 19:26:52 +08:00
chanx
84482762d5 feat: support custom editing for model list (#15855)
### What problem does this PR solve?

feat: support custom editing for model list

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2026-06-09 19:24:43 +08:00
Wang Qi
7ed1f1c865 Fix VLLM cannot add without /v1 (#15851)
Fix VLLM cannot add without /v1
2026-06-09 19:11:15 +08:00
Jack
3eff41361b fix: prevent None values in auto-metadata from causing KeyError (#15842)
## Problem

When users configure auto-metadata for a dataset, parsing crashes with:

```
KeyError: 'properties' in gen_metadata → schema["properties"]
```

## Root Cause

Pydantic `AutoMetadataField` defaults `enum` and `description` to `None`
when the frontend omits these fields:

```python
class AutoMetadataField(Base):
    enum: Annotated[list[str] | None, Field(default=None)]
    description: Annotated[str | None, Field(default=None)]
```

These `None` values propagate through the call chain and cause two
crashes:
2026-06-09 19:10:48 +08:00
Wang Qi
2773208159 Fix: MinerU cannot be added (#15841)
Fix: MinerU cannot be added
2026-06-09 19:06:51 +08:00
Lynn
08a40711a0 Fix: model list (#15839)
### What problem does this PR solve?

Dedup api_key and migrate `is_tools `in migration.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-09 19:06:31 +08:00
euvre
f97d6396b4 fix: BaiduYiyan API key validation fails in set_api_key (#15828)
### What problem does this PR solve?

When setting the API key for the BaiduYiyan provider, all model
validations fail with the error "Fail to access model using this api
key. No valid response received".

**Root cause:**

1. `BaiduYiyanChat` in `rag/llm/chat_model.py` does not override
`async_chat_streamly()`. The `verify_api_key()` function uses
`mdl.async_chat_streamly()` to validate, but `BaiduYiyanChat` inherits
`Base.async_chat_streamly()` which uses the OpenAI client, not the Baidu
Qianfan SDK (qianfan). Since BaiduYiyan has no OpenAI-compatible
base_url, validation always fails.

2. `verify_api_key()` in `provider_api_service.py` does not format the
raw API key string into the JSON format (`{"yiyan_ak": "...",
"yiyan_sk": "..."}`) that `BaiduYiyanChat.__init__()` expects via
`json.loads(key)`.

**Fix:**

1. Add `async_chat_streamly()` method to `BaiduYiyanChat` using the
qianfan SDK, consistent with the existing `chat_streamly()` method.
2. Add BaiduYiyan API key formatting in `provider_api_service.py`
`verify_api_key()` to match the format expected by
`BaiduYiyanChat.__init__()`.

### 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):
2026-06-09 19:05:58 +08:00
buua436
7b8d6f34b3 fix: force image parser json output (#15847)
### What problem does this PR solve?
Force image parser runtime output format to JSON so downstream chunking
reads OCR results from the JSON output and image parser chunks can be
displayed.

### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)

Co-authored-by: Wang Qi <wangq8@outlook.com>
2026-06-09 19:02:37 +08:00
Jin Hai
719ce15c95 Go CLI: update list supported models (#15845)
### What problem does this PR solve?

Now list supported models will show more info.

```
RAGFlow(api/default)> list supported models from 'gitee' 'test';
+-----------+------------+-------------+----------------------------------------------------------+---------------------------------------------+
| dimension | max_tokens | model_types | name                                                     | thinking                                    |
+-----------+------------+-------------+----------------------------------------------------------+---------------------------------------------+
|           |            |             | Wan2.7                                                   |                                             |
|           |            |             | HappyHorse-1.0                                           |                                             |
|           |            |             | Qwen3.6-27B@Qwen                                         |                                             |
|           |            |             | Qwen3.6-35B-A3B@Qwen                                     |                                             |
|           | 1048576    | [chat]      | DeepSeek-V4-Flash@deepseek-ai                            | map[clear_thinking:true default_value:true] |
|           | 1048576    | [chat]      | DeepSeek-V4-Pro@deepseek-ai                              | map[clear_thinking:true default_value:true] |
+-----------+------------+-------------+----------------------------------------------------------+---------------------------------------------+
```

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-09 19:01:00 +08:00
Ju Boxiang
f0efa63bf2 fix: Remove trailing comma in CREATE TABLE tenant_model SQL (#15832) (#15836)
### What problem does this PR solve?
The last column definition `INDEX idx_instance_id (instance_id),` in the
`CREATE TABLE tenant_model` statement has a trailing comma, which causes
a MySQL syntax error during deployment.

Closes #15832

### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)

### How was this tested?
- [x] Visual inspection: the trailing comma on line 837 has been removed
2026-06-09 17:54:18 +08:00
buua436
c1496ffd43 fix: propagate memory tenant id in task collect (#15837)
### What problem does this PR solve?
Propagate `tenant_id` from memory task messages into task collection so
refactored task execution can build a valid context.

### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-09 17:47:48 +08:00
Hz_
d1c436b804 feat(api): implement GET /api/v1/agents/prompts endpoint in Go (#15748)
### Description
This PR ports the `GET /api/v1/agents/prompts` endpoint from the Python
backend to the Go backend.

### Changes Made
- **Handler**: Added `GetPrompts` method to `internal/handler/agent.go`.
- **Router**: Registered the `agents.GET("/prompts")` route in
`internal/router/router.go`.
- **Logic**: Leveraged the existing `service.LoadPrompt` utility to read
`analyze_task_system`, `analyze_task_user`, `next_step`, `reflect`, and
`citation_prompt` templates directly from the `rag/prompts` directory.
- **Unit Test**: Added `TestGetPrompts_Success` to
`internal/handler/agent_test.go` to mock the HTTP context and validate
the JSON response structure.

### Motivation
This is part of the ongoing effort to port the Agent API surface to Go.
Since this specific endpoint only serves static prompt templates and
does not require the complex DAG/Canvas execution engine, it can be
seamlessly and safely handled by the Go backend.

### Testing
- [x] Added automated unit test `TestGetPrompts_Success` (verified
passing).
- [x] Tested locally via `curl` against the Go server (port 9380) and
Python server (port 9384).
- [x] Verified that the Go JSON response structure and loaded prompt
text are logically 100% identical to the Python implementation.
2026-06-09 17:03:42 +08:00
Yingfeng
01a2a44766 Clean CLI for filesystem (#15838)
### Type of change

- [x] Refactoring
2026-06-09 17:00:10 +08:00
balibabu
287a4cfd2b Fix: An error message appears when accessing the agent's launch page: "pagesize exceeds maximum value". (#15835)
### What problem does this PR solve?
Fix: An error message appears when accessing the agent's launch page:
"pagesize exceeds maximum value".

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

---------

Co-authored-by: balibabu <assassin_cike@163.com>
2026-06-09 16:56:47 +08:00
Jonathan Chang
c586292993 feat: Implement checkpoint/resume support for GraphRAG community extraction and entity resolution (#15523)
## Summary

This PR adds checkpoint/resume support for the GraphRAG
`extract_community` and `resolve_entities` stages.

The implementation stores successful intermediate results in the
document store so interrupted ingestion can resume without repeating
already-completed LLM work. Checkpoints are loaded before each stage,
reused when available, saved after successful batch/community
processing, and cleaned up after the stage completes successfully.
## Related Issue
Closes: #15518
## Change Type
- [x] Feature
- [x] Bug fix
- [x] Test
- [ ] Refactor
- [ ] Documentation
- [ ] Breaking change
## Real Behavior Proof

Validation commands run locally:

```bash
uv run python -m py_compile \
  rag/graphrag/checkpoints.py \
  rag/graphrag/general/community_reports_extractor.py \
  rag/graphrag/entity_resolution.py \
  rag/graphrag/general/index.py \
  test/unit_test/rag/graphrag/test_checkpoints.py
```
Result:

```text
Passed
```

```bash
uv run pytest test/unit_test/rag/graphrag/test_checkpoints.py
```
Result:

```text
4 passed
```

```bash
uv run pytest \
  test/unit_test/rag/graphrag/test_phase_markers.py \
  test/unit_test/rag/graphrag/test_graphrag_utils.py \
  test/unit_test/rag/graphrag/test_checkpoints.py
```
Result:

```text
95 passed
```

```bash
git diff --check
```
Result:

```text
Passed
```

## Checklist

- [x] Implemented checkpoint/resume support for `extract_community`.
- [x] Implemented checkpoint/resume support for `resolve_entities`.
- [x] Avoided touching unrelated API behavior.
- [x] Added unit tests for the new checkpoint helper logic.
- [x] Verified Python syntax compilation.
- [x] Ran related GraphRAG unit tests successfully.
- [x] Ran `git diff --check`.
- [ ] Ran full project test suite.

---------

Co-authored-by: Wang Qi <wangq8@outlook.com>
2026-06-09 15:34:47 +08:00
Jin Hai
d02eb6b596 Go: refactor CLI (#15728)
### What problem does this PR solve?

```
RAGFlow(user)> add api server 'ccc' host '127.0.0.1:9980';
SUCCESS
RAGFlow(user)> list api server;
+------------+---------------+-----------------+---------+-------------+---------------+
| api_server | api_server_ip | api_server_port | auth    | user_name   | user_password |
+------------+---------------+-----------------+---------+-------------+---------------+
| ccc        | 127.0.0.1     | 9980            | no auth |             |               |
| default    | 127.0.0.1     | 9384            | login   | aaa@aaa.com | ***           |
+------------+---------------+-----------------+---------+-------------+---------------+
RAGFlow(user)> delete api server 'ccc';
SUCCESS
RAGFlow(user)> list api server;
+------------+---------------+-----------------+---------+
| api_server | api_server_ip | api_server_port | auth    |
+------------+---------------+-----------------+---------+
| default    | 127.0.0.1     | 9384            | no auth |
+------------+---------------+-----------------+---------+

RAGFlow(user)> show admin server;
+--------------+-------+
| field        | value |
+--------------+-------+
| admin_server | N/A   |
+--------------+-------+
RAGFlow(user)> add admin server host '127.0.0.1:9880';
SUCCESS
RAGFlow(user)> show admin server;
+-------------------+-----------+
| field             | value     |
+-------------------+-----------+
| admin_server_ip   | 127.0.0.1 |
| admin_server_port | 9880      |
| auth              | no auth   |
+-------------------+-----------+
RAGFlow(user)> delete admin server;
SUCCESS
RAGFlow(user)> show admin server;
+--------------+-------+
| field        | value |
+--------------+-------+
| admin_server | N/A   |
+--------------+-------+

RAGFlow(user)> show current
+-----------------+-------------+
| field           | value       |
+-----------------+-------------+
| api_server_port | 9384        |
| user_name       | aaa@aaa.com |
| user_password   | ***         |
| mode            | api         |
| verbose         | false       |
| api_server      | default     |
| api_server_ip   | 127.0.0.1   |
| auth            | login       |
| output          | table       |
| interactive     | true        |
+-----------------+-------------+
```
### Type of change

- [x] Refactoring

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-09 15:22:50 +08:00
Lynn
1ab51a27bf Fix: list intl Tongyi-Qianwen base_url (#15831)
### What problem does this PR solve?

Display intl `base_url` for Tongyi-Qianwen

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-09 13:19:39 +08:00
chanx
298a23f74c fix: resolve issue where some models do not use modelInfo parameter (#15830)
### What problem does this PR solve?

fix: resolve issue where some models do not use modelInfo parameter

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-09 13:18:01 +08:00
Wang Qi
9c0cc77e35 Fix empty response set not take effect (#15824)
Fix empty response set not take effect
2026-06-09 13:06:58 +08:00
Idriss Sbaaoui
faf78d3069 Fix: OceanBase tenant startup drift and docs (#15829)
### What problem does this PR solve?

OceanBase could start without the `ragflow` tenant, so RAGFlow failed to
connect with `root@ragflow`. This PR adds a safe startup reconcile step
and documents the required host limits before using OceanBase.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Documentation Update
2026-06-09 13:04:05 +08:00
Wang Qi
93e4f6bc09 Fix: Add bge as embedding (#15784)
Fix: Add bge as embedding
2026-06-09 09:31:24 +08:00
DearsisHS
d94b8c14cb fix(api): await asyncio.wait_for in verify_api_key embedding branch (#15620)
## Summary
The embedding branch of `verify_api_key` was missing `await` on
`asyncio.wait_for(...)`, so valid embedding-only providers always failed
API-key verification.

## Root cause
`arr, tc = asyncio.wait_for(...)` (no `await`) returns a coroutine;
unpacking it raises `TypeError: cannot unpack non-iterable coroutine`,
which the `except` swallows as a failure. The chat branch (`await
asyncio.wait_for(check_streamly())`) and rerank branch (`arr, tc = await
asyncio.wait_for(...)`) already `await` correctly.

## Fix
```diff
-                arr, tc = asyncio.wait_for(
+                arr, tc = await asyncio.wait_for(
                     asyncio.to_thread(mdl.encode, ["Test if the api key is available"]),
                     timeout=timeout_seconds,
                 )
```

## Files changed
- `api/apps/services/provider_api_service.py`

## Verification
- `ruff check` — clean
- Fix mirrors the already-correct chat/rerank branches in the same
function. Local full pytest not run (heavy RAG deps); CI validates.

## Note
Implemented with LLM assistance (model: claude-opus-4-8).

Closes #15619

Co-authored-by: dearsishs <MCarter112116@outlook.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 23:01:36 +08:00
DearsisHS
cbb3896aaa fix(api): guard missing row in SearchService.get_detail (#15622)
## Summary
`SearchService.get_detail` crashed with `AttributeError` (HTTP 500) when
no matching row existed, because it called `.first().to_dict()` before
the `if not search` guard — making that guard dead code.

## Root cause
`.first()` returns `None` when the query matches nothing (deleted search
app, or joined `User` not `VALID`). `None.to_dict()` raises before the
guard runs.

## Fix
```diff
             .first()
-            .to_dict()
         )
         if not search:
             return {}
-        return search
+        return search.to_dict()
```
Guard the `None` first, then serialize — restoring the intended `{}`
"not found" return that every caller (`search_api`, `bot_api`,
`chat_api`, `dataset_api_service`) already handles.

## Files changed
- `api/db/services/search_service.py`

## Verification
- `ruff check` — clean
- Logic: `.first()` → `None` now hits `return {}` instead of
`None.to_dict()`. Local full pytest not run (heavy RAG deps); CI
validates.

## Note
Implemented with LLM assistance (model: claude-opus-4-8).

Closes #15621

Co-authored-by: dearsishs <MCarter112116@outlook.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 23:01:28 +08:00
Jin Hai
55abf4f565 Go: new CLI command, list all models and show model (#15786)
### What problem does this PR solve?

```
RAGFlow(user)> list models;
+---------------------------+------------+-------------+--------------------+---------------------------------------------+
| alias                     | max_tokens | model_types | name               | thinking                                    |
+---------------------------+------------+-------------+--------------------+---------------------------------------------+
|                           | 1048576    | [chat]      | deepseek-v4-flash  | map[clear_thinking:true default_value:true] |
|                           | 1048576    | [chat]      | deepseek-v4-pro    | map[clear_thinking:true default_value:true] |
|                           | 1024000    | [chat]      | minimax-m3         | map[clear_thinking:true default_value:true] |
|                           | 64000      | [vision]    | glm-4.5v           | map[clear_thinking:true default_value:true] |
| [baai/bge-m3]             | 8192       | [embedding] | bge-m3             |                                             |
| [baai/bge-reranker-v2-m3] | 1024       | [rerank]    | bge-reranker-v2-m3 |                                             |
|                           |            | [tts]       | step-audio-tts-3b  |                                             |
| [qwen/qwen3-asr-1.7b]     |            | [asr]       | qwen3-asr-1.7b     |                                             |
| [paddleocr-vl-1.5]        |            | [ocr]       | paddleocr-vl-0.9b  |                                             |
+---------------------------+------------+-------------+--------------------+---------------------------------------------+
RAGFlow(user)> show model 'minimax-m3';
+--------------+---------------------------------------------+
| field        | value                                       |
+--------------+---------------------------------------------+
| name         | minimax-m3                                  |
| max_tokens   | 1024000                                     |
| model_types  | [chat]                                      |
| thinking     | map[clear_thinking:true default_value:true] |
| class        |                                             |
| alias        |                                             |
| ModelTypeMap |                                             |
+--------------+---------------------------------------------+
RAGFlow(user)> show model 'baai/bge-m3';
+--------------+---------------+
| field        | value         |
+--------------+---------------+
| model_types  | [embedding]   |
| thinking     |               |
| class        |               |
| alias        | [baai/bge-m3] |
| ModelTypeMap |               |
| name         | bge-m3        |
| max_tokens   | 8192          |
+--------------+---------------+
```

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-08 21:38:15 +08:00
Jack
35527f6755 fix: guard http.DefaultTransport type assertion in xiaomi for Go 1.25 (#15787)
## Problem

`TestXiaomiNewModelWithCustomDefaultTransport` panics on Go 1.25:

```
panic: interface conversion: http.RoundTripper is models.roundTripperFunc, not *http.Transport
```

In Go 1.25, `http.DefaultTransport` is no longer `*http.Transport`, so
the unchecked type assertion in `NewXiaomiModel` panics when the test
replaces it with a `roundTripperFunc`.

## Fix

Use a safe type assertion with fallback to a new `http.Transport`,
matching the pattern already used in `modelscope.go`.

## Verification

```bash
go test -run TestXiaomiNewModelWithCustomDefaultTransport ./internal/entity/models/...
# PASS
```

Internal contributors only.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 21:11:21 +08:00
Yash Raj Pandey
f2aadd3871 Fix: is_english() returns False for any list argument (broken language detection) (#15489)
### What problem does this PR solve?

`is_english()` in `rag/nlp/__init__.py` compiles a **single-character**
regex class and `fullmatch`es it against each item:

```python
pattern = re.compile(r"[`a-zA-Z0-9\s.,':;/\"?<>!\(\)\-]")   # no quantifier
...
eng = sum(1 for t in texts if pattern.fullmatch(t.strip()))
```

For a **string** argument the text is first split into single characters
(`texts = list(texts)`), so each `fullmatch` sees one character and
works. But for a **list** argument each item is a whole multi-character
string, and `fullmatch` of a one-character pattern against a
multi-character string always fails — so `is_english()` returns `False`
for **any** list, regardless of content.

```python
is_english("This is English")                              # True   (ok)
is_english(["The quick brown fox jumps.", "Hello world."]) # False  (bug — should be True)
is_english(["这是中文。"])                                    # False  (right answer, wrong reason)
```

Many call sites pass lists and were therefore silently always-`False`,
e.g.:

- `rag/llm/chat_model.py:1088`, `rag/llm/cv_model.py:168,1155` —
`is_english([ans])` when an answer is truncated at `max_tokens`, so an
English reply gets the Chinese "······由于长度的原因,回答被截断了,要继续吗?" continuation
suffix instead of the English one.
- `rag/app/book.py` — `remove_contents_table(...,
eng=is_english([...sections...]))`, so English books have their contents
table stripped in Chinese mode.
- `common/doc_store/es_conn_base.py:339`,
`rag/utils/opensearch_conn.py:733` — `is_english(txt.split())` in
highlight handling.
- plus `rag/app/qa.py`, `rag/flow/parser/utils.py`,
`common/doc_store/infinity_conn_base.py`.

### Fix

Add a `+` quantifier so an all-English multi-character item matches:

```python
pattern = re.compile(r"[`a-zA-Z0-9\s.,':;/\"?<>!\(\)\-]+")
```

The string path is unchanged (single characters still match) and
non-English lists still return `False`. Adds
`test/unit_test/rag/test_is_english.py`; the two list cases fail before
this change and pass after.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

Used the Claude CLI while working on this.
2026-06-08 20:25:23 +08:00
Lynn
b9f06e6095 Feat: model list (#15774)
### What problem does this PR solve?

Support model list for VolcEngine.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-06-08 20:18:00 +08:00
Jack
338fdb65fb feat(ci): enable go test in CI pipeline (#15750)
## What problem does this PR solve?

Go test files are never compiled in CI — only production binaries via
`go build`. This allowed a missing `"sort"` import in
`metadata_filter_test.go` to be merged without detection.

## Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)

## Changes

- Add `go test -count=1 ./internal/...` step after Go build in CI
workflow
- Fix missing `"sort"` import in `metadata_filter_test.go` (pre-existing
compile error)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 20:06:57 +08:00
Wang Qi
c5d0060e0b Delete not supported model providers list (#15783)
Delete not supported model providers list
2026-06-08 20:06:03 +08:00
oktofeesh
6fc3955cab fix(go-models): normalize Qwen reasoning families (#15735)
## Summary

Normalizes Qwen model-family names before reasoning extraction so
provider-prefixed Qwen models use the existing `<think>...</think>`
fallback.
2026-06-08 19:32:19 +08:00
oktofeesh
e0dc7af5dd fix(go-models): fix MiniMax driver requests (#15527)
## Summary
- keep MiniMax chat calls in non-streaming mode and streaming calls in
SSE mode
- make MiniMax model listing and connection checks use a bodyless GET
/v1/models
- add focused MiniMax request/response regression tests
2026-06-08 19:32:01 +08:00
oktofeesh
25df0a6725 fix(go-models): validate URL suffix config keys (#15734)
## Summary

Fixes typoed model-provider URL suffix keys and adds strict nested
decoding so future URL suffix config mistakes fail during provider
loading instead of being silently ignored.
2026-06-08 19:29:36 +08:00
Haruko386
8dc7f1d95e Go: implement ASR and TTS for xiaomi (#15765)
### What problem does this PR solve?

**Verified from CLI**
```
RAGFlow(user)> chat with 'mimo-v2.5@test@xiaomi' message 'who r u'
Answer: Hello! I'm MiMo-v2.5, a large language model developed by Xiaomi's LLM Core Team. You can think of me as a friendly AI assistant ready to help you answer questions, have conversations, or work on creative tasks. My context window can handle up to 1 million tokens, so we can dive into pretty long discussions or documents if you'd like. What can I help you with today?
Time: 3.831830

RAGFlow(user)> stream chat with 'mimo-v2.5@test@xiaomi' message 'who r u'
Answer:  there! I'm MiMo-v2.5, an AI assistant created by the Xiaomi LLM Core Team. I'm here to chat, help out, answer questions, or just have a friendly conversation. Think of me as a helpful buddy with a pretty big memory (1 million tokens worth!). What can I do for you today?😊
Time: 2.421630

RAGFlow(user)> think chat with 'mimo-v2.5@test@xiaomi' message 'who r u'
Thinking: The user is asking a simple question about who I am. According to my system prompt, I should:
- Identify myself as **MiMo-v2.5**
- State that I was developed by the **Xiaomi LLM Core Team**
- Answer in first person and be warm and conversational
Answer: Hey there! 👋

I'm **MiMo**, an AI assistant created by the **Xiaomi LLM Core Team**. Think of me as a friendly chat buddy who's here to help you with all sorts of questions and tasks!

I love having conversations, answering questions, brainstorming ideas, and helping people figure things out. Whether you want to chat, need help with something specific, or just want to explore ideas together — I'm here for it! 😊

What can I help you with today?
Time: 6.651589

RAGFlow(user)> tts with 'mimo-v2.5-tts@test@xiaomi' text 'hello? show yourself' play format 'wav' param '{"voice": "Chloe"}'
SUCCESS

RAGFlow(user)> asr with 'mimo-v2.5-asr@test@xiaomi' audio './internal/test.wav' param '{"language": "zh"}'
+------------------------------------------------------------------------------------------------------------------------+
| text                                                                                                                   |
+------------------------------------------------------------------------------------------------------------------------+
| 1 The examination and testimony of the experts enabled the commission to conclude that five shots may have been fired. |
+------------------------------------------------------------------------------------------------------------------------+
```

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
- [x] Refactoring
2026-06-08 19:27:45 +08:00
oktofeesh
d63bd81d0d fix(go-models): fix Moonshot model and balance requests (#15528)
## Summary
- keep Moonshot chat calls in non-streaming mode and streaming calls in
SSE mode
- make Moonshot model listing and balance checks use bodyless GET
requests
- add focused Moonshot request/response regression tests
2026-06-08 19:27:19 +08:00
Wang Qi
8e4fba6cd2 Fix OpenRouter key JSONDecodeError (#15776)
Fix OpenRouter key JSONDecodeError
2026-06-08 19:19:10 +08:00
buua436
0c5245e454 fix: await lmstudio embedding verification (#15772)
### What problem does this PR solve?

Fix LM-Studio provider connection verification so embedding checks await
the async wrapper correctly and log the full traceback on failures.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-08 19:17:47 +08:00
balibabu
d025e18176 Fix: Add a waiting status to the messages on the chat page. (#15773)
### What problem does this PR solve?

Fix: Add a waiting status to the messages on the chat page.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-08 19:17:00 +08:00
chanx
7dd4030986 fix: Resolve error when checking pipeline parsing result (#15778)
### What problem does this PR solve?

fix: Resolve error when checking pipeline parsing result

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-08 19:16:21 +08:00
euvre
d9a04ef702 fix: support auto mode in table parser document metadata aggregation (#15780)
### What problem does this PR solve?

Table parser metadata aggregation previously only ran when
`table_column_mode` was set to `manual`. In auto mode (default), all
columns default to `"both"` role, meaning they should also be aggregated
into document-level metadata for UI/chat filters. Additionally, the task
snapshot could be stale — `table_column_names` are written to KB
`parser_config` during `chunk()` but the task may have been created
before that.

Changes:
- Renames `aggregate_table_manual_doc_metadata` →
`aggregate_table_doc_metadata`
- Supports both `"manual"` and `"auto"` `table_column_mode` (defaults to
`"auto"`)
- Reloads `table_column_names` from KB DB when missing from task
snapshot
- Removes the manual-only guard in `task_executor` and refactored
`post_processor`
- Updates all tests with new function name and adds auto mode test cases

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-08 19:08:23 +08:00
euvre
2c64febc93 feat: add ModelMeta implementations for Xinference, LocalAI, BaiduYiyan, and Tencent Cloud (#15752)
### What problem does this PR solve?

This PR adds `ModelMeta` implementations for four additional LLM/RAG
ecosystem platforms, building on the ModelMeta infrastructure introduced
in #15711.

Currently, only `Ollama` and `VolcEngine` have `ModelMeta` classes that
enable remote model list fetching. This PR extends that support to four
more platforms.

### Changes

Added four new `ModelMeta` subclasses in `rag/llm/model_meta.py`:

| Platform | `_FACTORY_NAME` | Has model list | Has full model info |
Approach |

|----------|-----------------|----------------|---------------------|----------|
| **Xinference** | `"Xinference"` |  |  | Parses `model_type` and
`context_length` from `/v1/models` response. Maps 6 model types
(LLM/embedding/rerank/image/TTS/speech2text). |
| **LocalAI** | `"LocalAI"` |  |  | Uses Ollama-compatible `GET
/api/tags` + `POST /api/show` endpoints. Returns capabilities
(completion/embedding/vision/tools/thinking) and
`general.context_length`. |
| **BaiduYiyan** | `"BaiduYiyan"` |  |  | Uses Qianfan SDK static
model catalog + `get_model_info()` for `max_input_tokens`. Returns 60
models (56 chat + 4 embedding) with real context lengths. |
| **Tencent Cloud** | `"Tencent Cloud"` |  |  | `NotImplementedError`
— uses SDK-based SID/SK HMAC signing, no model list REST API available.
|

All classes are automatically discovered and registered via the existing
`__init__.py` mechanism — no additional configuration needed.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-06-08 19:05:25 +08:00
天海蒼灆
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>
2026-06-08 18:12:56 +08:00
gaulin-ai
8abe627e69 i18n(it): complete Italian translation (49% → 100%) (#15729)
## Summary

Brings the Italian locale (`web/src/locales/it.ts`) from approximately
**49% coverage** (986 out of 2008 keys) to **100% coverage** (2008/2008
keys), fully aligned with `en.ts` in structure and key count.

### What was missing

Previously untranslated sections include:
- `skills`, `skillSearch` — agent skills UI
- `memories`, `memory` — memory management
- `datasetOverview` — dataset statistics
- `llmTools` — LLM tool configuration
- `explore` — explore/template page
- `dataflowParser` — ingestion pipeline parser settings
- `flow` (complete) — agent canvas / workflow editor
- `setting` connectors section — data source connectors (Google Drive,
Gmail, Box, RDBMS, etc.)
- Various `header`, `common`, `knowledgeBase`, `chat`, `fileManager`
additions

### Translation conventions

- Technical terms kept in English: RAG, LLM, API, token, chunk,
embedding, prompt, dataset, agent, canvas, knowledge graph, RAPTOR,
webhook, and all model/provider names (Bedrock, Tavily, SearXNG, etc.)
- `{{placeholder}}` variables preserved unchanged
- Informal *tu* form used consistently, matching the existing style
- All previously correct translations preserved
2026-06-08 18:06:47 +08:00
Worldwide
86b320e746 feat(web): show provider count on each model-type filter tag (#15444)
Fixes #15413 

### What problem does this PR solve?

In **Settings → Model providers**, the *Available models* panel lets you
filter
providers by model type (All, LLM, Embedding, Rerank, TTS, ASR, VLM, …),
but the
filter tags gave no hint of how many providers fall under each type.
Users had to
click a tag to find out, and an empty category looked identical to a
populated one.

This PR adds a count to each filter tag in `AvailableModels`:

- The **All** tag shows the total number of providers currently listed.
- Each model-type tag shows how many providers offer that model type.
- Counts respect the active search term, so the badge always matches the
number of
  cards shown once that tag is selected.
- Each provider is counted once per model type (deduplicated via a
`Set`), so a
  provider that lists the same type more than once isn't double-counted.

Counts are rendered with `tabular-nums` for stable width and dimmed via
`opacity-60`
so they read as secondary to the label. No API changes; the existing
filter logic is
untouched — this is purely an additive UI affordance.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-06-08 17:31:22 +08:00
Rintaro
453ade288c fix(opensearch): keep "id" in _source on insert so document metadata isn't empty (#15473)
### What problem does this PR solve?

Follow-up to #15393. After #15393 fixed the OpenSearch `search()`
signature and
the doc-meta mapping, document metadata still renders as **"0 fields"**
for every
document on the OpenSearch backend (`DOC_ENGINE=opensearch`).

**Root cause.** `OSConnection.insert()` pops `id` out of the document
before
indexing:

meta_id = d_copy.pop("id", "") # id used as _id, then DROPPED from
_source

so the stored `_source` never contains an `id` field. But the doc-meta
read path
filters and sorts on that field:

- `DocMetadataService.get_metadata_for_documents()` builds
`condition = {"kb_id": kb_id, "id": doc_ids}` -> `OSConnection.search()`
emits
  `Q("terms", id=doc_ids)` (a term query on the `id` field), and
- `_search_metadata()` sorts with `order_by.asc("id")`.

With `id` absent from `_source`, the terms filter matches nothing, so
`get_metadata_for_documents()` returns an empty map and the UI shows "0
fields"
-- even though the metadata was written correctly (it is visible via a
kb_id-only query).

`ESConnection.insert()` already keeps `id` (`d_copy.get("id", "")`) with
the
comment *"also keep 'id' as a regular field for sorting"*. This is a
plain
OpenSearch-only divergence (`pop()` vs `get()`).

### Fix

Mirror Elasticsearch: use `get("id")` instead of `pop("id")` so `id`
survives in
`_source`. The doc-meta mapping already declares `id` as `keyword`, so
the field
is searchable/sortable once populated.

### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)

### Affected backends
OpenSearch only. Elasticsearch already keeps `id`; Infinity / OceanBase
unaffected.

### How to reproduce
1. `DOC_ENGINE=opensearch`, create a KB, upload/parse a document, set
metadata.
2. Open the document list -> every document shows "0 fields" (the
metadata exists
in the `ragflow_doc_meta_*` index but its `_source` has no `id` field).

### Risk & backward compatibility
`insert()` is shared with the main chunk index; keeping `id` in
`_source` brings
OpenSearch in line with Elasticsearch (which already does this), so it
is parity,
not new behavior. No default / ES / Infinity / OceanBase behavior
change.

Note: affects new inserts only. Existing `ragflow_doc_meta_*` indices
created
before this change have no `id` in `_source`; re-sync metadata, or
backfill once
with `_update_by_query` (`ctx._source.id = ctx._id`).

### Test plan
- [ ] OpenSearch: after the fix the document list shows correct metadata
field
      counts (not "0 fields"); metadata filter/sort by id works.
- [ ] Elasticsearch regression: unchanged.
2026-06-08 17:31:04 +08:00
Yash Raj Pandey
14c460a525 Fix: Excel parser emits a spurious header-only chunk at exact chunk_rows multiples (#15490)
### What problem does this PR solve?

`RAGFlowExcelParser.html()` iterates `(len(rows) - 1) // chunk_rows + 1`
times. `rows[0]` is the header, so `len(rows) - 1` is the data-row
count. When that count is an exact multiple of `chunk_rows`, the `+ 1`
over-counts by one: the final iteration's data slice is empty, but the
header row is still appended — producing a chunk that contains only the
table header and no data.

This is reachable via `rag/app/naive.py` (`html4excel`, `chunk_rows=12`)
and `rag/app/one.py`. A sheet with 12/24/36… data rows (or 256/512… with
the default `chunk_rows=256`) produces an extra
`<table><caption>…</caption><tr><th>…</th></tr></table>` chunk. It is
non-empty, so it passes the `if _` filter and gets indexed as a real
(empty) chunk.

| data rows (chunk_rows=12) | before | after |
|---|---|---|
| 12 | 2 chunks (1 header-only) | 1 |
| 24 | 3 chunks (1 header-only) | 2 |
| 13 | 2 (unchanged) | 2 |

### Fix

Iterate `ceil(n_data / chunk_rows)` times instead of `n_data //
chunk_rows + 1`. Adds
`test/unit_test/deepdoc/parser/test_excel_parser.py`; the
header-only-chunk cases fail before this change and pass after.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

Used the Claude CLI while working on this.
2026-06-08 17:16:45 +08:00
seekmistar01
68b9360536 fix(nlp): tokenize content_tks by whitespace in FulltextQueryer.paragraph (#15721)
## Summary
Closes #15720

`FulltextQueryer.paragraph` normalized its `content_tks` token string
with `[c.strip() for c in content_tks.strip() ...]`, which iterates the
string **character by character** — `"machine learning model"` becomes
20 single characters instead of 3 tokens. Those single chars are fed to
`tw.weights(..., preprocess=False)`, producing meaningless term weights
and a garbage `MatchTextExpr`.

`paragraph()` backs `Dealer.tag_content` (the KB auto-tagging feature),
so tag retrieval/scoring is silently broken for tag-enabled knowledge
bases. Every other method in this file tokenizes with `.split()` — this
is a `.strip()`-vs-`.split()` typo.

## Change
- `rag/nlp/query.py` — change `content_tks.strip()` to
`content_tks.split()` in the `paragraph` token-normalization line.

## Why it's safe
- The caller passes a space-separated token string; `.split()` recovers
the real tokens, matching the contract of `tw.weights` and the
`.split()` tokenization used by the sibling methods (`similarity`,
`question`).
- No behavior depends on the per-character expansion.

## Verification
- `python -m py_compile rag/nlp/query.py` — OK.
- Demonstrated: `"machine learning model"` → 20 single-character entries
before, 3 real tokens after. No test references `paragraph`.

Co-authored-by: seekmistar01 <seekmistar01@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 17:16:30 +08:00
chanx
2bd8900638 Fix: Model provider bugs (#15770)
### What problem does this PR solve?

Fix: Model provider bugs

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-08 17:04:05 +08:00
Jack
04209ffccf feat: implement FetchChunkVectors for citation vector hydration (#15749)
## What problem does this PR solve?

Implements `FetchChunkVectors` — the infrastructure needed to hydrate
chunk embedding vectors on demand. This is a prerequisite for
`insert_citations` (citation insertion in the `searchbots/ask`
endpoint), matching the Python `Dealer.fetch_chunk_vectors` pattern.

Without this, citation insertion cannot compute answer-vs-chunk vector
similarity.

## Type of change

- [x] New Feature (non-breaking change which adds functionality)

## Changes

### New Function
- `FetchChunkVectors(engine, chunkIDs, tenantIDs, kbIDs, dim)` — fetches
embedding vectors for a set of chunk IDs
- Consumer-side `vectorFetcher` interface with only `Search` + `GetType`
methods
- Both `*elasticsearchEngine` and `*infinityEngine` implicitly satisfy
the interface

### Engine Behavior
- **ES**: queries by chunk ID list via `Search` with filter `{"id":
chunkIDs}`, parses tab-separated `q_N_vec` string format
- **Infinity / OceanBase**: skips the round-trip (vectors already
shipped with chunks)
- **Degrades gracefully**: engine errors return zero vectors — citation
insertion will use placeholders instead of failing

### Vector Parsing
- Handles ES tab-separated string format (`"0.1\t0.2\t0.3"`)
- Handles `[]float64` and `[]interface{}` formats
- Returns zero vector for wrong-dimension or unparseable input

### Bug Fix
- `metadata_filter_test.go`: add missing `"sort"` import (pre-existing
build break)

### Tests
- 12 unit tests: empty input, Infinity/OceanBase skip, ES string vector,
ES float slice, ES interface slice, search error degradation, missing
chunk → zero, wrong dimension → zero, parse edge cases

## Files Changed

| File | Change |
|------|--------|
| `internal/service/chunk_vector.go` | New — FetchChunkVectors + parse
helpers |
| `internal/service/chunk_vector_test.go` | New — 12 tests |
| `internal/service/metadata_filter_test.go` | Fix missing `"sort"`
import |

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 16:54:00 +08:00
buua436
c8c890b06c fix: refine think stream parsing (#15745)
### What problem does this PR solve?
Refine the stream parsing for `<think>` / `</think>` so MiniMax and
DeepSeek-style chunking both flush in the right order without mixing
think and answer buffers.

### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-08 16:53:22 +08:00
chanx
144abbe2eb feat: Unify the 'Add Model Provider' modal (#15768)
### What problem does this PR solve?

feat:Unify the 'Add Model Provider' modal

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
- [x] Refactoring
2026-06-08 16:46:52 +08:00
Wang Qi
4bbd59823a Addd OpenRouter OpenAI API compatible list models (#15764)
Addd OpenRouter OpenAI API compatible list models
1. openrouter
2. OpenAI API compatible
3. VLLM
4. LM Studio

Open Router
<img width="1318" height="1217" alt="image"
src="https://github.com/user-attachments/assets/1d11b1e3-8c72-44fd-bff2-e9502d88d97d"
/>

VLLM
<img width="1433" height="931" alt="image"
src="https://github.com/user-attachments/assets/088801a6-0481-4623-976b-e7e93253ea07"
/>
2026-06-08 16:42:17 +08:00
Idriss Sbaaoui
653d4bdbf5 Fix : Ci fail for infinity on level p3 (#15757)
### What problem does this PR solve?

fix failing p3 tests

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-08 16:35:33 +08:00
Haruko386
67ce0c896d feat[Go]: implement /api/v1/agents/<agent_id>/sessions (#15705)
### What problem does this PR solve?

As Title
Codes were tested by Postman

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-06-08 16:26:27 +08:00
Danut Matei
e2b0da9eea fix(opensearch): keep the BM25 leg in hybrid search (#15760)
### What problem does this PR solve?

Fixes the OpenSearch side of #10747: hybrid search drops the keyword
(BM25) leg and
ends up doing plain vector search.

When a search has both a text and a vector leg, `OSConnection.search()`
throws the text
query away:

    del q["query"]
    q["query"] = {"knn": knn_query}

The text clause only stays on as a filter inside the knn query, so it
narrows the
candidate set but doesn't count towards scoring. So hybrid search on
OpenSearch behaves
like plain vector search, unlike the Elasticsearch backend.

What I changed:

- when both legs are present, send a real hybrid query
`{"hybrid": {"queries": [bm25, {"knn": ...}]}}` and let a
normalization-processor
  search pipeline score and combine the two legs
- only the actual filters (kb_id, available_int, ...) go in the knn
filter, not the
  text must clause
- create the pipeline on startup if it's missing, so there's no separate
provisioning
step. name and weights can be set under `os:` in service_conf.yaml, or
via
`OS_HYBRID_PIPELINE`; defaults are `ragflow_hybrid_pipeline` and `[0.5,
0.5]`
- normalization-processor needs OpenSearch 2.10+. on older clusters, or
when the
pipeline can't be created, log a warning and fall back to vector-only
instead of
  pointing at a pipeline that doesn't exist

This is only the hybrid-search fix; `create_doc_meta_idx` is already on
main.

Testing (there's no OpenSearch path in CI): added a unit test
(`test/unit_test/rag/utils/test_opensearch_hybrid_search.py`, no
services needed) that
checks the query built in each case — hybrid + pipeline param for
text+vector, plain knn
for vector-only, plain bool for text-only, the knn filter never carrying
the text
query_string, and the vector-only fallback when the pipeline isn't
available. Also ran
it against a real OpenSearch 2.19.1 container with a doc that matches
the keyword but
sits outside the knn top-k: pure knn returns `['D1','D2','D5']` (keyword
doc missing),
the hybrid query returns `['A','D1','D2','D5']` (keyword doc present).

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

Signed-off-by: Danut Matei <matei.danut.dm@gmail.com>
2026-06-08 16:17:47 +08:00
Jack
8f4809d1b5 feat: implement POST /api/v1/searchbots/retrieval_test (#15710)
## What problem does this PR solve?

Implements `POST /api/v1/searchbots/retrieval_test` in the Go API
server, aligning with the Python `bot_api.py` counterpart. Also applies
security hardening and consistency fixes discovered during CTO-level
code review:

- **Missing endpoint**: `retrieval_test` was not available in Go,
requiring Python fallback
- **Security**: Both `chunkHandler` and `searchBotHandler` leaked
`err.Error()` to API consumers
- **Python alignment**: Default values, empty question handling, and
`top_k <= 0` validation differed from Python behavior
- **Test gaps**: `chunkHandler.RetrievalTest` had zero unit tests;
several edge cases uncovered

## Type of change

- [x] New Feature (non-breaking change which adds functionality)
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring

## Summary

### New Endpoint
- `POST /api/v1/searchbots/retrieval_test` — retrieval test with full
field support (page, size, top_k, use_kg, cross_languages, keyword,
similarity_threshold, vector_similarity_weight)

### New Type
- `common.StringSlice` — JSON type that accepts both `"kb1"` and
`["kb1", "kb2"]`, matching Python API flexibility

### Security
- Both `searchBotHandler` and `chunkHandler` now use `common.Warn()` +
generic error messages instead of leaking `err.Error()` to API consumers
- All error responses include consistent `"data": nil` shape
- `chunkHandler.RetrievalTest` uses interface-based DI (`chunkService`)
to enable testability

### Python Alignment
- Handler-level defaults align with Python `bot_api.py` (page=1,
size=30, top_k=1024, similarity_threshold=0.0,
vector_similarity_weight=0.3)
- `top_k <= 0` validation matching Python behavior
- Empty/whitespace question returns 200 + empty result (matches
`chunk_api.py`)
- `chunkHandler` `Datasets` field uses `common.StringSlice` for
string-or-array flexibility

### Refactoring
- `ChunkServiceIface` → `ChunkRetriever`, `chunkSvcIface` →
`chunkService` (Go-conventional naming)
- Extracted `applyRetrievalDefaults`, `toRetrievalServiceRequest` from
handler body
- Regex moved to package-level var in `parseRelatedQuestions`
- `service.RetrievalTestRequest.Datasets` type changed to
`common.StringSlice`
- `chunkHandler` now uses consumer-side interface for DI

### Tests
- 37 unit tests across both handlers: auth, validation, defaults,
StringSlice edge cases, empty/whitespace KbID, service errors, JSON
format, `top_k <= 0`, field mapping verification

## Files Changed

| File | Change |
|------|--------|
| `cmd/server_main.go` | Wire new handler + chunkService +
difyRetrievalHandler |
| `internal/common/json_types.go` | New StringSlice type |
| `internal/common/json_types_test.go` | StringSlice tests |
| `internal/handler/chunk.go` | Interface-based DI, security, Python
alignment, defaults |
| `internal/handler/chunk_test.go` | New — 9 comprehensive tests |
| `internal/handler/searchbot.go` | New endpoint + refactoring + `top_k
<= 0` validation |
| `internal/handler/searchbot_test.go` | 18 tests covering all edge
cases |
| `internal/router/router.go` | Register new route +
difyRetrievalHandler |
| `internal/service/chunk.go` | Datasets type → StringSlice, Question
binding relaxed |

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 16:16:56 +08:00
balibabu
9c32b73cf7 Fix: The embedded website floating component on the agent page does not display citations. (#15767)
### What problem does this PR solve?

Fix: The embedded website floating component on the agent page does not
display citations.
### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-08 16:09:36 +08:00
buua436
e81bca73d5 fix: normalize agent session chunks (#15756)
### What problem does this PR solve?

Normalize agent session chunk references so they are mapped through a
dedicated helper instead of duplicating the field extraction inline.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-08 15:29:55 +08:00
qinling0210
5e0a7ce408 Update Rerank logic in GO (#15755)
### What problem does this PR solve?

Sync the rerank logic in the following PR  to  GO.
https://github.com/infiniflow/ragflow/pull/15429
https://github.com/infiniflow/ragflow/pull/15434

### Type of change

- [x] Refactoring
2026-06-08 15:28:10 +08:00
buua436
6bf7056422 feat: add placeholder model metas (#15753)
### What problem does this PR solve?

add placeholder model metas

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-08 14:54:59 +08:00
balibabu
c935f305e2 Fix: The time zone is not displayed on the personal profile page. (#15759)
### What problem does this PR solve?

Fix: The time zone is not displayed on the personal profile page.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-08 14:33:52 +08:00
bitloi
220ee9dbfb fix: normalize reasoning model families (#15612)
### What problem does this PR solve?

Closes #15611.

RAGFlow's fallback reasoning parser only recognized the exact model
family `qwen3`. For provider-prefixed Qwen model names such as
SiliconFlow's `qwen/qwen3-8b`, the derived model class can be
`qwen/qwen3`, so inline `<think>...</think>` content was not split from
the visible answer when `reasoning_content` was absent.

This PR normalizes model-family detection before fallback reasoning
extraction, keeps the parser nil-safe, and adds focused tests for Qwen3
variants plus Gitee and SiliconFlow chat responses.

It also makes SiliconFlow propagate `ChatConfig.Thinking` into the chat
request body, matching the existing Gitee behavior, so Qwen thinking
mode is actually enabled when requested.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring

### Validation

- `/root/go/bin/gofmt -l internal/entity/models/common.go
internal/entity/models/common_test.go
internal/entity/models/reasoning_family_provider_test.go
internal/entity/models/siliconflow.go`
- `git diff --check`
- `/root/go/bin/go test ./internal/entity/models -run
'Test(NormalizeModelFamily|GetThinkingAndAnswer|GiteeChatExtractsQwenThinkingFromInlineContent|SiliconflowChatExtractsProviderPrefixedQwenThinkingFromInlineContent)'
-vet=off -count=1`

Note: the full package command `/root/go/bin/go test
./internal/entity/models -vet=off -count=1` now runs locally, but it
currently fails on an unrelated existing
`TestAstraflowEmbedReturnsNoSuchMethod` panic in
`internal/entity/models/astraflow.go:482`.
2026-06-08 13:32:52 +08:00
oktofeesh
b1a2210d06 fix(go-models): increase JieKouAI SSE scanner buffer (#15737)
## Summary
- Raise the JieKouAI streaming SSE scanner buffer to handle larger data
chunks without truncation.
2026-06-08 13:10:10 +08:00
tmimmanuel
5e25e2600b Go: implement Xiaomi chat provider (#15626)
### What problem does this PR solve?

Implements the Xiaomi MiMo chat provider for the Go model provider
layer.

Reference issue: #14736

Official docs used:
- Xiaomi MiMo OpenAI-compatible chat API:
https://platform.xiaomimimo.com/docs/en-US/api/chat/openai-api
- Xiaomi MiMo model and rate limits:
https://platform.xiaomimimo.com/docs/en-US/quick-start/model
- Xiaomi MiMo model hyperparameters:
https://platform.xiaomimimo.com/docs/en-US/quick-start/model-hyperparameters
2026-06-08 13:09:36 +08:00
cleanjunc
38f9ea5fec fix(rerank): normalize reranker scores onto a single scale before hybrid blend (#15429)
### What problem does this PR solve?

Closes #15428

The hybrid score in `rag/nlp/search.py` (`rerank_by_model`) blends
reranker similarity with token similarity on a fixed `[0, 1]` scale:

```python
return tkweight * np.array(tksim) + vtweight * vtsim + rank_fea  # tkweight=0.3, vtweight=0.7
```

The reranker implementations did not agree on that scale. Only three of
roughly 17 providers normalized their output, and `NvidiaRerank`
returned raw, unbounded logits. Weighted at `0.7`, a negative logit
could push a genuinely relevant chunk below pure keyword matches, and
its magnitude swamped `tksim`, which lives in `[0, 1]`. The practical
effect was that the same query produced differently scaled scores
depending on the configured reranker, and logit based providers degraded
retrieval quality instead of improving it.

This PR enforces a single scoring contract in one place:

- `Base.similarity` is now the only public entry point. It
short-circuits empty input and guarantees a normalized result. Each
provider implements its raw scoring in `_compute_rank`, which removes
sixteen duplicated empty input guards and the three scattered
normalization calls.
- Normalization is range aware. Providers that already return calibrated
`[0, 1]` relevance scores (Cohere, Jina, Voyage, and others) keep their
absolute magnitudes, so `similarity_threshold` filtering and the
reported `vector_similarity` stay meaningful. Only out-of-range output
such as NVIDIA logits is min-max rescaled into `[0, 1]`.
- The twelve leftover `[DEBUG ...]` prints in `rerank_by_model`,
introduced in #14231, are removed. They ran on every retrieval, added
per chunk overhead, and leaked queries, keywords, and document content
to stdout and logs.

A new regression suite in
`test/unit_test/rag/llm/test_rerank_normalization.py` covers logit
rescaling (positive, negative, and flat batches), preservation of
already calibrated scores, ordering, empty input handling, and the per
provider HTTP path. It also asserts that no provider overrides
`similarity()`, so the contract cannot silently drift.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-08 11:53:22 +08:00
dripsmvcp
3d7adf2193 feat[Go]: implement GET /plugin/tools (issue #15240) (#15570)
## Summary

Port the Python `GET /v1/plugin/tools` endpoint to the Go API server.
Listed in the Go-API port checklist of #15240.

Returns the metadata of every embedded LLM tool plugin in the same JSON
shape the Python endpoint emits (camelCase keys preserved), so existing
frontends bind to the Go server without changes.
2026-06-08 11:53:19 +08:00
cleanjunc
91983106f2 fix(retrieval): keep rerank window aligned to page_size for deep pagination (#15434)
### What problem does this PR solve?

Closes #15433

Reranked retrieval drops results and returns short pages once pagination
crosses the first candidate block, for the common page sizes 10 and 30.

In `rag/nlp/search.py`, the candidate window (`RERANK_LIMIT`) is rounded
up to a multiple of `page_size` to keep block based pagination aligned,
and then clamped back to 64:

```python
RERANK_LIMIT = math.ceil(64 / page_size) * page_size if page_size > 1 else 1  # e.g. 70 for page_size=10
RERANK_LIMIT = max(30, RERANK_LIMIT)
if rerank_mdl and top > 0:
    RERANK_LIMIT = min(RERANK_LIMIT, top, 64)  # clamps back to 64, breaking the multiple
```

`RERANK_LIMIT` is used both as the backend block size (`page =
global_offset // RERANK_LIMIT`) and as the modulus that slices a page
out of a reranked block (`begin = global_offset % RERANK_LIMIT`). When
it stops being a multiple of `page_size`, the block that gets fetched
and the slice taken from it no longer agree. With `page_size=10` and
`top=1024`, page 7 returns only 4 of 10 results and the head of the next
block is never shown on any page. This happens whenever the result set
spans more than one block, which is the default.

**Fix**

The window math is moved into a small reusable helper,
`Dealer._rerank_window`, which:

- targets a pool of about 64 candidates,
- bounds it by `top` when a reranker is active, and
- always rounds to a whole number of pages, so the window stays an exact
multiple of `page_size`.

The call site becomes a single line, and the alignment invariant now
lives in one documented place. Behavior is unchanged on every path that
was already aligned (the non reranked path and any `top` that already
produced a page multiple).

**Verification**

A simulation of the full retrieval path (per block rerank, similarity
threshold filter, and the exact `page // window` and `offset % window`
math) confirms the fix loses nothing where the old code lost real
results:

```
ps=10 top=1024:  new window=70  dropped_valid=0   |  old window=64  dropped_valid=16
ps=30 top=1024:  new window=90  dropped_valid=0   |  old window=64  dropped_valid=66
```

New unit tests in `test/unit_test/rag/test_search_pagination.py` cover
the alignment invariant, cross block pagination (every candidate
surfaced once, in order, no gaps, no short interior pages), the reported
regression, and parity with the old window on the previously correct
paths. All 114 cases pass and `ruff check` is clean.

Fixes the reranked deep pagination data loss described above.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-08 11:53:12 +08:00
qinling0210
c960dc2a4c Refine handling of POST /api/v1/datasets/search in GO (#15583)
### What problem does this PR solve?

Refine handling of POST /api/v1/datasets/search in GO

### Type of change

- [x] Refactoring
2026-06-08 11:49:37 +08:00
Hz_
074c331cdf fix(go-api): sync document handler interface and enforce preview acce… (#15688)
### Description

This PR syncs the `documentServiceIface` interface and introduces
handler methods for document preview, artifact fetching, and downloading
in the Go API. It also ensures that strict dataset alignment and access
checks are enforced when retrieving or downloading documents.

Furthermore, this PR introduces comprehensive unit tests for both the
newly added Handler and Service methods to ensure robustness and prevent
future regressions.

### Key Changes
* **Router & Handler Integration**: 
  * Added and wired new API endpoints in `internal/router/router.go`.
* Synchronized the `documentServiceIface` with `GetDocumentArtifact`,
`GetDocumentPreview`, and `DownloadDocument`.
* Implemented handlers for these endpoints in
`internal/handler/document.go`.
* **Access & Validation Enforcement**: 
* Refactored `internal/service/document.go` to strictly check if a
document belongs to the requested dataset before allowing downloads or
previews.
* Added robust artifact file sanitization (`sanitizeArtifactFilename`)
and attachment handling (`shouldForceArtifactAttachment`).
* **Comprehensive Unit Testing**:
* **Handler Layer (`internal/handler/document_test.go`)**: Added mock
service implementations and Gin router tests covering success,
not-found, and internal error states for all 3 new endpoints.
* **Service Layer (`internal/service/document_test.go`)**: Added
extensive business logic tests including dataset mismatch checks,
non-existent document checks, and artifact file validation.
2026-06-08 11:37:06 +08:00
Lynn
b05d5a5228 Feat: get model list from remote (#15711)
### What problem does this PR solve?

Feat:
- Get model list from remote provider. 

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-06-08 11:02:40 +08:00
kpdev
b0a45809ff fix(onedrive): normalize folder_path for Graph delta URL (#15503)
Prepend a leading slash and reject `..` segments so scoped OneDrive
delta queries use `root:/path:/delta` instead of `root:path:/delta`.

Fixes #15500

### What problem does this PR solve?

The OneDrive connector builds Microsoft Graph delta URLs from optional
`config.folder_path`. When users enter a path without a leading slash
(e.g. `Documents/Reports` instead of `/Documents/Reports`), the
connector produces a malformed URL such as
`root:Documents/Reports:/delta`. Per [Microsoft Graph path-based
addressing](https://learn.microsoft.com/en-us/graph/onedrive-addressing-driveitems),
the segment after `root:` must start with `/` (e.g.
`root:/Documents/Reports:/delta`). Sync and validation then fail or
return no documents, which is hard to diagnose from the UI because the
optional folder field does not enforce the format.

This PR normalizes `folder_path` at connector construction time (prepend
`/`, trim whitespace and trailing slashes) and rejects `..` segments
before any Graph request is made.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-08 09:56:47 +08:00
Jack
5a04ac0864 feat: Dify-compatible retrieval API endpoint (#15704)
## Summary

Dify-compatible retrieval API for external knowledge base integration.

## Changes

- **New handler**: DifyRetrievalHandler with POST/GET
/api/v1/dify/retrieval
- **Health check**: GET /api/v1/dify/retrieval/health
- **Full pipeline**: KB validation -> permission check -> embedding ->
metadata filter -> chunk retrieval -> child chunk aggregation ->
optional KG search -> response assembly
- **12 tests** covering all paths (success, errors, metadata filter, KG
mode)
- **Testability**: Handler dependencies defined as interfaces
(KBServiceIface, ModelServiceIface, etc.)

## Files

| File | Type |
|------|------|
| internal/handler/dify_retrieval_handler.go | New — handler +
interfaces |
| internal/handler/dify_retrieval_handler_test.go | New — 12 tests |
| internal/router/router.go | Modified — route registration |
| cmd/server_main.go | Modified — handler wiring |
| internal/service/kg/pipeline.go | Modified — SetChatModel/SetEmbModel
|
| internal/service/kg/retrieval.go | New — helper functions |
| internal/service/kg/scoring.go | Moved from service package |
| internal/service/kg/search.go | New — KG search functions |
| internal/service/kg/types.go | New — type definitions |

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 21:16:25 +08:00
Hz_
1deb1313d2 feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary

  This PR improves the Go CLI in two areas:

1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.

These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.

  ## What Changed

  ### 1. Batch model add/remove support

The CLI now supports operating on multiple model names provided in a
single quoted string.

  Supported commands include:

```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;

drop model 'x1 x2 x3' from 'vllm' 'test';

remove model 'x1 x2 x3' from 'vllm' 'test';

```

For add model, each config segment after with is matched to the
corresponding model name by position.

  Example mapping:

  - x1 -> tokens 1024, chat + vision, thinking=true
  - x2 -> tokens 2048, chat
  - x3 -> tokens 1024, vision, thinking=true

  The existing single-model syntax remains supported.

  ### 2. Optional embedding dimension

Previously, the Go CLI required dimension to be explicitly provided for
embed text.

  Before:

embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;

  Now both forms are supported:

embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;

  embed text 'what is rag' 'who are you' with 'model@test@provider';

When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.


  ## Tests

  Added parser tests covering:

  - Multiple models with multiple config segments
  - Model type deduplication
  - Model/config count mismatch
  - Drop/remove multiple models
  - Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
balibabu
9c14e3f377 Fix: When adding a chat in the main interface, a warning will automatically pop up (#15685)
### What problem does this PR solve?

Fix: When adding a chat in the main interface, a warning will
automatically pop up (even if embedding and LLM model have already been
configured).
### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-05 19:09:22 +08:00
Jack
ea79d65d08 feat: add KGSearchRetrieval for full KG pipeline (N-hop, scoring, query_rewrite, community) (#15690)
## Summary

`KGSearchRetrieval` composes entity search, type search, relation
search, N-hop analysis, score fusion, LLM-based query\_rewrite, and
community reports into a single synthetic chunk for KG-enhanced
retrieval.

### Components

| Component | Source | Status |
|-----------|--------|--------|
| Entity/relation/community search | Direct `DocEngine.Search` calls | 
|
| N-hop analysis + score fusion | `common.AnalyzeNHopPaths` /
`DoubleHitBoost` / `FuseRelationScores` |  #15666 |
| Query rewrite prompt + parser | `common.BuildQueryRewritePrompt` /
`ParseQueryRewriteResponse` |  #15669 |
| Token budget | `common.BuildKGContent` + `NumTokensFromString` | 
#15666 |
| LLM query rewrite integration | `queryRewrite` function with fallback
|  |

### Testing

11 tests (pure function + mock engine):

```
=== RUN   TestKgEntityFromChunk_Basic          --- PASS
=== RUN   TestKgEntityFromChunk_ScoreFallback  --- PASS
=== RUN   TestKgEntityFromChunk_MissingFields  --- PASS
=== RUN   TestKgRelationFromChunk_Basic        --- PASS
=== RUN   TestKgRelationFromChunk_MissingFrom  --- PASS
=== RUN   TestSearchKGTypeSamples_Success      --- PASS
=== RUN   TestSearchKGTypeSamples_Empty        --- PASS
=== RUN   TestKGSearchRetrieval_Basic          --- PASS
=== RUN   TestKGSearchRetrieval_NoEntities     --- PASS
=== RUN   TestQueryRewrite_Fallback            --- PASS
=== RUN   TestQueryRewrite_EmptyQuestion       --- PASS
```

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 18:00:27 +08:00
Wang Qi
aa9545e4c9 Revert "fix: duplicate document ingest guard" (#15707)
Reverts infiniflow/ragflow#15638
2026-06-05 17:45:29 +08:00
Wang Qi
214ee319f8 Revert "fix(api): authorize owner_ids for list chats and search apps (#14775) (#15698)
This reverts PR #14775  commit 5a5e766386.
2026-06-05 17:26:02 +08:00
Yufeng He
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.
2026-06-05 17:20:44 +08:00
Liu An
aab01af6f2 fix: Update Dockerfile and release workflow to use GitHub mirror instead of Gitee (#15700)
### What problem does this PR solve?

Update Dockerfile and release workflow to use GitHub mirror instead of
Gitee

### Type of change

- [x] Other (please describe): CI
2026-06-05 16:10:52 +08:00
tmimmanuel
f78ef328bb Go: implement Bedrock embeddings (#15543)
### What problem does this PR solve?

Fixes #15542.

AWS Bedrock support for the Go model provider layer was added in #15166,
but embedding support was intentionally left out of scope and
`BedrockModel.Embed(...)` still returned the `no such method` sentinel.
This PR implements Bedrock text embeddings under the umbrella provider
tracker #14736.

### What this PR includes

- `internal/entity/models/bedrock.go`: implement
`BedrockModel.Embed(...)` through Bedrock Runtime `InvokeModel` with
existing SigV4 auth, region resolution, and runtime URL helpers.
- Titan embeddings: supports `amazon.titan-embed-text-v1` and
`amazon.titan-embed-text-v2:0`; v2 forwards `EmbeddingConfig.Dimension`
as `dimensions` when provided, while v1 keeps the payload minimal.
- Cohere embeddings: supports `cohere.embed-english-v3`,
`cohere.embed-multilingual-v3`, and `cohere.embed-v4:0`; batches input
texts and maps returned vectors to RAGFlow `EmbeddingData` in input
order.
- `conf/models/bedrock.json`: adds the `embedding` URL suffix (`invoke`)
and Bedrock embedding model entries.
- `internal/entity/models/bedrock_test.go`: adds unit tests for Titan,
Cohere, typed Cohere responses, validation, empty input, unsupported
models, and HTTP error propagation.

Reference docs:

- Bedrock InvokeModel API:
https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_InvokeModel.html
- Titan Text Embeddings:
https://docs.aws.amazon.com/bedrock/latest/userguide/titan-embedding-models.html
- Cohere Embed models on Bedrock:
https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-embed.html

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

### How was this tested?

- [x] `jq empty conf/models/bedrock.json`
- [x] `git diff --check`
- [x] `go test ./internal/entity/models/... -run Bedrock -count=1`
- [x] `go test ./internal/entity/models/... -run '^$' -count=1`
- [x] `go test ./internal/entity/models/... -run Bedrock -race -count=1`

Note: `go test ./internal/entity/models/... -count=1` currently fails in
unrelated existing Astraflow coverage
(`TestAstraflowEmbedReturnsNoSuchMethod` panics in
`internal/entity/models/astraflow.go`). The Bedrock-specific tests and
compile-only package check pass.
2026-06-05 13:26:32 +08:00
web-dev0521
b8db200757 feat(go-api): implement MCP server management endpoints (#15281)
## Summary

Ports the MCP (Model Context Protocol) server management endpoints that
power `web/src/pages/user-setting/mcp/` from Python
(`api/apps/restful_apis/mcp_api.py`) to Go. There were no MCP routes in
the Go server before this change.

Closes #15275 (subtask of #15240).

## Endpoints implemented (base path `/api/v1`)

| Method | Path | Description |
|--------|------|-------------|
| GET | `/mcp/servers` | List tenant servers (keyword / order /
pagination) |
| POST | `/mcp/servers` | Create a server |
| GET | `/mcp/servers/{mcp_id}` | Get one (`?mode=download` exports
config) |
| PUT | `/mcp/servers/{mcp_id}` | Update a server |
| DELETE | `/mcp/servers/{mcp_id}` | Delete a server |
| POST | `/mcp/import` | Bulk import from JSON config |
| POST | `/mcp/servers/{mcp_id}/test` | Connect + list tools (see notes)
|

## Implementation

Follows the existing `handler → service → dao` layering (per PR #14790):

- **entity** (`internal/entity/mcp.go`): added `MCPServerType` constants
and `IsValidMCPServerType` over the existing `MCPServer` model.
- **dao** (`internal/dao/mcp.go`): new `MCPServerDAO` with tenant-scoped
CRUD, a keyword filter, and a **whitelisted order-column map** (guards
against SQL injection via the caller-supplied `orderby`).
- **service** (`internal/service/mcp.go`): new `MCPService` —
list/get/export/create/update/delete/import/test — mirroring
`MCPServerService` and the `mcp_api` request validation, with sentinel
errors for clean code mapping.
- **handler** (`internal/handler/mcp.go`): new `MCPHandler` with the
seven handlers and Python-compatible response codes.
- **router / server_main**: registered the `/mcp` group and wired the
handler.

## Deviations from Python (documented in code)

1. **Bulk import is at `POST /mcp/import`, not `/mcp/servers/import`.**
gin (v1.9.1) cannot register a static segment and a path param at the
same tree node, so `/mcp/servers/import` would collide with
`/mcp/servers/:mcp_id` and panic at startup. The frontend should call
`/mcp/import`.
2. **No live tool discovery on create/update/import.** The Python path
runs `get_mcp_tools` over SSE / streamable-HTTP and stores
`variables.tools`. The Go server has no MCP client yet, so these persist
`variables`/`headers` but leave `variables.tools` unpopulated.
3. **`/test` returns a data error (`ErrMCPTestUnsupported`)** until a Go
MCP client lands. Per the issue, the live-connection path is scoped as a
follow-up; the handler still validates `url` + `server_type`.

## Testing

- Added `internal/service/mcp_test.go` covering `IsValidMCPServerType`
and the `TestServer` validation/short-circuit paths (no DB required).
- No Go toolchain was available in the dev environment, so `go build
./...` / `go vet ./...` verification is left to CI.

## Follow-ups

- Go MCP client (SSE / streamable-HTTP) to enable live tool discovery
and the real `/test` behavior.
- Reconcile the `/mcp/import` vs `/mcp/servers/import` path with the
frontend.

---------
2026-06-05 13:25:09 +08:00
web-dev0521
1d7e45115b feat(connectors): add Salesforce CRM data source connector (#15462)
### What problem does this PR solve?

Closes #15461.

RAGFlow had no way to ingest Salesforce CRM data, so support / sales
teams couldn't ground responses on live Accounts, Contacts,
Opportunities, Cases, or Knowledge articles. This adds a first-class
Salesforce data source connector that authenticates against a Connected
App via OAuth 2.0 client-credentials, queries selected SObjects via
SOQL, and turns each record into an indexable document with incremental
sync.

**Highlights**
- `common/data_source/salesforce_connector.py`: new
`SalesforceConnector` (`CheckpointedConnectorWithPermSync` +
`SlimConnectorWithPermSync`).
- OAuth 2.0 client-credentials flow; canonical `instance_url` from the
token response so multi-pod orgs route correctly.
- Per-object `SystemModstamp` cursor stored in
`SalesforceCheckpoint.cursors` — a failure mid-object doesn't rewind
sibling objects, and re-syncs only fetch changed rows.
- Deterministic record-to-text formatter (sorted keys) so SOQL field
reordering on the server doesn't mark every row "changed" on each poll.
- `_get_json` raises on non-2xx so 429 / 5xx never silently advance the
checkpoint past missing data.
- `Knowledge__kav` is in the default object set but is skipped silently
when the org doesn't have Salesforce Knowledge enabled (404 on
describe).
- Slim-doc IDs are scoped as `<Object>/<Id>` so prune deletes can't
collide across object types.
- `common/constants.py`, `common/data_source/config.py`,
`common/data_source/__init__.py`: register `salesforce` in `FileSource`
/ `DocumentSource` and export `SalesforceConnector`.
- `rag/svr/sync_data_source.py`: new `Salesforce(SyncBase)` class routed
through `load_from_checkpoint` (poll_source would re-walk every object
each run) and added to `func_factory`.
- Frontend:
- `web/src/pages/user-setting/data-source/constant/index.tsx`: new
`DataSourceKey.SALESFORCE`, form fields (instance URL, client ID/secret,
objects, api_version, batch size), `syncDeletedFiles` capability,
default form values, and tile entry with the new icon.
  - `web/src/locales/{en,zh}.ts`: description + per-field tooltips.
- `web/src/assets/svg/data-source/salesforce.svg`: 48x48 brand-style
icon to match the other Microsoft / cloud tiles.

**Verification**
- `npm run build` (vite + esbuild) passes (1m 26s).

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-06-05 13:24:36 +08:00
Jack
e629c0203b feat: add KG entity/relation/community search functions (#15689)
## Summary

Knowledge Graph search functions for entity, relation, community report,
and type-samples retrieval. Uses DocEngine.SelectFields (PR #15684) for
KG-specific fields.

### Functions

| Function | Description |
|----------|-------------|
| `SearchKGEntities` | Hybrid search over KG entities (dense + text +
fusion) |
| `SearchKGEntitiesByTypes` | Entity search filtered by
`entity_type_kwd` |
| `SearchKGRelations` | Hybrid search over KG relations |
| `SearchKGCommunityReports` | Community report search by entity names |
| `SearchKGTypeSamples` | Type→entities mapping for query_rewrite |

### Internal helpers

| Helper | Description |
|--------|-------------|
| `buildHybridExpr` | Shared dense+text+fusion expression construction |
| `buildKGDenseExpr` | Wraps `Embed()` call for vector search |
| `Parse*` | Convert raw chunks to typed structs |

### Testing

35 tests (pure function + mock integration)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 13:23:04 +08:00
Haruko386
4b2af1347c feat[Go]: implement Agent/Workflow PUT /api/v1/agents/<canvas_id>/tags (#15641)
feat[Go]: implement Agent/Workflow PUT /api/v1/agents/<canvas_id>/tags (#15641)
2026-06-05 13:22:23 +08:00
buua436
71649db3b0 fix: prevent duplicated post-think text (#15651)
### What problem does this PR solve?
This fixes duplicated post-think text in streamed chat responses. When
the model emits text immediately after `</think>`, the stream state now
advances its cursor correctly so the same visible prefix is not emitted
twice.

### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-05 13:21:26 +08:00
Jack
f6ff862a24 fix: restore case-insensitive contains/not contains/not in and consolidate metadata filter pipeline (#15686)
## Summary

This PR fixes case-sensitivity regressions introduced in #15656 and
consolidates the metadata filtering pipeline by removing the duplicate
`applySingleCondition` adapter layer.

### Bug fixes
1. **contains / not contains**: restored case-insensitive matching (was
lost when `applySingleCondition` was replaced by
`common.MetaFilter.matchValue` which lacked `strings.ToLower`)
2. **not in**: restored case-insensitive matching (was lost for same
reason; uses `strings.EqualFold`)
3. **!= with date filter values**: non-date metadata values now
correctly match the `≠` operator (a non-date value IS not equal to any
date, but was returning false)

### Architecture
4. **Removed `applySingleCondition`** (65 lines) — the inline switch was
a duplicate of `common.MetaFilter` logic. `ApplyMetaFilter` now converts
conditions and delegates to `common.MetaFilter` once per filter set,
eliminating ~25 lines of duplicate AND/OR merge logic.
5. **Added `filterSet`** — O(n+m) hash-map fast path for `in`/`not in`
operators, replacing the O(n*m) linear scan in `matchValue`.
6. **Exported `NormalizeOperator`** from `common` for consistent
operator alias handling.

### Cleanup
7. Removed 18 lines of dead code (`matchValue`'s `in`/`not in` branches
already bypassed by `filterOut` delegation)
8. Fixed orphaned godoc comment for `convertOperator`
9. Fixed incorrect `filterSet` doc comment (claimed "matching EqualFold"
but used `strings.ToLower`)
10. Completed `convertToMetaCondition` operator normalization
documentation

### Testing
- 60 tests (24 service + 36 common), all passing
- New tests: `==`, `≠`, `>`, `<`, `≥`, `≤`, `empty`, `not empty` through
`ApplyMetaFilter`
- New tests: `<`, `≤`, `≠` through `MetaFilter`; `not-in-empty-list`
through `filterSet`
- All 18 `MetaFilter` tests pass; all 10 `filterSet` unit tests pass

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 12:47:55 +08:00
Jack
ee32d91aab feat: add EnrichChunksWithDocMetadata function to attach document metadata to chunks (#15659)
## Summary

Add `EnrichChunksWithDocMetadata` as a method on `MetadataService` that
attaches document metadata to retrieval chunks in-place. Equivalent to
Python's `enrich_chunks_with_document_metadata()` from
`api/utils/reference_metadata_utils.py`.

### Usage

```go
metadataSvc.EnrichChunksWithDocMetadata(chunks, tenantID, metadataFields)
```

### Changes

- **`service/metadata.go`**: Added `EnrichChunksWithDocMetadata` method
- **`service/enrich_metadata_test.go`** (new): 7 test cases

### Algorithm

1. Collect unique `(kb_id, doc_id)` pairs from chunks
2. Fetch metadata from ES via `SearchMetadata(kbID, tenantID, docIDs)`
3. Attach `document_metadata` field to each matching chunk
4. Optionally filter to specified `metadataFields`

### Testing

All 7 tests pass:

```
=== RUN   TestEnrichChunksWithDocMetadata_NoChunks       --- PASS
=== RUN   TestEnrichChunksWithDocMetadata_EmptyChunks     --- PASS
=== RUN   TestEnrichChunksWithDocMetadata_EmptyDocID      --- PASS
=== RUN   TestEnrichChunksWithDocMetadata_DuplicateDocIDs --- PASS
=== RUN   TestEnrichChunksWithDocMetadata_MultipleKBs     --- PASS
=== RUN   TestEnrichChunksWithDocMetadata_WithMetadataFields --- PASS
=== RUN   TestEnrichChunksWithDocMetadata_MixedFields     --- PASS
```

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 11:42:23 +08:00
Jack
3b1ae3f829 feat: support SelectFields override in DocEngine for KG-specific queries (#15684)
## Summary

Both ES and Infinity engines now respect `SearchRequest.SelectFields`,
allowing callers to specify output columns for KG
entity/relation/community queries instead of the default chunk columns.

### Changes

- **`internal/engine/elasticsearch/chunk.go`**: Added `SelectFields`
override after default `outputColumns`
- **`internal/engine/infinity/chunk.go`**: Added `SelectFields` override
after default `outputColumns`
- **`internal/engine/elasticsearch/kg_test.go`** (new): Integration test
(skipped unless `ES_TEST=1`)

### Usage

```go
result, err := docEngine.Search(ctx, \&types.SearchRequest{
    KbIDs:        kbIDs,
    SelectFields: []string{entity_kwd, entity_type_kwd, rank_flt, n_hop_with_weight},
    Filter:       map[string]interface{}{knowledge_graph_kwd: entity},
})
```

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 11:41:39 +08:00
Wang Qi
4cbe597d7e Refactor: consolidate to use @login_required (#15652)
Refactor: consolidate to use @login_required
2026-06-05 11:35:00 +08:00
bitloi
9f3e289b78 Fix: preserve markdown tables during delimiter extraction (#15632)
### What problem does this PR solve?

Markdown extraction can split tables row by row when delimiter-based
extraction uses a newline delimiter. That loses table structure during
chunking even though delimiters should still split normally outside
tables.

This PR keeps the follow-up to #15482 intentionally narrow:

- preserve Markdown pipe tables during delimiter-based extraction
- preserve borderless pipe tables during delimiter-based extraction
- preserve multiline HTML tables during delimiter-based extraction
- keep delimiter splitting unchanged outside protected table ranges

Refs #15482

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

### Testing

- `ruff check deepdoc/parser/markdown_parser.py
test/unit_test/deepdoc/parser/test_markdown_parser.py`
- `python3 run_tests.py -t
test/unit_test/deepdoc/parser/test_markdown_parser.py`
- `git diff --check`
2026-06-05 10:35:33 +08:00
dripsmvcp
431f52a5d4 feat[Go]: implement GET /agents/templates (issue #15240) (#15573)
## Summary

Port the canvas-template catalogue endpoint to the Go API server. Listed
in the Go-API port checklist of #15240.

Mirrors `list_agent_template` in `api/apps/restful_apis/agent_api.py`:
returns every row from the `canvas_template` table so that the UI can
render the template gallery on the New-Agent screen.

## What

- `internal/dao/canvas_template.go` — new `CanvasTemplateDAO.GetAll()`
ordered by `create_time desc` (newest templates first).
- `internal/service/agent.go` — wire the new DAO into `AgentService` and
expose `ListTemplates() ([]*entity.CanvasTemplate, error)`.
- `internal/handler/agent.go` — new `AgentHandler.ListTemplates` HTTP
handler (auth-gated, mirrors Python `@login_required`).
- `internal/router/router.go` — `agents.GET("/templates",
r.agentHandler.ListTemplates)` registered alongside the existing `GET
/agents`.
- `internal/handler/agent_test.go` — three new tests covering: success
path, empty-list → JSON array (not `null`), and the auth gate.

## Notes

- `CanvasTemplate` entity, GORM tags, and DB migration already exist in
`internal/entity/canvas.go` and `internal/dao/database.go` — no schema
change required.
- The handler coerces a `nil` slice to `[]*entity.CanvasTemplate{}` so
the JSON payload is always an array (the frontend does `data.map(...)`
on it).

## Test plan

- [x] `go vet ./internal/handler ./internal/service ./internal/dao
./internal/router` clean
- [x] Three unit tests added; existing `TestListAgents_Success`
untouched
- [ ] CI runs `go test ./internal/handler` with cgo binding linked

## Related

- Tracker: #15240
2026-06-05 10:13:30 +08:00
Jack
a237a89b90 feat: add QueryRewrite prompt builder and response parser (#15669)
QueryRewrite prompt builder and response parser. Zero external
dependencies.

### Functions
- `BuildQueryRewritePrompt`: Renders `minirag_query2kwd` prompt with
query and type pool
- `ParseQueryRewriteResponse`: Parses LLM JSON response with fallback
for markdown and extra text

### Testing
```
=== RUN   TestBuildQueryRewritePrompt             --- PASS
=== RUN   TestParseQueryRewriteResponse_ValidJSON --- PASS
=== RUN   TestParseQueryRewriteResponse_MarkdownBlock --- PASS
=== RUN   TestParseQueryRewriteResponse_ExtraText --- PASS
=== RUN   TestParseQueryRewriteResponse_Invalid   --- PASS
=== RUN   TestParseQueryRewriteResponse_EmptyEntities --- PASS
```

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 10:11:14 +08:00
Jack
bf6c091c9f feat: add KG scoring utilities (#15666)
KG scoring utilities as pure functions.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 10:10:59 +08:00
kpdev
bd49fd70aa fix(api): set SDK document download Content-Type from filename (#15112) (#15113)
## Summary

- Infer `Content-Type` from the stored document filename on SDK download
routes.
- Covers `GET /api/v1/datasets/<dataset_id>/documents/<document_id>` and
`GET /api/v1/documents/<document_id>`.
- Aligns with REST preview/download via `CONTENT_TYPE_MAP`.

## Test plan

- [x] `pytest
test/testcases/test_http_api/test_file_management_within_dataset/test_doc_sdk_routes_unit.py::TestDocRoutesUnit::test_download_mimetype_from_filename`
- [x] Manual: `curl -sSI` on SDK dataset document download for a PDF;
expect `Content-Type: application/pdf`

Fixes #15112.
2026-06-05 10:08:53 +08:00
Lynn
794c1f4b25 Fix: volc engine and other json key factories (#15653)
### What problem does this PR solve?

Fix:
- VolcEngine adapt to new api_key format
- Save dict api_key as json

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-05 09:45:44 +08:00
He Wang
7789862cc5 fix(docker): mount tmpfs on es01 /tmp for entrypoint permissions (#15655)
### What problem does this PR solve?

On some Linux hosts (e.g. x86_64 with enforced POSIX ACL on overlay
storage), the official `elasticsearch` Docker image cannot start because
`docker-entrypoint.sh` needs to create temporary files under `/tmp` for
bash here-documents, while the image ACL grants `user:elasticsearch`
only `r-x` on `/tmp`:

```
/usr/local/bin/docker-entrypoint.sh: line 73/84: cannot create temp file for here-document: Permission denied
```

RAGFlow users hit this when running `docker compose` with the default
`es01` service. See also Refs #284.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

## Summary

Mount a writable `tmpfs` at `/tmp` for the `es01` service so
Elasticsearch entrypoint scripts can run on ACL-enforced environments.
Closes the startup failure described in #284 for non-ARM deployments.

## Changes

- Add `tmpfs: /tmp:mode=1777,size=512m` to `es01` in
`docker/docker-compose-base.yml`
- Document why the mount is required (ES image `/tmp` ACL vs entrypoint
here-documents)

## Test plan

- [x] Verified on Linux (x86_64): `docker run --rm elasticsearch:8.11.3
bash -c 'mktemp'` fails without tmpfs and succeeds with `--tmpfs
/tmp:mode=1777,size=512m`
- [x] Verified `es01` becomes healthy after `docker compose up -d es01`
with this change
- [ ] Upstream maintainers: `docker compose -f
docker/docker-compose-base.yml --profile elasticsearch up -d es01` on a
host where ACL is enforced


Made with [Cursor](https://cursor.com)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 23:19:31 +08:00
Jack
eee6ad546f feat: add ResolveReferenceMetadata utility function (#15663)
Add `ResolveReferenceMetadata` to parse `include_metadata` /
`metadata_fields` from request and config payloads.

### Changes
- **New**: `internal/common/reference_metadata.go` — pure function, zero
dependencies
- **New**: `internal/common/reference_metadata_test.go` — 8 test cases

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 22:34:18 +08:00
Jack
96a416629d refactor: change GetFlattedMetaByKBs return type to common.MetaData (#15656)
## Summary

Change `GetFlattedMetaByKBs` return type from `map[string]interface{}`
to strongly-typed `common.MetaData`.

**Depends on**: #15648 (provides `MetaData`, `MetaValueDocs` types)

### Changes
- `service/metadata.go`: Changed return type, removed type assertions
- `service/metadata_filter.go`: Updated all metadata function signatures
- `service/metadata_filter_test.go` (new): 12 test cases

### Bug fix
`applySingleCondition` used `.([]interface{})` assertions on `[]string`
data, silently breaking operators like `!=`, `contains`, `start with`,
etc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 22:16:04 +08:00
web-dev0521
98f2a2e60b feat(connectors): add Azure Blob Storage data source connector (#15466)
### What problem does this PR solve?

Closes #15465.

RAGFlow supports S3, Google Cloud Storage, R2, and OCI as data sources
but not Azure Blob Storage, leaving Azure users without a way to index
container objects into a knowledge base. This adds a first-class Azure
Blob Storage data-source connector — distinct from RAGFlow's existing
Azure storage *backends* (`rag/utils/azure_sas_conn.py`,
`rag/utils/azure_spn_conn.py`) which store RAGFlow's own files.

**Highlights**
- `common/data_source/azure_blob_connector.py`: new `AzureBlobConnector`
(`CheckpointedConnectorWithPermSync` + `SlimConnectorWithPermSync`).
- Uses the existing `azure-storage-blob` dependency (already in
`pyproject.toml`).
  - Three auth modes, tried in order of precedence:
1. **Account key** — `account_name` + `account_key` + `container_name`.
    2. **Connection string** — `connection_string` + `container_name`.
3. **SAS token** — `container_url` + `sas_token` (same shape as
`RAGFlowAzureSasBlob`).
- ETag fingerprint stored per blob in `AzureBlobCheckpoint.etags` —
unchanged blobs (same ETag as last run) are skipped without a download.
Only new/modified blobs are fetched.
  - Optional `prefix` scopes indexing to a virtual folder.
- `validate_connector_settings()` probes `get_container_properties()`
and maps `AuthenticationFailed / 403 / ContainerNotFound` to typed
connector exceptions.
  - Slim-doc IDs are blob names so prune reconciles correctly.
- `common/constants.py`, `common/data_source/config.py`,
`common/data_source/__init__.py`: register `azure_blob` in `FileSource`
/ `DocumentSource` and export `AzureBlobConnector`.
- `rag/svr/sync_data_source.py`: new `AzureBlob(SyncBase)` class routed
through `load_from_checkpoint` (ETag fingerprint owns change-detection)
and added to `func_factory`.
- Frontend:
- `web/src/pages/user-setting/data-source/constant/index.tsx`: new
`DataSourceKey.AZURE_BLOB`, auth-mode selector (account key / connection
string / SAS token), all credential fields, prefix + batch-size,
`syncDeletedFiles` capability, default form values, tile entry with
icon.
- `web/src/locales/{en,zh}.ts`: description + per-field tooltips for all
9 new keys.
- `web/src/assets/svg/data-source/azure-blob.svg`: Azure-branded
stacked-cylinders icon.

**Verification**
- `npm run build` (vite + esbuild) passes (37 s).

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-06-04 21:06:01 +08:00
Jack
a78a3fdd47 fix: add nil guard to DocumentDAO.GetByIDs and add tests (#15649)
## Summary

`DocumentDAO.GetByIDs()` generated `WHERE id IN ()` for empty/nil ID
slices, which is invalid SQL and would fail on most databases. This PR
adds a nil guard and comprehensive tests.

### Changes

- **Modified**: `internal/dao/document.go` — Added `len(ids) == 0` guard
to `GetByIDs`
- **New**: `internal/dao/document_test.go` — 4 test cases covering
success, empty IDs, nil IDs, and no-match

### Testing

```
=== RUN   TestDocumentGetByIDs_Success   --- PASS
=== RUN   TestDocumentGetByIDs_EmptyIDs  --- PASS
=== RUN   TestDocumentGetByIDs_NilIDs    --- PASS
=== RUN   TestDocumentGetByIDs_NoMatch   --- PASS
```

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 21:00:02 +08:00
Jack
461c190c49 feat: migrate meta_filter and convert_conditions to Go (#15648)
## Summary

Migrate the metadata filtering utilities `meta_filter` and
`convert_conditions` from `common/metadata_utils.py` to Go as pure
functions with zero external dependencies.

These functions are used by `dify/retrieval`, `openai/chat/completions`,
`document_api`, and `chunk_api` for filtering documents by metadata
conditions.

### Changes

- **New**: `internal/common/metadata_utils.go` — `ConvertConditions()`
and `MetaFilter()` with full operator support
- **New**: `internal/common/metadata_utils_test.go` — 18 test cases
covering all operators and edge cases

### Supported Operators

`=`, `≠`, `>`, `<`, `≥`, `≤`, `contains`, `not contains`, `in`, `not
in`, `start with`, `end with`, `empty`, `not empty`

### Design

- Numeric comparison via `strconv.ParseFloat`
- Date comparison via YYYY-MM-DD format detection
- Case-insensitive string comparison fallback
- `and` / `or` logic support for multiple conditions
- Zero external dependencies — pure functions only
2026-06-04 20:14:27 +08:00
Jack
e627f5d8c5 feat: implement POST /api/v1/searchbots/related_questions API (#15639)
## Summary

Implement the `POST /api/v1/searchbots/related_questions` endpoint in
Go, generating related search questions via LLM.

### Changes

- **New**: `internal/handler/related_questions.go` — Handler with
injectable LLM interface, prompt constant, and response parsing
- **New**: `internal/handler/related_questions_test.go` — 9 tests (4
handler + 5 parse)
- **Modified**: `internal/router/router.go` — Added route +
`RelatedQuestionsHandler` to struct
- **Modified**: `cmd/server_main.go` — Wired handler with
`SearchService` and `ModelProviderService`

### Testing

All 9 tests pass:

```
=== RUN   TestRelatedQuestionsHandler_Success        --- PASS
=== RUN   TestRelatedQuestionsHandler_EmptyResponse  --- PASS
=== RUN   TestRelatedQuestionsHandler_LLMFailure     --- PASS
=== RUN   TestRelatedQuestionsHandler_MissingQuestion --- PASS
=== RUN   TestParseRelatedQuestions_Standard         --- PASS
=== RUN   TestParseRelatedQuestions_Empty            --- PASS
=== RUN   TestParseRelatedQuestions_NoNumberedLines  --- PASS
=== RUN   TestParseRelatedQuestions_MixedContent     --- PASS
=== RUN   TestParseRelatedQuestions_MultiDigit       --- PASS
```

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 19:13:58 +08:00
Jack
6143205b37 feat: implement GET /api/v1/agents/<agent_id>/versions/<version_id> API (#15640)
## Summary

Implement the `GET /api/v1/agents/<agent_id>/versions/<version_id>`
endpoint in Go, returning full version details including DSL.

Depends on #15629 which introduced the version list endpoint and
`UserCanvasVersionDAO` infrastructure.

### Changes

- **Modified**: `internal/handler/agent.go` — Added `GetAgentVersion`
handler with auth check and ownership verification
- **Modified**: `internal/router/router.go` — Registered `GET
/:agent_id/versions/:version_id` route
- **New/Modified tests**: Service and handler tests for the version
detail endpoint

### Testing

```
=== RUN   TestGetVersion_Success       --- PASS
=== RUN   TestGetVersion_WrongCanvas   --- PASS
=== RUN   TestGetVersion_NotFound      --- PASS
=== RUN   TestGetAgentVersionHandler_Success      --- PASS
=== RUN   TestGetAgentVersionHandler_VersionNotFound --- PASS
```

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 19:13:26 +08:00
buua436
423fb6faae fix: duplicate document ingest guard (#15638)
### What problem does this PR solve?
When a document is rerun or updated concurrently, the previous
unconditional update could overwrite a newer task state.
This change adds an `update_time`-based optimistic lock so the update
only succeeds if the record has not been modified by another flow in the
meantime.

### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-04 17:57:51 +08:00
Haruko386
baeb0c0431 Refactor[Go Model Provider]: refactor baseURL and modelConfig (#15627)
### What problem does this PR solve?

As Title

### Type of change

- [x] Refactoring
2026-06-04 17:50:22 +08:00
buua436
04dc3bb19c fix: pass search id to searchbots ask (#15646)
### What problem does this PR solve?
This change ensures `/searchbots/ask` receives `search_id` from the
frontend, so the backend can load the matching search configuration when
the shared search flow invokes the endpoint.

### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-04 17:41:56 +08:00
Jack
23aae19898 feat: implement POST /api/v1/agents/<agent_id>/upload API (#15633)
## Summary

Implement the `POST /api/v1/agents/<agent_id>/upload` endpoint in Go,
allowing file uploads associated with agent canvases.

### Changes

- **Modified**: `internal/service/agent.go` — Added `CheckCanvasAccess`
method (owner + team-level permission semantics)
- **Modified**: `internal/handler/agent.go` — Added `UploadAgentFile`
handler with auth check, multipart file parsing, and delegation to
`FileService`. Added `fileUploader` interface for testability.
- **Modified**: `internal/router/router.go` — Registered `POST
/:agent_id/upload` route
- **Modified**: `cmd/server_main.go` — Wired `fileService` into
`AgentHandler`
- **New**: `internal/service/agent_test.go` — 4 service-level tests for
`CheckCanvasAccess` (owner, team member, private denial, not found)
- **New**: `internal/handler/agent_upload_test.go` — 3 handler-level
tests (success with fake file service, cross-user denial, empty file
rejection)

### Testing

All 7 tests pass with zero mocking of the DB layer (in-memory SQLite):

```
=== RUN   TestCheckCanvasAccess_Owner               --- PASS
=== RUN   TestCheckCanvasAccess_NotOwner            --- PASS
=== RUN   TestCheckCanvasAccess_PrivateCanvas_Denied --- PASS
=== RUN   TestCheckCanvasAccess_NotFound            --- PASS
=== RUN   TestUploadAgentFileHandler_Success        --- PASS
=== RUN   TestUploadAgentFileHandler_NoPermission   --- PASS
=== RUN   TestUploadAgentFileHandler_NoFiles        --- PASS
```

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 17:21:47 +08:00
Lynn
b65b18ba4c Fix: model provider (#15634)
### What problem does this PR solve?

Not display `success` when check not passed.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-04 16:05:00 +08:00
Jack
02d163a177 feat: implement GET /api/v1/agents/<agent_id>/versions API (#15629)
## Summary

Implement the `GET /api/v1/agents/<agent_id>/versions` endpoint in Go,
listing all version snapshots for an agent canvas in descending update
time order.

### Changes

- **New**: `internal/dao/user_canvas_version.go` —
`UserCanvasVersionDAO` with `ListByCanvasID` (ordered by update_time
DESC) and `GetByID`
- **Modified**: `internal/service/agent.go` — Added `CheckCanvasAccess`,
`ListVersions`, `GetVersion` methods
- **Modified**: `internal/handler/agent.go` — Added `ListAgentVersions`
handler with auth check
- **Modified**: `internal/router/router.go` — Registered `GET
/:agent_id/versions` route
- **New**: `internal/service/agent_test.go` — 5 service-level tests
(SQLite in-memory DB, zero mock)
- **Modified**: `internal/handler/agent_test.go` — 3 handler-level tests
(real DB, pre-authenticated context)

### Testing

All 8 tests pass with zero mocking (in-memory SQLite replaces MySQL):

```
=== RUN   TestListVersions_Success         --- PASS
=== RUN   TestListVersions_Empty           --- PASS
=== RUN   TestCheckCanvasAccess_Owner      --- PASS
=== RUN   TestCheckCanvasAccess_NotOwner   --- PASS
=== RUN   TestCheckCanvasAccess_NotFound   --- PASS
=== RUN   TestListAgentVersionsHandler_Success      --- PASS
=== RUN   TestListAgentVersionsHandler_NoPermission --- PASS
=== RUN   TestListAgentVersionsHandler_CanvasNotFound --- PASS
```

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 15:36:26 +08:00
Jack
c6eee09ed3 feat: migrate POST /api/v1/datasets/<dataset_id>/documents/stop to Go (#15597)
## Summary

Migrate the stop parse documents endpoint from Python to Go.

### Python endpoint
`POST /api/v1/datasets/<dataset_id>/documents/stop` —
`api/apps/restful_apis/document_api.py:1542-1641`

### Changes
| File | Change |
|------|--------|
| `internal/dao/task.go` | Add `GetByDocID` method |
| `internal/dao/task_test.go` | 3 DAO tests (new file) |
| `internal/service/document.go` | Add `StopParseDocuments` + refactor
shared helpers |
| `internal/service/document_test.go` | 8 service tests |
| `internal/handler/document.go` | Add handler + request struct +
interface |
| `internal/handler/document_test.go` | 5 handler tests |
| `internal/router/router.go` | Add `POST /:dataset_id/documents/stop`
route |

### How it works
1. Validates all document IDs belong to the dataset
2. For each document in RUNNING/CANCEL state (or with unfinished tasks):
- Sets Redis cancel signal `{task_id}-cancel` for each associated task
   - Updates `document.run` to CANCEL ("2")
3. Returns `{"success_count": N, "errors": [...]}`

### Test strategy
- **DAO/Service**: SQLite in-memory DB, zero mocks. Redis is nil-safe by
design.
- **Handler**: `fakeDocumentService` implementing `documentServiceIface`
interface.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-04 14:16:13 +08:00
Yufeng He
5db1b296fb fix: fall back from empty Docling native chunks (#15601)
## Summary
- keep the native Docling chunking path when it returns usable chunks
- fall back to the standard Docling response parser when a chunked
request gets HTTP 200 but returns no usable chunks
- add a regression test for older Docling servers that accept the
chunking request but return a standard conversion payload

## Why
Older external Docling servers can accept a request containing
`do_chunking: true` and still return the standard conversion response
shape. The current code treats any HTTP 200 from the chunked request as
a native chunk response, finds no chunk entries, and returns zero
sections without trying the standard response parser.

Fixes #15569.

## Validation
- `python -m pytest
test\\unit_test\\deepdoc\\parser\\test_docling_parser_remote.py -q`
- `python -m py_compile deepdoc\\parser\\docling_parser.py
test\\unit_test\\deepdoc\\parser\\test_docling_parser_remote.py`
- `python -m ruff check deepdoc\\parser\\docling_parser.py
test\\unit_test\\deepdoc\\parser\\test_docling_parser_remote.py`
- `git diff --check`
2026-06-04 13:42:58 +08:00
bitloi
01a5598aa5 Fix: markdown fenced code block extraction (#15630)
### What problem does this PR solve?

Markdown extraction currently applies custom delimiters before
respecting fenced code blocks. When a delimiter such as a newline is
configured, fenced code can be split into separate chunks, and longer
outer fences can be closed incorrectly by shorter nested fences.

This PR keeps the fix intentionally narrow for the Markdown chunking
discussion in #15482:

- preserve fenced code blocks when delimiter-based extraction is used
- support both backtick and tilde fences
- respect fence length so longer outer fences can contain shorter inner
fences
- keep delimiter splitting unchanged outside fenced blocks

Refs #15482

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

### Testing

- `ruff check deepdoc/parser/markdown_parser.py
test/unit_test/deepdoc/parser/test_markdown_parser.py`
- `python3 run_tests.py -t
test/unit_test/deepdoc/parser/test_markdown_parser.py`
2026-06-04 13:33:46 +08:00
buua436
c70f19e138 Fix: remove duplicate document preview access check (#15625)
### What problem does this PR solve?

remove duplicate document preview access check

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-04 13:05:15 +08:00
Lynn
597ac1e900 Fix: search bot and verify model instance (#15588)
### What problem does this PR solve?

Fix:
- Verify provider with empty llm list in llm_factories.json
- Set search bot's chat_llm_name, use tenant default chat model as
default

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-04 11:59:55 +08:00
buua436
bbacb31226 Fix: think stream tail handling (#15582)
### What problem does this PR solve?

think stream tail handling
### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-04 10:04:35 +08:00
kpdev
d26d799467 fix(api): restore accessible check on document preview (#15505)
Restore `DocumentService.accessible` on `GET
/api/v1/documents/{doc_id}/preview` so cross-tenant users cannot stream
documents by UUID.

Fixes #15501

### What problem does this PR solve?

PR #15146 (`71a52d579`) moved the agent attachment download route and
accidentally removed the `DocumentService.accessible(doc_id,
current_user.id)` guard from the REST preview handler. The endpoint
still requires login, but any authenticated user who knows another
tenant's `doc_id` can download the raw file bytes.

This restores the same authorization check that existed before #15146,
returning a generic `"Document not found!"` when access is denied (no
cross-tenant ID enumeration). SDK download routes tracked in #15125 are
unchanged.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-04 09:59:07 +08:00
dripsmvcp
2196f2260a fix(api): restore DocumentService.accessible check on /preview (#15508)
## Summary
Restore the `DocumentService.accessible(doc_id, current_user.id)` check
that PR #15146 dropped from the REST document preview handler. Any
authenticated caller could download any tenant's document bytes by
guessing/knowing the `doc_id`.

## Root cause
`api/apps/restful_apis/document_api.py` — the `GET
/documents/<doc_id>/preview` handler called `DocumentService.get_by_id`
and went straight to `File2DocumentService.get_storage_address` +
`STORAGE_IMPL.get`, with no tenant check between the lookup and the
read. The handler's docstring even promises "user must belong to the
tenant that owns the document's knowledge base" — the code didn't
enforce it.

## Fix
- Add `current_user` to the existing `api.apps` import.
- Immediately after `get_by_id`, call
`DocumentService.accessible(doc_id, current_user.id)`; on denial, return
the **same** `get_data_error_result(message="Document not found!")`
shape used for the missing-doc branch. That makes a cross-tenant probe
indistinguishable from a missing-doc probe, preventing ID enumeration
(the issue body calls this out explicitly).
- Emit `logging.warning` with caller user + doc_id for audit.
- Restores symmetry with peer routes that already call
`accessible(doc_id, user_id)` (e.g. `_run_sync` at
`document_api.py:1380`).

## Test plan
Adds
`test/unit_test/api/apps/restful_apis/test_document_preview_accessible.py`:

- **`test_cross_tenant_preview_is_denied`** — owner tenant ≠ caller
tenant; asserts the response shape is `Document not found!` and the
storage backend (`thread_pool_exec(STORAGE_IMPL.get, ...)`) is **never**
invoked.
- **`test_missing_doc_returns_not_found`** — missing-doc behaviour
unchanged.

Stub-loader pattern mirrors
`test/unit_test/api/apps/sdk/test_dify_retrieval.py` (added in #15028,
passing in CI).

## Provenance — how this fix was produced

This PR was authored against a small cited knowledge base committed in
the working tree as a `.vouch/` (see
[vouchdev/vouch](https://github.com/vouchdev/vouch)). The loop used
here:

1. **Grounding first.** Before reading the handler, queried the KB for
prior context: `vouch context "tenant scoped accessible authorization"`
→ retrieved a cited claim distilled from PR #15028 (which restored the
same `accessible()` check on `/dify/retrieval`). The retrieved rule:

> *ragflow REST endpoints that load by tenant-scoped id must call
`<Service>.accessible(id, tenant_id)` after `get_by_id` and before
storage/DB read; deny with code 109 'No authorization.' and log a
warning. Established by PR #15028.*

2. **Applied the pattern with a domain refinement.** For an API/JSON
endpoint, `No authorization.` is the right denial shape. For a
**byte-streaming, browser-facing** endpoint like `/preview`, leaking
*existence* itself enables enumeration — so per the issue's expected
behaviour, this PR denies with `Document not found!` (indistinguishable
from missing) instead. Same auth check, narrower response.

3. **Recorded the refinement back into the KB** as a new cited claim, so
the next IDOR-class issue starts already grounded in both the general
pattern and the byte-route nuance.

Net effect of the workflow: the fix replicates a known-good pattern
instead of reinventing it, *and* the place where the pattern was nuanced
is now retrievable for the next pass. Mechanism is fully independent of
this PR — it's not a runtime dependency, just process discipline.

Closes #15501
2026-06-04 09:58:26 +08:00
euvre
9a9d3ddf5f fix: show default embedding model when provider is not yet registered (#15511)
### What problem does this PR solve?

### Problem

On the Model Providers page, the Embedding Model dropdown in System
Model Settings shows empty (no default selected), even though a default
embedding model is configured in `service_conf.yaml`.

### Root Cause

Two issues were identified:

1. **Backend: `_get_model_info` fails for unregistered providers**
The tenant's `embd_id` is set to `bge-m3@xxxx` during initialization
(from the placeholder config `factory: 'xxxx'`). The `_get_model_info`
function requires the provider to exist in `tenant_model_provider`
table, but `xxxx` is never a real provider. Even after the user adds a
real provider (e.g., ZHIPU-AI), the stale `embd_id` still references the
non-existent one, causing the function to return `None`.

2. **Frontend: default models cache not invalidated after adding
provider**
`useAddProviderInstance` only invalidates `addedProviders` and
`allModels` caches after adding a provider instance, but does **not**
invalidate the `defaultModels` cache. This means the default model list
is not re-fetched until the user manually refreshes the page.

### Fix

**`api/apps/services/models_api_service.py`**

- Added `_resolve_model_from_tenant_providers()` helper: when the
default model's provider doesn't exist (e.g., placeholder `xxxx`), it
searches through the tenant's actually registered providers for a model
of the same type and returns the first match.
- When an instance name doesn't match (e.g., `"default"` vs actual name
`"1"`), the function now auto-resolves to the first real instance under
that provider.
- Falls back to `FACTORY_LLM_INFOS` validation when neither provider nor
instance exists.

**`web/src/hooks/use-llm-request.tsx`**

- Added `queryClient.invalidateQueries({ queryKey:
LlmKeys.defaultModels() })` to `useAddProviderInstance` so that the
default model list is re-fetched immediately after a provider instance
is added, eliminating the need for a manual page refresh.

### Testing

- Verified with a tenant whose `embd_id=bge-m3@xxxx` and only provider
is ZHIPU-AI (instance `1`): `_resolve_model_from_tenant_providers`
correctly resolves to `embedding-2@1@ZHIPU-AI`.
- After adding a provider via the UI, the embedding model dropdown now
immediately shows the resolved default without requiring a page refresh.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

---------

Signed-off-by: noob <yixiao121314@outlook.com>
2026-06-04 09:55:49 +08:00
Jack
67c3e73d70 feat: migrate DELETE /api/v1/datasets/:dataset_id/documents to Go (#15577)
## Summary

Migrate the batch document deletion endpoint from Python to Go. Two
modes supported: explicit `ids` list and `delete_all`.

## Changes

| File | Change |
|------|--------|
| `internal/dao/file2document.go` | Add `GetByDocumentID`,
`DeleteByDocumentID` |
| `internal/dao/file2document_test.go` | 5 new tests |
| `internal/dao/kb_test.go` | 2 new tests (`DecreaseDocumentNum`) |
| `internal/service/document.go` | Add `deleteDocumentFull` +
`DeleteDocuments`, refactor `DeleteDocument` |
| `internal/service/document_test.go` | 10 new tests |
| `internal/handler/document.go` | Add `documentServiceIface` +
`DeleteDocuments` handler |
| `internal/handler/document_test.go` | 7 new tests |
| `internal/router/router.go` | Register `DELETE /:dataset_id/documents`
|
| `cmd/server_main.go` | Support `RAGFLOW_DICT_PATH` env var |
| `internal/binding/rag_analyzer.go` | Use `-lpcre2-8` dynamic linking |
| `internal/dao/database.go` | Skip Error 1091/1138 during migration |
| `internal/service/llm.go` | Fix vet warning |

## Per-document cleanup

- Delete tasks from DB
- Hard-delete document + decrement KB counters
- Delete chunks from document engine (nil-guarded)
- Delete metadata from document engine (nil-guarded)
- Remove file2document mapping + file record + storage blob

## Test Results

**24 unit tests all passing** (7 DAO + 10 service + 7 handler) using
SQLite :memory: + gin.TestMode.

See [test report](docs/test_report_delete_documents.md) for manual
integration test results.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 20:55:53 +08:00
Haruko386
df55880b44 feat[Go] implement /connectors/google/oauth (#15584)
### What problem does this PR solve?

The following API is available in go

> /api/v1/connectors/google/oauth/web/start POST
> /api/v1/connectors/gmail/oauth/web/callback GET
> /api/v1/connectors/google-drive/oauth/web/callback GET
> /api/v1/connectors/google/oauth/web/result POST


### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-06-03 20:08:55 +08:00
Wang Qi
b946df8ba2 Fix: consolidate beta auth (#15581)
Fix: consolidate beta auth
2026-06-03 19:58:06 +08:00
bitloi
2eed0d4679 refactor(go-models): add unsupported model driver defaults (#15431)
### What problem does this PR solve?

Adds a shared safe default implementation for unsupported Go
model-driver capability methods and migrates the confirmed panic-stub
providers to use it.

The Go `ModelDriver` interface requires providers to implement many
capability methods even when the provider does not support them. XunFei
had unsupported capability methods implemented as `panic("implement
me")`, Mistral still had a panic in `ParseFile`, and HuaweiCloud carried
an unreachable `panic("implement me")` after a normal chat return.

### Type of change

- [x] Refactoring


Co-authored-by: Haruko386 <tryeverypossible@163.com>
2026-06-03 19:16:28 +08:00
bohdansolovie
ae316b3415 fix(api): guard document rename when linked file row is missing (#15536)
## Summary
Fixes #15534 — `update_document_name_only()` crashes with
`AttributeError` when `File2Document` exists but the linked `File` row
was deleted.

`update_document_name_only()` in `document_api_service.py` called
`FileService.get_by_id()` when a `File2Document` row existed, then
accessed `file.id` without checking the lookup result. An orphan
`File2Document` link (file deleted, mapping left behind) caused document
rename via `PATCH /api/v1/datasets/{dataset_id}/documents/{document_id}`
to return HTTP 500.

This PR mirrors guards used in `file2document_api.py` and
`file_api_service.py`: skip the optional file rename when the file is
missing, and still update the document record and search index.

## Changes
- `api/apps/services/document_api_service.py` — check `e and file`
before `FileService.update_by_id`
- `test/unit_test/api/apps/services/test_update_document_name_only.py` —
regression tests (orphan link + happy path)

## Test plan
- [x] `pytest
test/unit_test/api/apps/services/test_update_document_name_only.py -v`
- [ ] Manual: PATCH document `name` when `File2Document` points to a
non-existent `file_id` → 200, document/index renamed, no 500
2026-06-03 17:57:19 +08:00
Jin Hai
2061edd308 Remove unused codes (#15579)
### What problem does this PR solve?

Remove unused code.

### Type of change

- [x] Refactoring

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-03 17:35:36 +08:00
Jack
b363146997 refactor: overhaul task executor with layered architecture and comprehensive test suite (#15471)
## Summary

Decomposes the monolithic `task_executor.py` (1945 lines) into a 6-layer
architecture with clear separation of concerns. The refactored code is
functionally equivalent to the original, verified through 400 passing
tests and a production-vs-dry-run comparison framework.

## Architecture

```
entry (task_manager)
  └─ orchestration (task_handler)
       ├─ services (chunk_service, embedding_service, dataflow_service, raptor_service, post_processor)
       │    └─ utilities (chunk_builder, chunk_post_processor, embedding_utils)
       └─ infrastructure (task_context, recording_context, interceptor)
```

Key design decisions:
- **TaskContext** — typed facade over raw task dict, injects rate
limiters + callbacks via composition
- **RecordingContext + Comparator** — enables side-by-side production vs
dry-run execution for safe migration
- **NullRecordingContext** — zero-allocation no-op for production, uses
`__slots__`
- **WriteOperationInterceptor** — FIFO replay of previous runs function
returns for comparison mode

## Migration Strategy

The original `handle_task()` in `task_executor.py` uses a 3-way switch
via `TE_RUN_MODE`:
- `TE_RUN_MODE=0` (default) → runs refactored code
- `TE_RUN_MODE=1` → runs both original + refactored, compares all
intermediate results
- `TE_RUN_MODE=2` → runs original code (fallback)

The comparison mode (`TE_RUN_MODE=1`) records ~40 intermediate values
(chunks, vectors, token counts, func return values) from the production
run and replays them during dry-run, then uses `ContextComparator` to
report mismatches.

## Functional Equivalence Fixes

All divergences between original and refactored code were identified and
fixed:
- Timeout decorators (handle/build_chunks/raptor/embedding)
- NullRecordingContext leak in finally block causing RuntimeError
- MinIO None-binary check with proper FileNotFoundError
- Dataflow dispatch after embedding binding + init_kb
- Memory task missing return after processing
- RAPTOR checkpoint progress reporting
- Tag cache (get_tags_from_cache/set_tags_to_cache) restoration
- dataflow_id correction in _load_dsl
- Language default Chinese, dead code guard removal
- embed_chunks made async with proper thread_pool_exec
- Full GraphRAG default configuration (10 parameters)
- Hardcoded q_768_vec fallback removal in RAPTOR

## Test Changes

- 20 new tests covering table parser manual mode, tag cache, embedding
edge cases, RAPTOR checkpoint, dataflow_id correction, storage binary
None, cancel cleanup, metadata=None boundary
- Unified `make_task_context`/`make_task_dict` factories eliminated 10+
duplicated helpers
- DataflowService tests migrated from internal method mocks to IO
boundary mocks (real orchestration code executes)
- Parametrized duplicate build_chunks post-processor tests
- 7 raptor tests modernized to @pytest.mark.asyncio
- Mock count per test reduced through boundary-level mocking strategy

**Test count: 400 passing, 0 warnings, 0 skips**

## Files Changed

| File | Change |
|------|--------|
| `rag/svr/task_executor.py` | +1 line (NullRecordingContext fix) |
| `rag/svr/task_executor_refactor/task_handler.py` | Orchestration
layer, 8 logic fixes |
| `rag/svr/task_executor_refactor/chunk_service.py` | +timeout +
None-check |
| `rag/svr/task_executor_refactor/embedding_service.py` | sync→async
rewrite |
| `rag/svr/task_executor_refactor/dataflow_service.py` | dataflow_id fix
+ timeout |
| `rag/svr/task_executor_refactor/raptor_service.py` | checkpoint fix +
assert |
| `rag/svr/task_executor_refactor/chunk_post_processor.py` | tag cache
restore |
| `rag/svr/task_executor_refactor/task_context.py` | language default
fix |
| `test/.../conftest.py` | +294 lines shared helpers |
| `test/.../*.py` | 15 test files refactored, 20 new tests |

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 17:18:31 +08:00
Jin Hai
d736f358ba Go: refactor model provider (#15568)
### What problem does this PR solve?

1. Add license announcement
2. Add sanity check on API config
3. Add base class: BaseModel
4. Add GetBaseURL

### Type of change

- [x] Refactoring

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-03 16:33:58 +08:00
Wang Qi
d6fc50a469 Fix: no more @token_required (#15562)
Fix: no more @token_required
2026-06-03 16:24:08 +08:00
chanx
a678ed7b1f Fix: Switching pagesize on a chunk page did not reset the current page. (#15401)
### What problem does this PR solve?

Fix: Switching pagesize on a chunk page did not reset the current page.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-03 15:57:57 +08:00
Idriss Sbaaoui
1134769940 Chore: update cohere models (#15576)
### What problem does this PR solve?

remove old and add latest cohere models

### Type of change

- [x] Refactoring
- [x] Other (please describe): update models
2026-06-03 15:55:45 +08:00
Haruko386
473d06d1ad feat[Go]: implement add multi_models (#15563) 2026-06-03 15:26:46 +08:00
buua436
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
2026-06-03 15:05:39 +08:00
Lynn
ac3964b6bc Feat: display intl url for siliconflow and verify model provider without llms in json (#15550)
### What problem does this PR solve?

As title.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-06-03 14:43:08 +08:00
Jin Hai
dbebc66ba8 Go: refactor provider code (#15564)
Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-03 14:09:07 +08:00
Jin Hai
e1f19f6679 Go: fix gitee balance api (#15554)
```
RAGFlow(user)> create provider 'gitee' instance 'intl' key 'api-token' url 'https://ai.gitee.com/v1' region 'intl';
SUCCESS
```

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-06-03 13:23:20 +08:00
chanx
c41855da81 Fix: Model provider add verify and fixed form in modal not resetting issue (#15520)
### What problem does this PR solve?

Fix: Model provider add verify and fixed form in modal not resetting
issue

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-03 11:59:57 +08:00
buua436
76fc1d547f Refa: refine mysql migration version workflow (#15549)
### What problem does this PR solve?

refine mysql migration version workflow

### Type of change

- [x] Refactoring
2026-06-03 11:51:42 +08:00
bitloi
a75ea7ba7c Fix: Chat completion generation parameter overrides (#15389)
### What problem does this PR solve?

Closes #15388.

Chat completion routes did not reliably honor per-request generation
settings:

- `/api/v1/chat/completions` copied generation settings with a
truthiness check, so valid zero values such as `temperature: 0`, `top_p:
0`, `frequency_penalty: 0`, `presence_penalty: 0`, and `max_tokens: 0`
were dropped.
- `/api/v1/openai/{chat_id}/chat/completions` did not forward standard
generation settings into the request-specific dialog LLM settings before
calling `async_chat`.

This PR preserves explicitly supplied generation parameters, including
zero values, and merges request-level overrides into existing dialog
settings where appropriate.

The supported generation parameter keys and merge behavior live in a
shared REST API helper to keep both completion routes aligned.

Validation:

- `git diff --check`
- `python3 -m py_compile api/apps/restful_apis/_generation_params.py
api/apps/restful_apis/chat_api.py api/apps/restful_apis/openai_api.py
test/testcases/test_http_api/test_session_management/test_session_sdk_routes_unit.py`
- `uv run ruff check api/apps/restful_apis/_generation_params.py
api/apps/restful_apis/chat_api.py api/apps/restful_apis/openai_api.py
test/testcases/test_http_api/test_session_management/test_session_sdk_routes_unit.py`
- `ZHIPU_AI_API_KEY=dummy uv run pytest
test/testcases/test_http_api/test_session_management/test_session_sdk_routes_unit.py
-q -k generation_params`

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-03 11:46:10 +08:00
kpdev
76968af0ba Guard missing storage blobs on preview and image endpoints (#15366)
Fixes [#15365](https://github.com/infiniflow/ragflow/issues/15365) —
`get_document_image()` and document preview call `make_response(None)`
when storage returns no bytes, causing HTTP 500.
2026-06-03 11:33:03 +08:00
VictorECDSA
ff5971448b [Fix] naive: force-merge short markdown headers to prevent separate chunks (#15488)
## Problem

When uploading `.md` files with `parser=naive` and `delimiter="\n"`,
markdown headers (e.g., `## Quick Travel`) become separate chunks with
very short content (16-18 characters). This causes retrieval issues:
when the header is matched, the corresponding body text is not included
in the chunk.

## Related Issues

Closes #15487

## Checklist

- [x] Code changes are minimal and focused
- [x] Unit tests added (12/12 passed)
- [x] No breaking changes
2026-06-03 10:49:28 +08:00
Wang Qi
583daf47d5 Fix: model provider orders (#15524)
Fix: model provider orders
2026-06-03 10:17:12 +08:00
Hz_
9799f33549 GOCli check provider region (#15474)
## Summary
- add CLI command `CHECK PROVIDER 'provider_name' REGION 'region_name'
KEY 'api_key';`
  - route the command through CLI parser and command dispatcher
- call `GET /api/v1/providers/:provider_name/connection` with `region`
and `api_key`

  ## Testing
  - `go test ./internal/cli/...`
  - manually verified CLI command parsing and request flow
2026-06-02 19:34:25 +08:00
ちー
5f8926410d feat[Go]: implement /api/v1/connectors/<connector_id> PATCH (#15512)
### What problem does this PR solve?

As title, all test are passed

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-06-02 19:34:07 +08:00
Haruko386
9f969feb89 feat[Go] implement check connection by using apikey and region (#15475)
### What problem does this PR solve?

**Verified from PostMan**


GET http://127.0.0.1:9384/api/v1/providers/gitee/connection
```json
body: 

{
    "api_key": "XXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "region": "default"

}

resp: 
{
    "code": 0,
    "message": "success"
}
```

GET http://127.0.0.1:9384/api/v1/providers/gitee/connection
```json
body: 

{
    "api_key": "XXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "region": "deprecated"

}

resp: 
{
    "code": 0,
    "message": "success"
}
```

GET http://127.0.0.1:9384/api/v1/providers/gitee/connection
```json
body: 

{
    "api_key": "XXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "region": "china"

}

resp: 
{
    "code": 0,
    "message": "success"
}

```

GET http://127.0.0.1:9384/api/v1/providers/lmstudio/connection
```json
body: 

{
    "api_key": "",
    "region": "test"

}

resp: 
{
    "code": 0,
    "message": "success"
}
```


### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-06-02 19:32:41 +08:00
Lynn
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)
2026-06-02 19:04:20 +08:00
Wang Qi
d41373cfa9 Feature: Add the new anthropic and voyage models (#15516)
add the newanthropic and voyage models. Strip opus 4.7 and 4.8 of
certain usnspported keys

Co-authored-by: Idriss Sbaaoui <112825897+6ba3i@users.noreply.github.com>
2026-06-02 17:29:18 +08:00
Wang Qi
c990badda1 Feature: Add MiniMax M3 (#15513)
Feature: Add MiniMax M3
2026-06-02 17:28:48 +08:00
Alexander Laurent
a98889cd76 feat: add Go MCP server update API (#15261)
## What

#15240
implementation for PUT /api/v1/mcp/servers/:mcp_id

## Changes

- Adds the Go implementation for `PUT /api/v1/mcp/servers/:mcp_id`.
- Wires MCP service and handler into the Go server/router for the update
route.
- Preserves Python-style behavior for ownership checks, partial update
fields, MCP type/name/URL validation, `headers`/`variables`
normalization, and tool metadata scrubbing.
2026-06-02 15:58:44 +08:00
Dexterity
2819d0ea24 fix(go-models): use per call context timeouts so long streaming responses are not truncated (#15380)
### What problem does this PR solve?

Closes #15379 

Around 29 Go model providers in `internal/entity/models/` share an
`http.Client` configured with `Timeout: 120 * time.Second`, and reuse
that same client for `ChatStreamlyWithSender`. Go's
`http.Client.Timeout` is a hard ceiling on the whole request that also
covers reading the response body, so it behaves as a wall clock on
streaming. Any streamed chat response that lasts longer than 120 seconds
gets cut off in the middle with a timeout error. Long generations,
reasoning model outputs, and slow or overloaded upstreams are the common
victims.

The providers that already behave correctly (`groq`, `mistral`,
`voyage`, `anthropic`) set no client `Timeout` and instead wrap each
request in a `context.WithTimeout`. This change converges the affected
providers onto that same pattern.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

---------

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-06-02 15:27:26 +08:00
buua436
4018f02d96 Feat: mark mysql migrations as applied (#15504)
### What problem does this PR solve?

mark mysql migrations as applied

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-06-02 15:04:33 +08:00
glorydavid03023
5733e0624c fix(go-models): harden N1N default transport handling (#15351)
## Summary
- Harden `NewN1NModel` to avoid panics when `http.DefaultTransport` is a
custom non-`*http.Transport` RoundTripper.
- Fallback to a safe transport (`ProxyFromEnvironment`) while preserving
existing pooling/timeout settings.
- Add `n1n_test.go` with coverage for name/factory plus
`TestN1NNewModelWithCustomDefaultTransport`.


Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 13:40:10 +08:00
Hz_
1092f624fb fix: post /api/v1/system/tokens (#15410)
### What problem does this PR solve?

This PR aligns `POST /api/v1/system/tokens` in Go with the Python
implementation.

### Type of change

- Keep the token creation flow under the system API route.
- Preserve the owner-tenant authorization check.
- Generate and persist API tokens consistently with the current Go
service flow.
- Return the created token payload in the standard API response format.

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-06-02 13:39:07 +08:00
Lynn
3bc5ed282e Fix: model-provider bugs (#15460)
### What problem does this PR solve?

Fix:
- Use @ to avoid split  by `_` in model_name.
- Verify api_key when add instance.
- Pop api_key in list intances response.
- Remove useless index.
- Sort providers, instances and models by name.
- Get `is_tools` from llm_factories.json

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-02 13:24:53 +08:00
Haruko386
0e9eeb7b88 feat[Go] implement /api/v1/datasets/<dataset_id>/metadata/config (#15493)
### What problem does this PR solve?

implement /api/v1/datasets/<dataset_id>/metadata/config

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-06-02 13:24:28 +08:00
dripsmvcp
d4f1c2c95c fix(go-models): remove duplicate roundTripperFunc from novita_test.go (#15492)
Remove duplicated function
2026-06-02 13:23:39 +08:00
ちー
e4ef9834da fix: rewrite enable thinking mode for minimax (#15496)
### What problem does this PR solve?

fix the bad thinking mode for minimax

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-02 13:22:11 +08:00
Aeovy
600590cd18 Fix: disable thinking to avoid potential infinite loops in Qwen3.5/Qwen3.6 models (#15101)
### What problem does this PR solve?

This PR fixes the issue where Qwen3.5/Qwen3.6 series models may spend
excessive time on simple document-parsing tasks, such as Auto Metadata
extraction, keyword extraction, question generation, and image
description when using the MinerU parser.

For these tasks, Qwen3.5/Qwen3.6 models may perform unnecessary
reasoning by default, which can lead to very long response times, high
token consumption, and, in some cases, potential infinite output loops.

Since Qwen3.5/Qwen3.6 multimodal models are instantiated as `CvModel`
when configured as `image2text`, the existing `enable_thinking=False`
logic in `chat_model.py` does not apply to them. This PR adds the
corresponding handling for the CV/image-to-text model path as well.

This helps reduce unnecessary thinking time, avoid potential infinite
loops, and improve parsing efficiency without noticeably affecting
output quality for these simple extraction and image-description tasks.

Fixes #15083.
2026-06-02 13:21:35 +08:00
nickmopen
5b02fe4841 fix(api): stop duplicating answer in openai-compatible chat completions stream (#15286) (#15443)
### What problem does this PR solve?

Fixes #15286.

When calling `/api/v1/openai/<chat_id>/chat/completions` with `"stream":
true`, the response contains the answer **twice** — the final message
repeats everything that was already streamed.

#### Root cause

RAGFlow's `async_chat` streams the body as incremental `delta.content`
chunks, then emits a terminating `final` event whose `answer` is the
**complete** (decorated) message. The handler re-emitted that full
answer as one more `delta.content` chunk:

```python
if ans.get("final"):
    if ans.get("answer"):
        full_content = ans["answer"]
        response["choices"][0]["delta"]["content"] = full_content   # <-- whole answer again
        yield ...
```

So a client accumulating `delta.content` ends up with the message
duplicated.

#### Fix

Drop the re-emission. The complete answer from the `final` event is now
surfaced **only** through the trailing chunk's `final_content` and
`reference` fields, which matches OpenAI streaming semantics: deltas are
incremental, and the final chunk carries only `finish_reason` / `usage`
(plus RAGFlow's `reference` / `final_content` extensions).

This matches the expected behavior described in the issue: "The stream
should only yield content chunks once, and the final message should only
contain reference, usage, and finish_reason."

#### Testability refactor

The streaming SSE assembly was a closure inside the request handler, so
it could only be exercised against a live server + real LLM. I extracted
it into a module-level `_stream_chat_completion_sse` async generator
(behavior-preserving) so it can be unit-tested with a fake event stream.

#### Tests

Adds
`test/unit_test/api/apps/restful_apis/test_openai_stream_no_duplicate.py`
(same import-stub pattern as the existing `test_get_agent_session.py`):

- body is streamed exactly once (the regression);
- the complete answer is never re-emitted as a content chunk;
- the terminating chunk has `finish_reason="stop"`, `content=None`, and
correct `usage`;
- `final_content` / `reference` are present on the trailing chunk;
- reasoning (`think`) deltas stream separately and are not duplicated.

> Note: this is unrelated to #15442, which only changes the `stream`
default — it does not touch the duplication logic.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Added test cases

---------

Co-authored-by: Wang Qi <wangq8@outlook.com>
2026-06-02 13:20:40 +08:00
buua436
2e02bf7ba4 Fix: migrate legacy model id configs (#15495)
### What problem does this PR solve?

migrate legacy model id configs

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-02 13:08:58 +08:00
Julian
33ef724b5f Add Bulk action for linking Multiple Files to Datasets (#14960)
### What problem does this PR solve?

Feature: #14961 


### Type of change

- [x] New Feature (non-breaking change which adds functionality)
- [x] Refactoring
2026-06-02 12:23:33 +08:00
kpdev
0f6f7b3c3c fix(api): document image_id parsing for hyphenated thumbnail keys (#15115) (#15116)
### What problem does this PR solve?

Fixes #15115.

`GET /api/v1/documents/images/<image_id>` returned **Image not found**
when the thumbnail storage object key contained hyphens (e.g.
`page-1.png`). Document APIs build URLs as `{dataset_id}-{thumbnail}`,
but `get_document_image()` used `image_id.split("-")` and required
exactly two segments, so keys like `<kb_id>-page-1.png` were rejected
even though the blob existed.

This PR splits only on the first hyphen (`split("-", 1)`) and sets
`Content-Type` from the object key extension via `CONTENT_TYPE_MAP`
instead of hardcoding `image/JPEG`.
2026-06-02 10:54:14 +08:00
kpdev
a4bc066f74 fix(rag): id2image parsing for hyphenated storage object keys (#15117) (#15118)
### What problem does this PR solve?

Fixes #15117.

Chunk images are stored with `img_id = f"{bucket}-{objname}"` in
`image2id()` (`rag/utils/base64_image.py`). When loading via
`id2image()`, the code used `image_id.split("-")` and required exactly
two segments. Object keys that contain hyphens (e.g. `page-1.jpg`)
produce more than two segments, so `id2image` returns `None` and chunk
image previews fail even though the blob exists.

This is the same parsing issue as #15115 (HTTP thumbnail route); this PR
fixes the indexing/retrieval path.

### 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):

### Test plan

- [x] `pytest test/unit_test/rag/utils/test_base64_image.py`
- [ ] Manual: index a chunk with an `objname` containing hyphens and
confirm `img_id` resolves to an image in retrieval

Fixes #15117.
2026-06-02 10:52:51 +08:00
jony376
088d8448ae fix(migration): parameterize tenant_model_provider inserts in mysql_migration (#15313)
### Related issues
Closes #15312

### What problem does this PR solve?

`tools/scripts/mysql_migration.py` built batch INSERT SQL for the
`tenant_model_provider` stage using f-strings with raw `llm_factory` and
`tenant_id` values. If either value contained a single quote, migration
SQL could fail; this also created unnecessary SQL-injection risk in the
migration path.

This PR replaces string interpolation with parameterized SQL
placeholders in `TenantModelProviderStage.execute()`. The migration now
safely handles quoted values and executes deterministically across
existing tenant data.
2026-06-02 10:29:41 +08:00
Hernandez Avelino
09d0a17453 fix(api): handle array message content on OpenAI chat completions (#15359)
### Related issues

Closes #15358

<!-- After filing upstream, replace XXXX with your issue number. -->

---

### What problem does this PR solve?

`POST /api/v1/openai/<chat_id>/chat/completions` forwards `messages` to
`async_chat` without normalizing `content`. Downstream, `dialog_service`
assumes string content:

```python
re.sub(r"##\d+\$\$", "", m["content"])
```

OpenAI-compatible clients may send `content` as an **array** of parts
(text, `image_url`, etc.), including text-only arrays. That causes
`TypeError` and HTTP **500** instead of a valid response or a clear
**400**.

`openai_api.py` also reads `messages[-1]["content"]` directly for
`prompt` without handling list-shaped content.

This PR normalizes array `content` to a string (concatenating `type:
text` parts) before calling `async_chat`, matching a minimal
OpenAI-compat path. Image parts can be documented as unsupported or
handled in a follow-up if vision integration is required.
2026-06-02 10:27:03 +08:00
Jack
67a3ed7558 Fix auto metadata type issue (#15338)
### What problem does this PR solve?

Fix auto metadata type issue
https://github.com/infiniflow/ragflow/issues/15323

Type information is missing at frontend - backend correctly store the
type information for the auto metadata type.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-02 10:23:04 +08:00
Rene Arredondo
e1403171f1 fix(chat): sanitize NaN/Inf scores before serializing chat completions (#15245) (#15266)
## Summary

Fixes #15245 — `POST /api/v1/chat/completions` with `stream=true`
intermittently returns 500:

```
data:{"code": 500, "message": "failed to encode response: json:
unsupported value: NaN (status code: 500)", "data": {...}}
```

…even though "the same question" works on retry.

## Root cause

The streaming path serialized the answer with bare `json.dumps(...)`
(`api/apps/restful_apis/chat_api.py:1221`). `json.dumps` defaults to
`allow_nan=True` and emits the literal token `NaN` for NaN /
Infinity float values. That is valid Python-flavored JSON but
**invalid per RFC 8259**, so downstream consumers reject it. The
reporter's gateway is Go-based and the error wording
(`failed to encode response: json: unsupported value: NaN`) is
straight from Go's `encoding/json`.

How NaN gets into the payload: retrieval scoring in
`rag/nlp/search.py` runs `np.mean(...)` over aggregations that can
be empty, and similarity denominators can be zero. Reference chunk
fields like `similarity`, `vector_similarity`, `term_similarity`
can therefore be NaN depending on which chunks a given query
retrieves — which is exactly why the failure is intermittent for
the same question.

The non-streaming branch (`get_json_result(data=answer)`,
`chat_api.py:1243`) has the same vulnerability — Quart's `jsonify`
also defaults to `allow_nan=True` and the same retrieval pipeline
feeds both branches.

`agent/tools/exesql.py:88-102` already has the same NaN/Inf guard
for SQL results. This PR brings the chat completions path up to
parity.

## Fix

Add a small `_sanitize_json_floats(obj)` helper near the top of
`api/apps/restful_apis/chat_api.py`. It walks `dict` / `list` /
`tuple` and replaces any `float` that is `NaN` or `±Infinity` with
`None`. Apply it at the two serialization boundaries:

- **Streaming branch** (`stream()`): sanitize the SSE payload before
  `json.dumps`.
- **Non-streaming branch**: sanitize the `answer` dict before
  `get_json_result(data=...)`.

The terminal `data:True` frame and the `code:500` error frame carry
no scores and are left untouched.

Added `import math` to the existing alphabetical import block.

No change to retrieval logic — replacing NaN with `null` at the
serialization boundary is conservative: clients still parse the
JSON, a missing-score chunk is a strictly better failure mode than
a 500 that kills the whole reply.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-02 10:08:34 +08:00
nickmopen
bebf6ed244 fix(llm): strip non-generation keys from gen_conf for LiteLLM providers (#15427) (#15432)
### What problem does this PR solve?

Fixes #15427.

All LiteLLM-routed chats fail with:

- Anthropic: `litellm.BadRequestError: AnthropicException -
{"type":"invalid_request_error","message":"model_type: Extra inputs are
not permitted"}`
- OpenAI: `litellm.BadRequestError: OpenAIException - Unknown parameter:
'model_type'`

This is a regression from v0.25.4.

#### Root cause

A chat assistant's `llm_setting` is forwarded to the model as
`gen_conf`. `llm_setting` can legitimately carry RAGFlow-internal
metadata such as `model_type` (the chat REST APIs in
`api/apps/restful_apis/` read it back out of `llm_setting`), so that key
ends up inside `gen_conf`.

`Base._clean_conf` (OpenAI-compatible providers) already **whitelists**
the keys it forwards, so direct-OpenAI providers were unaffected.
`LiteLLMBase._clean_conf` only dropped `max_tokens` and passed
everything else straight through to `litellm.acompletion`, which
forwarded `model_type` to the upstream provider — and Anthropic / OpenAI
reject it. Because both Claude and GPT route through LiteLLM, every chat
broke.

#### Fix

- Extract the allowed-key set into a shared `ALLOWED_GEN_CONF_KEYS`
constant and reuse it in `Base._clean_conf`.
- Apply the same whitelist in `LiteLLMBase._clean_conf`, plus the
LiteLLM-specific reasoning params (`thinking`, `reasoning_effort`,
`extra_body`) that the model-family policies inject for reasoning
models.

This covers all four LiteLLM completion paths (`async_chat`,
`async_chat_streamly`, `async_chat_with_tools`,
`async_chat_streamly_with_tools`), since they all route through
`_clean_conf`.

#### Tests

Adds `test/unit_test/rag/llm/test_clean_conf_whitelist.py` covering both
backends: `model_type` (and other stray keys) are dropped, genuine
generation params and `thinking` survive, `max_tokens` is removed, and
the whitelist invariants hold.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Added test cases
2026-06-02 10:04:11 +08:00
buua436
eaa19bdb02 Fix:empty chat model fallback (#15477)
### What problem does this PR solve?

empty chat model fallback

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-02 10:00:57 +08:00
web-dev0521
1696d4ead6 feat(go-api): implement password-reset flow (issue #15282) (#15293)
## Summary

Ports the Python password-reset flow to Go, adding 4 unauthenticated
endpoints under `/api/v1/auth/password/`:

- `POST /auth/password/forgot/captcha` — generates and returns a PNG
captcha image; stores the plaintext code in Redis (60 s TTL)
- `POST /auth/password/forgot/otp` — verifies captcha, enforces resend
cooldown (60 s), generates HMAC-SHA256-hashed OTP (300 s TTL), sends
plain-text email via SMTP
- `POST /auth/password/forgot/otp/verify` — verifies OTP with attempt
counting (lock after 5 failures for 30 min), sets a
`otp:verified:{email}` flag (300 s TTL) on success
- `POST /auth/password/reset` — checks verified flag, decrypts +
validates passwords, updates user record, auto-logs in (issues JWT,
returns user profile)

Closes #15282
2026-06-02 09:38:02 +08:00
Alexander Laurent
1748723971 feat: add Go MCP server list API (#15253)
## What
#15240 
Implements `GET /api/v1/mcp/servers` in the Go API server.

## Changes

- Added MCP server DAO list query with tenant scoping.
- Added MCP service response wrapper.
- Added MCP handler for list request parsing and response formatting.
- Wired `GET /api/v1/mcp/servers` under authenticated `/api/v1` routes.
- Initialized MCP service and handler in the Go server startup.
- update_time and update_date now both map to update_date
- create_time and create_date now both map to create_date
- default ordering now returns create_date
## API Behavior

Matches the Python endpoint behavior:

- Requires authenticated user.
- Lists MCP servers for the current user tenant.
- Supports `keywords`.
- Supports `mcp_id` and repeated/comma-separated `mcp_ids`.
- Supports `page`, `page_size`, `orderby`, and `desc`.
- Returns:

```json
{
  "code": 0,
  "message": "success",
  "data": {
    "mcp_servers": [],
    "total": 0
  }
}
```
2026-06-02 09:37:05 +08:00
David Myriel
3aea80f5f5 docs: add Tigris as S3-compatible storage backend, fix s3 region field name (#15361)
## Summary

Add Tigris configuration to the Configuration and Backup & migration
pages, using the existing AWS_S3 backend — no code changes required.
Fix `region` → `region_name` in the existing S3 config example in
`backup_and_migration.md`. The code in `s3_conn.py` reads `region_name`,
so the previous field name was silently ignored.

##Context

With MinIO's open-source repository archived (#13840 on
infiniflow/ragflow), users need documented alternatives for object
storage. Tigris is S3-compatible and works with RAGFlow's existing
AWS_S3 backend out of the box.

## Changes

`configurations.md`: Added `### s3 (Tigris)` section after `### minio`,
matching the existing reference style. Includes config block, field
descriptions, and a pointer to `service_conf.yaml.template` for other
S3-compatible backends.
`backup_and_migration.md`: Added Tigris config block under single-bucket
mode. Fixed region → region_name in the existing S3 example. Added
Tigris to the supported backends list.

##Notes

No new files — edits to existing docs only.
Config field names (`access_key`, `secret_key`, `region_name`,
`endpoint_url`, `bucket`, `prefix_path`, `signature_version`,
`addressing_style`) verified against `rag/utils/s3_conn.py`.
2026-06-01 20:47:33 +08:00
writinwaters
c2597f132e Docs: Added a guide on how to ingest an RSS feed. (#15467)
### What problem does this PR solve?

Added a guide on how to ingest an RSS feed.

### Type of change

- [x] Documentation Update
2026-06-01 20:23:36 +08:00
monsterDavid
d398d617ca fix(mineru): skip page chrome blocks to prevent duplicate chunks (#15387)
## Summary
- Skip MinerU `header`, `footer`, and `page_number` blocks when
converting `content_list.json` into sections.
- Ignore unsupported block types explicitly so future MinerU output
types cannot re-emit the previous text block.

Fixes duplicate text in General/naive chunks when parsing PDFs via
MinerU (reported with repeated page headers and body text in slices).

Closes #15335

## Test plan
- [x] `pytest test/unit_test/deepdoc/parser/test_mineru_parser.py -v`
(4/4 passed)
2026-06-01 20:15:04 +08:00
oktofeesh
f0e4f2d5d8 fix(go-models): apply custom Google base URLs (#15385)
## Summary
- Add custom `base_url` support to the Google Go model driver.
- Preserve Google URL suffix configuration when creating custom base URL
driver instances.
- Validate Google chat/stream request inputs before constructing the SDK
client.
- Cover Google model listing, connection checks, base URL resolution,
and request validation with focused tests.

## What changed
- `GoogleModel.NewInstance` now returns a Google driver configured with
the supplied base URL map.
- Google SDK client creation now resolves configured base URLs through
`genai.HTTPOptions.BaseURL`.
- Base URL lookup supports configured regions, empty-region keys, and
`default` fallback.
- Google chat, streaming chat, embeddings, and model listing now reject
blank API keys before creating SDK clients.
- Google chat and streaming chat now reject blank model names locally,
and streaming chat rejects a nil sender.
- Existing message handling, embeddings, pagination, and provider errors
are preserved.

## Why
Google custom model instances could not use configured base URLs because
`NewInstance` returned `nil` and the SDK client path ignored the driver
base URL map. The request validation keeps invalid Google calls from
reaching SDK client construction with blank credentials or incomplete
chat inputs.
2026-06-01 19:24:29 +08:00
euvre
fb3bd3de02 fix(deepdoc): add English caption patterns to fix missing figure/table numbering (#15481)
### What problem does this PR solve?
## Problem

When parsing PDFs containing English figure/table captions (e.g. "Fig.
20", "Figure 20", "Table 20"), the `is_caption` method in
`TableStructureRecognizer` failed to recognize them as captions. This
caused figure numbering gaps in the parsed output (e.g. Fig. 19 → Fig.
21, skipping Fig. 20).

## Root Cause

The `is_caption` regex only matched Chinese caption formats:

```python
patt = [r"[图表]+[ 0-9::]{2,}"]
```

When the layout recognizer also failed to assign a `caption` layout type
to a given text block, English captions were entirely missed.

## Fix

Added three case-insensitive English caption patterns to `is_caption` in
`deepdoc/vision/table_structure_recognizer.py`:

- `(?i)Fig\.?\s*\d+` — matches `Fig. 20`, `Fig 20`, `FIG. 20`, etc.
- `(?i)Figure\s+\d+` — matches `Figure 20`, `FIGURE 20`, etc.
- `(?i)Table\s+\d+` — matches `Table 20`, `TABLE 20`, etc.

## Files Changed

- `deepdoc/vision/table_structure_recognizer.py` — extended `is_caption`
regex patterns


- [x] Bug Fix (non-breaking change which fixes an issue)

Signed-off-by: noob <yixiao121314@outlook.com>
2026-06-01 19:22:11 +08:00
Wang Qi
1a6df01b53 Bug fix: Enhance embeding model to give better error message (#15346)
To resolve https://github.com/infiniflow/ragflow/issues/15343 enhance
the model embedding message to give extact failure message to customer.


# QWen

## Retrieval
<img width="3321" height="1033" alt="image"
src="https://github.com/user-attachments/assets/6b82921a-a3a7-4a33-a383-1cf316398ee2"
/>

## Chat
<img width="2241" height="311" alt="image"
src="https://github.com/user-attachments/assets/ec311365-62d5-407a-8915-5c8d72be9716"
/>


# SiliconFlow
## Retrieval
<img width="3321" height="1033" alt="image"
src="https://github.com/user-attachments/assets/ee2cd191-a27d-4729-b53d-2fbdb4e352cd"
/>

## Chat
<img width="1562" height="210" alt="image"
src="https://github.com/user-attachments/assets/10376a8e-a3f4-422f-bc2e-96f2a8a96448"
/>

# Baichuan
## Retrieval
<img width="3321" height="1107" alt="image"
src="https://github.com/user-attachments/assets/dcb5409d-f7fc-4804-b186-5e1ee11e09c4"
/>

## Chat
<img width="2241" height="311" alt="image"
src="https://github.com/user-attachments/assets/ec311365-62d5-407a-8915-5c8d72be9716"
/>


# Zhipu
zhipu is good.
2026-06-01 19:18:16 +08:00
kpdev
252cc19f93 Infer Content-Type for document image endpoint (#15368)
## Summary

Fixes [#15367](https://github.com/infiniflow/ragflow/issues/15367) —
`GET /api/v1/documents/images/<image_id>` always returned `Content-Type:
image/JPEG` even for PNG/WebP chunk images and extensioned thumbnails.

## Related Issue

Fixes #15367

## Change Type

- [x] Bug fix
- [x] Regression tests
- [ ] New feature
- [ ] Refactor

## What Changed

- Added `_detect_image_content_type_from_bytes()` —
PNG/JPEG/GIF/WebP/BMP magic-byte detection
- Added `_content_type_for_document_image()` — object-key extension via
`CONTENT_TYPE_MAP`, then magic bytes, else `application/octet-stream`
- **`get_document_image()`** — set inferred `Content-Type` instead of
hardcoded `image/JPEG`
- Also guards missing storage blob (`Image not found.`) to avoid
`make_response(None)` (same handler; complements #15365)

## Files Changed

| File | Change |
|------|--------|
| `api/apps/restful_apis/document_api.py` | MIME inference helpers +
handler update |
|
`test/testcases/test_web_api/test_document_app/test_document_metadata.py`
| 3 unit tests |

## Validation

```bash
cd /root/gittensor/ragflow
pytest test/testcases/test_web_api/test_document_app/test_document_metadata.py::TestDocumentMetadataUnit::test_get_document_image_content_type_from_object_extension_unit -v
pytest test/testcases/test_web_api/test_document_app/test_document_metadata.py::TestDocumentMetadataUnit::test_get_document_image_content_type_from_magic_bytes_unit -v
pytest test/testcases/test_web_api/test_document_app/test_document_metadata.py::TestDocumentMetadataUnit::test_get_document_image_missing_blob_unit -v
```

## Test Plan

- [x] `.png` object key → `image/png`
- [x] Extensionless chunk key + PNG bytes → `image/png` (magic bytes)
- [x] Missing blob → 4xx `"Image not found."`
- [ ] CI green
2026-06-01 19:08:32 +08:00
kpdev
b35266e9a5 Return 4xx when file download storage blob is missing (#15371)
## Summary

Fixes [#15369](https://github.com/infiniflow/ragflow/issues/15369) —
`GET /api/v1/files/<file_id>` calls `make_response(None)` when both
primary and fallback storage lookups return empty, causing HTTP 500.

## Related Issue

Fixes #15369

## Change Type

- [x] Bug fix
- [x] Regression tests

## What Changed

- **`file_api.download()`** — after fallback `STORAGE_IMPL.get`, return
`get_error_data_result(message="This file is empty.")` when `not blob`,
matching document REST download semantics.

## Files Changed

| File | Change |
|------|--------|
| `api/apps/restful_apis/file_api.py` | Empty-blob guard before
`make_response()` |
| `test/testcases/test_web_api/test_file_app/test_file_routes_unit.py` |
Regression test |

## Validation

```bash
cd /root/gittensor/ragflow
pytest test/testcases/test_web_api/test_file_app/test_file_routes_unit.py::test_download_missing_blob_returns_error -v
pytest test/testcases/test_web_api/test_file_app/test_file_routes_unit.py::test_download_falls_back_to_document_storage -v
```

## Test Plan

- [x] Both storage paths empty → `"This file is empty."` (no
`make_response(None)`)
- [x] Existing fallback success test still passes
- [ ] CI green
2026-06-01 19:08:06 +08:00
balibabu
f194e8b4c4 Fix: The newly added model did not appear in the drop-down menu. (#15476)
### What problem does this PR solve?

Fix: The newly added model did not appear in the drop-down menu.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-01 17:56:41 +08:00
euvre
1e80419c21 fix: restore TitleChunker output for json/chunks upstream formats (#15396)
fix: restore TitleChunker output for json/chunks upstream formats

## Summary

The refactor commit e194027b (#14247) introduced two regressions that
caused `TitleChunker` to produce zero chunks when the upstream Parser
node outputs `json` or `chunks` format (e.g. PDF parsing).

## Root Cause

### 1. Dead code in `extract_line_records` (critical)

After refactor, when `payload` is `None` (which is the case for `json`
and `chunks` output formats), the method returns an empty list
immediately via `return []`, so no records are ever extracted from
structured upstream output. The original `json`/`chunks` handling code
became unreachable dead code.

### 2. Unconditional overwrite in `build_chunks_from_record_groups`

The `chunks` variable assigned in the `if` branch for markdown/text/html
formats was unconditionally overwritten by the statement below it, due
to a missing `else` keyword.

## Fix

- Remove the premature `return []` so the `json`/`chunks` branch is
reachable again.
- Add `else` branch in `build_chunks_from_record_groups` so the two
format families are handled independently.

## Test Plan

- [x] Verified no lint errors on the changed file
- [ ] Tested with a PDF document parsed via DeepDOC → TitleChunker
pipeline
- [ ] Tested with markdown input through TitleChunker
- [ ] Tested hierarchy and group chunking modes

## Impact

- Fixes the regression where documents parsed with `json`/`chunks`
output format produced no chunks from `TitleChunker`.
- No API or configuration changes. Fully backward compatible.

Signed-off-by: noob <yixiao121314@outlook.com>
2026-06-01 17:14:22 +08:00
balibabu
82202fa469 Fix: Unable to create dataset (#15472)
### What problem does this PR solve?

Fix: Unable to create dataset

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-01 15:30:52 +08:00
Wang Qi
10e8690890 GraphRAG - NER - spacy - fix spacy extraction (#14783)
Fix spacy extraction
2026-06-01 13:05:54 +08:00
sxxtony
12579dbc3d Go: implement dataset ingestion log APIs (#15421)
### What problem does this PR solve?

Part of the Python → Go API server rewrite tracked in #15240 (Dataset
ingestion section). This PR implements the three dataset ingestion
endpoints in the Go API server, mirroring the existing Python
`dataset_api_service` behaviour:

- `GET /api/v1/datasets/<dataset_id>/ingestions/summary`
- `GET /api/v1/datasets/<dataset_id>/ingestions`
- `GET /api/v1/datasets/<dataset_id>/ingestions/<log_id>`

### Type of change

- [x] Refactoring
- [x] New Feature (non-breaking change which adds functionality)

Co-authored-by: sxxtony <sxxtony@users.noreply.github.com>
2026-06-01 11:23:44 +08:00
glorydavid03023
3774916060 Go: implement Embed in GPUStack driver (#15182)
### What problem does this PR solve?

The Go GPUStack driver returned a stub error for `Embed()` even though
GPUStack exposes OpenAI-compatible embeddings on the **v1-openai** route
(not `v1/embeddings`).

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-06-01 11:22:43 +08:00
Haruko386
2d7044b57e feat[Go] implement api/v1/thumbnails API (#15416)
### What problem does this PR solve?

As title

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality
2026-06-01 11:22:08 +08:00
Idriss Sbaaoui
da1ed6f0e7 Feat: add new tests and tescases for restful api suite (#15347)
### What problem does this PR solve?

extend restful api suite

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
- [x] Other (please describe): test
2026-06-01 11:02:40 +08:00
Wang Qi
4972af4367 Fix memory empty issue (#15411)
Fix memory empty issue
2026-06-01 10:25:56 +08:00
balibabu
e13431cdc0 Fix: If the filename is too long, it overflows the confirmation box for deleting the file. (#15287)
### What problem does this PR solve?

Fix: If the filename is too long, it overflows the confirmation box for
deleting the file.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-01 10:22:56 +08:00
web-dev0521
cd18cfab79 feat(connector): implement Outlook data source connector (issue #15332) (#15333)
### What problem does this PR solve?

Closes #15332.

RAGFlow can index Gmail and generic IMAP mailboxes but had no native
connector for Outlook / Microsoft 365 mail. Organisations on Microsoft
365 had no way to bring mailbox content into a knowledge base through
Microsoft Graph.

This PR adds a net-new Outlook data source that:

- Authenticates against Microsoft Graph with the same MSAL
client-credentials flow already used by the SharePoint and Teams
  connectors (no new auth primitives).
- Pages over `/users/{id}/mailFolders/{folder}/messages/delta` per
mailbox and persists `@odata.deltaLink` values in
`OutlookCheckpoint.delta_links`, so incremental syncs only fetch changed
messages.
- Supports two scoping modes:
- **Tenant-wide** (default): enumerates every user in the tenant via
`/users` and syncs each mailbox. Requires `User.Read.All`.
- **Targeted**: when `user_ids` is provided (comma-separated UPNs or
object IDs), only those mailboxes are synced. `User.Read.All` is not
needed in this mode.
- Lets the caller pick the mail folder (`inbox`, `sentitems`, `archive`,
...). Defaults to `inbox`.
- Maps each message to a `Document` shaped after the Gmail connector:
one `TextSection` carrying `From/To/Cc/Subject` headers + body, with
HTML bodies stripped to text inline (no extra dependency).
- Surfaces typed errors on the validation probe:
401 → `ConnectorMissingCredentialError`, 403 →
`InsufficientPermissionsError` (with `Mail.Read` / `User.Read.All`
hint), 404 on a configured mailbox → `ConnectorValidationError`, 5xx →
`UnexpectedValidationError`.
- Skips messages flagged `@removed` by the delta semantics and messages
whose `receivedDateTime` is older than `poll_range_start`.

#### Files

| File | Change |
|------|--------|
| `common/data_source/outlook_connector.py` | **New** —
`OutlookConnector` (`CheckpointedConnectorWithPermSync` +
`SlimConnectorWithPermSync`) + `OutlookCheckpoint` + tiny `_strip_html`
helper. |
| `common/data_source/config.py` | `DocumentSource.OUTLOOK = "outlook"`.
|
| `common/constants.py` | `FileSource.OUTLOOK = "outlook"`. |
| `common/data_source/__init__.py` | Export `OutlookConnector`. |
| `rag/svr/sync_data_source.py` | `Outlook(SyncBase)` with `batch_size`
normalisation, CSV/list parsing of `user_ids`; registered in
`func_factory`. |
| `web/src/pages/user-setting/data-source/constant/index.tsx` |
`DataSourceKey.OUTLOOK`, visibility map (`syncDeletedFiles: true`), info
entry, form fields (tenant_id, client_id, client_secret, folder,
user_ids, batch_size), default values. |
| `web/src/locales/en.ts`, `web/src/locales/zh.ts` |
`outlookDescription` + 5 tooltip keys (EN + ZH). |
| `test/unit_test/data_source/test_outlook_connector_unit.py` | **New**
— 19 unit tests (`p1`/`p2`/`p3`) covering auth, validation (tenant-wide
vs specific user vs error paths), checkpoint helpers, user enumeration
pagination, message filtering, HTML body stripping. |

#### Required Azure AD permissions

- `Mail.Read` (Application, admin-granted) — always.
- `User.Read.All` (Application, admin-granted) — only when `user_ids` is
left blank so the connector can enumerate mailboxes.

#### Out of scope

- **Attachment indexing.** The current connector emits message body +
headers; binary attachments are flagged via `metadata.has_attachments`
but not pulled. Adding attachment hydration is straightforward but
scoped out per the issue's "decide whether attachments are indexed in
the first version" note.
- **Delegated (per-user) OAuth.** The connector uses app-only
credentials, consistent with the SharePoint / Teams precedent in this
codebase.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-05-29 21:52:29 +08:00
Rintaro
11af34a895 fix(opensearch): repair document-metadata path broken by #14577 (#15393)
### What problem does this PR solve?

Document metadata is completely broken on the OpenSearch backend
(`DOC_ENGINE=opensearch`). Both failures were introduced by #14577,
which added
a doc-metadata dispatch surface but only validated it against
Elasticsearch.

**1. Index creation rejected (`mapper_parsing_exception`).**
`OSConnection.create_doc_meta_idx` feeds `conf/doc_meta_es_mapping.json`
verbatim to OpenSearch. That file declares a top-level `"dynamic":
"runtime"`.
Runtime fields are Elasticsearch-only; OpenSearch cannot parse the
value:

mapper_parsing_exception: Could not convert [dynamic.dynamic] to boolean
(400)

**2. `search()` signature mismatch (`TypeError`).**
`DocMetadataService` (added by #14577) calls `docStoreConn.search(...)`
with
snake_case kwargs (`select_fields=`, `index_names=`,
`knowledgebase_ids=`, …),
matching `ESConnection.search`. But `OSConnection.search` still uses
camelCase
parameters (`selectFields`, `indexNames`, `knowledgebaseIds`, …):

TypeError: OSConnection.search() got an unexpected keyword argument
'select_fields'

The UI then shows "0 fields" for every document on OpenSearch.

### Fix

1. In `OSConnection.create_doc_meta_idx`, normalize a top-level
`"dynamic": "runtime"` to `True` **for the OpenSearch request only**.
The
shared mapping file is left untouched, so the Elasticsearch backend
keeps its
runtime-field behavior. Dynamic field discovery is preserved on
OpenSearch.
2. Rename the `OSConnection.search()` parameters (and their in-method
local
uses) from camelCase to snake_case so they match `ESConnection.search()`
and
the `DocMetadataService` call sites. The change is confined to
`search()`;
`get/insert/update/delete` keep their existing positional signatures
(they
   are called positionally from `rag/nlp/search.py`).

### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)

### Affected backends
OpenSearch only. Elasticsearch, Infinity and OceanBase are untouched.

### How to reproduce
1. `DOC_ENGINE=opensearch`, restart the stack.
2. Upload/parse a document, then open the dataset's document list / set
metadata.
- Before: index creation 400s (`Could not convert [dynamic.dynamic]`),
and/or
     `TypeError ... 'select_fields'`; document metadata shows 0 fields.

### Risk & backward compatibility
- ES default deployment: no change. `doc_meta_es_mapping.json` is not
modified,
  so ES still receives `"dynamic": "runtime"`.
- `search()` rename is internal; the only kwarg caller
(`DocMetadataService`)
  already uses the snake_case names this PR aligns to.

### Test plan
- [ ] `DOC_ENGINE=opensearch`: per-tenant `ragflow_doc_meta_*` index is
created
(no `mapper_parsing_exception`); document metadata reads/writes work.
- [ ] `DOC_ENGINE=elasticsearch` regression: doc-meta index still
created with
      runtime mapping; metadata unchanged.
2026-05-29 21:49:36 +08:00
Rintaro
3dfc16973c fix(opensearch): implement get_scores for KNN second-pass scoring (#15390)
### What problem does this PR solve?

On the OpenSearch backend (`DOC_ENGINE=opensearch`), every retrieval
that
performs the KNN second-pass scoring crashes with:

    AttributeError: 'OSConnection' object has no attribute 'get_scores'

**Root cause.** #14970 ("Refactor: Drop the vector fetch for ES") added
a
`get_scores()` helper to `ESConnectionBase`
(`common/doc_store/es_conn_base.py`)
and introduced `Dealer._knn_scores()` in `rag/nlp/search.py`, which
calls
`self.dataStore.get_scores(res)`. `search.py` routes Infinity and
OceanBase to
their own similarity paths via `DOC_ENGINE_INFINITY` /
`DOC_ENGINE_OCEANBASE`,
but OpenSearch sets neither flag, so it falls into the Elasticsearch
branch and
calls `get_scores`. `OSConnection` (which subclasses
`DocStoreConnection`
directly, not `ESConnectionBase`) never received that method, so any
vector-search hit triggers the crash. It reproduces with any normal
embedding
(e.g. 1024-dim mistral-embed) as soon as a KNN query returns hits.

### Fix

Add `OSConnection.get_scores()`, mirroring
`ESConnectionBase.get_scores()`.
OpenSearch hit headers expose `_score` exactly like Elasticsearch (the
existing
`OSConnection.__getSource` already reads `d["_score"]`), so the
implementation
is identical.

Scope note: Infinity and OceanBase deliberately do not use `get_scores`
(#14970 routes them elsewhere), so this fix is intentionally limited to
the
OpenSearch backend, which is the only one reaching the ES KNN-score
path.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

### Affected backends
OpenSearch only. Elasticsearch already implements `get_scores`; Infinity
/
OceanBase are routed away from it.

### How to reproduce
1. `DOC_ENGINE=opensearch` (docker `.env`), restart the stack.
2. Create a knowledge base with any dense embedding model and parse a
document.
3. Run a retrieval / chat over that KB -> 500 with the AttributeError
above.

### Risk & backward compatibility
None for the default Elasticsearch deployment -- the change only adds a
method
to `OSConnection`. No default values or ES/Infinity/OceanBase behavior
change.

### Test plan
- [ ] With `DOC_ENGINE=opensearch`, retrieval over a KB returns scored
chunks
      (no AttributeError).
- [ ] `DOC_ENGINE=elasticsearch` regression: retrieval unchanged.
- [ ] Empty-result path: `_knn_scores` early-returns `{}` (guarded),
get_scores
      handles an empty `hits` list gracefully.
2026-05-29 21:49:15 +08:00
jony376
a2500fed43 fix(api): move dify retrieval health check to /dify/retrieval/health (#15311)
### Related issues
Closes #15310

### What problem does this PR solve?

`/api/v1/dify/retrieval` had duplicate `GET` route registrations in
`dify_retrieval_api.py`: one for authenticated retrieval and another for
unauthenticated health checks. Sharing the same path and method created
ambiguous routing behavior and an unstable API contract for Dify
external knowledge base integration.

This PR separates concerns by moving the health-check endpoint to `GET
/api/v1/dify/retrieval/health`, while keeping retrieval on
`/api/v1/dify/retrieval`. This makes auth behavior deterministic and
prevents route shadowing/conflicts.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-05-29 21:47:55 +08:00
OrbisAI Security
b4c8711d51 fix: upgrade crawl4ai to 0.8.0 (CVE-2026-26217) (#15415)
## Summary
Upgrade crawl4ai from 0.7.6 to 0.8.0 to fix CVE-2026-26217.

## Vulnerability
| Field | Value |
|-------|-------|
| **ID** | CVE-2026-26217 |
| **Severity** | CRITICAL |
| **Scanner** | trivy |
| **Rule** | `CVE-2026-26217` |
| **File** | `uv.lock` |
| **Assessment** | Likely exploitable |

**Description**: Crawl4AI Has Local File Inclusion in Docker API via
file:// URLs

## Evidence

**Scanner confirmation**: trivy rule `CVE-2026-26217` flagged this
pattern.

**Production code**: This file is in the production codebase, not
test-only code.

## Threat Model Context

This is a web service - vulnerabilities in request handlers are directly
exploitable by remote attackers.

## Changes
- `pyproject.toml`
- `uv.lock`

## Verification
- [x] Build passes
- [x] Scanner re-scan confirms fix
- [x] LLM code review passed

---
*This change addresses a pattern flagged by static analysis. The code
path handles user-influenced input and the fix reduces the attack
surface against both manual and automated exploitation.*

---
*Automated security fix by [OrbisAI Security](https://orbisappsec.com)*
2026-05-29 21:38:41 +08:00
Attili-sys
a28a0c6986 File addition .rooignore (#15414)
This PR introduces a `.rooignore` file to the root of the repository to
optimize how AI coding assistants (like Roo) interact with the RAGFlow
codebase.

Currently, when AI agents index the workspace, they can waste tokens and
processing time reading through generated files, caches, large
dependency artifacts, and runtime logs. This `.rooignore` file provides
a standard configuration to exclude these irrelevant directories and
files (such as `.venv/`, `node_modules/`, `__pycache__/`, logs, and
large binaries). This significantly reduces indexing noise, prevents
accidental reads of sensitive or bulky local data, and ensures AI coding
agents remain focused strictly on relevant source code.

### Type of change

- [x] Other (please describe): Developer Experience (DX) / AI Tooling
configuration
2026-05-29 20:37:44 +08:00
Hz_
539d38bc20 fix: backfill missing api token beta values (#15405)
### What problem does this PR solve?

This PR updates `SystemService.ListAPITokens` to lazily backfill missing
`beta` values for API tokens, matching the Python behavior of
`/api/v1/system/tokens`.

### Type of change
  
  - When an API token has an empty `beta`, generate a new one.
  - Persist the generated `beta` back to the `api_token` table.
  - Keep the handler/routing unchanged.
- `GET /api/v1/system/tokens` now returns tokens with `beta` filled in
for older records that were missing it.
  - This aligns Go behavior with the Python implementation.
2026-05-29 20:04:10 +08:00
oktofeesh
be28177955 fix(go-models): harden Hunyuan embedding validation (#15249)
## Summary
- Validate Hunyuan embedding model name and API key before building
requests.
- Reuse region-aware base URL validation for embedding requests.
- Replace the stale unsupported Embed test with happy-path and
validation coverage.

## What changed
- Added early Hunyuan Embed validation for missing model names and API
keys.
- Routed Embed through the same base URL region guard used by the other
Hunyuan methods.
- Updated Hunyuan tests to configure the embedding suffix and cover
Embed success plus invalid inputs.

## Why
Hunyuan Embed is implemented, but the existing test still expected it to
be unsupported and could panic before returning a normal validation
error. This keeps the implemented embedding path aligned with the
current driver behavior and prevents nil input panics.

Closes #15087
Refs #14736
2026-05-29 19:50:01 +08:00
galuis116
d1f6594618 Fix: JWT algorithm-confusion in OIDC ID token verification (#15181)
### What problem does this PR solve?

Closes #15180.

`OIDCClient.parse_id_token` in `api/apps/auth/oidc.py` read the JWT
signing
algorithm from the **unverified** JWT header and passed it through to
`jwt.decode(..., algorithms=[alg], ...)` as the trust anchor. This is
the
textbook JWT algorithm-confusion vulnerability (CWE-345 / CWE-347). Any
unauthenticated client capable of reaching the OIDC callback could take
over
an arbitrary account on any RAGFlow deployment with OIDC login enabled:

1. **`alg: "none"`** — present a JWT with `{"alg": "none"}` and no
   signature segment → `jwt.decode(..., algorithms=["none"])` → PyJWT's
   `NoneAlgorithm` accepts the token without verification → login as any
   user.
2. **RSA / HMAC confusion** — fetch the public RSA key from the
provider's
   JWKS (it's public), forge a JWT with `{"alg": "HS256"}` HMAC-signed
   using the public-key bytes as the secret → `jwt.decode(...,
   algorithms=["HS256"], key=public_key)` → verifier accepts → login as
   any user. (Modern PyJWT independently refuses to use a PEM-formatted
   key as an HMAC secret, which mitigates this leg for PEM key formats;
the fix here is the only mitigation for raw / DER / JWK octet keys and
   for older PyJWT versions.)

### What changed

**`api/apps/auth/oidc.py`:**

- New module constants `_ALLOWED_OIDC_SIGNING_ALGS` (asymmetric-only:
  `RS*`, `ES*`, `PS*`, `EdDSA` — explicitly excludes `none` and `HS*`)
  and `_DEFAULT_OIDC_SIGNING_ALGS = ("RS256",)` (the OIDC Core 1.0 §2
  spec default).
- New helper `_resolve_id_token_signing_algs(metadata)` — intersects the
  provider's advertised `id_token_signing_alg_values_supported` from
`/.well-known/openid-configuration` with the safe allowlist; falls back
  to RS256 when the field is missing or contains only unsafe values.
- `OIDCClient.__init__` now stores the resolved allowlist on
  `self.id_token_signing_algs` — pinned once, from a trusted source, at
  construction time.
- `parse_id_token` no longer calls `jwt.get_unverified_header` and no
  longer reads `alg` from the JWT header. It passes
  `self.id_token_signing_algs` to `jwt.decode(..., algorithms=...)`.
  `PyJWKClient.get_signing_key_from_jwt` still reads the `kid` from the
  header internally for JWKS lookup — that's fine, `kid` is not a
  security decision; the signature still proves which key was actually
  used.


**`test/testcases/test_web_api/test_auth_app/test_oidc_client_unit.py`:**

- Existing `test_parse_id_token_success_and_error` drops its
`jwt.get_unverified_header` mock (no longer called by `parse_id_token`).
- `_metadata` and `_make_client` helpers grew an optional `signing_algs`
parameter so tests can configure what the discovery document advertises.
- New `TestSSRFValidation` / algorithm-confusion regression block (7
  tests):
  - `test_id_token_signing_algs_default_to_rs256_when_metadata_missing`
  - `test_id_token_signing_algs_intersect_metadata_with_safe_allowlist`
  - `test_id_token_signing_algs_fall_back_when_only_unsafe_advertised`
  - `test_id_token_signing_algs_ignores_non_string_entries`
  - `test_id_token_signing_algs_handles_non_list_metadata_field`
  - `test_parse_id_token_passes_pinned_algorithms_to_jwt_decode` —
    sabotages `jwt.get_unverified_header` to raise on call, proving the
    verification path never consults the unverified header.
- `test_parse_id_token_rejects_alg_none` — uses real PyJWT to encode an
    `alg: "none"` token; `parse_id_token` raises `ValueError("Error
    parsing ID Token: …")` instead of accepting it.
  - `test_parse_id_token_rejects_hs256_when_allowlist_is_asymmetric` —
    uses real PyJWT to forge an `alg: "HS256"` token with a non-PEM
    shared secret (so PyJWT's incidental PEM-as-HMAC refusal isn't what
    blocks it); `parse_id_token` raises because `HS256` is not in the
    pinned allowlist.

Sanity-checked end-to-end with real PyJWT outside the project test
runner:

- `alg=none` forged token + `algorithms=["RS256"]` →
`InvalidAlgorithmError` ✓
- `alg=HS256` forged token + `algorithms=["RS256"]` →
`InvalidAlgorithmError` ✓
- Same `alg=HS256` token + `algorithms=["HS256"]` → **accepted**
({'sub': 'admin'})
  — confirming the attack path was real before the fix.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

Co-authored-by: galuis116 <contact@duerrimports.com>
2026-05-29 19:37:01 +08:00
kpdev
cb1ea5a47f Validate chunk image_base64 before doc-store write (#15364)
## Summary

Fixes [#15363](https://github.com/infiniflow/ragflow/issues/15363) —
`add_chunk` / `update_chunk` indexed chunks with `image_id` before
validating or storing `image_base64`, leaving orphan chunks on invalid
input.

## Related Issue

Fixes #15363

## Change Type

- [x] Bug fix
- [x] Regression tests

## What Changed

- Added `_decode_chunk_image_base64()` — strict base64 decode with
structured 4xx errors
- Added `_store_chunk_image_or_error()` — catches `store_chunk_image`
failures
- **`add_chunk` / `update_chunk`**: decode + store image **before**
`docStoreConn.insert` / `update`; only set `img_id` after successful
storage

## Files Changed

| File | Change |
|------|--------|
| `api/apps/restful_apis/chunk_api.py` | Helpers + reorder image
handling |
| `test/testcases/test_web_api/test_chunk_app/test_chunk_routes_unit.py`
| 3 regression tests |

## Validation

```bash
cd /root/gittensor/ragflow
pytest test/testcases/test_web_api/test_chunk_app/test_chunk_routes_unit.py::test_restful_add_chunk_invalid_image_base64_does_not_index_chunk -v
pytest test/testcases/test_web_api/test_chunk_app/test_chunk_routes_unit.py::test_restful_update_chunk_invalid_image_base64_does_not_update_chunk -v
pytest test/testcases/test_web_api/test_chunk_app/test_chunk_routes_unit.py::test_restful_add_chunk_valid_image_base64_stores_before_insert -v
pytest test/testcases/test_web_api/test_chunk_app/test_chunk_routes_unit.py -v
```

## Test Plan

- [x] Invalid `image_base64` on add → 4xx, no doc-store insert
- [x] Invalid `image_base64` on update → 4xx, no doc-store update
- [x] Valid PNG base64 on add → image stored, chunk indexed with
`img_id`
- [ ] CI green
2026-05-29 19:36:46 +08:00
Dexterity
04aa8d04e8 fix(go-models): raise SSE scanner buffer so large stream chunks are not dropped (#15382)
### Summary

Closes #15381 

Every provider in `internal/entity/models/` reads its streaming response
with `bufio.NewScanner(resp.Body)` and iterates over `scanner.Scan()`.
The default `bufio.Scanner` maximum token size is 64KB, so when an
upstream sends a single SSE `data:` line larger than 64KB (long content
deltas, large tool or function call argument blobs, bundled
`reasoning_content`, or providers that emit a whole message in one
event) `scanner.Scan()` returns `false` and `scanner.Err()` returns
`bufio.ErrTooLong`. Streaming chat then ends with an error partway
through the response.

This change adds `scanner.Buffer(make([]byte, 64*1024), 1024*1024)`
immediately after every SSE scanner that was still bare, raising the cap
to 1MB. 1MB is the value already used for streaming chat in `openai.go`,
`modelscope.go`, `groq.go`, `mistral.go`, `xai.go` and the other already
patched providers (the 8MB cap in the repo is reserved for TTS and
embedding paths), so this simply converges the remaining providers onto
the established pattern. Nothing else changes: line parsing, `data:`
prefix handling, `[DONE]` detection, JSON unmarshalling, error handling,
and the existing `scanner.Err()` checks all stay the same.

Providers covered (23 scanners across 22 files): 302ai, aliyun,
baichuan, baidu, cohere, deepinfra, deepseek, gitee, huggingface,
lmstudio, minimax (the chat scanner, whose TTS scanner was already
bumped), moonshot, nvidia, ollama, openrouter, orcarouter, paddleocr,
siliconflow, tokenhub, vllm, volcengine, xunfei, zhipu-ai. `jiekouai.go`
is excluded because it is covered by the in flight #15337.

A table driven regression test (`sse_scanner_buffer_test.go`) streams a
single 128KB `data:` content delta followed by `data: [DONE]` through an
`httptest` server and asserts that `ChatStreamlyWithSender` delivers the
full content with no error across a representative subset of providers.
Without the buffer fix the test fails with `bufio.Scanner: token too
long`.

This PR also removes three duplicate declarations of the package level
`roundTripperFunc` test helper that several recently merged provider PRs
each added independently, which had left the `internal/entity/models`
test package unable to compile. The helper now lives in a single place
and is shared.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-05-29 19:34:00 +08:00
monsterDavid
53bb2bd9e8 fix(metadata): preserve empty AND results across filter conditions (#15386)
## Summary
- Fix `meta_filter()` AND logic so an empty result from an early
condition is not overwritten when a later condition matches.
- Add regression tests for empty-first AND, successful AND intersection,
and OR behavior after an empty first condition.

Fixes incorrect `/retrieval` metadata filtering when multiple AND
conditions are used and the first condition matches no documents.

Closes #15360

## Test plan
- [x] `pytest test/unit_test/common/test_metadata_filter_operators.py
-v` (19/19 passed)
2026-05-29 19:33:26 +08:00
bitloi
2d229dd8aa fix(go): resolve custom base_url for empty default region (#15043)
### What problem does this PR solve?

Fixes custom `base_url` resolution when a model instance has no
configured region.

Some drivers read custom base URLs from `BaseURL[""]` when
`apiConfig.Region` is empty, while others normalize empty region to
`"default"` and read `BaseURL["default"]`. This PR adds the `"default"`
alias only for empty-region custom base URLs while preserving the
existing empty-region key.

Closes #15042

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-05-29 19:33:09 +08:00
Haruko386
d766e49128 feat[Go]: implement /system/stats and refactor /system/config/log (#15407)
### What problem does this PR solve?

As title

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
- [x] Refactoring
2026-05-29 19:32:21 +08:00
Hz_
d2f0a18f42 fix: persist logout access token invalidation (#15397)
### What this PR fixes

This PR fixes an issue in the Python backend where user logout did not
reliably persist the invalidated access_token to the database.
Although the logout endpoint returned success and logged that the token
had been invalidated, the user.access_token value could remain
unchanged in the database, which meant the previous login token could
stay valid longer than expected.

  ### What changed

  - Resolve the real user object before updating the token
  - Persist the invalidated access_token before calling logout_user()
- Return a server error if the token update is not written successfully

  ### Impact

- Logging out now correctly replaces the stored access_token with an
INVALID_... value
  - The previous login session is properly invalidated
- The change is limited to the logout flow and is intentionally small in
scope
2026-05-29 19:31:45 +08:00
Alexander Laurent
faa9c5469e feat: add Go MCP server delete API (#15262)
## What

#15240
Implementation for DELETE /api/v1/mcp/servers/:mcp_id
2026-05-29 19:29:55 +08:00
Hz_
09e91a8e61 Fix user registration initialization in Go API (#15349)
### What problem does this PR solve?

This PR fixes several behavior gaps in the Go implementation of the user
registration API.

### Type of change

- Make `nickname` required for user registration.
- Align registration error messages and response data with expected API
behavior.
- Handle password decryption errors for registration more consistently.
- Generate UUID v1-style IDs for new users, access tokens, tenants,
user-tenant records, and root files.
- Initialize default user fields during registration, including:
  - language
  - color schema
  - timezone
  - last login time
- Create user, tenant, user-tenant relation, tenant LLM records, and
root folder in a single DB transaction.
- Initialize default tenant LLM records from configured default models.
- Avoid partial registration data when one creation step fails.
- Use locale-based default language fallback for user profile responses.
2026-05-29 19:29:23 +08:00
呆萌闷油瓶
658ff06ca4 feat: add 4 new models for siliconflow (#15383)
### What problem does this PR solve?

Added 4 new models:
deepseek-ai/DeepSeek-V4-Pro
deepseek-ai/DeepSeek-V4-Flash
Pro/moonshotai/Kimi-K2.6
Pro/zai-org/GLM-5.1

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-05-29 19:28:29 +08:00
web-dev0521
bda2117a25 feat(connector): implement OneDrive data source connector (issue #15330) (#15331)
### What problem does this PR solve?

Closes #15330.

RAGFlow had no connector for OneDrive / OneDrive for Business. Users who
store working documents in OneDrive could not index them into a
knowledge base without manually downloading and re-uploading files.

This PR adds a net-new OneDrive data source that:

- Authenticates against Microsoft Graph with the same MSAL
client-credentials flow already used by the SharePoint and Teams
connectors (no new auth primitives).
- Enumerates every drive visible to the service principal and pages
through `/drives/{id}/root/delta`, persisting `@odata.deltaLink` values
per drive so subsequent syncs only fetch changed items.
- Optionally narrows ingestion to a sub-folder (`folder_path`) without
needing a separate code path.
- Surfaces typed errors on the validation probe (`GET /drives?$top=1`):
401 → `ConnectorMissingCredentialError`, 403 →
`InsufficientPermissionsError` (with a `Files.Read.All` hint), 5xx →
`UnexpectedValidationError`.
- Filters folders, soft-deleted items, and unsupported extensions (`.pdf
.docx .doc .xlsx .xls .pptx .ppt .txt .md .csv`).

#### Files

| File | Change |
|------|--------|
| `common/data_source/onedrive_connector.py` | **New** —
`OneDriveConnector` + `OneDriveCheckpoint`. |
| `common/data_source/config.py` | `DocumentSource.ONEDRIVE =
"onedrive"`. |
| `common/constants.py` | `FileSource.ONEDRIVE = "onedrive"`. |
| `common/data_source/__init__.py` | Export `OneDriveConnector`. |
| `rag/svr/sync_data_source.py` | `OneDrive(SyncBase)` with `batch_size`
normalisation; registered in `func_factory`. |
| `web/src/pages/user-setting/data-source/constant/index.tsx` |
`DataSourceKey.ONEDRIVE`, visibility map (`syncDeletedFiles: true`),
info entry, form fields (tenant_id, client_id, client_secret,
folder_path, batch_size), default values. |
| `web/src/locales/en.ts`, `web/src/locales/zh.ts` |
`onedriveDescription` + 4 tooltip keys (EN + ZH). |
| `test/unit_test/data_source/test_onedrive_connector_unit.py` | **New**
— 13 unit tests (`p1`/`p2`) covering auth, validation, checkpoint
helpers, and document filtering. |

#### Required Azure AD permission

`Files.Read.All` (Application, admin-granted).

#### Out of scope

- Interactive end-user OAuth (delegated permissions) — the connector
uses app-only credentials, consistent with the SharePoint / Teams
precedent.
- Binary download of file contents — the sync layer emits `Document`s
carrying `webUrl` + metadata; bytes are hydrated downstream by the parse
pipeline.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-05-29 19:26:06 +08:00
buua436
bd6251f462 Fix: default OpenAI chat completions to non-stream (#15394)
### What problem does this PR solve?

default OpenAI chat completions to non-stream when `stream` is omitted
https://github.com/infiniflow/ragflow/issues/15356
### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-05-29 17:47:47 +08:00
Lynn
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>
2026-05-29 17:39:41 +08:00
glorydavid03023
b79f79d9b9 fix(go-models): harden Novita default transport handling (#15350)
## Summary
- Harden `NewNovitaModel` to avoid panics when `http.DefaultTransport`
is a custom non-`*http.Transport` RoundTripper.
- Fallback to a safe transport (`ProxyFromEnvironment`) while preserving
existing pooling/timeout settings.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-29 14:28:46 +08:00
bitloi
ea3a5dba11 fix: validate custom model inputs (#15200)
### What problem does this PR solve?

Closes #15199.

The add-custom-model endpoint is routed through
`/api/v1/providers/:provider_name/instances/:instance_name/models`, but
the handler previously trusted `provider_name` and `instance_name` from
the JSON body instead of the path target. A request could therefore hit
one provider/instance URL while operating on a different body
provider/instance.

The same handler only rejected `model_types` when the slice was nil. An
empty array passed validation and reached
`ModelProviderService.AddCustomModel`, where `request.ModelTypes[0]`
could panic.

This PR makes the path provider/instance authoritative, rejects
mismatched body values, rejects missing or empty `model_types`, and adds
a service-level guard so direct service callers cannot hit the same
panic path.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-05-29 10:15:01 +08:00
web-dev0521
550bdf215c feat(go-api): implement tenant member management (issue #15294) (#15295)
## Summary

Ports the Python `tenant_api` team/member management endpoints to Go,
adding 4 endpoints under `/api/v1/tenants/:tenant_id/`:

- `GET /tenants/:tenant_id/users` — list non-owner members with user
details (owner only)
- `POST /tenants/:tenant_id/users` — invite a user by email; creates
invite-role join record (owner only)
- `DELETE /tenants/:tenant_id/users` — remove a member by `user_id`;
owner can remove anyone, members can remove themselves
- `PATCH /tenants/:tenant_id` — accept a pending invitation,
transitioning role `invite → normal`

Closes #15294
2026-05-29 10:13:09 +08:00
Haruko386
834236a3ec feat[Go]: implement /api/v1/system/status GET (#15348)
### What problem does this PR solve?

As title

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
- [x] Refactoring
2026-05-29 10:12:12 +08:00
oktofeesh
58eb957c30 fix(go-models): harden JieKouAI driver requests (#15337)
## Summary
- Harden JieKouAI request validation before outbound provider calls
- Force non-streaming and streaming chat methods to use their expected
stream modes
- Make model listing use a bodyless GET and parse model responses
without panics

Closes #14736

---------

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-29 10:09:27 +08:00
nickmopen
e023c165b6 Fix(kb): enforce tenant authorization on UpdateMetadataSetting (#15268) (#15270)
## Summary

Closes #15268.

The `UpdateMetadataSetting` handler at `internal/handler/kb.go:126`
retrieved the authenticated user via `GetUser(c)` but discarded the user
object (`_, errorCode, errorMessage := GetUser(c)`), then forwarded the
caller-supplied `kb_id` straight to the service layer with no ownership
check. Any authenticated user could mutate the `parser_config` /
metadata of any knowledge base in the system by guessing or harvesting a
`kb_id` — a classic IDOR (CWE-284, OWASP A01).

This is the only handler in `internal/handler/kb.go` missing the check;
every sibling (`ListTags`, `ListTagsFromKbs`, `RenameTag`,
`KnowledgeGraph`, `DeleteKnowledgeGraph`, `GetMeta`, `GetBasicInfo`)
already calls `h.kbService.Accessible(kbID, user.ID)`. The same
defensive check on the document preview endpoint was added in PR #14625
— this PR closes the matching gap on the KB metadata endpoint.

---------

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-29 10:08:55 +08:00
glorydavid03023
7fc909acc9 fix(go-models): harden ModelScope default transport handling (#15339)
## Summary
- Harden `NewModelScopeModel` to avoid panics when
`http.DefaultTransport` is a custom non-`*http.Transport` RoundTripper.
- Fallback to a safe transport (`ProxyFromEnvironment`) while preserving
existing pooling/timeout settings.
- Add `TestModelScopeNewModelWithCustomDefaultTransport` regression
coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-28 19:41:11 +08:00
web-dev0521
0a7662cf3e feat(go-api): implement GET /api/v1/agents list endpoint (issue #15328) (#15329)
## Summary

Closes: #15328 
- Implements `GET /api/v1/agents` — the agent/canvas listing endpoint
needed to complete the Home dashboard tile in `web/src/pages/home/`.
- Mirrors Python `api/apps/restful_apis/agent_api.py::list_agents`
exactly: tenant-join auth, optional `owner_ids` guard, keyword filter,
pagination, ordering, and `canvas_category` filter (default:
`agent_canvas`).
- **Scope:** read-only list only. Full agent CRUD and canvas runtime are
explicitly out of scope (separate slice of #15240).
2026-05-28 19:40:54 +08:00
web-dev0521
f80ec17fc5 feat(go-api): implement connector (data source) management endpoints (#15274)
## Summary

Ports the connector (data source) management endpoints that power
`web/src/pages/user-setting/data-source/` from Python
(`api/apps/restful_apis/connector_api.py`) to Go. Previously only `GET
/connectors` (list) was implemented in Go; this adds the rest of the
lifecycle.

Closes #15273 (subtask of #15240).

## Endpoints implemented

All under base path `/api/v1` (mirrors the Python routes):

| Method | Path | Description |
|--------|------|-------------|
| POST | `/connectors/{connector_id}/test` | Validate stored credentials
|

`GET /connectors` (list) was already present and is unchanged.

---------

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-28 19:40:15 +08:00
web-dev0521
98bc9ca6ac feat: implement Microsoft Teams data source connector (#15193)
### What problem does this PR solve?

Closes #15191.

RAGFlow shipped a Microsoft Teams connector stub
(`common/data_source/teams_connector.py`) whose document-loading methods
all returned `[]`, `Teams._generate()` was a `pass`, and Teams was
commented out of the data-source settings UI. As a result there was no
way to index Teams channel conversations into a knowledge base.

This PR implements the connector end to end on top of Microsoft Graph
(Office365-REST-Python-Client). It shares the MSAL client-credentials
auth shape with the SharePoint connector.

**Backend**

- `common/data_source/teams_connector.py`
- `load_credentials()` now builds the Graph client using an MSAL
client-credentials **token callback** — the form `GraphClient` actually
expects. (The previous stub passed a raw access-token string to
`GraphClient(...)`, which is not how that client is driven.) Token
acquisition is lazy, so credential loading performs no network call.
  - `validate_connector_settings()` lists teams via Graph.
- `load_from_checkpoint()` is now a generator that pages teams →
channels → messages, flattens each top-level post together with its
replies into one blob-based `Document` (`extension` `.txt`/`.html`,
`blob`, `size_bytes`, `doc_updated_at`). Incremental syncs are bounded
by message `lastModifiedDateTime` (falling back to `createdDateTime`).
Per-message errors surface as `ConnectorFailure` instead of aborting the
run.
- `retrieve_all_slim_docs_perm_sync()` yields id-only `SlimDocument`
batches and the checkpoint helpers return proper `TeamsCheckpoint`s.
- ACL → `ExternalAccess` mapping is intentionally left best-effort
(`load_from_checkpoint_with_perm_sync` delegates to the standard load)
because the sync pipeline does not currently persist `ExternalAccess`.
- `rag/svr/sync_data_source.py`
- Implemented `Teams._generate()` using the existing
`CheckpointOutputWrapper` pattern (same shape as Confluence/Jira/Google
Drive), supporting full reindex and incremental polling from
`poll_range_start`.
- `TeamsConnector` is already exported from
`common/data_source/__init__.py`.

**Frontend (`web/`)**

- Enabled the `TEAMS` data-source enum and added its form fields
(`tenant_id`, `client_id`, `client_secret`), default values, display
metadata, and a Teams icon.
- Added `teamsDescription` / `teamsTenantIdTip` to `en.ts` and `zh.ts`.

**Tests**

- `test/unit_test/data_source/test_teams_connector_unit.py`: mock-based
unit tests covering credential loading (incomplete creds raise, happy
path sets the Graph client, fetch-without-creds raises), post/reply
flattening (incl. the HTML vs text extension), incremental
`lastModifiedDateTime` filtering, and slim-doc listing. All 6 pass;
`ruff check` is clean.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-05-28 17:10:38 +08:00
glorydavid03023
b7d88f0b09 fix(go-models): harden Voyage default transport handling (#15341)
## Summary
- Harden `NewVoyageModel` to avoid panics when `http.DefaultTransport`
is a custom non-`*http.Transport` RoundTripper.
- Fallback to a safe transport (`ProxyFromEnvironment`) while preserving
existing pooling/timeout settings.
- Add `TestVoyageNewModelWithCustomDefaultTransport` regression
coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 16:46:58 +08:00
glorydavid03023
ff9aa4e2c7 fix(go-models): harden LongCat default transport handling (#15340)
## Summary
- Harden `NewLongCatModel` to avoid panics when `http.DefaultTransport`
is a custom non-`*http.Transport` RoundTripper.
- Fallback to a safe transport (`ProxyFromEnvironment`) while preserving
existing pooling/timeout settings.
- Add `TestLongCatNewModelWithCustomDefaultTransport` regression
coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 16:45:59 +08:00
Haruko386
ed878930fb feat[Go]: implement delete/ rebuild/ listlog api for connector (#15300)
### What problem does this PR solve?

implement delete, rebuild api for connector

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-05-28 16:44:35 +08:00
Alexander Laurent
32d5bf9791 feat: add Go MCP server create API (#15260)
## What
Implementation for POST /api/v1/mcp/servers
#15240
2026-05-28 16:43:21 +08:00
Jack
bea8092007 Update developer doc (#15336)
### What problem does this PR solve?

update developer doc

### Type of change

- [x] Documentation Update
2026-05-28 15:58:09 +08:00
web-dev0521
5de021ebb4 feat: implement Slack data source connector (#15188)
### What problem does this PR solve?

Closes #15187.

RAGFlow shipped a Slack connector
(`common/data_source/slack_connector.py`) but it was never usable:
`Slack._generate()` in the sync worker was a `pass` stub, the
connector's document-generating code was incompatible with the current
data model,
and Slack was commented out of the data-source settings UI. As a result,
teams had no way to index Slack channels/threads into a knowledge base.

This PR completes the connector end to end.

**Backend**

- `common/data_source/slack_connector.py`
- Rewrote `thread_to_doc` to produce a blob-based `Document`
(`extension`/`blob`/`size_bytes`). The previous implementation built the
doc with a `sections=[...]` argument and omitted the now-required
`blob`/`extension`/ `size_bytes` fields, so it raised a validation error
against the current `Document` model. Thread messages are now cleaned
and flattened into a single UTF-8 text blob.
- Added `load_from_state()` / `poll_source(start, end)` generators. The
connector's checkpoint interface is a no-op stub, so both full and
incremental syncs run through a single channel-iterating generator built
on the existing module helpers (`get_channels`, `filter_channels`,
`get_channel_messages`, `_process_message`), with per-channel thread
de-duplication.
- `rag/svr/sync_data_source.py`
- Implemented `Slack._generate()`. Credentials are loaded via
`StaticCredentialsProvider` (the connector requires `slack_bot_token`
and does not support `load_credentials`). Supports full reindex and
incremental polling from `poll_range_start`, plus the optional channel
filter. Modeled on the Confluence/Dropbox wrappers.
- `SlackConnector` was already exported from
`common/data_source/__init__.py`.

**Frontend (`web/`)**

- Enabled the `SLACK` data-source enum and added its form fields (Slack
bot token + optional channel filter), default values, display metadata,
and a Slack icon.
- Added `slackDescription` / `slackBotTokenTip` / `slackChannelsTip`
strings to `en.ts` and `zh.ts`.

**Tests**

- `test/unit_test/data_source/test_slack_connector_unit.py`: unit tests
covering credential loading (`load_credentials` raises,
`set_credentials_provider` initializes clients, missing credentials
raises) and document generation (standalone message + flattened thread,
blob/extension/size_bytes/metadata, and the incremental poll time
window). All 5 pass; `ruff check` is clean.

Required Slack scopes: `channels:read`, `channels:history`,
`users:read`.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-05-28 15:46:07 +08:00
chanx
7e83643536 Fix: Clustering method echo error (#15322)
### What problem does this PR solve?

Fix: Clustering method echo error

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-05-28 14:32:31 +08:00
oktofeesh
8468227a1a fix(go-models): harden 302.AI driver requests (#15289)
## Summary
- Harden the 302.AI model driver request validation and response parsing
paths.
- Add focused tests for chat request mode, model listing, malformed
provider responses, and input validation.

## What changed
- Validate API keys, model names, rerank queries, ASR file paths, OCR
inputs, parse URLs, task IDs, and model-list IDs before use.
- Keep chat and streaming methods from accepting conflicting `stream`
values in request payloads.
- Send `ListModels` as a bodyless GET and parse the response with typed
JSON structs instead of unchecked assertions.
- Remove raw SSE event logging from stream handling.

## Why
The driver could panic or send inconsistent requests when optional
config fields were nil, empty, malformed, or contradicted the method
path. This keeps provider-driver behavior explicit while preserving the
existing supported 302.AI flows.

Closes #14736
2026-05-28 13:33:01 +08:00
Hz_
0694b4af57 fix: include user model settings in /user/me response (#15320)
### What problem does this PR solve?

Fixes the `/user/me` response so it returns the current user's model
settings correctly.

### Type of change

- Added model settings data to the `/user/me` response.
- Kept the response structure compatible with existing user profile
fields.
- Avoided changing unrelated user/session behavior.
2026-05-28 13:31:16 +08:00
tmimmanuel
085241b039 Go: implement system healthz API (#15307)
## Summary
- Add Go REST support for `GET /api/v1/system/healthz`.
- Return Python-compatible `ok`/`nok` dependency fields for DB, Redis,
document engine, and storage.
- Return HTTP 200 only when all checks pass; otherwise return HTTP 500
with `_meta` failure details.
- Add focused service coverage for the unhealthy dependency response
when Go dependencies are not initialized.

## Scope
This is a small, isolated slice of #15240. It avoids current open
connector PRs (#15274, #15300, #15265, #15264), tenant/member PRs
(#15295, #15301, #15276), MCP PRs (#15281, #15253, #15254, #15260,
#15261, #15262), and the memory-message PR (#15256).

Refs #15240
2026-05-28 13:30:22 +08:00
web-dev0521
c4c4e228e3 feat: implement SharePoint data source connector (#15190)
### What problem does this PR solve?

Closes #15189.

RAGFlow shipped a SharePoint connector stub
(`common/data_source/sharepoint_connector.py`) whose document-loading
methods all returned `[]`, `SharePoint._generate()` was a `pass`, and
SharePoint was commented out of the data-source settings UI. As a result
there was no way to index files stored in SharePoint document libraries.

This PR implements the connector end to end on top of Microsoft Graph
(Office365-REST-Python-Client).

**Backend**

- `common/data_source/sharepoint_connector.py`
- `load_credentials()` now builds the Graph client using an MSAL
client-credentials **token callback** — the form `GraphClient` actually
expects. (The previous stub passed a raw access-token string to
`GraphClient(...)`, which is not how that client is driven.) Token
acquisition is lazy, so credential loading does no network call.
- `validate_connector_settings()` resolves the configured site via
Graph.
- `load_from_checkpoint()` is now a generator that enumerates every
document library under the site, walks folders depth-first, downloads
each file, and yields blob-based `Document` objects (`extension` /
`blob` / `size_bytes` / `doc_updated_at`). Incremental syncs are bounded
by file `lastModifiedDateTime`. Per-file errors are surfaced as
`ConnectorFailure` rather than aborting the run.
- `retrieve_all_slim_docs_perm_sync()` yields id-only `SlimDocument`
batches (no downloads) and the checkpoint helpers return proper
checkpoints.
- ACL → `ExternalAccess` mapping is intentionally left best-effort
(`load_from_checkpoint_with_perm_sync` delegates to the standard load)
because the sync pipeline does not currently persist `ExternalAccess`;
this can be extended once that plumbing exists.
- `rag/svr/sync_data_source.py`
- Implemented `SharePoint._generate()` using the existing
`CheckpointOutputWrapper` pattern (same shape as Confluence/Jira/Google
Drive), supporting full reindex and incremental polling from
`poll_range_start`.
- `SharePointConnector` is already exported from
`common/data_source/__init__.py`.

**Frontend (`web/`)**

- Enabled the `SHAREPOINT` data-source enum and added its form fields
`site_url`, `tenant_id`, `client_id`, `client_secret`), default values,
display metadata, and a SharePoint icon.
- Added `sharepointDescription` / `sharepointSiteUrlTip` to `en.ts` and
`zh.ts`.

**Tests**

- `test/unit_test/data_source/test_sharepoint_connector_unit.py`:
mock-based unit tests covering credential loading (incomplete creds
raise, happy path sets the Graph client, fetch-without-creds raises),
drive traversal + file download, incremental `lastModifiedDateTime`
filtering, and slim-doc listing. All 6 pass; `ruff check` is clean.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-05-28 13:26:08 +08:00
Wang Qi
0aff6a3f32 Feature: Allow page_size max value 100 (#15292)
Feature: Allow page_size max value 100
2026-05-28 11:13:01 +08:00
Idriss Sbaaoui
0940f1a135 Feat: add new tests and tescases for restful api suite (#15299)
### What problem does this PR solve?

extend restful api suite

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
- [x] Other (please describe): test
2026-05-28 11:03:12 +08:00
Hz_
b472ceeb68 go: add PATCH /api/v1/users/me user settings update (#15297)
### What problem does this PR solve?

- Add Go implementation parity for `PATCH /api/v1/users/me`.

- This updates the Go user settings endpoint to match the Python
behavior for updating the current user's profile settings.

### Changes

- Route `PATCH /api/v1/users/me` through the authenticated current user
from middleware.
- Add `password` and `new_password` support to `UpdateSettingsRequest`.
- Prevent `email` from being updated through this endpoint, matching the
Python blacklist behavior.
  - Support updating:
    - `nickname`
    - `avatar`
    - `language`
    - `color_schema`
    - `timezone`
    - `password`
  - Align password handling with Python:
    - invalid plaintext password payload returns `CodeExceptionError`
    - wrong old password returns `Password error!`
- successful update returns `{ code: 0, data: true, message: "success"
}`

### Test

Tested manually with Python and Go backends using the same request
bodies:

  - `PATCH /api/v1/users/me` with nickname/timezone update
- plaintext password payload returns Python-compatible `Incorrect
padding`
  - wrong old password returns `Password error!`
2026-05-28 07:08:50 +08:00
Jack
f0cb7a544b Refactor: Task Executor (#15154)
### What problem does this PR solve?

1. Break huge function into smaller pieces
2. Add unit test for the smaller pieces function
3. Layer-ed design
a. infra layer - task_context.py, recording_context.py,
write_operation_interceptor.py, ...
    b. service layer - *_service.py
    c. business layer - task_handler.py
4. Default behavior: use "refactor-ed version" - can switch to original
version by change env variable

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
- [x] Performance Improvement

---------

Co-authored-by: Liu An <asiro@qq.com>
Co-authored-by: Zhichang Yu <yuzhichang@gmail.com>
2026-05-27 21:54:17 +08:00
writinwaters
0071e98c11 Docs: Finalized v0.25.6 release notes. (#15305)
### What problem does this PR solve?

Finalized v0.25.6 release notes.

### Type of change

- [x] Documentation Update
2026-05-27 20:26:15 +08:00
writinwaters
129e1e3196 Docs: Updated converse with agent API reference. (#15257)
### What problem does this PR solve?

API reference updates based on #14542.

### Type of change


- [x] Documentation Update
2026-05-27 17:45:23 +08:00
nickmopen
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)
2026-05-27 16:37:14 +08:00
Haruko386
82318dee5d feat[Go]: implement create_connector API (#15285)
### What problem does this PR solve?

implement create_connector API

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-05-27 15:54:11 +08:00
balibabu
2c099bbb95 Fix: Uploading TSV format documents to the knowledge base did not generate any error messages. (#15284)
### What problem does this PR solve?

Fix: Uploading TSV format documents to the knowledge base did not
generate any error messages.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-05-27 14:42:53 +08:00
oktofeesh
7fb9a26623 fix(go-models): validate TokenHub chat requests (#15283)
## Summary
- centralize TokenHub chat request validation for chat and streaming
calls
- reject blank TokenHub model names before sending provider requests
- send TokenHub model listing requests as bodyless GET requests

## What changed
- Added shared TokenHub chat request validation for API key, model name,
and messages.
- Updated `ListModels` to call `GET /models` without a request body.
- Added focused tests for blank model names and accidental GET request
bodies.
- Replaced an httptest handler callback `t.Fatalf` with `t.Errorf` plus
an HTTP error and return.

## Why
TokenHub chat requests should fail locally for invalid model names
instead of sending avoidable malformed requests upstream. Model listing
should also match normal GET semantics and avoid sending an empty JSON
body.

Closes #14736

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-27 14:39:41 +08:00
Haruko386
ae88578451 Go: implement TTS and ASR for X.AI (#15247)
### What problem does this PR solve?

As title

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
- [x] Refactoring
2026-05-27 14:08:35 +08:00
tmimmanuel
0b000b833e Go: implement connector get API (#15259)
## Summary
- Add Go REST support for `GET /api/v1/connectors/:connector_id`.
- Reuse the Python API behavior by returning the connector only when the
current user can access its tenant.
- Add focused handler coverage for success and unauthorized responses.

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-27 14:07:55 +08:00
sxxtony
17b5b33574 Go: implement Rerank in Replicate driver (#15278)
### What problem does this PR solve?

`ReplicateModel.Rerank` in `internal/entity/models/replicate.go` was a
`"replicate, no such method"` stub. The chat path landed in #14958 and
the embed path in #15073; rerank is the last major retrieval surface
still missing on this provider.

Until this PR, a tenant who selected a Replicate reranker model got the
sentinel error on every rerank call.

Co-authored-by: sxxtony <sxxtony@users.noreply.github.com>
Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-27 14:07:00 +08:00
Alexander Laurent
ae5f48f233 feat: add GiteeAI provider support to Go API server (#15131)
### What problem does this PR solve?

Closes #15090.

Adds GiteeAI support to the Go model-provider layer so GiteeAI chat
models can be routed through the Go API server using the same
OpenAI-compatible chat, streaming, model listing, and connection-check
flow used by other SaaS providers.

GiteeAI is implemented as a separate provider from the existing `gitee`
provider.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

## Summary

- Added a GiteeAI Go model driver.
- Added the GiteeAI provider catalog with default base URL
`https://ai.gitee.com/v1`.
- Registered `giteeai` in the model factory separately from `gitee`.
- Added focused provider tests for sync chat, streaming chat, model
listing, connection checks, base URL override, SSE parsing, `[DONE]`
handling, and unsupported methods.

## What changed

- Implemented `ChatWithMessages` for `POST /chat/completions`.
- Implemented `ChatStreamlyWithSender` with SSE parsing, `delta`
extraction, `finish_reason`, `[DONE]`, and `<think>` tag handling.
- Implemented `ListModels` for `GET /models`.
- Implemented `CheckConnection` by delegating to `ListModels`.
- Returned standard `no such method` errors for unsupported embedding,
rerank, image-to-text, ASR, and TTS paths.

## Tests

```bash
go test -vet=off ./internal/entity/models -run 'TestGiteeAI' -count=1
go test -vet=off ./internal/entity -run 'Test.*Provider|Test.*Model' -count=1
```

---------

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-27 14:06:34 +08:00
Hz_
47626bbe63 go: add Qiniu model provider (#15280)
### What problem does this PR solve?

This PR adds Qiniu provider integration for the Go model driver layer in
RAGFlow.

  Supported capabilities:

  - [X] Chat
  - [X] Think Chat
  - [X] Stream Chat
  - [X] Stream Think Chat
  - [X] Model listing
  - [X] Provider configuration and factory registration

  Verified examples from the CLI:

  ```
  login user '***' password '***';

  ADD PROVIDER 'qiniu';

  CREATE PROVIDER 'qiniu' INSTANCE 'test' KEY '***';

chat with 'deepseek/deepseek-v3.1-terminus-thinking@test@qiniu' message
'hello';

think chat with 'deepseek/deepseek-v3.1-terminus-thinking@test@qiniu'
message 'hello';

stream chat with 'deepseek/deepseek-v3.1-terminus-thinking@test@qiniu'
message 'hello, what are you';

stream think chat with
'deepseek/deepseek-v3.1-terminus-thinking@test@qiniu' message 'hello,
what are you';

stream think chat with 'qwen3-max-2026-01-23@test@qiniu' message 'hello,
what are you';

  LIST MODELS FROM 'qiniu' 'test';

```

  ### Type of change

  - [X] New Feature
  - [X] Provider integration
2026-05-27 13:19:39 +08:00
oktofeesh
a3c6e075f6 fix(go-models): add VolcEngine model listing suffix (#15234)
## Summary
- add the VolcEngine `models` URL suffix used by the existing Go
`ListModels` implementation
- return a clear error when the VolcEngine models suffix is missing
- add focused VolcEngine model-listing regression tests

## What changed
- Added `url_suffix.models` to `conf/models/volcengine.json`.
- Normalized the configured models suffix before building the request
URL.
- Covered config loading, successful model listing, upstream errors, and
missing suffix handling.

## Why
`VolcEngine.ListModels` already builds requests from `URLSuffix.Models`,
but the bundled VolcEngine config did not define that suffix. That left
the model-listing path unable to call the documented `/models` endpoint
from the existing provider config.

Fixes #14701

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-27 13:14:56 +08:00
Idriss Sbaaoui
1f34a18242 Feat: add new tests and tescases for restful api suite (#15277)
### What problem does this PR solve?

extend restful api suite

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
- [x] Other (please describe): test
2026-05-27 13:07:49 +08:00
balibabu
187dc8a1e6 Fix: The Creativity parameter of chat was not saved. (#15243)
### What problem does this PR solve?

Fix: The Creativity parameter of chat was not saved.
### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-05-27 11:02:30 +08:00
writinwaters
8f0632c8d9 Docs: v0.25.6 release notes draft (#15255)
### What problem does this PR solve?

v0.25.6 release notes draft updated.

### Type of change

- [x] Documentation Update
2026-05-26 20:56:36 +08:00
oktofeesh
5ae41dc1eb fix(go-models): route hosted OCR providers through drivers (#15233)
## Summary
- route hosted MinerU.Net and PaddleOCR.Net provider names to their
existing Go drivers
- add regression coverage for loading the hosted OCR provider configs
through ProviderManager

## What changed
- Added canonical provider-name aliases for the hosted OCR provider
display names.
- Covered both bundled configs with a focused provider-manager test.

## Why
The hosted provider configs use display names with `.Net`, while model
factory dispatch lowercases the provider name. Without aliases, those
configs fall through to `DummyModel` instead of using the existing
MinerU and PaddleOCR drivers.

---------

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-26 20:40:40 +08:00
Wang Qi
303221c1f4 Fix: show tag list for chunk (#15251) 2026-05-26 20:24:22 +08:00
oktofeesh
22a3b8cdf9 feat(go-models): list LongCat models (#15241)
## Summary
- Add LongCat model-list support through the documented
OpenAI-compatible models endpoint.

## What changed
- Add the LongCat `models` URL suffix for `/openai/v1/models`.
- Implement `ListModels` for the LongCat Go driver.
- Delegate `CheckConnection` to the lightweight model-list request.
- Add focused regression coverage for successful, malformed, oversized,
and missing-key responses.

## Why
LongCat documents a models endpoint under the OpenAI-compatible API
surface, but the Go driver still returned `no such method` for model
listing and connection checks.

## Validation
- `go test ./internal/entity/models -run TestLongCat -count=1`
- `go test -race ./internal/entity/models -run TestLongCat -count=1`
- `go test ./internal/entity -count=1`
- `git diff --check`

## Notes
- Related to the broader Go model provider tracking in #14736, but this
PR only handles LongCat model listing.
- `go test ./internal/entity/models -count=1` is currently blocked by an
unrelated Astraflow test panic outside this LongCat change.

---------

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-26 19:58:53 +08:00
oktofeesh
557024e7d4 fix(go-models): add xAI model listing suffix (#15236)
## Summary
- add the xAI `models` URL suffix used by the existing Go `ListModels`
implementation
- return a clear error when the xAI models suffix is missing
- add focused xAI model-listing and connection-check regression tests

## What changed
- Added `url_suffix.models` to `conf/models/xai.json`.
- Normalized the configured models suffix before building the request
URL.
- Covered config loading, successful model listing, upstream errors,
API-key validation, missing suffix handling, and `CheckConnection`
delegation.

## Why
`XAIModel.ListModels` already builds requests from `URLSuffix.Models`,
and `CheckConnection` delegates to that method. The bundled xAI config
did not define that suffix, which left the model-listing path unable to
call the provider `/models` endpoint from the existing provider config.

## Validation
- `go test ./internal/entity/models -run TestXAI -count=1`
- `go test ./internal/entity -count=1`
- `git diff HEAD~1..HEAD --check`

## Notes
- `go test ./internal/entity/models -count=1` currently fails in
unchanged Astraflow coverage: `TestAstraflowEmbedReturnsNoSuchMethod`
panics before reaching any xAI assertions.

---------

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-26 19:58:20 +08:00
writinwaters
af48a22ff4 Docs: Initial draft for v0.25.6 release notes. (#15250)
### What problem does this PR solve?

Initial draft: v0.25.6 release notes.

### Type of change

- [x] Documentation Update
2026-05-26 19:46:40 +08:00
Liu An
0639dba89a Docs: Update version references to v0.25.6 in READMEs and docs (#15248)
### What problem does this PR solve?

- Update version tags in README files (including translations) from
v0.25.5 to v0.25.6
- Modify Docker image references and documentation to reflect new
version
- Update version badges and image descriptions
- Maintain consistency across all language variants of README files

### Type of change

- [x] Documentation Update
2026-05-26 19:45:43 +08:00
Haruko386
3619ceca01 Go: implement provider: OrcaRouter (#15235)
### What problem does this PR solve?

implement provider `OrcaRouter`
**The following functionalities are now supported:**

**Cohere:**
- [x] Chat / Think Chat / Stream Chat / Stream Think Chat
- [x] Model listing
- [x] TTS
- [ ] Balance


### Type of change

- [x] New Feature (non-breaking change which adds functionality)

---------

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-26 18:20:33 +08:00
dripsmvcp
a48bcf814d Go: implement provider: ModelScope (#15041)
Closes #15040.

ModelScope was listed unchecked in the Go-rewrite tracker #14736 and
already had an llm_factories.json entry (tags: LLM) but no Go driver, so
the new Go API server could not route ModelScope instances. The Python
side has supported it through the OpenAI-compatible base at
rag/llm/chat_model.py:618 (ModelScopeChat), which requires a
user-supplied base URL and appends /v1.

This adds:
- internal/entity/models/modelscope.go: self-hosted OpenAI-compatible
driver with chat (sync + SSE stream with idle-timeout cancellation),
list_models, and check_connection. Auth header is optional, matching the
xinference pattern, so deployments without auth and auth-enabled
deployments both work. Base URL is normalized so users can configure
either the root endpoint or the /v1 endpoint.
- internal/entity/models/modelscope_test.go: 12 tests covering name, URL
normalization, factory routing, chat happy path / auth header /
reasoning_content extraction, stream happy path / stream=false rejection
/ idle cancellation, list_models + check_connection, missing-base-URL
clear error, and the no-such-method sentinels.
- conf/models/modelscope.json: shipped config (class: "local",
url_suffix v1/chat/completions and v1/models).
- internal/entity/models/factory.go: case "modelscope" →
ModelScopeModel.
- internal/service/llm.go: ModelScope added to the selfDeployed map
alongside Ollama, Xinference, LocalAI, LM-Studio, GPUStack — the Python
side requires user-supplied URL with no default, so the Go side
classifies it the same way.

Follow-on issues will add Embed and Rerank, in line with how Novita,
NVIDIA, TogetherAI, and other providers landed method-by-method.

---------

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-26 18:18:46 +08:00
Hz_
84add43208 Add HuaweiCloud model provider (#15237)
### What problem does this PR solve?

  This PR adds HuaweiCloud provider integration in RAGFlow.

  Supported capabilities:

  - [x] Chat / Think Chat / Stream Chat / Stream Think Chat
  - [x] Embedding
  - [x] Rerank
  - [x] Model listing
  - [x] Provider connection checking

  Verified examples from the CLI:

  ```
  check instance 'test' from 'HuaweiCloud';

  chat with 'deepseek-v4-flash@test@HuaweiCloud' message 'hello';

  think chat with 'deepseek-v4-flash@test@HuaweiCloud' message 'hello';

  stream chat with 'deepseek-v4-flash@test@HuaweiCloud' message 'hello';

stream think chat with 'deepseek-v4-flash@test@HuaweiCloud' message
'hello';

embed text 'what is rag' 'who are you' with 'bge-m3@test@HuaweiCloud'
dimension 1024;

rerank query 'what is rag' document 'rag is retrieval augmented
generation' 'rag need llm' 'famous rag
project includes ragflow' with 'bge-reranker-v2-m3@test@HuaweiCloud' top
3;

  list supported models from 'HuaweiCloud' 'test';

  LIST MODELS FROM 'HuaweiCloud' 'test';
```
  ### Type of change

  - [x] New Feature
  - [x] Provider integration
2026-05-26 17:13:15 +08:00
ghost
a7d25391dc fix(tokenhub): wire Go driver and harden requests (#15224)
## Summary
- Wire the Go TokenHub provider through the model factory.
- Harden TokenHub request handling for chat, streaming, embeddings, and
model listing.
- Add focused TokenHub unit coverage for factory wiring and provider
behavior.

## Notes
- Refs #14736.
- Follows up #15159.

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-26 17:12:37 +08:00
Jake Armstrong
0fb85a66bc feat(go-models): add AWS Bedrock provider driver (#15166)
## Summary

Closes #15165.

Implements the AWS Bedrock model provider for the Go API server, tracked
under #14736. Adds Converse + Converse-Stream chat and foundation-model
listing, with SigV4 signing over a hand-rolled `net/http` path that
matches the established pattern in `internal/entity/models/` (no new
direct `go.mod` deps).

## Linked tracker

Tracked under #14736 (Implement model providers of RAGFlow API server in
Go). Closes #15165.
2026-05-26 17:10:06 +08:00
Idriss Sbaaoui
036ed5b236 Feat: add new tests and tescases for restful api suite (#15230)
### What problem does this PR solve?

extend restful api suite

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
- [x] Other (please describe): test
2026-05-26 13:24:22 +08:00
chanx
bce11527c3 Fix: Fixed metadata issue (#15226)
### What problem does this PR solve?

Fix: Fixed metadata issue

- The dataset's built-in metadata is now active, but it appears to be
disabled in the individual file configuration.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-05-26 13:16:15 +08:00
Wang Qi
619b971785 Fix: empty file with better message (#15232)
Fix: empty file with better message
2026-05-26 12:28:53 +08:00
天海蒼灆
0d2a17254c fix(api): allow canvas_type in agent create and update APIs (#15201)
### What problem does this PR solve?

Creating or updating an agent via `POST /api/v1/agents` and `PUT
/api/v1/agents/{agent_id}` did not persist `canvas_type` because the
handler `req` dict never assigned the field before
`UserCanvasService.save` / `update_by_id`.


### 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: Cursor <cursoragent@cursor.com>
2026-05-26 11:31:46 +08:00
glorydavid03023
3dbd874a79 Go: implement Rerank in DeepInfra driver (#15185)
### What problem does this PR solve?

The Go DeepInfra driver returned a stub error for `Rerank()` even though
DeepInfra serves reranker models at `POST /v1/inference/{model}` with
`query`, `documents`, and a `scores[]` response.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 10:52:09 +08:00
sxxtony
67f7d87dff Go: implement provider: FuturMix (#15013)
### What problem does this PR solve?

Add a Go driver for **FuturMix** (https://futurmix.ai/docs), one of the
unchecked providers on the umbrella tracking issue #14736. FuturMix is
documented as an "OpenAI-compatible API" aggregator over Claude / GPT /
Gemini / DeepSeek (~22 models per their `/models` page).

Until this PR, a tenant who configured `futurmix` as a model provider in
the Go layer fell through to the default branch of
`internal/entity/models/factory.go` and got the dummy driver.

---------

Co-authored-by: sxxtony <sxxtony@users.noreply.github.com>
Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-26 10:51:29 +08:00
Renzo
806414df43 Go: validate Baidu OCR inputs (#15168)
### What problem does this PR solve?

Closes #15167.

The Baidu Go provider advertises OCR support through
`paddleocr-vl-0.9b`, but `BaiduModel.OCRFile` dereferenced required
inputs before validating them. Calling OCR with a missing API config,
API key, or model name could panic instead of returning a normal error.

This PR adds explicit input validation for those required values.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

---------

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-26 10:51:05 +08:00
Jake Armstrong
b961810e79 Go: implement OCR in ZhipuAI driver (#15143)
### What problem does this PR solve?

Closes #15142.

ZhipuAI lists `glm-ocr` as an OCR model, but the Go driver still
returned `no such method` from `OCRFile`. This wires the advertised
model to Z.AI's documented `layout_parsing` endpoint and returns the
`md_results` Markdown output through the existing `OCRFileResponse.Text`
field.

This PR also adds focused tests for URL input, raw file-content base64
input, and validation errors.

### 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):

### Test

- [x] `go test -vet=off ./internal/entity/models -run
'TestZhipuAIOCRFile'`
2026-05-26 10:50:06 +08:00
Idriss Sbaaoui
c3b38d397f Feat: add new tests and tescases for restful api suite (#15223)
### What problem does this PR solve?

extend restful api suite

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
- [x] Other (please describe): test
2026-05-26 10:08:45 +08:00
Jay Xu
54c3d23513 Fix [Bug]: Save parser configs in dataset configuration page is not working #15175 (#15177)
### What problem does this PR solve?

Fix [Bug]: Save parser configs in dataset configuration page is not
working #15175

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-05-26 10:04:43 +08:00
wdeveloper16
4b36801b53 fix: resolve asyncio correctness issues (fire-and-forget tasks, event loop nesting) (#14761)
## Summary

Fixes the confirmed asyncio anti-patterns from #14755. Only the three
verified bugs are addressed; patterns already correctly using
`asyncio.new_event_loop()` in a fresh thread are left untouched.

### Changes

**`api/apps/restful_apis/tenant_api.py` — fire-and-forget
`send_invite_email`**

`asyncio.create_task()` was called without storing the `Task` reference.
CPython's GC can collect an unfinished task, silently cancelling it and
swallowing exceptions. Fixed by storing the task in a module-level
`_background_tasks: set[Task]` with a `done_callback` to discard it on
completion — the standard Python idiom for safe background tasks.

**`api/apps/restful_apis/agent_api.py` — fire-and-forget
`background_run`**

Same root cause in the webhook "Immediately" execution path. Same fix
applied.

**`rag/llm/chat_model.py` (`LocalLLM._stream_response`) —
`asyncio.get_event_loop()` on running loop**

`asyncio.get_event_loop()` returns Quart's running event loop when
called from an async context.
Calling `loop.run_until_complete()` on it raises `RuntimeError`.
Replaced with `asyncio.new_event_loop()` so the generator
uses a dedicated fresh loop, closed in a `finally` block.

## What was NOT changed

- `llm_service._sync_from_async_stream` and
`evaluation_service._sync_from_async_gen`: both already correctly use
`asyncio.new_event_loop()` inside a fresh thread.
- `llm_service._run_coroutine_sync`: only caller is `rag/app/resume.py`
(sync context), so `thread.join()` is correct there.
- `requests` in agent tools: sync methods dispatched through thread
pools; httpx migration is a separate, larger refactor.

## Test plan

- [ ] Invite a team member and confirm the email is sent with no task
warnings in logs.
- [ ] Trigger a webhook agent in "Immediately" mode; confirm canvas
state is persisted after background run.
- [ ] Verify `LocalLLM` (Jina backend) chat and streaming work
end-to-end.

Closes #14755

---------

Co-authored-by: Zhichang Yu <yuzhichang@gmail.com>
2026-05-25 22:45:40 +08:00
balibabu
ed179ce684 Fix: The prompt variable for the agent operator disappears after input. (#15218)
### What problem does this PR solve?

Fix: The prompt variable for the agent operator disappears after input.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-05-25 20:36:51 +08:00
writinwaters
67e43e7df7 Docs: Minimum required Python version increased to 3.13. (#15219)
### What problem does this PR solve?

Minimum Python version increased to 3.13.

### Type of change


- [x] Documentation Update
2026-05-25 20:23:30 +08:00
qinling0210
af85aa9c7b Implement Elasticsearch functions in GO (#15160)
### What problem does this PR solve?

Implement Elasticsearch functions in GO (except for Search)

### Type of change

- [x] Refactoring
2026-05-25 19:15:07 +08:00
Idriss Sbaaoui
7d200d5bd7 Feat: add new tests and tescases for restful api suite (#15208)
### What problem does this PR solve?

extend restful api suite

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
- [x] Other (please describe): test
2026-05-25 19:03:56 +08:00
balibabu
c7c75c0a87 Feat: Enable agent messages to display base64 images (#15212)
### What problem does this PR solve?

Feat: Enable agent messages to display base64 images

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2026-05-25 19:02:03 +08:00
Wang Qi
f4d36f7082 Fix #15170 cannot filter document status (#15216)
Fix #15170 cannot filter document status

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-05-25 18:58:37 +08:00
Haruko386
4783ce9951 fix(Go): rewrite chat, listmodels, embed for Ollama (#15213)
### What problem does this PR solve?

IDK how to implement **`Ollama`** on #14580 but it's totally wrong.
This is the rewrite version for **`Ollama`**

**Verified from CLI**
```
# Embed
RAGFlow(user)> embed text 'what is rag' 'who are you' with 'nomic-embed-text:latest@test12@ollama' dimension 1024;
+-----------+-------+
| dimension | index |
+-----------+-------+
| 768       | 0     |
| 768       | 1     |
+-----------+-------+

# Chat
RAGFlow(user)> think chat with 'qwen3:0.6b@test12@ollama' message 'who r u'
Thinking: Okay, the user asked, "Who r u?" I need to respond appropriately. First, I should acknowledge their question. Since I'm an AI, I don't have a physical form, but I can confirm that I'm a large language model. I should keep the response friendly and offer help. Let me make sure I'm not making up any information and that the response is natural. Also, I should check for any typos and ensure clarity. Alright, that should cover it.

Answer: I'm an AI language model, and I don't have a physical form. However, I can tell you that I'm designed to assist with questions and tasks. How can I help you today?
Time: 2.914285


RAGFlow(user)> stream think chat with 'qwen3:0.6b@test12@ollama' message 'who r u'
Thinking: , the user asked, "Who are you?" I need to respond appropriately. Since I'm an AI assistant, I should mention that I don't have a physical form or a mind. I should also clarify that I can help with various tasks like answering questions or providing information. It's important to keep the response friendly and informative while maintaining the correct tone.
Answer:  don't have a physical form or a mind, but I'm here to help with your questions or tasks! What can I do for you today?
Time: 1.740047

# LisyModels
RAGFlow(user)> list supported models from 'ollama' 'test12'
+-------------------------+
| model_name              |
+-------------------------+
| nomic-embed-text:latest |
| qwen3:0.6b              |
+-------------------------+
```

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
2026-05-25 18:55:03 +08:00
balibabu
0f92353bd9 Fix: Replace the red highlight at the top of the PDF document with yellow. (#15203)
### What problem does this PR solve?

Fix: Replace the red highlight at the top of the PDF document with
yellow.
### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-05-25 17:21:36 +08:00
Wang Qi
4776bfa8a2 Fix: Correct the API path (#15204)
Follow on PR #15146 to reslove the backwad compatability issue.

1. /agents/<attachment_id>/download ->
/agents/attachments/<attachment_id>/download

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-05-25 17:11:24 +08:00
Jonathan Chang
9d1006e4ec fix: The output of the parser in the ingestion pipeline contains HTML tags (#14920)
## Summary
This change fixes ingestion quality issues where MinerU parser output
may contain HTML fragments (for example, table-related tags like `<tr>`,
`<td>`, `<br>`), which were previously passed directly into
chunking/tokenization and degraded chunk quality.

The fix adds a sanitization step in the MinerU parser path so parsed
sections are normalized to clean text before chunking.

## Change Type (select all)
- [x] Bug fix
- [x] Ingestion pipeline improvement
- [x] Parser/chunking quality fix

## Related Issue
- https://github.com/infiniflow/ragflow/issues/14831
2026-05-25 16:06:36 +08:00
Ahmad Intisar
e6068a7f7e Fix: table parser metadata (#15127)
### What problem does this PR solve?

This PR improves the table upload flow for CSV/Excel files by allowing
table column role configuration at upload time.

Previously, users had to:
1. Upload and parse a table file.
2. Open parser settings and manually set table column roles.
3. Re-parse the file for the roles to take effect.

This was inefficient and required an unnecessary second parse.

With this change:
1. When the knowledge base uses table parsing, the upload dialog
extracts CSV/Excel headers client-side.
2. Users can choose Auto mode or Manual mode.
3. In Manual mode, users can assign per-column roles before upload.
4. The selected parser config is sent with the upload request and
applied server-side during document creation.

Result: configured table column roles are applied from the first parse.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
2026-05-25 16:05:38 +08:00
nickmopen
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)
2026-05-25 16:05:00 +08:00
Haruko386
69f301b84a Go: implement embed for Tencent Hunyuan (#15207)
### What problem does this PR solve?

Implement embed for Tencent Hunyuan

**Verified from CLI**
```
RAGFlow(user)> embed text 'what is rag' 'who are you' with 'hunyuan-embedding@test1@hunyuan' dimension 16;
+-----------+-------+
| dimension | index |
+-----------+-------+
| 1024      | 0     |
| 1024      | 1     |
+-----------+-------+
```

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
- [x] Refactoring
2026-05-25 16:04:17 +08:00
ちー
bb6cfc14e6 feat[go]: implement provider: TokenHub (#15159)
### What problem does this PR solve?

implement provider TokenHub

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-05-25 16:02:50 +08:00
Wang Qi
5069561abc Fix /chat/completions to allow send only the latest message (#15197)
### What problem does this PR solve?

1. Fix /chat/completions to send only the latest message
2. Allo chat stream=False

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-05-25 14:23:33 +08:00
Wang Qi
bb148edf4c Revert "Fix: /openai/<chat_id>/chat/completions not aware of session_id" (#15205)
Reverts infiniflow/ragflow#15155 because this is never supported, keep
it as it is.
2026-05-25 14:23:10 +08:00
Jin Hai
f8c626bbc8 Go: add ingestion server (#15094)
### What problem does this PR solve?

1. Go ingestion server will connected with admin server with gRPC stream
2. Go ingestion server will be responsible for ingestion tasks
```

RAGFlow(admin)> list ingestors;
+-----------------+-----------+----------------------------------+---------------------------+----------+------------+--------------+--------+------------+---------------+
| address         | cpu_usage | id                               | last_heartbeat            | name     | process_id | rss_usage    | status | task_count | vms_usage     |
+-----------------+-----------+----------------------------------+---------------------------+----------+------------+--------------+--------+------------+---------------+
| 127.0.0.1:58564 | 0         | bdd1870eea2646e0aacb8a2cd3307aa2 | 2026-05-24T18:16:17+08:00 | ingestor | 680152     | 212.72265625 | active | 0          | 2589.12109375 |
+-----------------+-----------+----------------------------------+---------------------------+----------+------------+--------------+--------+------------+---------------+

RAGFlow(admin)> start ingestion 'abc';
+----------------------------------+
| task_id                          |
+----------------------------------+
| e714777639ca4760ab427b5f211e81ad |
+----------------------------------+

RAGFlow(admin)> stop ingestion 'f7bd39d0a724457eb5fdce6d81699776';
+----------------------------------+
| task_id                          |
+----------------------------------+
| f7bd39d0a724457eb5fdce6d81699776 |
+----------------------------------+

RAGFlow(admin)> list tasks;
+-----+----------------------------------+-------+------+----------------------------------+---------------------------+------------+------------+
| ETA | assign_to                        | error | from | id                               | last_update               | start_time | status     |
+-----+----------------------------------+-------+------+----------------------------------+---------------------------+------------+------------+
| 0   | 17937da188b84f23a5c10bb87588944b |       | CLI  | eae6431da72a40e796cff3a03008091b | 2026-05-24T19:46:03+08:00 |            | COMPLETED  |
| 0   | 17937da188b84f23a5c10bb87588944b |       | CLI  | 6cccdd174bd049ecb05a774bbb47593f | 2026-05-24T19:46:03+08:00 |            | COMPLETED  |
| 0   | 17937da188b84f23a5c10bb87588944b |       | CLI  | ef360d777e57485799adb96b30f2b4b8 | 2026-05-24T19:46:03+08:00 |            | CANCELED   |
| 0   | 17937da188b84f23a5c10bb87588944b |       | CLI  | bcc5c5448cb64de48b6b6171c36fb790 | 2026-05-24T19:46:03+08:00 |            | CANCELED   |
| 0   | 17937da188b84f23a5c10bb87588944b |       | CLI  | bfc25384c43a443294fe2da979a38ac2 | 2026-05-24T19:46:03+08:00 |            | DISPATCHED |
| 0   | 17937da188b84f23a5c10bb87588944b |       | CLI  | 84960537b85d413b8990a9efd5952d67 | 2026-05-24T19:46:04+08:00 |            | DISPATCHED |
| 0   | 17937da188b84f23a5c10bb87588944b |       | CLI  | 3d223c1b51e24b36861a3bfb2f1d58d4 | 2026-05-24T19:46:03+08:00 |            | CANCELED   |
| 0   | 17937da188b84f23a5c10bb87588944b |       | CLI  | e433b0e356b846c89c301621a3c54494 | 2026-05-24T19:46:03+08:00 |            | COMPLETED  |
| 0   | 17937da188b84f23a5c10bb87588944b |       | CLI  | 7c93a3880f074ebd8eca14e6b51bb7ef | 2026-05-24T19:46:03+08:00 |            | COMPLETED  |
| 0   | 17937da188b84f23a5c10bb87588944b |       | CLI  | df2e4ef51aaf4390bff9a23f2692486e | 2026-05-24T19:46:04+08:00 |            | DISPATCHED |
| 0   | 17937da188b84f23a5c10bb87588944b |       | CLI  | 7377c53010194ef7a83aa206698d66ff | 2026-05-24T19:46:05+08:00 |            | DISPATCHED |
| 0   | 17937da188b84f23a5c10bb87588944b |       | CLI  | df64d1a1f9d348e3a2f174c4d7d69e73 | 2026-05-24T19:46:05+08:00 |            | DISPATCHED |
| 0   | 17937da188b84f23a5c10bb87588944b |       | CLI  | b59834512e2847e1bdf13ace04b8a456 | 2026-05-24T19:46:06+08:00 |            | DISPATCHED |
| 0   | 17937da188b84f23a5c10bb87588944b |       | CLI  | 0064bb0ab69344028d1ecfda053826f4 | 2026-05-24T19:46:03+08:00 |            | QUEUED     |
+-----+----------------------------------+-------+------+----------------------------------+---------------------------+------------+------------+


```


### Type of change

- [x] New Feature (non-breaking change which adds functionality)

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-05-25 14:00:08 +08:00
Haruko386
5d022d83e8 Go: implement provider: PaddleOCR_Local (#15158)
### What problem does this PR solve?

Go: implement provider: PaddleOCR_Local

**Verified from CLI**

```
RAGFlow(user)> ocr with 'PaddleOCR-VL@test@paddleocr_local' file './internal/test1.jpg'
+----------------------+
| text                 |
+----------------------+
| ## Parallel to these |
+----------------------+
```

### Type of change

- [X] Bug Fix (non-breaking change which fixes an issue)
- [X] New Feature (non-breaking change which adds functionality)
- [X] Refactoring
2026-05-25 12:12:57 +08:00
dripsmvcp
8d8ea71877 Go: implement provider: Tencent Hunyuan (#15092)
## Summary
- Adds a `Hunyuan` Go driver so the new API server can route Tencent
Hunyuan chat instances (registered in `conf/llm_factories.json:3830` as
`Tencent Hunyuan`). Follows the same SaaS-driver shape used for
Astraflow, Avian, Novita, TogetherAI, Replicate, DeepInfra, Upstage, and
LongCat.

Closes #15087

---------

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-25 11:04:39 +08:00
Wang Qi
0ce6655789 Fix: /chat/completions not aware of conversation_id (#15162)
### What problem does this PR solve?

Fix /chat/completions not aware of conversation_id

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-05-25 10:47:08 +08:00
VincentLambert
50424df48e feat(i18n): complete French translation — add ~1400 missing keys (#15192)
## Summary

- Brings the French locale (`web/src/locales/fr.ts`) to full parity with
the English reference
- Adds ~1400 missing translation keys across **all sections**: `common`,
`chat`, `header`, `login`, `admin`, `setting`, `flow`,
`knowledgeDetails`, `knowledgeConfiguration`, `memory`, `skills`,
`skillSearch`, `chunk`, `mcp`, `fileManager`, `search`,
`dataflowParser`, `datasetOverview`, `deleteModal`, `empty`, `explore`,
`memories`, `pagination`, `language`, `knowledgeList`
- All strings containing French apostrophes use double-quote delimiters
(prevents JS syntax errors)

## Test plan

- [ ] `npx esbuild src/locales/fr.ts --bundle=false` — no errors
- [ ] `npx eslint src/locales/fr.ts` — no errors
- [ ] Switch UI language to French and verify key sections render
correctly (chat, knowledge base, admin panel, agent flow)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 10:32:45 +08:00
bitloi
432e966414 fix(go): support OpenAI audio endpoints (#15104)
### What problem does this PR solve?

Closes #15102.

OpenAI's Go provider config advertises `whisper-1` as ASR and `tts-1` as
TTS, but the Go driver returned `openai, no such method` for both audio
paths and did not define `url_suffix.asr` / `url_suffix.tts`.

This PR:

- adds OpenAI audio URL suffixes for `audio/transcriptions` and
`audio/speech`
- implements non-streaming `TranscribeAudio` using multipart form
uploads
- implements non-streaming `AudioSpeech` using the OpenAI speech JSON
request shape
- keeps streaming TTS explicitly unsupported instead of sending binary
audio through the text SSE sender
- adds focused tests for config coverage, ASR/TTS request shape,
required TTS voice validation, and unsupported streaming TTS


### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

---------

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-25 10:25:53 +08:00
Wang Qi
e6dd397531 Fix: /openai/<chat_id>/chat/completions not aware of session_id (#15155)
### What problem does this PR solve?

Fix: /openai/<chat_id>/chat/completions not aware of session_id

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-05-22 20:38:56 +08:00
Tohka
302f97de50 Go: implement reasoning_chat, TTS, ASR for Groq (#15153)
### What problem does this PR solve?

Go: implement reasoning_chat, TTS, ASR for Groq

**Verify from CLI**
```
RAGFlow(user)> think chat with 'qwen/qwen3-32b@test@groq' message 'who r u'
Thinking: Okay, the user asked, who r u. I need to determine what the user is asking. They may be asking about my identity. I should introduce my name and basic functions. The user might want to know what I can do, so I should list some common use cases, such as answering questions, creating writing, coding, and expressing opinions. The user may be curious about how they can interact with me, so they can be advised to ask any questions or provide instructions. Keep your answers conversational, avoid overly technical terms, keep answers concise, and encourage further interaction. Check if there's any ambiguity in the answer and make sure it's accurate and meets the user's needs. Also consider if there are other aspects the user may be interested in, such as my training data or performance. But since the question is basic, I'll focus on the essentials first and invite the user to ask more. In summary, respond to the user's questions by introducing yourself, your functions, and encouraging further interaction.

Answer: Hello! I'm Qwen. I am a large-scale language model developed by Tongyi Lab, designed to assist you in various ways, such as answering questions, creating text, logical reasoning, programming, and more. I aim to provide clear, accurate, and helpful information and support. How can I assist you today? Feel free to ask any questions or give me tasks! 😊
Time: 2.199908


RAGFlow(user)> stream think chat with 'openai/gpt-oss-20b@test@groq' message 'who r u'
Thinking:  to respond politely.
Answer: ’m ChatGPT—an AI language model created by OpenAI. I’m here to answer questions, offer explanations, and help with a wide range of topics. How can I assist you today?


RAGFlow(user)> tts with 'canopylabs/orpheus-arabic-saudi@test@groq' text 'hello? show yourself' play format 'wav' param '{"voice": "fahad"}'
SUCCESS


RAGFlow(user)> asr with 'whisper-large-v3-turbo@test@groq' audio './internal/test.wav' param '{"language": "en"}'
+----------------------------------------------------------------------------------------------------------------------+
| text                                                                                                                 |
+----------------------------------------------------------------------------------------------------------------------+
|  The examination and testimony of the experts enabled the Commission to conclude that five shots may have been fired |
+----------------------------------------------------------------------------------------------------------------------+
```

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-05-22 18:02:30 +08:00
Haruko386
3f02ca7ba1 Go: implement embed, rerank, tts for AstraFlow (#15135)
### What problem does this PR solve?

implement embed, rerank, tts for AstraFlow

**Verify from CLI**

```
# Astraflow
RAGFlow(user)> tts with 'IndexTeam/IndexTTS-2@test3@astraflow' text 'hello? show yourself' play format 'wav' param '{"voice": "jack_cheng"}'
SUCCESS

RAGFlow(user)> rerank query 'what is rag' document 'rag is retrieval augment generation' 'rag need llm' 'famous rag project includes ragflow' with 'bge-reranker-v2-m3@test3@astraflow' top 3;
+-------+---------------------+
| index | relevance_score     |
+-------+---------------------+
| 0     | 0.9837390184402466  |
| 2     | 0.06322699040174484 |
| 1     | 0.04663187265396118 |
+-------+---------------------+

RAGFlow(user)> embed text 'walkerwhat' 'jumperwho' with 'text-embedding-3-large@test3@astraflow' dimension 16
+-----------+-------+
| dimension | index |
+-----------+-------+
| 3072      | 0     |
| 3072      | 1     |
+-----------+-------+

# Xinference


```

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
- [x] Refactoring
2026-05-22 18:02:01 +08:00
writinwaters
bf9297a343 Docs: Added a guide on integrating Discord. (#15156)
### What problem does this PR solve?

How to ingest messages from your Discord server.

### Type of change

- [x] Documentation Update
2026-05-22 17:49:18 +08:00
Wang Qi
87918650ff Refactor: Move API files (#15151)
Refactor: Move API files
2026-05-22 17:44:05 +08:00
Wang Qi
7e6844118b Fix search vector_similarity_weight (#15108)
### What problem does this PR solve?

Fix search vector_similarity_weight

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-05-22 16:05:13 +08:00
ghost
f9ce07ced1 feat(go-models): add Groq provider driver (#15097)
### What problem does this PR solve?

Closes #15088.

Adds Groq support to the Go model-provider layer so Groq instances can
be routed through the Go API server with the same OpenAI-compatible
chat, streaming, model listing, and connection-check flow used by other
SaaS providers.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

## Summary

- Added a Groq Go model driver.
- Added the Groq provider catalog and default OpenAI-compatible API URL.
- Registered Groq in the model factory.
- Added focused provider tests.

## What changed

- Implemented chat completions, SSE streaming, ListModels, and
CheckConnection for Groq.
- Covered request shape, stream termination, reasoning fallback, model
listing, custom base URLs, safe transport setup, and unsupported
methods.
- Kept the provider catalog scoped to current Groq chat-capable model
IDs.
- Cleaned up pre-existing Go model package validation blockers so the
package can be tested normally with vet enabled.

## Why

The existing Python/provider catalog path includes Groq, but the Go
model-provider layer did not have a Groq driver, so the Go API server
could not instantiate or use Groq as requested in #15088.

## Notes

The model package now validates without disabling vet.

---------

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-22 15:24:52 +08:00
Lynn
893980ed8f Fix: add model_type into llm_setting (#15141)
### What problem does this PR solve?

Add model_type into llm_setting

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-05-22 15:23:07 +08:00
buua436
71a52d579c fix: move agent attachment download api (#15146)
### What problem does this PR solve?

move agent attachment download api to the correct route and update
frontend callers

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

### Notes

- Move the attachment download endpoint from document routes to agent
routes.
- Update frontend download callers to use the agent attachment endpoint.
- Reuse the shared file response header helper instead of duplicating it
in `agent_api.py`.
2026-05-22 15:22:05 +08:00
dripsmvcp
ed04893415 Go: implement provider: TokenPony (#15091)
## Summary
- Adds a `TokenPony` Go driver so the new API server can route TokenPony
chat instances, matching the existing Python `TokenPonyChat`
(`rag/llm/chat_model.py:1210`). Follows the same SaaS-driver shape used
for Astraflow, Avian, Novita, TogetherAI, Replicate, DeepInfra, Upstage,
and LongCat.

Closes #15086

---------

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-22 15:21:45 +08:00
kpdev
faf77a5a8a feat(evaluation): track token usage in evaluation results (#13487)
## Summary

Implements the TODO in `evaluation_service.py`: **Track token usage** in
evaluation results.

## Changes

- **Import** `num_tokens_from_string` from `common.token_utils`
- **Prompt tokens**: Use the full prompt returned by `async_chat` when
available (includes system prompt + knowledge base + query), otherwise
fall back to the question token count
- **Completion tokens**: Count tokens in the generated answer
- **Storage**: Store `token_usage` as `{prompt_tokens,
completion_tokens, total_tokens}` in each `EvaluationResult` instead of
`None`

## Why

The evaluation pipeline previously saved `token_usage: None` for every
result. This change allows downstream consumers (e.g. evaluation
dashboards, cost tracking) to see approximate token usage per test case
using the same tokenizer (tiktoken cl100k_base) used elsewhere in
RAGFlow.

## Testing

- No new tests added; existing evaluation flow unchanged
- Token counting uses existing `num_tokens_from_string` utility

---------

Co-authored-by: kiannidev <kiannidev@users.noreply.github.com>
2026-05-22 15:19:53 +08:00
Jake Armstrong
b1ef5d365f Go: implement ASR in OpenRouter driver (#15067)
### What problem does this PR solve?

Fixes #15066

OpenRouter now exposes an official speech-to-text endpoint at `POST
/api/v1/audio/transcriptions`, but the Go model driver still returned
`openrouter, no such method` from `TranscribeAudio`. This left
OpenRouter ASR models unavailable through the Go API server even though
the provider already has OpenRouter audio support for TTS.

Related provider-tracking context: #14736

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-22 15:19:38 +08:00
Full Stack Developer
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)
2026-05-22 15:15:49 +08:00
dale053
c33d0b8081 fix: prevent sensitive fields from leaking in user API responses (#14792)
Closes #14789

### What problem does this PR solve?

User API endpoints (`login`, `user_profile`, `user_add`,
`forget_reset_password`) were returning full user objects via
`to_json()` / `to_dict()`, which included sensitive fields like
`password` and `access_token` in the response body. This leaks
credentials to the client.

This PR adds a `to_safe_dict()` method on the `User` model that strips
sensitive fields (`password`, `access_token`) and replaces all affected
call sites to use it.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-05-22 15:14:26 +08:00
Wang Qi
f4e63ef33f Refactor: enahnce CI (#15147)
### What problem does this PR solve?

Refactor: enahnce CI

### Type of change

- [x] Refactoring
2026-05-22 14:45:09 +08:00
Wang Qi
a9ec78cb9c Refactor: enahnce retry and timeout (#14983)
### What problem does this PR solve?

1. Enhance retry and timeout, and adjust the default timeout
2. NER: spacy do not batch chunks
3. extract _has_cancel_and_exit
4. enhance log messages

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
- [x] Refactoring
2026-05-22 13:16:39 +08:00
Calixto Ong
11ff848b04 feat: Add SDK and cURL examples for chunk management, chat assistant, and retrieval (#4310) (#14208)
Closes #4310

### What problem does this PR solve?

Issue #4310 requests practical examples for the RAGFlow SDK and HTTP API
to help developers get started faster. The existing `example/sdk/`
folder only contains `dataset_example.py`. This PR fills the remaining
gaps by adding examples for three key API areas not yet covered in
`main` or by other open PRs (#13904, #13284):

- **Chunk management** — add, list, update, delete, and retrieve chunks
within a dataset
- **Chat assistant** — create a chat assistant, open a session, send
messages (streaming and non-streaming), and clean up
- **Retrieval** — perform semantic retrieval across one or multiple
datasets

### Type of change

- [x] Documentation Update
- [x] New Feature (non-breaking change which adds functionality)
2026-05-22 12:13:00 +08:00
dale053
6ab25bf715 fix: block SSRF in misc_utils.download_img for OAuth avatars (#14868)
### What problem does this PR solve?

Closes #14865

`download_img` in `common/misc_utils.py` is used for OAuth avatar URLs.
The previous implementation called `async_request` from
`common.http_client`, which followed redirects without re-validating
each hop and did not apply the same SSRF protections as this path needs.
That made it possible to reach non-public or disallowed targets (for
example via redirects or unsafe URLs) when fetching avatars.

This change replaces that flow with an explicit, bounded fetch: each URL
(including every redirect target) is checked with
`common.ssrf_guard.assert_url_is_safe`, DNS is pinned with
`pin_dns_global`, `httpx` streams the body with `follow_redirects=False`
and a manual redirect loop (capped by
`RAGFLOW_OAUTH_AVATAR_MAX_REDIRECTS`), and total response size is capped
(`RAGFLOW_OAUTH_AVATAR_MAX_BYTES`). Timeouts, proxy, and user agent
align with `HTTP_CLIENT_*` env vars without importing `http_client`, so
lightweight tests stay simple.

Unit tests cover empty/None URLs, loopback, cloud metadata-style
addresses, and disallowed schemes so SSRF regressions are caught early.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

---------

Co-authored-by: Kevin Hu <kevinhu.sh@gmail.com>
2026-05-22 12:12:04 +08:00
Jake Armstrong
b2bf9155ed Go: implement ASR in ZhipuAI driver (#15134)
### What problem does this PR solve?

This PR implements ASR and TTS support for the ZhipuAI Go driver.

The ZhipuAI model config already advertises `glm-asr-2512` as an ASR
model, but the Go driver returned `zhipu, no such method` from
`TranscribeAudio`. This adds the documented audio transcription endpoint
suffix and sends multipart transcription requests with `model`,
`stream=false`, and `file` fields.

Per maintainer review, this also adds the ZhipuAI TTS endpoint suffix
and implements `AudioSpeech` / `AudioSpeechWithSender` for `glm-tts`.

Closes #15133

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
2026-05-22 11:53:18 +08:00
ghost
b2053cc3c7 feat(go-models): add PPIO provider driver (#15099)
### What problem does this PR solve?

Closes #15089.

Adds PPIO support to the Go model-provider layer so PPIO instances can
be routed through the Go API server with the same OpenAI-compatible
chat, streaming, model listing, and connection-check flow used by other
SaaS providers.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

## Summary

- Added a PPIO Go model driver.
- Added the PPIO provider catalog and default OpenAI-compatible API URL.
- Registered PPIO in the model factory.
- Added focused provider and provider-manager tests.

## What changed

- Implemented chat completions, SSE streaming, ListModels, and
CheckConnection for PPIO.
- Covered request shape, stream termination, reasoning fallback, model
listing, custom base URLs, safe transport setup, unsupported methods,
and provider config loading.
- Kept the provider catalog aligned with the existing RAGFlow PPIO
factory model set.
- Cleaned up pre-existing Go model package validation blockers so the
scoped provider tests can run normally with vet enabled.

## Why

The existing Python/provider catalog path includes PPIO, but the Go
model-provider layer did not have a PPIO driver, so the Go API server
could not instantiate or use PPIO as requested in #15089.
2026-05-22 11:52:18 +08:00
buua436
04bdb41909 Fix: guard missing task language (#15136)
### What problem does this PR solve?

guard missing task language

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-05-22 11:46:38 +08:00
buua436
ea1764a7dc Revert "fix(api): infer /documents/{id}/download Content-Type from filename when ext is omitted (#15052)" (#15138)
Reverts infiniflow/ragflow#15053
2026-05-22 11:46:01 +08:00
writinwaters
57ddd79183 Docs: Fixed a deployment issue (#15114)
### What problem does this PR solve?

Fixed a docusaurus deployment issue.

### Type of change

- [x] Documentation Update
2026-05-21 22:43:49 +08:00
writinwaters
8995662ee6 Docs: Updated v0.25.5 release notes (#15109)
### What problem does this PR solve?

Updated v0.25.5 release notes.

### Type of change


- [x] Documentation Update
2026-05-21 22:04:44 +08:00
Haruko386
1ece1c81da Go: implement rerank, asr, tts for TogetherAI (#15107)
### What problem does this PR solve?

implement rerank, asr, tts for TogetherAI

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-05-21 20:57:04 +08:00
Wang Qi
c5a46fda44 Fix: <asyncio.locks.Semaphore object at 0xabcd [locked]> is bound to a different event loop (#15100)
Fix: <asyncio.locks.Semaphore object at 0xabcd [locked]> is bound to a
different event loop
2026-05-21 19:23:41 +08:00
Jin Hai
775ea55679 Docs: update python version to 3.13 (#15103)
### What problem does this PR solve?

1. update python version to 3.13
2. upgrade ormsgpack to 1.6.0

### Type of change

- [x] Refactoring

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-05-21 19:09:19 +08:00
Haruko386
a725e114f9 Go: implement ASR and TTS for Xinference (#15096)
### What problem does this PR solve?

implement ASR and TTS for Xinference

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
- [x] Refactoring
2026-05-21 18:28:06 +08:00
Jonathan Hill
111cdc77b5 fix: guard LLM response against empty choices (fixes #14711) (#14988)
## Summary

Fixes 10 unguarded `response.choices[0]` accesses that cause
`IndexError` or `AttributeError` when the LLM returns an empty `choices`
list — the scenario described in #14711.

- `rag/llm/cv_model.py`
- `rag/llm/chat_model.py`

Each access site is now guarded with:
```python
if not response.choices:
    raise ValueError("LLM returned empty response")
```

## Verification

Detected and verified by [pact](https://github.com/qizwiz/pact) — a
sheaf-cohomological LLM contract checker using Z3 as a local theory
solver.

**pact sheaf-cohomological proof status after fix:**

| File | Ȟ¹ (after) | Z3 |
|------|-----------|-----|
| `rag/llm/cv_model.py` | 0 | UNSAT ✓ |
| `rag/llm/chat_model.py` | 0 | UNSAT ✓ |

All access sites proven safe (Z3 UNSAT certificate).

The checker was also used to verify the autogen streaming-None fix in
[microsoft/autogen#7711](https://github.com/microsoft/autogen/pull/7711).

## Test plan
- [ ] Existing test suite passes
- [ ] Manually test with a provider that returns empty `choices` under
load (e.g. Vertex AI)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Jonathan Hill <jonathan.f.hill@gmail.com>
2026-05-21 15:37:19 +08:00
dripsmvcp
12a148d541 fix(api): guard against missing session in get_agent_session (#15011)
`GET /agents/<agent_id>/sessions/<session_id>` crashed with
`AttributeError: 'NoneType' object has no attribute 'to_dict'` when the
session lookup failed: `_, conv =
API4ConversationService.get_by_id(...)` returned `(False, None)`, then
`conv.to_dict()` was called unconditionally.

This is reachable in multi-instance deployments: the session row may not
yet be visible on the node servicing the immediate follow-up GET after a
session is created on a different node.

Add the same `if not exists` guard already used by every other call site
of `API4ConversationService.get_by_id` (see agent_api.py:1147,
sdk/session.py:179, conversation_service.py:248, canvas_service.py:323).

Closes #14989

### 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

- [ ] 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):
2026-05-21 15:37:10 +08:00
dripsmvcp
ce9a4425d2 fix(imap): handle multi-address headers in _parse_singular_addr (#15006)
Replace the RuntimeError with a warning + first-address fallback so a
single email whose From header contains multiple addresses no longer
crashes the entire IMAP sync task. Also add regression tests covering:

- #14963: RFC 5322 quoted display names with commas (e.g. "Schlüter,
Sabine" <s@x>) parsed as one address, not two.
- #14964: multi-address headers warn instead of raising.

Closes #14964
Refs #14963
2026-05-21 15:37:02 +08:00
dripsmvcp
85caad5558 fix(docker): bump nginx to 1.31.0 (CVE-2026-42945) (#15007)
## Summary
- Bump pinned nginx in `Dockerfile` from `1.29.5-1~noble` (vulnerable)
to `1.31.0-1~noble` to remediate **CVE-2026-42945**.

## Root Cause
`Dockerfile:58` pinned `ARG NGINX_VERSION=1.29.5-1~noble`. Per the
official nginx security advisory, **CVE-2026-42945** is a buffer
overflow in `ngx_http_rewrite_module` triggered via the `rewrite` and
`set` directives, affecting nginx **0.6.27 through 1.30.0**. `1.29.5`
falls inside that range, so the shipped image is vulnerable.

References:
- nginx security advisories:
https://nginx.org/en/security_advisories.html
- Vendor advisory: https://my.f5.com/manage/s/article/K000161019
- Fixed versions: `1.31.0` (mainline) and `1.30.1` (stable)

## Fix
Single-line change in `Dockerfile:58`:

```diff
-ARG NGINX_VERSION=1.29.5-1~noble
+ARG NGINX_VERSION=1.31.0-1~noble
2026-05-21 15:36:51 +08:00
Prateek Jain
bf4864e614 fix(infinity): declare extra field + serialize dict on write to unbreak RAPTOR (#14998)
### What problem does this PR solve?

Fixes #14997.

RAPTOR builds on the Infinity backend have been broken since v0.25.2
introduced the `extra` field in code (`rag/svr/task_executor.py:1011`)
without declaring it in `conf/infinity_mapping.json`. Every RAPTOR job
fails with:

```
infinity.common.InfinityException: (3013, 'Fail to bind the expression: extra@src/planner/expression_binder_impl.cpp:99')
```

The auto-migration in
`common/doc_store/infinity_conn_base.py:_migrate_db()` adds any columns
it finds in the mapping JSON to existing tables — so the only thing
standing between users and a working RAPTOR build is that one missing
declaration. OceanBase, ES, and OpenSearch were unaffected because they
store `extra` as a native JSON type; only Infinity (which has a strict
`varchar`/`integer`/`float` schema) needed the addition.

### The fix

Two-part change:

1. **`conf/infinity_mapping.json`**: declare `"extra": {"type":
"varchar", "default": ""}`. On next startup, `_migrate_db()` adds the
column to all existing chunk tables — no manual DDL needed for upgrading
installations.
2. **`rag/utils/infinity_conn.py` `insert()`**: serialize the `extra`
dict to a JSON string at write time, since Infinity's `varchar` can't
store a Python dict directly. Modelled on the existing `chunk_data`
handling a few lines above.

The read path (`rag/utils/raptor_utils.py:_as_extra_dict`) already
normalises both dict and JSON-string inputs, so no read-side change is
needed. Other backends are untouched — `task_executor.py` still writes
the dict, and the OceanBase/ES/OpenSearch insert paths handle dicts
natively.

### Verification

Tested on a v0.25.4 deployment with the Infinity backend by applying the
same two changes via mounted-volume override:

- Confirmed `_migrate_db()` adds the `extra` column to all pre-existing
chunk tables on startup (column visible via Infinity's
`show_columns()`).
- Triggered RAPTOR builds on four datasets (~21k chunks total) via `POST
/api/v1/datasets/<id>/index?type=raptor`.
- All four progressed past the previously-failing
`get_raptor_chunk_methods()` call into actual entity-extraction and
clustering work without the (3013) error.
- GraphRAG builds (which can trigger the same path indirectly via
`task_executor.py:857`) also progressed cleanly.

### Type of change

- [X] Bug Fix (non-breaking change which fixes an issue)
2026-05-21 15:36:15 +08:00
tmimmanuel
38a8bc3dab fix(upstage): extract reasoning delta from streaming responses (#14817)
### What problem does this PR solve?

`UpstageModel.ChatStreamlyWithSender` (in the driver merged via #14819)
only extracted `delta.content` from each SSE event. For the `solar-pro3`
reasoning family (and any future Upstage model that follows the same
wire shape), the chain-of-thought is streamed in a **separate
`delta.reasoning` field**, and the driver was silently dropping all of
it.

The non-streaming path already extracts `message.reasoning` into
`ChatResponse.ReasonContent` (added earlier in this PR's history), so
the same model produced **inconsistent behavior** between streaming and
non-streaming: a tenant calling `solar-pro3` with `reasoning_effort:
high` would see the reasoning trace if they used `ChatWithMessages` but
not if they used `ChatStreamlyWithSender`.

### Live evidence

Probed against `api.upstage.ai/v1/chat/completions` with `solar-pro3` +
`reasoning_effort: high` + `stream: true` (8000-token budget so the
reasoning has room to finish):

```
$ curl -sN -H "Authorization: Bearer <key>" -H "Content-Type: application/json" \
       -X POST https://api.upstage.ai/v1/chat/completions \
       -d '{"model":"solar-pro3","messages":[{"role":"user","content":"Compute 15% of 80."}],
            "max_tokens":8000,"stream":true,"reasoning_effort":"high"}'

# across 168 SSE events:
#   delta keys seen: [content reasoning role]
#   delta.content total len:   121 chars   (the visible answer)
#   delta.reasoning total len: 159 chars   (the chain-of-thought) <- driver dropped this
```

A representative event showing both fields side by side:

```json
data: {"choices":[{"index":0,"delta":{"reasoning":"15% = 0.15."}}]}
data: {"choices":[{"index":0,"delta":{"content":"15% of 80 is "}}]}
```

The 159 chars of reasoning were arriving on the wire and being thrown
away. `solar-pro2` was also probed (625 events); it does **not** emit
`delta.reasoning` — its reasoning is inlined into `delta.content` — so
this change is a no-op for it and for `solar-mini`.

### What this PR includes

- `internal/entity/models/upstage.go`: in the SSE scanner loop, extract
`delta.reasoning` before `delta.content` and forward each non-empty
chunk via the sender's second arg (the existing `reasonContent` channel
the non-stream path already populates).

The ordering contract is documented inline: reasoning chunks within a
single SSE event are emitted before content chunks, so a UI that pipes
both sees the chain-of-thought start before the answer for that token,
matching the wire order Upstage emits.

- `internal/entity/models/upstage_test.go`: three new tests pinning the
new behavior:
- `TestUpstageStreamExtractsReasoningDelta` — reasoning + content
forwarded to the right sender args; one-of invariant per call
- `TestUpstageStreamReasoningChunksArriveBeforeContent` — ordering
pinned within a single SSE event that carries both fields
- `TestUpstageStreamWithoutReasoningStillWorks` — regression net:
non-reasoning models (`solar-mini`, `solar-pro2`) continue to work; the
reason callback never fires

No interface change. No factory change. No config change.

### How was this tested?

```
$ go test -vet=off -run TestUpstage -count=1 -v ./internal/entity/models/...
... (existing tests 1..9 still pass) ...
=== RUN   TestUpstageStreamExtractsReasoningDelta
--- PASS: TestUpstageStreamExtractsReasoningDelta (0.01s)
=== RUN   TestUpstageStreamReasoningChunksArriveBeforeContent
--- PASS: TestUpstageStreamReasoningChunksArriveBeforeContent (0.01s)
=== RUN   TestUpstageStreamWithoutReasoningStillWorks
--- PASS: TestUpstageStreamWithoutReasoningStillWorks (0.00s)
PASS
ok      ragflow/internal/entity/models  0.034s
```

12/12 Upstage tests pass on go 1.25. `go build
./internal/entity/models/...` exits 0.

**Live integration test** (smoke test not committed) — the patched
driver was run directly against `api.upstage.ai/v1` with the same prompt
that produced the curl evidence above:

```
=== RUN   TestUpstageStreamReasoningLiveSmoke
    [OK] visible content: 50 chunks, 84 chars
    [OK] reasoning:       39 chunks, 90 chars
    content head 200:   "\\(15\\% = \\frac{15}{100}=0.15\\).\n\n\\[\n0.15 \\times 80 = 12.\n\\]\n\n**15 % of 80 is 12.**"
    reasoning head 200: "We need to compute 15% of 80. That's 0.15 * 80 = 12. So answer is 12. Provide explanation."

UPSTAGE STREAM REASONING SMOKE PASSED
--- PASS: TestUpstageStreamReasoningLiveSmoke (1.97s)
```

Before this fix, the same call would have produced **0 reasoning
chunks**. The 90 chars of reasoning that the patched driver now surfaces
are the chain-of-thought solar-pro3 emits when reasoning_effort is high.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-05-21 15:33:21 +08:00
tmimmanuel
85d0b46d8e fix(mistral): handle structured content from magistral reasoning models (#14805)
### What problem does this PR solve?

`MistralModel.ChatWithMessages` (in the driver merged via #14807)
assumes that `choices[0].message.content` from `/v1/chat/completions` is
always a string and falls through to `return nil, fmt.Errorf("invalid
content format")` on anything else.

That assumption breaks for the **magistral reasoning family**
(`magistral-small-*`, `magistral-medium-*`). When the model needs a
chain-of-thought to answer, Mistral returns `content` as a **structured
array of typed parts**:

```json
"content": [
  {"type": "thinking",
   "thinking": [{"type": "text", "text": "Combined speed is 150 mph. 300 / 150 = 2 hours."}],
   "closed": true},
  {"type": "text", "text": "They will meet after **2 hours**."}
]
```

Concretely, this is what the live API returns today (probed against
`api.mistral.ai/v1`):

```
$ curl -H "Authorization: Bearer <key>" -H "Content-Type: application/json" \
       -X POST https://api.mistral.ai/v1/chat/completions \
       -d '{"model":"magistral-medium-latest",
            "messages":[{"role":"user","content":"two trains 60mph and 90mph, 300mi apart, when do they meet? step by step."}],
            "max_tokens":1024}'
HTTP 200
{ "choices":[{"message":{
    "role":"assistant",
    "content":[
      {"type":"thinking","thinking":[{"type":"text","text":"Okay, let's see..."}],"closed":true},
      {"type":"text","text":"To determine when the two trains meet..."}
    ]}}] }
```

With the current driver, every call like that returns the generic
`"invalid content format"` error. Trivial prompts that happen to fit in
a string answer still succeed, so the breakage is **non-deterministic
from the tenant's POV**: same model, same provider, sometimes works,
sometimes 500s with no useful error.

A secondary issue: `conf/models/mistral.json` does not include any
magistral model. The picker hid the broken path, which is why this
wasn't caught during #14807's review.

### What this PR includes

- New helper `extractMistralContent(raw interface{}) (answer,
reasonContent string, err error)` in
`internal/entity/models/mistral.go`, which normalizes both shapes
Mistral can return:
- `string` → historical path. `Answer = content`, `ReasonContent = ""`.
Preserves behavior for every non-reasoning model (`mistral-large-*`,
`mistral-small-*`, `ministral-*`, `codestral-*`, `pixtral-*`,
`open-mistral-nemo`).
- `[]interface{}` → walk the parts. Concatenate every `{"type":"text",
"text":...}` part into `Answer`; concatenate the inner text inside every
`{"type":"thinking", "thinking":[...]}` part into `ReasonContent`.
- `ChatWithMessages` now calls the helper instead of doing the raw
`.(string)` cast.
- Unknown part types are **skipped, not failed**. Mistral has been
adding new content variants quickly (audio chunks, citations, etc.);
this driver should not 500 every call when a new part type appears.
- `conf/models/mistral.json`: add `magistral-medium-latest` and
`magistral-small-latest`. Both are visible in `/v1/models` today.

No interface change. No factory change. No new dependencies.

### How was this tested?

**Unit tests** — 5 new tests in `internal/entity/models/mistral_test.go`
on top of the 27 already shipped via #14807:

- `TestMistralChatHandlesStringContent` — regression net for the
historical path
- `TestMistralChatExtractsReasoningFromStructuredContent` — the fixture
body is a trimmed copy of the actual `magistral-medium-latest` response
captured above; asserts both `Answer` and `ReasonContent` are populated
correctly
- `TestMistralChatHandlesStructuredContentWithoutThinking` —
`magistral-*` with a trivial answer returns a structured shape that has
only a `text` part; `ReasonContent` must stay empty
- `TestMistralChatIgnoresUnknownContentPartTypes` — `audio_url` and
`future_part_type` parts are skipped, `text` parts still flow through
- `TestExtractMistralContent` — table-driven unit coverage of the helper
for string, empty string, nil, empty array, text-only, thinking+text,
unsupported root type

```
$ go test -vet=off -run "TestMistral|TestExtractMistralContent" -count=1 -v ./internal/entity/models/...
=== RUN   TestMistralChatHandlesStringContent
--- PASS: TestMistralChatHandlesStringContent (0.00s)
=== RUN   TestMistralChatExtractsReasoningFromStructuredContent
--- PASS: TestMistralChatExtractsReasoningFromStructuredContent (0.00s)
=== RUN   TestMistralChatHandlesStructuredContentWithoutThinking
--- PASS: TestMistralChatHandlesStructuredContentWithoutThinking (0.00s)
=== RUN   TestMistralChatIgnoresUnknownContentPartTypes
--- PASS: TestMistralChatIgnoresUnknownContentPartTypes (0.00s)
=== RUN   TestExtractMistralContent
=== RUN   TestExtractMistralContent/plain_string
=== RUN   TestExtractMistralContent/empty_string
=== RUN   TestExtractMistralContent/nil
=== RUN   TestExtractMistralContent/empty_array
=== RUN   TestExtractMistralContent/text_only
=== RUN   TestExtractMistralContent/thinking_then_text
=== RUN   TestExtractMistralContent/unknown_root_type
--- PASS: TestExtractMistralContent (0.00s)
PASS
ok      ragflow/internal/entity/models  0.046s
```

All 32 Mistral tests pass on go 1.25. `go build
./internal/entity/models/...` exits 0.

**Live integration test** — driver exercised against `api.mistral.ai/v1`
with the patched code:

```
=== RUN   TestMistralMagistralSmoke
    [OK] "magistral-small-latest" present upstream
    [OK] "magistral-medium-latest" present upstream
    [OK trivial]    Answer="7"  ReasonContent=""
    [OK reasoning]  Answer len=797   head="To determine when the two trains meet, we can follow these steps:\n\n1. **Identify..."
                    ReasonContent len=1069 head="Okay, let's see. There are two trains, one going 60 mph and the other going 90 mph. They're moving towards each other, s..."

MAGISTRAL SMOKE PASSED
--- PASS: TestMistralMagistralSmoke (18.09s)
PASS
ok      ragflow/internal/entity/models  18.112s
```

What the live run proves on the wire:

- `magistral-small-latest` with a trivial prompt still uses the
string-content shape; the regression-net path is exercised against the
real server, not just the mock.
- `magistral-medium-latest` with a reasoning prompt uses the
structured-array shape; the new code path extracts a 1069-character
reasoning trace into `ChatResponse.ReasonContent` and a 797-character
visible answer into `ChatResponse.Answer`. Before this fix, the same
call returned `"invalid content format"` and the caller saw nothing.

The smoke-test file itself is not committed (live tests live outside the
PR diff, same convention used for prior provider PRs).

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-05-21 15:33:14 +08:00
sapienza yoan
9d37234953 build(go): make bash build.sh work on macOS arm64 (Homebrew) (#15009)
## Problem

The Go server build pipeline (`build.sh` + CMake + CGO bindings) was
tested on Ubuntu only. On macOS arm64 with Homebrew it fails in five
orthogonal places. None of these require platform-specific code paths —
the same source builds on both Linux and Darwin after these fixes.

## Reproduction (before)

```
$ uname -a
Darwin … 25.4.0 arm64
$ brew install cmake pcre2 simde
$ bash build.sh
…
error: 'simde/x86/sse4.1.h' file not found
error: implicit instantiation of undefined template 'std::basic_istringstream<char>'
error: no matching function for call to 'Join'
…
clang: error: no such file or directory: '/usr/local/lib/libpcre2-8.a'
```

## Fix (5 small, orthogonal changes)

### 1. `internal/cpp/CMakeLists.txt` — find Homebrew + libpcre2-8
portably

- Detect Apple platforms via `if(APPLE)`, call `brew --prefix` once, add
`${HOMEBREW_PREFIX}/include` and `${HOMEBREW_PREFIX}/lib`. No effect on
Linux.
- Replace the literal `libpcre2-8.a` link token (which only the Linux
linker finds in `/usr/local/lib` by default) with
`find_library(PCRE2_LIB NAMES pcre2-8 REQUIRED)`. Works on
`/usr/lib/x86_64-linux-gnu` (Debian/Ubuntu), `/usr/local/lib` (Intel Mac
& legacy Linux), `/opt/homebrew/lib` (Apple Silicon).

### 2. `internal/cpp/wordnet_lemmatizer.cpp` +
`internal/cpp/rag_analyzer.cpp` — explicit `#include <sstream>`

libstdc++ (Linux) pulls `<sstream>` in transitively via `<fstream>`;
libc++ (Apple Clang) doesn't, so the existing `std::istringstream` /
`std::ostringstream` uses fail to compile on macOS. One-line include in
each file.

### 3. `internal/cpp/rag_analyzer.cpp` — `Join` template overload fix

`Join(tokens, start, tokens.size(), delim)` at line 146 passes `size_t`
to an `int` parameter. C++23 strict mode in Apple Clang refuses the
implicit narrowing and reports the 4-arg overload as a substitution
failure, leaving the call ambiguous between the 3-arg and 4-arg
templates. Fix: explicit `static_cast<int>(tokens.size())`. Behaviour
identical on libstdc++ — the narrowing was always intentional.

### 4. `internal/binding/rag_analyzer.go` — split darwin CGO LDFLAGS

The existing `#cgo darwin LDFLAGS: ... /usr/local/lib/libpcre2-8.a` only
matches Intel Macs. Apple Silicon Homebrew installs to `/opt/homebrew`.
Split into `darwin,arm64` and `darwin,amd64` build constraints with the
right absolute path on each.

### 5. `build.sh` — accept Homebrew path in the pcre2 sanity check

The sanity check looked at two Linux paths only and then fell through to
`sudo apt -y install libpcre2-dev` on failure. Added
`/opt/homebrew/lib/libpcre2-8.a`, and on Darwin failure now exits
cleanly with the right `brew install pcre2` hint instead of trying
`apt`.

## Verified

- `bash build.sh` now completes on macOS arm64 (Apple Silicon, brew 4.x,
cmake 4.x, Apple Clang 17, Go 1.25, pcre2 10.x, simde 0.8.x).
- Produced binaries: `bin/server_main`, `bin/admin_server`,
`bin/ragflow_cli`.
- `bin/server_main` boots, connects MySQL, runs migrations, loads the 64
model provider configs cleanly.
- Still builds on Linux — the CMake additions are inside an `if(APPLE)`
guard, the `find_library` call matches Linux paths too, the build.sh
check still tries `apt` when not on Darwin.

## Out of scope

The Go server itself currently fails at runtime when not pointing at
Elasticsearch (`Failed to initialize doc engine: failed to ping
Elasticsearch`), but that's the placeholder Infinity engine documented
in `internal/engine/README.md` — unrelated to this build patchset.

---

Happy to split this into smaller PRs if you'd prefer (one per file). The
five changes are independent.
2026-05-21 15:33:09 +08:00
BitToby
bd4ce39038 Go: implement provider: Perplexity (#15008)
## What
- Add Perplexity as a chat and embedding provider backed by its
OpenAI-compatible `/chat/completions` and `/v1/embeddings` APIs
- Register Perplexity in the Go model factory and provider config
- Support non-streaming chat, SSE streaming chat, embeddings, model
listing, and connection checks

Refs #14736

---------

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-21 15:33:02 +08:00
dripsmvcp
d5ba14a128 feat(go): implement provider Astraflow (#15062) (#15064)
- Adds an `Astraflow` Go driver so the new API server can route
Astraflow (UCloud ModelVerse) chat instances, matching the existing
Python `AstraflowChat` (`rag/llm/chat_model.py:1237`). Follows the same
SaaS-driver shape used for Avian, Novita, TogetherAI, Replicate,
DeepInfra, Upstage, and LongCat.

Closes #15062

---------

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-21 15:32:56 +08:00
dripsmvcp
5a18df0fd0 Go: implement provider: Avian (#15045)
Closes #15044.

Avian was listed unchecked in the Go-rewrite tracker #14736 and already
had an llm_factories.json entry with 4 preconfigured chat models
(deepseek-v3.2, kimi-k2.5, glm-5, minimax-m2.5), but the Go API server
had no driver to route them. The Python side has supported Avian at
rag/llm/chat_model.py:1220 (AvianChat) via the LiteLLM openai/ provider
with default base https://api.avian.io/v1.

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-21 15:32:49 +08:00
sxxtony
7740ec6c95 Go: implement Embed (embeddings) in Replicate driver (#15073)
### What problem does this PR solve?

`ReplicateModel.Embed` in `internal/entity/models/replicate.go` was a
`"replicate, no such method"` stub. Tracking issue #14736 lists
Replicate's embedding surface as not implemented. This PR wires it up
against Replicate's documented embedding schema.

Until this PR, a tenant who selected a Replicate embedding model got the
sentinel error on every embed call.

Co-authored-by: sxxtony <sxxtony@users.noreply.github.com>
Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-21 15:32:41 +08:00
天海蒼灆
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)
2026-05-21 15:32:32 +08:00
Stephen Hu
da112e3db0 Refactor:improve dify retrieval logic (#15036)
### What problem does this PR solve?

improve dify retrieval logic for o(n) io to o(1)

### Type of change
- [x] Refactoring
2026-05-21 15:32:24 +08:00
Kevin Hu
e7544562cc Feat: @tool decorator for chat-model tool registration (#15047)
## Summary

- Adds a lightweight `@tool` decorator and `FunctionToolSession` adapter
in `rag/llm/tool_decorator.py` that let callers register plain Python
functions as LLM tools without hand-writing OpenAI function schemas or
building an MCP-style session.
- Refactors `Base.bind_tools` and `LiteLLMBase.bind_tools` in
`rag/llm/chat_model.py` to accept either the new decorator form
`bind_tools(tools=[fn1, fn2])` or the existing `(toolcall_session,
tools_schemas)` form, so existing agent/dialog call-sites in
`agent/component/agent_with_tools.py`, `api/db/services/llm_service.py`,
and `api/db/services/dialog_service.py` are unaffected.
- Adds 8 unit tests in `test/unit_test/rag/llm/test_tool_decorator.py`
covering schema shape, required/optional inference, sync + async
dispatch, and bad-input rejection.

## Usage

```python
from rag.llm.tool_decorator import tool

@tool
def get_weather(city: str) -> str:
    """Get current weather for a city.

    :param city: City name to look up.
    """
    return f"{city}: 21 C, partly cloudy"

chat_mdl.bind_tools(tools=[get_weather])
ans, tk = await chat_mdl.async_chat_with_tools(system, history)
```

The decorator introspects `inspect.signature` + type hints + the
docstring (`:param name:` style) and attaches an OpenAI-format
`openai_schema` to the callable. `FunctionToolSession` duck-types the
existing `ToolCallSession` protocol, dispatching async callables
directly and sync ones through `thread_pool_exec` so the event loop is
never blocked.

## Design notes

- `tool_decorator.py` deliberately does **not** live inside
`rag/llm/__init__.py` to avoid forcing every consumer through the heavy
provider auto-discovery loop and to sidestep a circular import
(`__init__.py` imports `chat_model`, which would otherwise need symbols
from `__init__.py`).
- `FunctionToolSession` is duck-typed against
`common.mcp_tool_call_conn.ToolCallSession` rather than explicitly
inheriting from it, so importing the decorator doesn't pull the MCP
client SDK into the import graph.
- Docstring parsing is intentionally minimal (`:param name:` only) to
keep this dependency-free; Google/NumPy styles can be added later via
`docstring_parser` if needed.

## Test plan

- [x] `python -m pytest test/unit_test/rag/llm/test_tool_decorator.py
-v` — 8 passed
- [x] `python -m pytest test/unit_test/rag/llm/
--ignore=test/unit_test/rag/llm/test_perplexity_embed.py` — 11 passed
(the ignored test has a pre-existing `numpy` import that's unrelated)
- [ ] Reviewer: smoke-test the new path end-to-end with a live model via
`chat_mdl.bind_tools(tools=[my_fn])` to confirm the OpenAI-format
schemas pass through unchanged

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 15:32:17 +08:00
bitloi
a6186244ee fix: handle missing SDK authorization headers (#15050)
### What problem does this PR solve?

Closes #15048.

Several SDK session routes in `api/apps/sdk/session.py` called
`.split()` directly on `request.headers.get("Authorization")`. When
clients omitted the header, the handlers raised `AttributeError` before
returning the existing `Authorization is not valid!` response.

This PR centralizes SDK Authorization parsing in a small helper and
keeps the existing error response for missing, empty, or malformed
headers.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

### Tests

- `ZHIPU_AI_API_KEY=dummy uv run --python 3.13 --group test pytest
test/testcases/test_http_api/test_session_management/test_session_sdk_routes_unit.py::test_sdk_session_routes_missing_authorization_unit
-q`
- `uv run --python 3.13 --group test ruff check api/apps/sdk/session.py
test/testcases/test_http_api/test_session_management/test_session_sdk_routes_unit.py`
- `python3 -m py_compile api/apps/sdk/session.py
test/testcases/test_http_api/test_session_management/test_session_sdk_routes_unit.py`
- `git diff --check`
2026-05-21 15:32:00 +08:00
kingloon
da4eaf9fb0 Fix: remove duplicate function definitions (#15063)
### What problem does this PR solve?

Remove duplicate function definitions in
`api/db/services/dialog_service.py`.

**Problem:** Two helper functions were defined twice in the same file,
but with different parameter orders:

- First definition (line 57):
`_resolve_reference_metadata(request_payload=None, config=None)`
- Second definition (line 136): `_resolve_reference_metadata(config,
request_payload=None)`

**Solution:** Keep the second definition (which is actually used by
other modules) and remove the first one to avoid confusion.

Additionally, remove duplicate `_enrich_chunks_with_document_metadata`
definition (keep line 140 version).
<img width="1584" height="313" alt="image"
src="https://github.com/user-attachments/assets/7daee832-244f-4bb2-8488-e3b65012a3f9"
/>
<img width="1672" height="359" alt="image"
src="https://github.com/user-attachments/assets/4fd2f523-273c-4b20-a7c9-ab35740b7834"
/>


### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [ ] New Feature (non-breaking change which adds functionality)
- [ ] Documentation Update
- [x] Refactoring
- [ ] Performance Improvement
- [ ] Other (please describe):
2026-05-21 15:31:51 +08:00
kpdev
6932615852 fix(api): infer /documents/{id}/download Content-Type from filename when ext is omitted (#15052) (#15053)
## Summary

- Align **GET `/api/v1/documents/<doc_id>/download`** with
**`/preview`**: resolve extension and MIME type from the stored document
name when the **`ext` query parameter is omitted**, instead of
defaulting to `markdown`.
- When **`?ext=`** is present, behavior stays the same as before
(explicit extension / `Content-Type` mapping).
- Enforce the same access + document lookup pattern as preview
(**`accessible`** + **`get_by_id`**).
- Extend unit tests for the no-`ext` PDF filename case.

## Test plan

- [x] `uv run pytest
test/testcases/test_web_api/test_document_app/test_document_metadata.py::TestDocumentMetadataUnit::test_download_attachment_success_and_exception_unit`
- [x] Optional: `curl -sSI` against
`/api/v1/documents/<pdf_doc_id>/download` without `ext` and confirm
`Content-Type: application/pdf`

Fixes #15052.
2026-05-21 15:31:36 +08:00
dripsmvcp
440153c378 fix(api): check kb ownership in /dify/retrieval (#15028)
POST /api/v1/dify/retrieval resolved the caller via @apikey_required
(injecting tenant_id) but then fetched the requested knowledge_id with
no tenant filter and ran the full retrieval pipeline against
kb.tenant_id (the owner). Any valid Dify-compatible API key could
retrieve chunks from any tenant whose KB UUID was known. Adds the
missing ownership check.

## Root Cause
api/apps/sdk/dify_retrieval.py line 253:
KnowledgebaseService.get_by_id(kb_id) fetched the KB by id alone, then
the handler used kb.tenant_id (the OWNER) to build the embedding model
and call the retriever. The caller tenant_id was only used downstream at
line 278 for retrieval_by_children, well after cross-tenant data was
already retrieved.

grep confirmed there was no KnowledgebaseService.accessible call
anywhere in the handler.

## Fix
Two-line guard immediately after the existing get_by_id lookup,
mirroring the pattern PR #14749 lands for the sibling sdk/doc.py routes
(download, parse, stop_parsing, retrieval_test):

    e, kb = KnowledgebaseService.get_by_id(kb_id)
    if not e:
return build_error_result(message="Knowledgebase not found!",
code=RetCode.NOT_FOUND)
+   if not KnowledgebaseService.accessible(kb_id, tenant_id):
+ return build_error_result(message="No authorization.",
code=RetCode.AUTHENTICATION_ERROR)
    if kb.tenant_embd_id:
        ...

KnowledgebaseService.accessible already handles solo-tenant ownership,
team membership via TenantService.get_joined_tenants_by_user_id, and the
permission=ME distinction. No behavior change for legitimate callers;
cross-tenant callers now receive RetCode.AUTHENTICATION_ERROR (109).

## Test Plan
- [x] Regression test added:
test/unit_test/api/apps/sdk/test_dify_retrieval.py
- test_cross_tenant_request_is_rejected -- attacker tenant calling owner
tenant KB gets 109; retriever is not invoked
- test_same_tenant_request_succeeds -- owner tenant gets the records
back
- test_missing_knowledge_base_returns_not_found -- missing KB returns
404 BEFORE the access check fires (legit callers see the clearer
message)
- [x] All 3 tests pass after the fix
- [x] Cross-tenant test FAILS on pre-fix main (KeyError on result[code]
because handler leaks records dict instead of returning auth error)
- [x] ruff check clean on both changed files
- [x] No drive-by reformatting in dify_retrieval.py -- only the 2 added
lines

### Post-fix output

    test_cross_tenant_request_is_rejected           PASSED [ 33%]
    test_same_tenant_request_succeeds               PASSED [ 66%]
    test_missing_knowledge_base_returns_not_found   PASSED [100%]

============================== 3 passed in 0.04s
===============================

Closes #15027
2026-05-21 13:29:00 +08:00
Chan
0c93161a14 fix: prevent session user_id spoofing via request body (#15077)
### What problem does this PR solve?

Closes #15076 

Two endpoints in `api/apps/restful_apis/chat_api.py` accepted a
`user_id` field from the request body and used it directly when creating
a session:

```python
# before (vulnerable)
"user_id": req.get("user_id", current_user.id)          # create_session
conv = await _create_session_for_completion(chat_id, dia, req.get("user_id", current_user.id))  # session_completion
```

Any authenticated caller could supply an arbitrary `user_id` and have
the new session attributed to a different user — effectively spoofing
session ownership. Both call sites are now fixed to always use
`current_user.id`, which is set by the authentication middleware and
cannot be tampered with via the request payload.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

### Changes

| File | Change |
|------|--------|
| `api/apps/restful_apis/chat_api.py` | Remove `req.get("user_id", ...)`
fallback in `create_session` and `session_completion`; always use
`current_user.id` |
|
`test/testcases/test_http_api/test_session_management/test_session_sdk_routes_unit.py`
| Add `test_create_session_user_id_not_spoofable` and
`test_session_completion_user_id_not_spoofable` (both `@pytest.mark.p2`)
|

### Testing

Two new unit tests assert that a `user_id` value supplied in the request
body is silently ignored and the session is always owned by the
authenticated user:

```
test_create_session_user_id_not_spoofable
test_session_completion_user_id_not_spoofable
```

Run with:

```bash
uv run pytest test/testcases/test_http_api/test_session_management/test_session_sdk_routes_unit.py -k "spoofable" -v
```
2026-05-21 13:28:14 +08:00
web-dev0521
2d3a1a4483 feat(go-models): add Azure OpenAI model driver (#15022)
## What problem does this PR solve?

Closes #15021.

The Go model-provider layer had no support for **Azure OpenAI**. Azure
OpenAI is *not* a drop-in base-URL swap of the OpenAI driver — it
differs in authentication, endpoint structure, and how models are listed
— so it needs its own `ModelDriver` implementation.

## Type of change

- [x] New feature (non-breaking change which adds functionality)

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-21 11:52:56 +08:00
Renzo
c7ac9b7171 Go: implement provider: GPUStack (chat) (#15024)
### What problem does this PR solve?

Fixes #15023

GPUStack is listed as unchecked in the Go-rewrite tracker #14736, and
`internal/service/llm.go:171` already classifies it as a self-deployed
provider alongside Ollama, Xinference, LocalAI, and LM Studio — but
`internal/entity/models/` had no `gpustack.go` driver, so the new Go API
server could not route GPUStack instances. This PR adds the chat surface
for GPUStack so it lines up with the existing self-hosted Go drivers.

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-21 11:49:18 +08:00
Renzo
394cd5d116 Go: implement Embed in Xinference driver (#14932)
## Summary

- Replaces the `"no such method"` stub on `XinferenceModel.Embed`
(`internal/entity/models/xinference.go`) with a real implementation
against Xinference's OpenAI-compatible `/v1/embeddings` endpoint.
- Adds the `"embedding": "v1/embeddings"` URL suffix to
`conf/models/xinference.json`.
- Mirrors the Python `XinferenceEmbed` class in
`rag/llm/embedding_model.py:407` for payload shape (OpenAI-compatible
`model + input` → `data[*].index + data[*].embedding`) and tolerates the
same no-auth default Xinference deployments use. Authorization is only
sent when a non-empty API key is configured, via the existing
`setXinferenceAuth` helper.
- Reuses the existing `normalizeXinferenceBaseURL` + `baseURLForRegion`
helpers so both `http://127.0.0.1:9997` and `http://127.0.0.1:9997/v1`
resolve to the same `/v1/embeddings` target without doubled `/v1`.
- Validates response indices — duplicate, missing, or out-of-range
`data[*].index` values fail with a clear error rather than silently
producing misaligned vectors.
- Returns `[]EmbeddingData` in original input order (placed by `Index`)
so downstream callers can index positionally without re-sorting.
- Forwards `EmbeddingConfig.Dimension` as `dimensions` when `> 0`,
matching the OpenAI cluster pattern.

Closes #14810

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-21 11:47:30 +08:00
Renzo
fec0b968e7 Go: implement Rerank in Novita driver (#15014)
### What problem does this PR solve?

Fixes #15012

The Novita Go driver landed in #14850 and shipped a stub `Rerank` method
that returned `"novita, no such method"`, so Novita could not be used as
a rerank provider in RAGFlow. This PR fills that gap, in the same way
#14895 filled the Embed gap on the same driver.

Novita exposes a public rerank endpoint at `POST
https://api.novita.ai/openai/v1/rerank` that accepts the
Cohere-compatible request shape (`{model, query, documents, top_n}`)
with `Authorization: Bearer <api_key>`. `baai/bge-reranker-v2-m3` is
documented in Novita's model library with a 1024-token limit.
2026-05-21 10:19:17 +08:00
Renzo
536ed07d27 Go: implement Rerank in Xinference driver (#15032)
### What problem does this PR solve?

Fixes #14816

The Xinference Go driver landed chat in #14938 and Embed is in review in
#14932, but `Rerank` shipped as a stub that returns `"xinference, no
such method"`. Tenants who launch a rerank model with `--model-type
rerank` on their Xinference instance cannot route it through the Go API
server. This PR fills the gap.

Xinference exposes an OpenAI-compatible REST API. The rerank endpoint is
at `POST <base>/v1/rerank` and accepts the Cohere-shaped body `{model,
query, documents, top_n}`, returning `{results: [{index,
relevance_score}]}` — the same wire shape used by the merged NVIDIA
(#14778), Aliyun (#14676), Gitee (#14656), ZhipuAI (#14608), Novita
(#15014), and LocalAI (#14813) Rerank implementations. Documented in
[Xinference rerank
docs](https://inference.readthedocs.io/en/v1.6.1/models/model_abilities/rerank.html);
the [builtin rerank model
catalog](https://inference.readthedocs.io/en/stable/models/builtin/rerank/)
lists `bge-reranker-base`, `bge-reranker-large`, `bge-reranker-v2-m3`,
and others.
2026-05-21 10:14:30 +08:00
sxxtony
63db30f0d9 Go: implement provider: n1n.ai (#15010)
### What problem does this PR solve?

Add a Go driver for **n1n.ai** (https://docs.n1n.ai), one of the
unchecked providers on the umbrella tracking issue #14736. n1n.ai is an
OpenAI-compatible aggregator hosting a 450+ model catalog (GPT, Claude,
Gemini, DeepSeek, Kimi, Qwen, embedding + reranker families) under
`https://api.n1n.ai/v1`.

Until this PR, a tenant who configured `n1n` as a model provider in the
Go layer fell through to the default branch of
`internal/entity/models/factory.go` and got the dummy driver.

---------

Co-authored-by: sxxtony <sxxtony@users.noreply.github.com>
2026-05-21 10:13:15 +08:00
Jack Storment
dc01e0e51c Go: implement Embed (embeddings) in TogetherAI driver (#15017)
### What problem does this PR solve?

Fixes #15015

The TogetherAI Go driver in `internal/entity/models/togetherai.go`
shipped a stub `Embed` method that returned `"TogetherAI, no such
method"`, so TogetherAI could not be used as an embedding provider in
RAGFlow. This PR fills that gap.

TogetherAI exposes a public OpenAI-compatible embeddings endpoint at
`POST https://api.together.ai/v1/embeddings` that accepts the standard
`{model, input}` shape with `Authorization: Bearer <api_key>` (confirmed
in TogetherAI's official docs:
https://docs.together.ai/docs/embeddings-overview). Documented embedding
models include `intfloat/multilingual-e5-large-instruct`,
`BAAI/bge-large-en-v1.5`, and `BAAI/bge-base-en-v1.5`.

### Changes

- `internal/entity/models/togetherai.go`: implement
`TogetherAIModel.Embed`.
- Validate inputs (api key, model name) and short-circuit on empty
texts.
  - Resolve region with the existing `baseURLForRegion` helper.
  - Build URL from `URLSuffix.Embedding`.
- Send `{model, input}` POST body, add `dimensions` when
`embeddingConfig.Dimension > 0` (matches the pattern in #14735).
  - Bearer auth + JSON content type, mirroring the chat path.
- Parse `{data: [{embedding, index}]}` and reorder by `index`, rejecting
out-of-range indices, duplicates, and missing entries so the output
always lines up with the input. Same shape as the merged Mistral,
Upstage, and Novita Embed implementations.
- `conf/models/togetherai.json`:
  - Add `"embedding": "embeddings"` to `url_suffix`.
- Add default embedding model entries for
`intfloat/multilingual-e5-large-instruct`, `BAAI/bge-large-en-v1.5`, and
`BAAI/bge-base-en-v1.5`.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-05-20 20:48:44 +08:00
chanx
aa0a3d6988 Fix: The logs on the data source details page are not fully displayed. (#15056)
### What problem does this PR solve?

Fix: The logs on the data source details page are not fully displayed.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-05-20 20:32:31 +08:00
qinling0210
dbef3e361f Update chunk/metadata cli (#15055)
### What problem does this PR solve?

Update chunk/metadata cli

### Type of change

- [ ] Refactoring
2026-05-20 20:32:06 +08:00
3147 changed files with 625372 additions and 74546 deletions

View File

@@ -3,4 +3,4 @@ name: go-naming
description: Go naming conventions and best practices. Use this skill when working with Go code and need to name packages, files, directories, structs, interfaces, functions, variables, or constants. Provides comprehensive naming guidelines following Go community standards.
---
Strictly follow the naming conventions in [rules/named.md](rules/named.md)
Strictly follow the naming conventions in [rules/named.md](../../rules/named.md)

58
.dockerignore Normal file
View File

@@ -0,0 +1,58 @@
# RAGFlow .dockerignore
# Reduces Docker build context sent to the daemon.
# All excluded items are either rebuilt inside Docker, mounted from
# infiniflow/ragflow_deps, or are local-only artifacts.
# ── Python virtual environments ─────────────────────────────────────────────
.venv/
venv/
__pycache__/
*.pyc
*.pyo
*.egg-info/
.pytest_cache/
# ── Frontend dependencies and build outputs ─────────────────────────────────
web/node_modules/
web/dist/
# ── Runtime logs ────────────────────────────────────────────────────────────
logs/
*.log
docker/ragflow-logs/
# ── Docker runtime data ─────────────────────────────────────────────────────
docker/data/
docker/oceanbase/
docker/seekdb/
# ── Go and C++ build outputs ────────────────────────────────────────────────
internal/binding/cpp/build/
internal/binding/cpp/cmake-build-release/
internal/binding/cpp/cmake-build-debug/
target/
# ── ragflow_deps build context (built as a separate image, mounted ──
# ── from infiniflow/ragflow_deps:latest by the main Dockerfile) ──
# Excluding the entire directory keeps the main build context small
# regardless of which deps files download_deps.py currently fetches.
# The deps image is built from this directory with:
# cd ragflow_deps && docker build -f Dockerfile -t infiniflow/ragflow_deps .
ragflow_deps/
# ── IDE and editor config ──────────────────────────────────────────────────
.idea/
.vscode/
.cursor/
.trae/
.DS_Store
# ── Test and coverage artifacts ─────────────────────────────────────────────
coverage/
htmlcov/
.coverage
.hypothesis/
.nox/
# ── Docker env (contains secrets) ───────────────────────────────────────────
docker/.env

25
.github/codeql/codeql-config.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
# CodeQL configuration. The default CodeQL Analysis workflow (managed by
# GitHub) reads this file when scanning the repository. We use it to
# exclude files that the Go analysis cannot compile — the rest of the
# repo compiles fine, but the CGO-based office_oxide bindings require
# a native header (`office_oxide.h`) that isn't present in the CodeQL
# runner image. Without this exclusion the entire Go analysis aborts
# with `fatal error: office_oxide.h: No such file or directory`, which
# means no Go alerts can be re-evaluated and alerts on these files
# stay open indefinitely even after their root cause is fixed.
#
# The excluded files are MS Office document parsers. They are also
# excluded from `go test` and `go build` in local development when
# the office_oxide C library is not installed, so this exclusion
# brings CodeQL in line with the rest of the toolchain.
paths-ignore:
- internal/ingestion/parser/doc_parser.go
- internal/ingestion/parser/docx_parser.go
- internal/ingestion/parser/ppt_parser.go
- internal/ingestion/parser/pptx_parser.go
- internal/ingestion/parser/xls_parser.go
- internal/ingestion/parser/xlsx_parser.go
# Generated / vendored — also break analysis without adding signal.
- "**/testdata/**"
- "**/node_modules/**"
- "**/*.pb.go"

View File

@@ -1,12 +1,3 @@
### What problem does this PR solve?
### Summary
_Briefly describe what this PR aims to solve. Include background context that will help reviewers understand the purpose of the PR._
### Type of change
- [ ] 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):

View File

@@ -9,6 +9,7 @@ on:
push:
tags:
- "v*.*.*" # normal release
- 'nightly' # mutable tag
permissions:
contents: write
@@ -22,8 +23,12 @@ concurrency:
cancel-in-progress: true
jobs:
release:
prepare:
runs-on: [ "self-hosted", "ragflow-release" ]
outputs:
release_tag: ${{ steps.release.outputs.release_tag }}
prerelease: ${{ steps.release.outputs.prerelease }}
steps:
- name: Ensure workspace ownership
run: echo "chown -R ${USER} ${GITHUB_WORKSPACE}" && sudo chown -R ${USER} ${GITHUB_WORKSPACE}
@@ -36,7 +41,15 @@ jobs:
fetch-depth: 0
fetch-tags: true
- name: Prepare release body
# https://github.com/actions/setup-go
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- name: Prepare release metadata
id: release
run: |
if [[ ${GITHUB_EVENT_NAME} != "schedule" ]]; then
RELEASE_TAG=${GITHUB_REF#refs/tags/}
@@ -53,8 +66,8 @@ jobs:
fi
echo "RELEASE_TAG=${RELEASE_TAG}" >> ${GITHUB_ENV}
echo "PRERELEASE=${PRERELEASE}" >> ${GITHUB_ENV}
RELEASE_DATETIME=$(date --rfc-3339=seconds)
echo Release ${RELEASE_TAG} created from ${GITHUB_SHA} at ${RELEASE_DATETIME} > release_body.md
echo "release_tag=${RELEASE_TAG}" >> ${GITHUB_OUTPUT}
echo "prerelease=${PRERELEASE}" >> ${GITHUB_OUTPUT}
- name: Move the existing mutable tag
# https://github.com/softprops/action-gh-release/issues/171
@@ -72,20 +85,197 @@ jobs:
fi
fi
- name: Create or overwrite a release
# https://github.com/actions/upload-release-asset has been replaced by https://github.com/softprops/action-gh-release
build_cli:
needs: prepare
strategy:
fail-fast: false
matrix:
include:
- goos: linux
goarch: amd64
runner: ubuntu-24.04
- goos: linux
goarch: arm64
runner: ubuntu-24.04-arm
- goos: darwin
goarch: amd64
runner: macos-15-intel
- goos: darwin
goarch: arm64
runner: macos-14
- goos: windows
goarch: amd64
runner: windows-latest
output_ext: .exe
- goos: windows
goarch: arm64
runner: windows-11-arm
output_ext: .exe
runs-on: ${{ matrix.runner }}
env:
CLI_NAME: ragflow-cli
CLI_MAIN: ./cmd/ragflow-cli.go
DIST_DIR: dist/cli
RELEASE_TAG: ${{ needs.prepare.outputs.release_tag }}
steps:
# https://github.com/actions/checkout/blob/v6/README.md
- name: Check out code
uses: actions/checkout@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
fetch-tags: true
# https://github.com/actions/setup-go
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- name: Build Go CLI release binaries on non-Windows
if: runner.os != 'Windows'
shell: bash
run: |
set -euo pipefail
mkdir -p "${DIST_DIR}"
if [[ ! -e "${CLI_MAIN}" ]]; then
echo "::error::Go CLI entry does not exist: ${CLI_MAIN}"
echo "::error::Please update CLI_MAIN in .github/workflows/release.yml"
exit 1
fi
echo "Building Go CLI release binaries"
echo "CLI name: ${CLI_NAME}"
echo "CLI main: ${CLI_MAIN}"
echo "Release tag: ${RELEASE_TAG}"
echo "Commit: ${GITHUB_SHA}"
output="${DIST_DIR}/${CLI_NAME}-${RELEASE_TAG}-${{ matrix.goos }}-${{ matrix.goarch }}"
echo "Building ${{ matrix.goos }}/${{ matrix.goarch }} -> ${output}"
CGO_ENABLED=0 \
GOOS="${{ matrix.goos }}" \
GOARCH="${{ matrix.goarch }}" \
go build \
-trimpath \
-ldflags="-s -w -X main.version=${RELEASE_TAG} -X main.commit=${GITHUB_SHA}" \
-o "${output}" \
"${CLI_MAIN}"
chmod +x "${output}"
- name: Build Go CLI release binaries on Windows
if: runner.os == 'Windows'
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path $env:DIST_DIR | Out-Null
if (-not (Test-Path $env:CLI_MAIN)) {
Write-Error "Go CLI entry does not exist: $env:CLI_MAIN"
exit 1
}
$output = Join-Path $env:DIST_DIR "${env:CLI_NAME}-${env:RELEASE_TAG}-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.output_ext }}"
Write-Host "Building ${{ matrix.goos }}/${{ matrix.goarch }} -> $output"
$env:CGO_ENABLED = "0"
$env:GOOS = "${{ matrix.goos }}"
$env:GOARCH = "${{ matrix.goarch }}"
go build `
-trimpath `
-ldflags="-s -w -X main.version=$env:RELEASE_TAG -X main.commit=$env:GITHUB_SHA" `
-o "$output" `
"$env:CLI_MAIN"
- name: Upload CLI artifact
uses: actions/upload-artifact@v4
with:
name: cli-${{ matrix.goos }}-${{ matrix.goarch }}
path: dist/cli/*
if-no-files-found: error
publish_cli_assets:
needs:
- prepare
- build_cli
runs-on: [ "self-hosted", "ragflow-release" ]
steps:
- name: Ensure workspace ownership
run: echo "chown -R ${USER} ${GITHUB_WORKSPACE}" && sudo chown -R ${USER} ${GITHUB_WORKSPACE}
# https://github.com/actions/checkout/blob/v6/README.md
- name: Check out code
uses: actions/checkout@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
fetch-tags: true
- name: Download CLI artifacts
uses: actions/download-artifact@v5
with:
pattern: cli-*
path: dist/cli
merge-multiple: true
- name: Prepare CLI release assets
env:
RELEASE_TAG: ${{ needs.prepare.outputs.release_tag }}
run: |
set -euo pipefail
RELEASE_DATETIME=$(date --rfc-3339=seconds)
echo Release ${RELEASE_TAG} created from ${GITHUB_SHA} at ${RELEASE_DATETIME} > release_body.md
cd dist/cli
sha256sum * > SHA256SUMS
cd -
echo "Generated CLI release assets:"
ls -lh dist/cli
- name: Upload Go CLI release assets
uses: softprops/action-gh-release@v2
with:
token: ${{ secrets.GITHUB_TOKEN }} # Use the secret as an environment variable
prerelease: ${{ env.PRERELEASE }}
tag_name: ${{ env.RELEASE_TAG }}
# The body field does not support environment variable substitution directly.
token: ${{ secrets.GITHUB_TOKEN }}
prerelease: ${{ needs.prepare.outputs.prerelease }}
tag_name: ${{ needs.prepare.outputs.release_tag }}
body_path: release_body.md
files: |
dist/cli/*
tools/scripts/install.sh
tools/scripts/install.ps1
release:
needs:
- prepare
- publish_cli_assets
runs-on: [ "self-hosted", "ragflow-release" ]
env:
RELEASE_TAG: ${{ needs.prepare.outputs.release_tag }}
steps:
- name: Ensure workspace ownership
run: echo "chown -R ${USER} ${GITHUB_WORKSPACE}" && sudo chown -R ${USER} ${GITHUB_WORKSPACE}
# https://github.com/actions/checkout/blob/v6/README.md
- name: Check out code
uses: actions/checkout@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
fetch-tags: true
- name: Build and push image
run: |
sudo docker login --username infiniflow --password-stdin <<< ${{ secrets.DOCKERHUB_TOKEN }}
sudo docker build --build-arg NEED_MIRROR=1 --build-arg HTTPS_PROXY=${HTTPS_PROXY} --build-arg HTTP_PROXY=${HTTP_PROXY} -t infiniflow/ragflow:${RELEASE_TAG} -f Dockerfile .
sudo docker build -t infiniflow/ragflow:${RELEASE_TAG} -f Dockerfile .
sudo docker tag infiniflow/ragflow:${RELEASE_TAG} infiniflow/ragflow:latest
sudo docker push infiniflow/ragflow:${RELEASE_TAG}
sudo docker push infiniflow/ragflow:latest

1189
.github/workflows/sep-tests.yml vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,7 @@ on:
# — pull_request_target workflows use the workflow files from the default branch, and secrets are available.
# — pull_request workflows use the workflow files from the pull request branch, and secrets are unavailable.
pull_request:
types: [ synchronize, ready_for_review ]
types: [opened, synchronize, reopened, ready_for_review, labeled]
paths-ignore:
- 'docs/**'
- '*.md'
@@ -29,12 +29,14 @@ concurrency:
cancel-in-progress: true
jobs:
ragflow_tests:
name: ragflow_tests
ragflow_preflight:
name: ragflow_preflight
# https://docs.github.com/en/actions/using-jobs/using-conditions-to-control-job-execution
# https://github.com/orgs/community/discussions/26261
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && contains(github.event.pull_request.labels.*.name, 'ci')) }}
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && contains(github.event.pull_request.labels.*.name, 'ci') && (github.event.action != 'labeled' || github.event.label.name == 'ci')) }}
runs-on: [ "self-hosted", "ragflow-test" ]
outputs:
http_api_test_level: ${{ steps.test_level.outputs.http_api_test_level }}
steps:
- name: Ensure workspace ownership
run: |
@@ -90,163 +92,320 @@ jobs:
echo "ARTIFACTS_DIR=${ARTIFACTS_DIR}" >> ${GITHUB_ENV}
rm -rf ${ARTIFACTS_DIR} && mkdir -p ${ARTIFACTS_DIR}
# https://github.com/astral-sh/ruff-action
- name: Static check with Ruff
uses: astral-sh/ruff-action@v3
with:
version: ">=0.11.x"
args: "check"
- name: Check comments of changed Python files
if: ${{ false }}
- name: Run Lefthook on changed files
run: |
if [[ ${{ github.event_name }} == 'pull_request' || ${{ github.event_name }} == 'pull_request_target' ]]; then
CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} \
| grep -E '\.(py)$' || true)
if [ -n "$CHANGED_FILES" ]; then
echo "Check comments of changed Python files with check_comment_ascii.py"
readarray -t files <<< "$CHANGED_FILES"
HAS_ERROR=0
for file in "${files[@]}"; do
if [ -f "$file" ]; then
if python3 check_comment_ascii.py "$file"; then
echo "✅ $file"
else
echo "❌ $file"
HAS_ERROR=1
set -euo pipefail
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" || "${GITHUB_EVENT_NAME}" == "pull_request_target" ]]; then
changed_files=$(mktemp)
trap 'rm -f "$changed_files"' EXIT
git diff --name-only ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} \
| while read -r file; do
if [[ -f "$file" ]]; then
printf '%s\0' "$file"
fi
fi
done
if [ $HAS_ERROR -ne 0 ]; then
exit 1
fi
done > "$changed_files"
echo "Changed files to run lefthook on:"
if [[ -s "$changed_files" ]]; then
tr '\0' '\n' < "$changed_files" | sed 's/^/ /'
else
echo "No Python files changed"
echo " (none — lefthook will be a no-op)"
fi
# LEFTHOOK_CHECK_ONLY=1 makes the pre-commit jobs verify without
# applying --fix or `git add`, so CI only checks and reports
# failures instead of rewriting the working tree.
LEFTHOOK_CHECK_ONLY=1 lefthook run pre-commit --files-from-stdin --no-auto-install < "$changed_files"
fi
- name: Build ragflow go server
- name: Set test level
id: test_level
run: |
BUILDER_CONTAINER=ragflow_build_$(od -An -N4 -tx4 /dev/urandom | tr -d ' ')
echo "BUILDER_CONTAINER=${BUILDER_CONTAINER}" >> ${GITHUB_ENV}
TZ=${TZ:-$(readlink -f /etc/localtime | awk -F '/zoneinfo/' '{print $2}')}
sudo docker run --privileged -d --name ${BUILDER_CONTAINER} -e TZ=${TZ} -e UV_INDEX=https://mirrors.aliyun.com/pypi/simple -v ${PWD}:/ragflow -v ${PWD}/internal/cpp/resource:/usr/share/infinity/resource infiniflow/infinity_builder:ubuntu22_clang20
sudo docker exec ${BUILDER_CONTAINER} bash -c "git config --global safe.directory \"*\" && cd /ragflow && ./build.sh --cpp"
./build.sh --go
if [[ -n "${BUILDER_CONTAINER}" ]]; then
sudo docker rm -f -v "${BUILDER_CONTAINER}"
fi
- name: Build ragflow:nightly
run: |
RUNNER_WORKSPACE_PREFIX=${RUNNER_WORKSPACE_PREFIX:-${HOME}}
RAGFLOW_IMAGE=infiniflow/ragflow:${GITHUB_RUN_ID}
echo "RAGFLOW_IMAGE=${RAGFLOW_IMAGE}" >> ${GITHUB_ENV}
sudo docker pull ubuntu:24.04
sudo DOCKER_BUILDKIT=1 docker build --build-arg NEED_MIRROR=1 --build-arg HTTPS_PROXY=${HTTPS_PROXY} --build-arg HTTP_PROXY=${HTTP_PROXY} -f Dockerfile -t ${RAGFLOW_IMAGE} .
set -euo pipefail
if [[ ${GITHUB_EVENT_NAME} == "schedule" ]]; then
export HTTP_API_TEST_LEVEL=p3
else
export HTTP_API_TEST_LEVEL=p2
fi
echo "HTTP_API_TEST_LEVEL=${HTTP_API_TEST_LEVEL}" >> ${GITHUB_ENV}
echo "RAGFLOW_CONTAINER=${GITHUB_RUN_ID}-ragflow-cpu-1" >> ${GITHUB_ENV}
echo "http_api_test_level=${HTTP_API_TEST_LEVEL}" >> ${GITHUB_OUTPUT}
- name: Prepare Python test environment
run: |
uv sync --python 3.13 --group test --frozen
uv pip install -e sdk/python
- name: Run unit test
run: |
uv sync --python 3.13 --group test --frozen
source .venv/bin/activate
which pytest || echo "pytest not in PATH"
echo "Start to run unit test"
python3 run_tests.py -i
ragflow_tests_infinity:
name: ragflow_tests_infinity
needs: ragflow_preflight
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && contains(github.event.pull_request.labels.*.name, 'ci') && (github.event.action != 'labeled' || github.event.label.name == 'ci')) }}
runs-on: [ "self-hosted", "ragflow-test" ]
env:
DOC_ENGINE: infinity
RAGFLOW_IMAGE: infiniflow/ragflow:${{ github.run_id }}-infinity
HTTP_API_TEST_LEVEL: ${{ needs.ragflow_preflight.outputs.http_api_test_level }}
steps:
- name: Ensure workspace ownership
run: |
echo "Workflow triggered by ${{ github.event_name }}"
echo "chown -R ${USER} ${GITHUB_WORKSPACE}" && sudo chown -R ${USER} ${GITHUB_WORKSPACE}
- name: Check out code
uses: actions/checkout@v6
with:
ref: ${{ (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && format('refs/pull/{0}/merge', github.event.pull_request.number) || github.sha }}
fetch-depth: 0
fetch-tags: true
- name: Build ragflow go server
run: |
set -euo pipefail
BUILDER_CONTAINER=ragflow_build_${GITHUB_RUN_ID}_${DOC_ENGINE}_$(od -An -N4 -tx4 /dev/urandom | tr -d ' ')
cleanup_builder() {
if [[ -n "${BUILDER_CONTAINER:-}" ]]; then
sudo docker rm -f -v "${BUILDER_CONTAINER}" >/dev/null 2>&1 || true
fi
}
trap cleanup_builder EXIT
TZ=${TZ:-$(readlink -f /etc/localtime | awk -F '/zoneinfo/' '{print $2}')}
sudo docker run --privileged -d --name "${BUILDER_CONTAINER}" \
-e TZ="${TZ}" \
-e UV_INDEX=https://mirrors.aliyun.com/pypi/simple \
-v "${PWD}:/ragflow" \
-v "${PWD}/internal/binding/cpp/resource:/usr/share/infinity/resource" \
infiniflow/infinity_builder:ubuntu22_clang20
sudo docker exec "${BUILDER_CONTAINER}" bash -c 'git config --global safe.directory "*" && cd /ragflow && ./build.sh --cpp'
./build.sh --go
- name: Run Go unit tests
# Runs after `./build.sh --go`, which guarantees the C++ static
# library (librag_tokenizer_c_api.a) is present on disk. The Go
# test binaries link against it transitively through
# `internal/binding`, so running `go test` before the C++ build
# fails the link step.
#
# Excludes packages whose tests fail for environmental reasons
# unrelated to the diff:
# - internal/storage: TestMinioStorage_* needs a MinIO server
# at localhost:9000; not started by this job.
# - internal/tokenizer: tests need /usr/share/infinity/resource
# dict files, only mounted inside the docker builder, not
# in the Go test environment.
# - internal/handler: TestListAgentVersionsHandler_Success and
# sqlite setup (e.g. "no such table: user_tenant") are
# pre-existing flakes unrelated to the diff.
run: |
set -euo pipefail
PKGS=$(go list ./... 2>/dev/null \
| grep -v '/internal/storage$' \
| grep -v '/internal/agent$' \
| grep -v '/internal/tokenizer$' \
| grep -v '/internal/handler$' || true)
if [ -z "$PKGS" ]; then
./build.sh --test
else
./build.sh --test -- $PKGS
fi
- name: Build ragflow:nightly
run: |
set -euo pipefail
sudo docker pull ubuntu:24.04
sudo DOCKER_BUILDKIT=1 docker build --build-arg NEED_MIRROR=1 --build-arg HTTPS_PROXY=${HTTPS_PROXY} --build-arg HTTP_PROXY=${HTTP_PROXY} -f Dockerfile -t ${RAGFLOW_IMAGE} .
- name: Prepare Python test environment
run: |
uv sync --python 3.13 --group test --frozen
uv pip install -e sdk/python
- name: Prepare function test environment
working-directory: docker
run: |
set -euo pipefail
# install ss
sudo apt update && sudo apt install -y iproute2
RUNNER_WORKSPACE_PREFIX=${RUNNER_WORKSPACE_PREFIX:-${HOME}}
COMPOSE_PROJECT_NAME="${GITHUB_RUN_ID}-${DOC_ENGINE}"
echo "COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME}" >> ${GITHUB_ENV}
echo "RAGFLOW_CONTAINER=${COMPOSE_PROJECT_NAME}-ragflow-cpu-1" >> ${GITHUB_ENV}
ARTIFACTS_DIR=${RUNNER_WORKSPACE_PREFIX}/artifacts/${GITHUB_REPOSITORY}/${GITHUB_RUN_ID}/${DOC_ENGINE}
echo "ARTIFACTS_DIR=${ARTIFACTS_DIR}" >> ${GITHUB_ENV}
rm -rf "${ARTIFACTS_DIR}" && mkdir -p "${ARTIFACTS_DIR}"
# Determine runner number (default to 1 if not found)
RUNNER_NUM=$(sudo docker inspect $(hostname) --format '{{index .Config.Labels "com.docker.compose.container-number"}}' 2>/dev/null || true)
RUNNER_NUM=${RUNNER_NUM:-1}
# Compute port numbers using bash arithmetic
ES_PORT=$((1200 + RUNNER_NUM * 10))
OS_PORT=$((1201 + RUNNER_NUM * 10))
INFINITY_THRIFT_PORT=$((23817 + RUNNER_NUM * 10))
INFINITY_HTTP_PORT=$((23820 + RUNNER_NUM * 10))
INFINITY_PSQL_PORT=$((5432 + RUNNER_NUM * 10))
EXPOSE_MYSQL_PORT=$((5455 + RUNNER_NUM * 10))
MINIO_PORT=$((9000 + RUNNER_NUM * 10))
MINIO_CONSOLE_PORT=$((9001 + RUNNER_NUM * 10))
REDIS_PORT=$((6379 + RUNNER_NUM * 10))
TEI_PORT=$((6380 + RUNNER_NUM * 10))
KIBANA_PORT=$((6601 + RUNNER_NUM * 10))
SVR_HTTP_PORT=$((9380 + RUNNER_NUM * 10))
ADMIN_SVR_HTTP_PORT=$((9381 + RUNNER_NUM * 10))
SVR_MCP_PORT=$((9382 + RUNNER_NUM * 10))
GO_HTTP_PORT=$((9384 + RUNNER_NUM * 10))
GO_ADMIN_PORT=$((9383 + RUNNER_NUM * 10))
SANDBOX_EXECUTOR_MANAGER_PORT=$((9385 + RUNNER_NUM * 10))
SVR_WEB_HTTP_PORT=$((80 + RUNNER_NUM * 10))
SVR_WEB_HTTPS_PORT=$((443 + RUNNER_NUM * 10))
# Engine-specific offset partitions keep concurrent engine jobs from
# choosing the same host ports when they land on the same self-hosted runner.
# A lock plus reservation file closes the check/start race between parallel jobs.
PORT_BASES=(1200 1201 23817 23820 5432 5455 9000 9001 6379 6380 6601 9380 9381 9382 9384 9383 9385 80 443 4222)
PARTITION_SIZE=6000
case "${DOC_ENGINE}" in
elasticsearch) PARTITION_BASE=1000 ;;
infinity) PARTITION_BASE=31000 ;;
*) echo "Unsupported DOC_ENGINE=${DOC_ENGINE}" >&2; exit 1 ;;
esac
PORT_LOCK_DIR=${RUNNER_WORKSPACE_PREFIX}/artifacts/${GITHUB_REPOSITORY}/port-locks
mkdir -p "${PORT_LOCK_DIR}"
# Persist computed ports into .env so docker-compose uses the correct host bindings
echo "" >> .env
echo -e "ES_PORT=${ES_PORT}" >> .env
echo -e "OS_PORT=${OS_PORT}" >> .env
echo -e "INFINITY_THRIFT_PORT=${INFINITY_THRIFT_PORT}" >> .env
echo -e "INFINITY_HTTP_PORT=${INFINITY_HTTP_PORT}" >> .env
echo -e "INFINITY_PSQL_PORT=${INFINITY_PSQL_PORT}" >> .env
echo -e "EXPOSE_MYSQL_PORT=${EXPOSE_MYSQL_PORT}" >> .env
echo -e "MINIO_PORT=${MINIO_PORT}" >> .env
echo -e "MINIO_CONSOLE_PORT=${MINIO_CONSOLE_PORT}" >> .env
echo -e "REDIS_PORT=${REDIS_PORT}" >> .env
echo -e "TEI_PORT=${TEI_PORT}" >> .env
echo -e "KIBANA_PORT=${KIBANA_PORT}" >> .env
echo -e "SVR_HTTP_PORT=${SVR_HTTP_PORT}" >> .env
echo -e "ADMIN_SVR_HTTP_PORT=${ADMIN_SVR_HTTP_PORT}" >> .env
echo -e "SVR_MCP_PORT=${SVR_MCP_PORT}" >> .env
echo -e "GO_HTTP_PORT=${GO_HTTP_PORT}" >> .env
echo -e "GO_ADMIN_PORT=${GO_ADMIN_PORT}" >> .env
echo -e "SANDBOX_EXECUTOR_MANAGER_PORT=${SANDBOX_EXECUTOR_MANAGER_PORT}" >> .env
echo -e "SVR_WEB_HTTP_PORT=${SVR_WEB_HTTP_PORT}" >> .env
echo -e "SVR_WEB_HTTPS_PORT=${SVR_WEB_HTTPS_PORT}" >> .env
echo -e "COMPOSE_PROFILES=\${COMPOSE_PROFILES},tei-cpu" >> .env
echo -e "TEI_MODEL=BAAI/bge-small-en-v1.5" >> .env
echo -e "RAGFLOW_IMAGE=${RAGFLOW_IMAGE}" >> .env
port_offset_available() {
local offset=$1
local base port
for base in "${PORT_BASES[@]}"; do
port=$((base + offset))
if ss -ltnH "sport = :${port}" | grep -q .; then
return 1
fi
done
return 0
}
cleanup_stale_port_locks() {
local now stale_after lock lock_ts
now=$(date -u +%s)
stale_after=$((6 * 60 * 60))
for lock in "${PORT_LOCK_DIR}"/*.lock; do
[[ -e "${lock}" ]] || continue
lock_ts=$(awk '{print $3}' "${lock}" 2>/dev/null || true)
if [[ "${lock_ts}" =~ ^[0-9]+$ ]] && (( now - lock_ts > stale_after )); then
rm -f "${lock}"
fi
done
}
reserve_port_offset() {
local attempt candidate reservation
cleanup_stale_port_locks
for attempt in $(seq 0 59); do
candidate=$(( PARTITION_BASE + ((GITHUB_RUN_ID + RUNNER_NUM * 1000 + attempt * 97) % PARTITION_SIZE) ))
reservation="${PORT_LOCK_DIR}/${candidate}.lock"
if ( set -o noclobber; echo "${GITHUB_RUN_ID} ${DOC_ENGINE} $(date -u +%s)" > "${reservation}" ) 2>/dev/null; then
if port_offset_available "${candidate}"; then
PORT_OFFSET=${candidate}
PORT_RESERVATION=${reservation}
return 0
fi
rm -f "${reservation}"
fi
done
return 1
}
if ! reserve_port_offset; then
echo "Failed to reserve a free host port range for ${DOC_ENGINE} docker compose" >&2
exit 1
fi
echo "PORT_RESERVATION=${PORT_RESERVATION}" >> ${GITHUB_ENV}
echo "Using ${DOC_ENGINE} host port offset ${PORT_OFFSET}"
ES_PORT=$((1200 + PORT_OFFSET))
OS_PORT=$((1201 + PORT_OFFSET))
INFINITY_THRIFT_PORT=$((23817 + PORT_OFFSET))
INFINITY_HTTP_PORT=$((23820 + PORT_OFFSET))
INFINITY_PSQL_PORT=$((5432 + PORT_OFFSET))
EXPOSE_MYSQL_PORT=$((5455 + PORT_OFFSET))
MINIO_PORT=$((9000 + PORT_OFFSET))
MINIO_CONSOLE_PORT=$((9001 + PORT_OFFSET))
REDIS_PORT=$((6379 + PORT_OFFSET))
NATS_PORT=$((4222 + PORT_OFFSET))
TEI_PORT=$((6380 + PORT_OFFSET))
KIBANA_PORT=$((6601 + PORT_OFFSET))
SVR_HTTP_PORT=$((9380 + PORT_OFFSET))
ADMIN_SVR_HTTP_PORT=$((9381 + PORT_OFFSET))
SVR_MCP_PORT=$((9382 + PORT_OFFSET))
GO_HTTP_PORT=$((9384 + PORT_OFFSET))
GO_ADMIN_PORT=$((9383 + PORT_OFFSET))
SANDBOX_EXECUTOR_MANAGER_PORT=$((9385 + PORT_OFFSET))
SVR_WEB_HTTP_PORT=$((80 + PORT_OFFSET))
SVR_WEB_HTTPS_PORT=$((443 + PORT_OFFSET))
# Persist computed ports into .env so docker-compose uses the correct host bindings.
# Remove previous CI overrides first; docker compose uses the last duplicate key.
sed -i '/^ES_PORT=/d;/^OS_PORT=/d;/^INFINITY_THRIFT_PORT=/d;/^INFINITY_HTTP_PORT=/d;/^INFINITY_PSQL_PORT=/d;/^EXPOSE_MYSQL_PORT=/d;/^MINIO_PORT=/d;/^MINIO_CONSOLE_PORT=/d;/^REDIS_PORT=/d;/^TEI_PORT=/d;/^KIBANA_PORT=/d;/^SVR_HTTP_PORT=/d;/^ADMIN_SVR_HTTP_PORT=/d;/^SVR_MCP_PORT=/d;/^GO_HTTP_PORT=/d;/^GO_ADMIN_PORT=/d;/^SANDBOX_EXECUTOR_MANAGER_PORT=/d;/^SVR_WEB_HTTP_PORT=/d;/^SVR_WEB_HTTPS_PORT=/d;/^NATS_PORT=/d;/^COMPOSE_PROFILES=/d;/^TEI_MODEL=/d;/^RAGFLOW_IMAGE=/d;/^DOC_ENGINE=/d' .env
{
echo ""
echo "ES_PORT=${ES_PORT}"
echo "OS_PORT=${OS_PORT}"
echo "INFINITY_THRIFT_PORT=${INFINITY_THRIFT_PORT}"
echo "INFINITY_HTTP_PORT=${INFINITY_HTTP_PORT}"
echo "INFINITY_PSQL_PORT=${INFINITY_PSQL_PORT}"
echo "EXPOSE_MYSQL_PORT=${EXPOSE_MYSQL_PORT}"
echo "MINIO_PORT=${MINIO_PORT}"
echo "MINIO_CONSOLE_PORT=${MINIO_CONSOLE_PORT}"
echo "REDIS_PORT=${REDIS_PORT}"
echo "NATS_PORT=${NATS_PORT}"
echo "TEI_PORT=${TEI_PORT}"
echo "KIBANA_PORT=${KIBANA_PORT}"
echo "SVR_HTTP_PORT=${SVR_HTTP_PORT}"
echo "ADMIN_SVR_HTTP_PORT=${ADMIN_SVR_HTTP_PORT}"
echo "SVR_MCP_PORT=${SVR_MCP_PORT}"
echo "GO_HTTP_PORT=${GO_HTTP_PORT}"
echo "GO_ADMIN_PORT=${GO_ADMIN_PORT}"
echo "SANDBOX_EXECUTOR_MANAGER_PORT=${SANDBOX_EXECUTOR_MANAGER_PORT}"
echo "SVR_WEB_HTTP_PORT=${SVR_WEB_HTTP_PORT}"
echo "SVR_WEB_HTTPS_PORT=${SVR_WEB_HTTPS_PORT}"
echo "COMPOSE_PROFILES=${DOC_ENGINE},cpu,tei-cpu,deepdoc"
echo "TEI_MODEL=BAAI/bge-small-en-v1.5"
echo "RAGFLOW_IMAGE=${RAGFLOW_IMAGE}"
echo "DOC_ENGINE=${DOC_ENGINE}"
} >> .env
echo "HOST_ADDRESS=http://host.docker.internal:${SVR_HTTP_PORT}" >> ${GITHUB_ENV}
# Patch entrypoint.sh for coverage
sed -i '/"\$PY" api\/ragflow_server.py \${INIT_SUPERUSER_ARGS} &/c\ echo "Ensuring coverage is installed..."\n "$PY" -m pip install coverage -i https://mirrors.aliyun.com/pypi/simple\n export COVERAGE_FILE=/ragflow/logs/.coverage\n echo "Starting ragflow_server with coverage..."\n "$PY" -m coverage run --source=./api/apps --omit="*/tests/*,*/migrations/*" -a api/ragflow_server.py ${INIT_SUPERUSER_ARGS} &' ./entrypoint.sh
cd ..
uv sync --python 3.13 --group test --frozen && uv pip install -e sdk/python
- name: Start ragflow:nightly for Infinity
run: |
sed -i 's/^DOC_ENGINE=.*$/DOC_ENGINE=infinity/' docker/.env
sudo docker compose -f docker/docker-compose.yml -p ${GITHUB_RUN_ID} up -d
sudo docker compose -f docker/docker-compose.yml -p ${COMPOSE_PROJECT_NAME} down -v || true
sudo docker ps -a --filter "label=com.docker.compose.project=${COMPOSE_PROJECT_NAME}" -q | xargs -r sudo docker rm -f
sudo docker compose -f docker/docker-compose.yml -p ${COMPOSE_PROJECT_NAME} up -d
- name: Run sdk tests against Infinity
run: |
export http_proxy=""; export https_proxy=""; export no_proxy=""; export HTTP_PROXY=""; export HTTPS_PROXY=""; export NO_PROXY=""
until sudo docker exec ${RAGFLOW_CONTAINER} curl -s --connect-timeout 5 ${HOST_ADDRESS}/v1/system/ping > /dev/null 2>&1; do
echo "Waiting for service to be available... (last exit code: $?)"
svc_ready=0
for i in $(seq 1 60); do
if sudo docker exec ${RAGFLOW_CONTAINER} curl -sf --connect-timeout 5 "${HOST_ADDRESS}/api/v1/system/ping" > /dev/null 2>&1; then
svc_ready=1
break
fi
echo "Waiting for service to be available... ($i/60)"
sleep 5
done
if [ "$svc_ready" -ne 1 ]; then
echo "Service did not become ready after 5 minutes. Docker logs:"
sudo docker logs ${RAGFLOW_CONTAINER}
exit 1
fi
echo "Start to run test sdk on Infinity"
source .venv/bin/activate && set -o pipefail; DOC_ENGINE=infinity pytest -s --tb=short --level=${HTTP_API_TEST_LEVEL} --junitxml=pytest-infinity-sdk.xml --cov=sdk/python/ragflow_sdk --cov-branch --cov-report=xml:coverage-infinity-sdk.xml test/testcases/test_sdk_api 2>&1 | tee infinity_sdk_test.log
- name: Run New RESTFUL api tests against Infinity
run: |
export http_proxy=""; export https_proxy=""; export no_proxy=""; export HTTP_PROXY=""; export HTTPS_PROXY=""; export NO_PROXY=""
until sudo docker exec ${RAGFLOW_CONTAINER} curl -s --connect-timeout 5 ${HOST_ADDRESS}/v1/system/ping > /dev/null 2>&1; do
echo "Waiting for service to be available... (last exit code: $?)"
svc_ready=0
for i in $(seq 1 60); do
if sudo docker exec ${RAGFLOW_CONTAINER} curl -sf --connect-timeout 5 "${HOST_ADDRESS}/api/v1/system/ping" > /dev/null 2>&1; then
svc_ready=1
break
fi
echo "Waiting for service to be available... ($i/60)"
sleep 5
done
if [ "$svc_ready" -ne 1 ]; then
echo "Service did not become ready after 5 minutes. Docker logs:"
sudo docker logs ${RAGFLOW_CONTAINER}
exit 1
fi
source .venv/bin/activate && set -o pipefail; DOC_ENGINE=infinity pytest -s --tb=short --level=${HTTP_API_TEST_LEVEL} test/testcases/restful_api 2>&1 | tee infinity_restful_api_test.log
- name: RAGFlow CLI retrieval test Infinity
@@ -309,10 +468,20 @@ jobs:
ADMIN_HOST="${USER_HOST}"
ADMIN_PORT="${ADMIN_SVR_HTTP_PORT}"
until sudo docker exec ${RAGFLOW_CONTAINER} curl -s --connect-timeout 5 ${HOST_ADDRESS}/v1/system/ping > /dev/null 2>&1; do
echo "Waiting for service to be available... (last exit code: $?)"
svc_ready=0
for i in $(seq 1 60); do
if sudo docker exec ${RAGFLOW_CONTAINER} curl -sf --connect-timeout 5 "${HOST_ADDRESS}/api/v1/system/ping" > /dev/null 2>&1; then
svc_ready=1
break
fi
echo "Waiting for service to be available... ($i/60)"
sleep 5
done
if [ "$svc_ready" -ne 1 ]; then
echo "Service did not become ready after 5 minutes. Docker logs:"
sudo docker logs ${RAGFLOW_CONTAINER}
exit 1
fi
admin_ready=0
for i in $(seq 1 30); do
@@ -363,7 +532,7 @@ jobs:
else
echo "ragflow_server.py not found!"
fi
sudo docker compose -f docker/docker-compose.yml -p ${GITHUB_RUN_ID} stop
sudo docker compose -f docker/docker-compose.yml -p ${COMPOSE_PROJECT_NAME} stop
- name: Generate server coverage report Infinity
if: ${{ !cancelled() }}
@@ -406,31 +575,280 @@ jobs:
if: always() # always run this step even if previous steps failed
run: |
# Sometimes `docker compose down` fail due to hang container, heavy load etc. Need to remove such containers to release resources(for example, listen ports).
sudo docker compose -f docker/docker-compose.yml -p ${GITHUB_RUN_ID} down -v || true
sudo docker ps -a --filter "label=com.docker.compose.project=${GITHUB_RUN_ID}" -q | xargs -r sudo docker rm -f
sudo docker compose -f docker/docker-compose.yml -p ${COMPOSE_PROJECT_NAME} down -v || true
sudo docker ps -a --filter "label=com.docker.compose.project=${COMPOSE_PROJECT_NAME}" -q | xargs -r sudo docker rm -f
if [[ -n ${RAGFLOW_IMAGE} ]]; then
sudo docker rmi -f ${RAGFLOW_IMAGE}
fi
if [[ -n ${PORT_RESERVATION:-} ]]; then
rm -f "${PORT_RESERVATION}"
fi
ragflow_tests_elasticsearch:
name: ragflow_tests_elasticsearch
needs: ragflow_preflight
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && contains(github.event.pull_request.labels.*.name, 'ci') && (github.event.action != 'labeled' || github.event.label.name == 'ci')) }}
runs-on: [ "self-hosted", "ragflow-test" ]
env:
DOC_ENGINE: elasticsearch
RAGFLOW_IMAGE: infiniflow/ragflow:${{ github.run_id }}-elasticsearch
HTTP_API_TEST_LEVEL: ${{ needs.ragflow_preflight.outputs.http_api_test_level }}
steps:
- name: Ensure workspace ownership
run: |
echo "Workflow triggered by ${{ github.event_name }}"
echo "chown -R ${USER} ${GITHUB_WORKSPACE}" && sudo chown -R ${USER} ${GITHUB_WORKSPACE}
- name: Check out code
uses: actions/checkout@v6
with:
ref: ${{ (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && format('refs/pull/{0}/merge', github.event.pull_request.number) || github.sha }}
fetch-depth: 0
fetch-tags: true
- name: Build ragflow go server
run: |
set -euo pipefail
BUILDER_CONTAINER=ragflow_build_${GITHUB_RUN_ID}_${DOC_ENGINE}_$(od -An -N4 -tx4 /dev/urandom | tr -d ' ')
cleanup_builder() {
if [[ -n "${BUILDER_CONTAINER:-}" ]]; then
sudo docker rm -f -v "${BUILDER_CONTAINER}" >/dev/null 2>&1 || true
fi
}
trap cleanup_builder EXIT
TZ=${TZ:-$(readlink -f /etc/localtime | awk -F '/zoneinfo/' '{print $2}')}
sudo docker run --privileged -d --name "${BUILDER_CONTAINER}" \
-e TZ="${TZ}" \
-e UV_INDEX=https://mirrors.aliyun.com/pypi/simple \
-v "${PWD}:/ragflow" \
-v "${PWD}/internal/binding/cpp/resource:/usr/share/infinity/resource" \
infiniflow/infinity_builder:ubuntu22_clang20
sudo docker exec "${BUILDER_CONTAINER}" bash -c 'git config --global safe.directory "*" && cd /ragflow && ./build.sh --cpp'
./build.sh --go
- name: Run Go unit tests
# Runs after `./build.sh --go`, which guarantees the C++ static
# library (librag_tokenizer_c_api.a) is present on disk. The Go
# test binaries link against it transitively through
# `internal/binding`, so running `go test` before the C++ build
# fails the link step.
#
# Excludes packages whose tests fail for environmental reasons
# unrelated to the diff:
# - internal/storage: TestMinioStorage_* needs a MinIO server
# at localhost:9000; not started by this job.
# - internal/tokenizer: tests need /usr/share/infinity/resource
# dict files, only mounted inside the docker builder, not
# in the Go test environment.
# - internal/handler: TestListAgentVersionsHandler_Success and
# sqlite setup (e.g. "no such table: user_tenant") are
# pre-existing flakes unrelated to the diff.
run: |
set -euo pipefail
PKGS=$(go list ./... 2>/dev/null \
| grep -v '/internal/storage$' \
| grep -v '/internal/tokenizer$' \
| grep -v '/internal/handler$' || true)
if [ -z "$PKGS" ]; then
./build.sh --test
else
./build.sh --test -- $PKGS
fi
- name: Build ragflow:nightly
run: |
set -euo pipefail
sudo docker pull ubuntu:24.04
sudo DOCKER_BUILDKIT=1 docker build --build-arg NEED_MIRROR=1 --build-arg HTTPS_PROXY=${HTTPS_PROXY} --build-arg HTTP_PROXY=${HTTP_PROXY} -f Dockerfile -t ${RAGFLOW_IMAGE} .
- name: Prepare Python test environment
run: |
uv sync --python 3.13 --group test --frozen
uv pip install -e sdk/python
- name: Prepare function test environment
working-directory: docker
run: |
set -euo pipefail
# install ss
sudo apt update && sudo apt install -y iproute2
RUNNER_WORKSPACE_PREFIX=${RUNNER_WORKSPACE_PREFIX:-${HOME}}
COMPOSE_PROJECT_NAME="${GITHUB_RUN_ID}-${DOC_ENGINE}"
echo "COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME}" >> ${GITHUB_ENV}
echo "RAGFLOW_CONTAINER=${COMPOSE_PROJECT_NAME}-ragflow-cpu-1" >> ${GITHUB_ENV}
ARTIFACTS_DIR=${RUNNER_WORKSPACE_PREFIX}/artifacts/${GITHUB_REPOSITORY}/${GITHUB_RUN_ID}/${DOC_ENGINE}
echo "ARTIFACTS_DIR=${ARTIFACTS_DIR}" >> ${GITHUB_ENV}
rm -rf "${ARTIFACTS_DIR}" && mkdir -p "${ARTIFACTS_DIR}"
# Determine runner number (default to 1 if not found)
RUNNER_NUM=$(sudo docker inspect $(hostname) --format '{{index .Config.Labels "com.docker.compose.container-number"}}' 2>/dev/null || true)
RUNNER_NUM=${RUNNER_NUM:-1}
# Engine-specific offset partitions keep concurrent engine jobs from
# choosing the same host ports when they land on the same self-hosted runner.
# A lock plus reservation file closes the check/start race between parallel jobs.
PORT_BASES=(1200 1201 23817 23820 5432 5455 9000 9001 6379 6380 6601 9380 9381 9382 9384 9383 9385 80 443 4222)
PARTITION_SIZE=6000
case "${DOC_ENGINE}" in
elasticsearch) PARTITION_BASE=1000 ;;
infinity) PARTITION_BASE=31000 ;;
*) echo "Unsupported DOC_ENGINE=${DOC_ENGINE}" >&2; exit 1 ;;
esac
PORT_LOCK_DIR=${RUNNER_WORKSPACE_PREFIX}/artifacts/${GITHUB_REPOSITORY}/port-locks
mkdir -p "${PORT_LOCK_DIR}"
port_offset_available() {
local offset=$1
local base port
for base in "${PORT_BASES[@]}"; do
port=$((base + offset))
if ss -ltnH "sport = :${port}" | grep -q .; then
return 1
fi
done
return 0
}
cleanup_stale_port_locks() {
local now stale_after lock lock_ts
now=$(date -u +%s)
stale_after=$((6 * 60 * 60))
for lock in "${PORT_LOCK_DIR}"/*.lock; do
[[ -e "${lock}" ]] || continue
lock_ts=$(awk '{print $3}' "${lock}" 2>/dev/null || true)
if [[ "${lock_ts}" =~ ^[0-9]+$ ]] && (( now - lock_ts > stale_after )); then
rm -f "${lock}"
fi
done
}
reserve_port_offset() {
local attempt candidate reservation
cleanup_stale_port_locks
for attempt in $(seq 0 59); do
candidate=$(( PARTITION_BASE + ((GITHUB_RUN_ID + RUNNER_NUM * 1000 + attempt * 97) % PARTITION_SIZE) ))
reservation="${PORT_LOCK_DIR}/${candidate}.lock"
if ( set -o noclobber; echo "${GITHUB_RUN_ID} ${DOC_ENGINE} $(date -u +%s)" > "${reservation}" ) 2>/dev/null; then
if port_offset_available "${candidate}"; then
PORT_OFFSET=${candidate}
PORT_RESERVATION=${reservation}
return 0
fi
rm -f "${reservation}"
fi
done
return 1
}
if ! reserve_port_offset; then
echo "Failed to reserve a free host port range for ${DOC_ENGINE} docker compose" >&2
exit 1
fi
echo "PORT_RESERVATION=${PORT_RESERVATION}" >> ${GITHUB_ENV}
echo "Using ${DOC_ENGINE} host port offset ${PORT_OFFSET}"
ES_PORT=$((1200 + PORT_OFFSET))
OS_PORT=$((1201 + PORT_OFFSET))
INFINITY_THRIFT_PORT=$((23817 + PORT_OFFSET))
INFINITY_HTTP_PORT=$((23820 + PORT_OFFSET))
INFINITY_PSQL_PORT=$((5432 + PORT_OFFSET))
EXPOSE_MYSQL_PORT=$((5455 + PORT_OFFSET))
MINIO_PORT=$((9000 + PORT_OFFSET))
MINIO_CONSOLE_PORT=$((9001 + PORT_OFFSET))
REDIS_PORT=$((6379 + PORT_OFFSET))
NATS_PORT=$((4222 + PORT_OFFSET))
TEI_PORT=$((6380 + PORT_OFFSET))
KIBANA_PORT=$((6601 + PORT_OFFSET))
SVR_HTTP_PORT=$((9380 + PORT_OFFSET))
ADMIN_SVR_HTTP_PORT=$((9381 + PORT_OFFSET))
SVR_MCP_PORT=$((9382 + PORT_OFFSET))
GO_HTTP_PORT=$((9384 + PORT_OFFSET))
GO_ADMIN_PORT=$((9383 + PORT_OFFSET))
SANDBOX_EXECUTOR_MANAGER_PORT=$((9385 + PORT_OFFSET))
SVR_WEB_HTTP_PORT=$((80 + PORT_OFFSET))
SVR_WEB_HTTPS_PORT=$((443 + PORT_OFFSET))
# Persist computed ports into .env so docker-compose uses the correct host bindings.
# Remove previous CI overrides first; docker compose uses the last duplicate key.
sed -i '/^ES_PORT=/d;/^OS_PORT=/d;/^INFINITY_THRIFT_PORT=/d;/^INFINITY_HTTP_PORT=/d;/^INFINITY_PSQL_PORT=/d;/^EXPOSE_MYSQL_PORT=/d;/^MINIO_PORT=/d;/^MINIO_CONSOLE_PORT=/d;/^REDIS_PORT=/d;/^TEI_PORT=/d;/^KIBANA_PORT=/d;/^SVR_HTTP_PORT=/d;/^ADMIN_SVR_HTTP_PORT=/d;/^SVR_MCP_PORT=/d;/^GO_HTTP_PORT=/d;/^GO_ADMIN_PORT=/d;/^SANDBOX_EXECUTOR_MANAGER_PORT=/d;/^SVR_WEB_HTTP_PORT=/d;/^SVR_WEB_HTTPS_PORT=/d;/^NATS_PORT=/d;/^COMPOSE_PROFILES=/d;/^TEI_MODEL=/d;/^RAGFLOW_IMAGE=/d;/^DOC_ENGINE=/d' .env
{
echo ""
echo "ES_PORT=${ES_PORT}"
echo "OS_PORT=${OS_PORT}"
echo "INFINITY_THRIFT_PORT=${INFINITY_THRIFT_PORT}"
echo "INFINITY_HTTP_PORT=${INFINITY_HTTP_PORT}"
echo "INFINITY_PSQL_PORT=${INFINITY_PSQL_PORT}"
echo "EXPOSE_MYSQL_PORT=${EXPOSE_MYSQL_PORT}"
echo "MINIO_PORT=${MINIO_PORT}"
echo "MINIO_CONSOLE_PORT=${MINIO_CONSOLE_PORT}"
echo "REDIS_PORT=${REDIS_PORT}"
echo "NATS_PORT=${NATS_PORT}"
echo "TEI_PORT=${TEI_PORT}"
echo "KIBANA_PORT=${KIBANA_PORT}"
echo "SVR_HTTP_PORT=${SVR_HTTP_PORT}"
echo "ADMIN_SVR_HTTP_PORT=${ADMIN_SVR_HTTP_PORT}"
echo "SVR_MCP_PORT=${SVR_MCP_PORT}"
echo "GO_HTTP_PORT=${GO_HTTP_PORT}"
echo "GO_ADMIN_PORT=${GO_ADMIN_PORT}"
echo "SANDBOX_EXECUTOR_MANAGER_PORT=${SANDBOX_EXECUTOR_MANAGER_PORT}"
echo "SVR_WEB_HTTP_PORT=${SVR_WEB_HTTP_PORT}"
echo "SVR_WEB_HTTPS_PORT=${SVR_WEB_HTTPS_PORT}"
echo "COMPOSE_PROFILES=${DOC_ENGINE},cpu,tei-cpu,deepdoc"
echo "TEI_MODEL=BAAI/bge-small-en-v1.5"
echo "RAGFLOW_IMAGE=${RAGFLOW_IMAGE}"
echo "DOC_ENGINE=${DOC_ENGINE}"
} >> .env
echo "HOST_ADDRESS=http://host.docker.internal:${SVR_HTTP_PORT}" >> ${GITHUB_ENV}
# Patch entrypoint.sh for coverage
sed -i '/"\$PY" api\/ragflow_server.py \${INIT_SUPERUSER_ARGS} &/c\ echo "Ensuring coverage is installed..."\n "$PY" -m pip install coverage -i https://mirrors.aliyun.com/pypi/simple\n export COVERAGE_FILE=/ragflow/logs/.coverage\n echo "Starting ragflow_server with coverage..."\n "$PY" -m coverage run --source=./api/apps --omit="*/tests/*,*/migrations/*" -a api/ragflow_server.py ${INIT_SUPERUSER_ARGS} &' ./entrypoint.sh
- name: Start ragflow:nightly for Elasticsearch
run: |
sed -i 's/^DOC_ENGINE=.*$/DOC_ENGINE=elasticsearch/' docker/.env
sudo docker compose -f docker/docker-compose.yml -p ${GITHUB_RUN_ID} up -d
sudo docker compose -f docker/docker-compose.yml -p ${COMPOSE_PROJECT_NAME} down -v || true
sudo docker ps -a --filter "label=com.docker.compose.project=${COMPOSE_PROJECT_NAME}" -q | xargs -r sudo docker rm -f
sudo docker compose -f docker/docker-compose.yml -p ${COMPOSE_PROJECT_NAME} up -d
- name: Run sdk tests against Elasticsearch
run: |
export http_proxy=""; export https_proxy=""; export no_proxy=""; export HTTP_PROXY=""; export HTTPS_PROXY=""; export NO_PROXY=""
until sudo docker exec ${RAGFLOW_CONTAINER} curl -s --connect-timeout 5 ${HOST_ADDRESS}/v1/system/ping > /dev/null 2>&1; do
echo "Waiting for service to be available... (last exit code: $?)"
svc_ready=0
for i in $(seq 1 60); do
if sudo docker exec ${RAGFLOW_CONTAINER} curl -sf --connect-timeout 5 "${HOST_ADDRESS}/api/v1/system/ping" > /dev/null 2>&1; then
svc_ready=1
break
fi
echo "Waiting for service to be available... ($i/60)"
sleep 5
done
if [ "$svc_ready" -ne 1 ]; then
echo "Service did not become ready after 5 minutes. Docker logs:"
sudo docker logs ${RAGFLOW_CONTAINER}
exit 1
fi
echo "Start to run test sdk on Elasticsearch"
source .venv/bin/activate && set -o pipefail; pytest -s --tb=short --level=${HTTP_API_TEST_LEVEL} --junitxml=pytest-infinity-sdk.xml --cov=sdk/python/ragflow_sdk --cov-branch --cov-report=xml:coverage-es-sdk.xml test/testcases/test_sdk_api 2>&1 | tee es_sdk_test.log
source .venv/bin/activate && set -o pipefail; pytest -s --tb=short --level=${HTTP_API_TEST_LEVEL} --junitxml=pytest-es-sdk.xml --cov=sdk/python/ragflow_sdk --cov-branch --cov-report=xml:coverage-es-sdk.xml test/testcases/test_sdk_api 2>&1 | tee es_sdk_test.log
- name: Run New RESTFUL api tests against Elasticsearch
run: |
export http_proxy=""; export https_proxy=""; export no_proxy=""; export HTTP_PROXY=""; export HTTPS_PROXY=""; export NO_PROXY=""
until sudo docker exec ${RAGFLOW_CONTAINER} curl -s --connect-timeout 5 ${HOST_ADDRESS}/v1/system/ping > /dev/null 2>&1; do
echo "Waiting for service to be available... (last exit code: $?)"
svc_ready=0
for i in $(seq 1 60); do
if sudo docker exec ${RAGFLOW_CONTAINER} curl -sf --connect-timeout 5 "${HOST_ADDRESS}/api/v1/system/ping" > /dev/null 2>&1; then
svc_ready=1
break
fi
echo "Waiting for service to be available... ($i/60)"
sleep 5
done
if [ "$svc_ready" -ne 1 ]; then
echo "Service did not become ready after 5 minutes. Docker logs:"
sudo docker logs ${RAGFLOW_CONTAINER}
exit 1
fi
source .venv/bin/activate && set -o pipefail; pytest -s --tb=short --level=${HTTP_API_TEST_LEVEL} test/testcases/restful_api 2>&1 | tee es_restful_api_test.log
- name: RAGFlow CLI retrieval test Elasticsearch
@@ -493,10 +911,20 @@ jobs:
ADMIN_HOST="${USER_HOST}"
ADMIN_PORT="${ADMIN_SVR_HTTP_PORT}"
until sudo docker exec ${RAGFLOW_CONTAINER} curl -s --connect-timeout 5 ${HOST_ADDRESS}/v1/system/ping > /dev/null 2>&1; do
echo "Waiting for service to be available... (last exit code: $?)"
svc_ready=0
for i in $(seq 1 60); do
if sudo docker exec ${RAGFLOW_CONTAINER} curl -sf --connect-timeout 5 "${HOST_ADDRESS}/api/v1/system/ping" > /dev/null 2>&1; then
svc_ready=1
break
fi
echo "Waiting for service to be available... ($i/60)"
sleep 5
done
if [ "$svc_ready" -ne 1 ]; then
echo "Service did not become ready after 5 minutes. Docker logs:"
sudo docker logs ${RAGFLOW_CONTAINER}
exit 1
fi
admin_ready=0
for i in $(seq 1 30); do
@@ -547,7 +975,7 @@ jobs:
else
echo "ragflow_server.py not found!"
fi
sudo docker compose -f docker/docker-compose.yml -p ${GITHUB_RUN_ID} stop
sudo docker compose -f docker/docker-compose.yml -p ${COMPOSE_PROJECT_NAME} stop
- name: Generate server coverage report Elasticsearch
if: ${{ !cancelled() }}
@@ -569,7 +997,7 @@ jobs:
else
echo ".coverage file not found!"
fi
- name: Collect ragflow log Elasticsearch
if: ${{ !cancelled() }}
run: |
@@ -585,8 +1013,11 @@ jobs:
if: always() # always run this step even if previous steps failed
run: |
# Sometimes `docker compose down` fail due to hang container, heavy load etc. Need to remove such containers to release resources(for example, listen ports).
sudo docker compose -f docker/docker-compose.yml -p ${GITHUB_RUN_ID} down -v || true
sudo docker ps -a --filter "label=com.docker.compose.project=${GITHUB_RUN_ID}" -q | xargs -r sudo docker rm -f
sudo docker compose -f docker/docker-compose.yml -p ${COMPOSE_PROJECT_NAME} down -v || true
sudo docker ps -a --filter "label=com.docker.compose.project=${COMPOSE_PROJECT_NAME}" -q | xargs -r sudo docker rm -f
if [[ -n ${RAGFLOW_IMAGE} ]]; then
sudo docker rmi -f ${RAGFLOW_IMAGE}
fi
if [[ -n ${PORT_RESERVATION:-} ]]; then
rm -f "${PORT_RESERVATION}"
fi

34
.gitignore vendored
View File

@@ -22,6 +22,7 @@ Cargo.lock
.idea/
.vscode/
.cursor/settings.json
.opencode/
# Exclude Mac generated files
.DS_Store
@@ -137,6 +138,9 @@ web_modules/
# Output of 'npm pack'
*.tgz
# Claude Code plans / state — local-only artifacts
.claude/
# Yarn Integrity file
.yarn-integrity
@@ -222,9 +226,9 @@ uv-aarch64-unknown-linux-gnu.tar.gz
docker/launch_backend_service_windows.sh
# C++ build directories
internal/cpp/build/
internal/cpp/cmake-build-release/
internal/cpp/cmake-build-debug/
internal/binding/cpp/build/
internal/binding/cpp/cmake-build-release/
internal/binding/cpp/cmake-build-debug/
# Trae IDE config
.trae/
@@ -232,4 +236,26 @@ internal/cpp/cmake-build-debug/
# Go server build output
bin/*
!bin/.gitkeep
.claude/settings.local.json
.claude/settings.local.json
.run/
# Local agent tooling state (per-developer; not for commit)
.omc/
.marscode/
# Parser test fixtures and python tools
internal/deepdoc/parser/pdf/testdata/
internal/deepdoc/parser/pdf/tools-py/
# IDE tooling artifacts
.codebuddy/
# Local build output
build/
internal/deepdoc/parser/docx/testdata/
internal/deepdoc/parser/docx/tool/
# test data compare tool
internal/ingestion/task/tool/generate_dataflow_golden.py
internal/ingestion/task/tool/README.md
internal/cpp/cmake-build-release

View File

@@ -1,19 +0,0 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: check-yaml
- id: check-json
- id: end-of-file-fixer
- id: trailing-whitespace
- id: check-case-conflict
- id: check-merge-conflict
- id: mixed-line-ending
- id: check-symlinks
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.6
hooks:
- id: ruff
args: [ --fix ]
- id: ruff-format

85
.rooignore Normal file
View File

@@ -0,0 +1,85 @@
# .rooignore for RAGFlow
# Purpose: reduce indexing noise, token waste, and accidental reads of generated files
# Git / platform
.git/
.github/
# IDE / local editor
.idea/
.vscode/
.trae/
# Python caches / build artifacts
__pycache__/
*.pyc
*.pyo
*.pyd
.pytest_cache/
.mypy_cache/
.ruff_cache/
.hypothesis/
.coverage
*.egg-info/
ragflow.egg-info/
sdk/python/ragflow_sdk.egg-info/
sdk/python/build/
sdk/python/dist/
build/
dist/
# Virtual environments
.venv/
venv/
env/
# Node / frontend dependencies and build output
node_modules/
web/node_modules/
web/dist/
web/build/
web/.cache/
*.tsbuildinfo
# Logs / runtime artifacts
logs/
docker/ragflow-logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# Large local dependency artifacts
libssl*.deb
tika-server*.jar*
cl100k_base.tiktoken
chrome*
huggingface.co/
nltk_data/
uv-x86_64*.tar.gz
uv-aarch64*.tar.gz
# Temp / data / local storage
tmp/
cache/
backup/
docker/data/
docker/oceanbase/conf
docker/oceanbase/data
docker/seekdb
# Native / compiled build dirs
target/
bin/
internal/binding/cpp/build/
internal/binding/cpp/cmake-build-release/
internal/binding/cpp/cmake-build-debug/
# Optional: skip tests and docs from indexing
# test/
# tests/
# docs/
# Ignore Roo's own config file
.rooignore

193
AGENTS.md
View File

@@ -1,110 +1,109 @@
# RAGFlow Project Instructions for GitHub Copilot
# RAGFlow Instructions
This file provides context, build instructions, and coding standards for the RAGFlow project.
It is structured to follow GitHub Copilot's [customization guidelines](https://docs.github.com/en/copilot/concepts/prompting/response-customization).
Use this file as the local operating guide for the current codebase. Prefer the code and the current CLAUDE.md over any older convention or remembered project shape.
## 1. Project Overview
RAGFlow is an open-source RAG (Retrieval-Augmented Generation) engine based on deep document understanding. It is a full-stack application with a Python backend and a React/TypeScript frontend.
## Core stance
- Treat legacy code as liability, not as a compatibility target.
- Prefer deletion over shims, deprecated branches, wrapper APIs, and dual-track migration notes.
- If old and new implementations coexist, converge to one path unless an external contract forces compatibility.
- Remove dead tests, commented-out code, stale docs, and "move later" notes instead of preserving them.
- Reduce public surface area when a helper can be made private or internal.
- Keep refactors centered on the owning abstraction, not on adjacent compatibility layers.
- **Backend**: Python 3.10+ (Flask/Quart)
- **Frontend**: TypeScript, React, UmiJS
- **Architecture**: Microservices based on Docker.
- `api/`: Backend API server.
- `rag/`: Core RAG logic (indexing, retrieval).
- `deepdoc/`: Document parsing and OCR.
- `web/`: Frontend application.
## Current stack
- Backend: Python 3.13+, Quart-based API server, Peewee ORM, async workers.
- Frontend: React + TypeScript + Vite in `web/`.
- Go: the repository also has a substantial Go module for servers, ingestion, parser/runtime, CLI, and supporting services.
- Runtime services commonly include MySQL/PostgreSQL, Redis, MinIO, and Elasticsearch/Infinity/OpenSearch depending on configuration.
## 2. Directory Structure
- `api/`: Backend API server (Flask/Quart).
- `apps/`: API Blueprints (Knowledge Base, Chat, etc.).
- `db/`: Database models and services.
- `rag/`: Core RAG logic.
- `llm/`: LLM, Embedding, and Rerank model abstractions.
- `deepdoc/`: Document parsing and OCR modules.
- `agent/`: Agentic reasoning components.
- `web/`: Frontend application (React + UmiJS).
- `docker/`: Docker deployment configurations.
- `sdk/`: Python SDK.
- `test/`: Backend tests.
## Code layout to expect
- `api/`: Python API server entrypoints, blueprints, services, and database code.
- `rag/`: ingestion, retrieval, LLM integration, and graph RAG logic.
- `deepdoc/`: parsing and OCR.
- `agent/`: workflow canvas, components, tools, and templates.
- `cmd/`: Go entrypoints. `ragflow_main` is the main server/admin/ingestor binary surface; `ragflow-cli` is the CLI entrypoint.
- `internal/`: main Go application code. Important subtrees:
- `internal/agent/`: Go agent runtime, canvas execution, components, tool bindings, workflow helpers.
- `internal/cli/`: CLI parsing, HTTP transport, command execution, response formatting.
- `internal/dao/`: Go data-access layer and persistence-facing helpers.
- `internal/deepdoc/`: Go DeepDOC integrations, especially native-backed PDF/DOCX parsing.
- `internal/engine/`: search/index backends such as Elasticsearch and Infinity.
- `internal/entity/`: shared Go entities and model definitions.
- `internal/handler/`: HTTP handlers and route-facing request logic.
- `internal/ingestion/`: Go ingestion pipeline, canvas adapter, components, wiring, service orchestration.
- `internal/ingestion/component/`: stage implementations such as file/parser/chunker/tokenizer/extractor.
- `internal/ingestion/pipeline/`: DSL translation, canvas-driven execution, checkpoints, resume/run logic.
- `internal/parser/`: parser and chunk libraries used by ingestion and other Go paths.
- `internal/parser/parser/`: typed parse-result parsers for markdown/html/pdf/docx/xlsx/text and related families.
- `internal/parser/chunk/`: chunk operator library and DSL/typed execution helpers.
- `internal/service/`: higher-level business services used by handlers and server flows.
- `internal/storage/`: storage backends and in-memory test doubles.
- `internal/router/`: HTTP route registration.
- `internal/server/`: server bootstrap/config wiring.
- `internal/cpp/`: C++ sources used by native-backed Go features.
- `web/`: frontend application.
- `docker/`: local and production compose files.
- `sdk/` and `test/`: SDK and automated tests.
## 3. Build Instructions
## Go-specific rules
- Treat `internal/ingestion`, `internal/parser`, and `internal/deepdoc` as actively refactored code. Prefer collapsing duplicate paths over preserving transitional wrappers.
- Do not add or preserve deprecated Go APIs just to ease migration inside the repo.
- Remove commented-out Go code instead of leaving recovery notes in place.
- Keep package comments and doc comments aligned with the current runtime path, not with migration history.
### Backend (Python)
The project uses **uv** for dependency management.
## Working rules
- Before editing, inspect the nearest code path that actually owns the behavior.
- Keep changes small and local unless the task is explicitly a broader refactor.
- Prefer one implementation path instead of preserving old and new versions side by side.
- Preserve behavior with focused tests when the behavior is still valid; do not keep tests that protect obsolete behavior.
- If a surface is only there for compatibility, remove it unless the user asks to keep it.
- Do not add new compatibility wording in comments or docs.
- When a maintainer takes over a community PR, a new commit generated by rewriting history (e.g. `merge`, `rebase -i`) must preserve the original author and add the maintainer as co-author (via a `Co-authored-by:` trailer) instead of overwriting the author with the maintainer alone.
1. **Setup Environment**:
```bash
uv sync --python 3.12 --all-extras
uv run python3 download_deps.py
```
2. **Run Server**:
- **Pre-requisite**: Start dependent services (MySQL, ES/Infinity, Redis, MinIO).
```bash
docker compose -f docker/docker-compose-base.yml up -d
```
- **Launch**:
```bash
source .venv/bin/activate
export PYTHONPATH=$(pwd)
bash docker/launch_backend_service.sh
```
### Frontend (TypeScript/React)
Located in `web/`.
1. **Install Dependencies**:
```bash
cd web
npm install
```
2. **Run Dev Server**:
```bash
npm run dev
```
Runs on port 8000 by default.
### Docker Deployment
To run the full stack using Docker:
## Commands
### Backend
```bash
cd docker
docker compose -f docker-compose.yml up -d
uv sync --python 3.13 --all-extras
uv run python3 ragflow_deps/download_deps.py
docker compose -f docker/docker-compose-base.yml up -d
source .venv/bin/activate
export PYTHONPATH=$(pwd)
bash docker/launch_backend_service.sh
uv run pytest
ruff check
ruff format
```
## 4. Testing Instructions
### Frontend
```bash
cd web
npm install
npm run dev
npm run build
npm run lint
npm run test
npm run type-check
```
### Backend Tests
- **Run All Tests**:
```bash
uv run pytest
```
- **Run Specific Test**:
```bash
uv run pytest test/test_api.py
```
### Go
```bash
uv run ragflow_deps/download_deps.py
bash build.sh --test ./path/to/package/...
bash build.sh --go
# or build specific binaries:
bash build.sh --all
```
### Frontend Tests
- **Run Tests**:
```bash
cd web
npm run test
```
## 5. Coding Standards & Guidelines
- **Python Formatting**: Use `ruff` for linting and formatting.
```bash
ruff check
ruff format
```
- **Frontend Linting**:
```bash
cd web
npm run lint
```
- **Pre-commit**: Ensure pre-commit hooks are installed.
```bash
pre-commit install
pre-commit run --all-files
```
## Validation preference
- Run the narrowest relevant test, lint, or build command after a change.
- For backend changes, prefer targeted pytest or ruff checks over full-suite runs.
- For frontend changes, prefer the touched-package lint, type-check, or test command.
- For Go changes, prefer package-scoped `bash build.sh --test ...` first.
- Do not default to raw `go test`, `go build`, or IDE Run/Debug for Go in this repo. They often miss the required CGO flags and native static libraries (`office_oxide`, `pdfium-static`, `pdf_oxide`) that `build.sh` wires correctly.
- If Go native builds fail, inspect `build.sh` and `internal/development.md` before changing code. Common environment issues are missing downloaded native deps and missing `lld` on Linux.
## Default review checklist
- Remove instead of retaining `deprecated`, `legacy`, or compatibility-only code.
- Collapse duplicate implementations to one path.
- Drop stale comments and documentation that describe a superseded design.
- Keep exported APIs only when the current code actually needs them.

134
CLAUDE.md
View File

@@ -1,134 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
RAGFlow is an open-source RAG (Retrieval-Augmented Generation) engine based on deep document understanding. It's a full-stack application with:
- Python backend (Flask-based API server)
- React/TypeScript frontend (built with vitejs)
- Microservices architecture with Docker deployment
- Multiple data stores (MySQL, Elasticsearch/Infinity, Redis, MinIO)
## Architecture
### Backend (`/api/`)
- **Main Server**: `api/ragflow_server.py` - Flask application entry point
- **Apps**: Modular Flask blueprints in `api/apps/` for different functionalities:
- `kb_app.py` - Knowledge base management
- `dialog_app.py` - Chat/conversation handling
- `document_app.py` - Document processing
- `canvas_app.py` - Agent workflow canvas
- `file_app.py` - File upload/management
- **Services**: Business logic in `api/db/services/`
- **Models**: Database models in `api/db/db_models.py`
### Core Processing (`/rag/`)
- **Document Processing**: `deepdoc/` - PDF parsing, OCR, layout analysis
- **LLM Integration**: `rag/llm/` - Model abstractions for chat, embedding, reranking
- **RAG Pipeline**: `rag/flow/` - Chunking, parsing, tokenization
- **Graph RAG**: `rag/graphrag/` - Knowledge graph construction and querying
### Agent System (`/agent/`)
- **Components**: Modular workflow components (LLM, retrieval, categorize, etc.)
- **Templates**: Pre-built agent workflows in `agent/templates/`
- **Tools**: External API integrations (Tavily, Wikipedia, SQL execution, etc.)
### Frontend (`/web/`)
- React/TypeScript with vitejs framework
- shadcn/ui components
- State management with Zustand
- Tailwind CSS for styling
## Common Development Commands
### Backend Development
```bash
# Install Python dependencies
uv sync --python 3.13 --all-extras
uv run python3 download_deps.py
pre-commit install
# Start dependent services
docker compose -f docker/docker-compose-base.yml up -d
# Run backend (requires services to be running)
source .venv/bin/activate
export PYTHONPATH=$(pwd)
bash docker/launch_backend_service.sh
# Run tests
uv run pytest
# Linting
ruff check
ruff format
```
### Frontend Development
```bash
cd web
npm install
npm run dev # Development server
npm run build # Production build
npm run lint # ESLint
npm run test # Jest tests
```
### Docker Development
```bash
# Full stack with Docker
cd docker
docker compose -f docker-compose.yml up -d
# Check server status
docker logs -f ragflow-server
# Rebuild images
docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly .
```
## Key Configuration Files
- `docker/.env` - Environment variables for Docker deployment
- `docker/service_conf.yaml.template` - Backend service configuration
- `pyproject.toml` - Python dependencies and project configuration
- `web/package.json` - Frontend dependencies and scripts
## Testing
- **Python**: pytest with markers (p1/p2/p3 priority levels)
- **Frontend**: Jest with React Testing Library
- **API Tests**: HTTP API and SDK tests in `test/` and `sdk/python/test/`
## Database Engines
RAGFlow supports switching between Elasticsearch (default) and Infinity:
- Set `DOC_ENGINE=infinity` in `docker/.env` to use Infinity
- Requires container restart: `docker compose down -v && docker compose up -d`
## Development Environment Requirements
- Python 3.10-3.13
- Node.js >=18.20.4
- Docker & Docker Compose
- uv package manager
- 16GB+ RAM, 50GB+ disk space
1. Think before acting. Read existing files before writing code.
2. Be concise in output but thorough in reasoning.
3. Prefer editing over rewriting whole files.
4. Do not re-read files you have already read.
5. Test your code before declaring done.
6. No sycophantic openers or closing fluff.
7. Keep solutions simple and direct.
8. User instructions always override this file.

1
CLAUDE.md Symbolic link
View File

@@ -0,0 +1 @@
AGENTS.md

View File

@@ -35,15 +35,19 @@ RUN --mount=type=cache,id=ragflow_apt,target=/var/cache/apt,sharing=locked \
apt update && \
apt --no-install-recommends install -y ca-certificates; \
if [ "$NEED_MIRROR" == "1" ]; then \
sed -i 's|http://archive.ubuntu.com/ubuntu|https://mirrors.aliyun.com/ubuntu|g' /etc/apt/sources.list.d/ubuntu.sources; \
sed -i 's|http://security.ubuntu.com/ubuntu|https://mirrors.aliyun.com/ubuntu|g' /etc/apt/sources.list.d/ubuntu.sources; \
# CI runners may inject a proxy whose TLS certificate is not trusted inside
# the fresh Ubuntu base image yet. Keep the Ubuntu mirror on HTTP here so
# the mirror switch remains usable before the full CA store is available.
sed -i 's|http://archive.ubuntu.com/ubuntu|http://mirrors.aliyun.com/ubuntu|g' /etc/apt/sources.list.d/ubuntu.sources; \
sed -i 's|http://security.ubuntu.com/ubuntu|http://mirrors.aliyun.com/ubuntu|g' /etc/apt/sources.list.d/ubuntu.sources; \
fi; \
rm -f /etc/apt/apt.conf.d/docker-clean && \
echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache && \
chmod 1777 /tmp && \
apt update && \
apt install -y \
build-essential libglib2.0-0 libglx-mesa0 libgl1 pkg-config libicu-dev libgdiplus default-jdk libatk-bridge2.0-0 libpython3-dev libgtk-4-1 libnss3 xdg-utils libgbm-dev libjemalloc-dev gnupg unzip curl wget git vim less ghostscript pandoc texlive texlive-latex-extra texlive-xetex texlive-lang-chinese fonts-freefont-ttf fonts-noto-cjk postgresql-client
libglib2.0-0 libglx-mesa0 libgl1 pkg-config libgdiplus default-jdk libatk-bridge2.0-0 libgtk-4-1 libnss3 xdg-utils libjemalloc-dev gnupg unzip curl wget git vim less ghostscript pandoc texlive texlive-latex-extra texlive-xetex texlive-lang-chinese fonts-freefont-ttf fonts-noto-cjk postgresql-client && \
rm -rf /var/lib/apt/lists/*
# Download resource from GitHub to /usr/share/infinity
RUN mkdir -p /usr/share/infinity/resource && \
@@ -55,14 +59,15 @@ RUN mkdir -p /usr/share/infinity/resource && \
cp -r /tmp/resource/* /usr/share/infinity/resource && \
rm -rf /tmp/resource
ARG NGINX_VERSION=1.29.5-1~noble
ARG NGINX_VERSION=1.31.2-1~noble
RUN --mount=type=cache,id=ragflow_apt,target=/var/cache/apt,sharing=locked \
mkdir -p /etc/apt/keyrings && \
curl --retry 5 --retry-delay 2 --retry-all-errors -fsSL https://nginx.org/keys/nginx_signing.key | gpg --dearmor -o /etc/apt/keyrings/nginx-archive-keyring.gpg && \
echo "deb [signed-by=/etc/apt/keyrings/nginx-archive-keyring.gpg] https://nginx.org/packages/mainline/ubuntu/ noble nginx" > /etc/apt/sources.list.d/nginx.list && \
apt -o Acquire::Retries=5 update && \
apt -o Acquire::Retries=5 install -y nginx=${NGINX_VERSION} && \
apt-mark hold nginx
apt-mark hold nginx && \
rm -rf /var/lib/apt/lists/*
# Install uv
RUN --mount=type=bind,from=infiniflow/ragflow_deps:latest,source=/,target=/deps \
@@ -91,7 +96,51 @@ RUN --mount=type=cache,id=ragflow_apt,target=/var/cache/apt,sharing=locked \
apt purge -y nodejs npm && \
apt autoremove -y && \
apt update && \
apt install -y nodejs
apt install -y nodejs && \
rm -rf /var/lib/apt/lists/*
# stagehand-server-v3 (Node.js SEA binary used by Browser component
# in local mode).
#
# The `v3.21.0` value below is the `stagehand-go/v3` Go module
# version pinned in `go.mod`. It is used here only to compute the
# `go_<ver>/` subdirectory that `local.go:cacheDir()` will look in
# for the binary at runtime — that subdirectory name is keyed by
# the Go module's own `internal.PackageVersion`, NOT by the server
# binary's release tag.
#
# The server binary itself is fetched separately by `download_deps.py`
# from the browserbase/stagehand GitHub releases. The two are
# LOOSELY MATCHED — both stay on the v3.x line and remain protocol-
# compatible, but the version numbers do NOT track each other (Go
# SDK is at v3.21.0, server binary is at v3.7.2 today). On every
# go.mod bump, refresh the server binary pin in `download_deps.py`
# to the current latest server release; no version correspondence
# is required to maintain.
#
# Drift on the Go SDK pin (this ARG vs go.mod) forces a fresh
# GitHub download at process boot — a hard failure in air-gapped
# deployments. CI cross-checks the two values.
#
# The binary is pre-fetched by `download_deps.py` and shipped via
# the ragflow_deps image, then written directly to the stagehand-go
# cache path that `local.go:cacheDir()` constructs at runtime —
# `/root/.cache/stagehand/lib/go_<ver>/stagehand-server-v3-<arch>`.
ARG STAGEHAND_GO_VERSION=v3.21.0
RUN --mount=type=bind,from=infiniflow/ragflow_deps:latest,source=/,target=/deps \
set -eux; \
arch="$(uname -m)"; \
case "$arch" in \
x86_64) stagehand_arch=x64 ;; \
aarch64|arm64) stagehand_arch=arm64 ;; \
*) echo "Unsupported architecture: $arch" >&2; exit 1 ;; \
esac; \
stagehand_version="${STAGEHAND_GO_VERSION#v}"; \
stagehand_cache_dir="/root/.cache/stagehand/lib/go_${stagehand_version}"; \
mkdir -p "${stagehand_cache_dir}"; \
cp "/deps/stagehand-server-v3-linux-${stagehand_arch}" \
"${stagehand_cache_dir}/stagehand-server-v3-linux-${stagehand_arch}"; \
chmod +x "${stagehand_cache_dir}/stagehand-server-v3-linux-${stagehand_arch}"
# Add msssql ODBC driver
# macOS ARM64 environment, install msodbcsql18.
@@ -107,7 +156,8 @@ RUN --mount=type=cache,id=ragflow_apt,target=/var/cache/apt,sharing=locked \
else \
# x86_64 or others \
ACCEPT_EULA=Y apt install -y unixodbc-dev msodbcsql17; \
fi || \
fi && \
rm -rf /var/lib/apt/lists/* || \
{ echo "Failed to install ODBC driver"; exit 1; }
@@ -136,26 +186,54 @@ USER root
WORKDIR /ragflow
# Install build-only dependencies for compiling Python C extensions.
# These are not inherited from base to keep the production image smaller.
RUN --mount=type=cache,id=ragflow_apt,target=/var/cache/apt,sharing=locked \
apt update && \
apt install -y build-essential libpython3-dev libicu-dev libgbm-dev && \
rm -rf /var/lib/apt/lists/*
# install dependencies from uv.lock file
COPY pyproject.toml uv.lock ./
# https://github.com/astral-sh/uv/issues/10462
# uv records index url into uv.lock but doesn't failover among multiple indexes
# Also rewrite pypi.tuna.tsinghua.edu.cn to mirrors.aliyun.com/pypi so locks
# that were resolved against the Tsinghua mirror (e.g. when UV_INDEX pointed
# there) get normalized to the Aliyun mirror in NEED_MIRROR=1 builds. Without
# this, stale Tsinghua URLs slip through and `uv sync --frozen` 404s on
# packages that the Tsinghua mirror no longer carries.
RUN --mount=type=cache,id=ragflow_uv,target=/root/.cache/uv,sharing=locked \
if [ "$NEED_MIRROR" == "1" ]; then \
sed -i 's|pypi.org|mirrors.aliyun.com/pypi|g' uv.lock; \
sed -i 's|pypi.tuna.tsinghua.edu.cn|mirrors.aliyun.com/pypi|g' uv.lock; \
else \
sed -i 's|mirrors.aliyun.com/pypi|pypi.org|g' uv.lock; \
sed -i 's|pypi.tuna.tsinghua.edu.cn|pypi.org|g' uv.lock; \
sed -i 's|gitee.com|github.com|g' uv.lock; \
fi; \
uv sync --python 3.13 --frozen && \
# --refresh-package litellm forces a re-download of litellm from the
# (post-sed) URLs in uv.lock even if BuildKit's persistent uv cache mount
# holds a stale wheel from a previous build. litellm 1.88.x has had
# multiple internal ImportError issues (1.88.1 missing
# DEFAULT_HEALTH_CHECK_STALENESS_MULTIPLIER, 1.88.0 wheel pulled via
# some proxies missing RedisPipelineLpopOperation) — always re-fetching
# the locked version avoids serving a half-broken cached copy.
uv sync --python 3.13 --frozen --refresh-package litellm && \
# Ensure pip is available in the venv for runtime package installation (fixes #12651)
.venv/bin/python3 -m ensurepip --upgrade
# Install frontend dependencies — depends only on package manifests so
# web source / docs changes don't invalidate this layer.
COPY web/package.json web/package-lock.json web/.npmrc ./web/
RUN --mount=type=cache,id=ragflow_npm,target=/root/.npm,sharing=locked \
cd web && NODE_OPTIONS="--max-old-space-size=8192" npm install
# Copy full web source and docs for the frontend build.
COPY web web
COPY docs docs
RUN --mount=type=cache,id=ragflow_npm,target=/root/.npm,sharing=locked \
cd web && NODE_OPTIONS="--max-old-space-size=8192" npm install && \
NODE_OPTIONS="--max-old-space-size=8192" VITE_BUILD_SOURCEMAP=false VITE_MINIFY=esbuild npm run build
cd web && NODE_OPTIONS="--max-old-space-size=8192" VITE_BUILD_SOURCEMAP=false VITE_MINIFY=esbuild npm run build
COPY .git /ragflow/.git
@@ -177,7 +255,6 @@ ENV PATH="${VIRTUAL_ENV}/bin:${PATH}"
ENV PYTHONPATH=/ragflow/
COPY web web
COPY admin admin
COPY api api
COPY conf conf
@@ -189,6 +266,7 @@ COPY mcp mcp
COPY common common
COPY memory memory
COPY bin bin
COPY tools/scripts tools/scripts
COPY docker/service_conf.yaml.template ./conf/service_conf.yaml.template
COPY docker/entrypoint.sh ./

View File

@@ -1,10 +0,0 @@
# This builds an image that contains the resources needed by Dockerfile
#
FROM scratch
# Copy resources downloaded via download_deps.py
COPY chromedriver-linux64-121-0-6167-85 chrome-linux64-121-0-6167-85 cl100k_base.tiktoken libssl1.1_1.1.1f-1ubuntu2_amd64.deb libssl1.1_1.1.1f-1ubuntu2_arm64.deb tika-server-standard-3.3.0.jar tika-server-standard-3.3.0.jar.md5 libssl*.deb uv-x86_64-unknown-linux-gnu.tar.gz uv-aarch64-unknown-linux-gnu.tar.gz /
COPY nltk_data /nltk_data
COPY huggingface.co /huggingface.co

66
Dockerfile_deepdoc_oss Normal file
View File

@@ -0,0 +1,66 @@
# OSS DeepDoc server — minimal image with ONNX-only inference.
# Build: docker build -f docker/Dockerfile_deepdoc_oss -t deepdoc_oss:latest .
# With mirror (China): docker build --build-arg NEED_MIRROR=1 -f docker/Dockerfile_deepdoc_oss -t deepdoc_oss:latest .
FROM ubuntu:24.04
ARG NEED_MIRROR=1
ENV PYTHONPATH=/app
ENV DEBIAN_FRONTEND=noninteractive
# ── System dependencies (onnxruntime + opencv runtime libs) ──
RUN apt-get update && apt-get install -y --no-install-recommends \
-o Acquire::Retries=5 \
python3.12 python3.12-venv \
libglib2.0-0 libglx-mesa0 libgl1 libgomp1 \
libgdiplus curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# ── Python venv with ONNX inference stack ──
RUN python3.12 -m venv /app/.venv
COPY deepdoc/server/pyproject.toml /tmp/pyproject.toml
RUN PIP_INDEX="https://pypi.org/simple" && \
PIP_TRUSTED="" && \
if [ "$NEED_MIRROR" = "1" ]; then \
PIP_INDEX="https://mirrors.aliyun.com/pypi/simple"; \
PIP_TRUSTED="mirrors.aliyun.com"; \
fi && \
if [ -n "$PIP_TRUSTED" ]; then \
/app/.venv/bin/pip install --no-cache-dir -i "$PIP_INDEX" --trusted-host "$PIP_TRUSTED" \
litserve onnxruntime opencv-python-headless numpy pillow pyclipper \
python-multipart shapely six huggingface_hub; \
else \
/app/.venv/bin/pip install --no-cache-dir -i "$PIP_INDEX" \
litserve onnxruntime opencv-python-headless numpy pillow pyclipper \
python-multipart shapely six huggingface_hub; \
fi
# ── ONNX models (downloaded from HuggingFace) ──
COPY deepdoc/server/download_deps.py /tmp/download_deps.py
RUN if [ "$NEED_MIRROR" = "1" ]; then \
export HF_ENDPOINT=https://hf-mirror.com; \
fi && \
mkdir -p /app/rag/res/deepdoc && \
/app/.venv/bin/python3 /tmp/download_deps.py /app/rag/res/deepdoc
# ── Vision module (ONNX inference logic) ──
RUN mkdir -p /app/deepdoc/vision
COPY deepdoc/vision/ /app/deepdoc/vision/
# ── Docker stubs (lightweight replacements for heavy common/rag/deepdoc imports) ──
COPY deepdoc/server/docker_stubs.py /tmp/docker_stubs.py
RUN /app/.venv/bin/python3 /tmp/docker_stubs.py
# ── Server code ──
RUN mkdir -p /app/deepdoc/server/endpoints /app/deepdoc/server/adapters
COPY deepdoc/server/deepdoc_server.py /app/deepdoc/server/
COPY deepdoc/server/endpoints/ /app/deepdoc/server/endpoints/
COPY deepdoc/server/adapters/ /app/deepdoc/server/adapters/
EXPOSE 9390
HEALTHCHECK --interval=10s --timeout=10s --retries=5 \
CMD curl -f http://localhost:9390/health || exit 1
ENTRYPOINT ["/app/.venv/bin/python3", "/app/deepdoc/server/deepdoc_server.py", "--model-dir", "/app/rag/res/deepdoc"]

View File

@@ -1,6 +1,6 @@
<div align="center">
<a href="https://cloud.ragflow.io/">
<img src="web/src/assets/logo-with-text.svg" width="520" alt="ragflow logo">
<img src="https://raw.githubusercontent.com/infiniflow/ragflow/main/web/src/assets/logo-with-text.svg" width="520" alt="ragflow logo">
</a>
</div>
@@ -25,7 +25,7 @@
<img alt="Static Badge" src="https://img.shields.io/badge/Get-Started-4e6b99">
</a>
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
<img src="https://img.shields.io/docker/pulls/infiniflow/ragflow?label=Docker%20Pulls&color=0db7ed&logo=docker&logoColor=white&style=flat-square" alt="docker pull infiniflow/ragflow:v0.25.5">
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/infiniflow/ragflow-stats/main/badges/docker-pulls.json&style=flat-square&logo=docker&logoColor=white" alt="docker pull infiniflow/ragflow:v0.26.4">
</a>
<a href="https://github.com/infiniflow/ragflow/releases/latest">
<img src="https://img.shields.io/github/v/release/infiniflow/ragflow?color=blue&label=Latest%20Release" alt="Latest Release">
@@ -87,6 +87,7 @@ Try our cloud service at [https://cloud.ragflow.io](https://cloud.ragflow.io).
## 🔥 Latest Updates
- 2026-06-15 Support multiple chat channels such as Feishu, Discord, Telegram, Line, etc.
- 2026-04-24 Supports DeepSeek v4.
- 2026-03-24 [RAGFlow Skill on OpenClaw](https://clawhub.ai/yingfeng/ragflow-skill) — Provides an official skill for accessing RAGFlow datasets via OpenClaw.
- 2025-12-26 Supports 'Memory' for AI agent.
@@ -97,7 +98,6 @@ Try our cloud service at [https://cloud.ragflow.io](https://cloud.ragflow.io).
- 2025-08-08 Supports OpenAI's latest GPT-5 series models.
- 2025-08-01 Supports agentic workflow and MCP.
- 2025-05-23 Adds a Python/JavaScript code executor component to Agent.
- 2025-05-05 Supports cross-language query.
- 2025-03-19 Supports using a multi-modal model to make sense of images within PDF or DOCX files.
## 🎉 Stay Tuned
@@ -152,6 +152,7 @@ releases! 🌟
- RAM >= 16 GB
- Disk >= 50 GB
- Docker >= 24.0.0 & Docker Compose >= v2.26.1
- Python >= 3.13
- [gVisor](https://gvisor.dev/docs/user_guide/install/): Required only if you intend to use the code executor (sandbox) feature of RAGFlow.
> [!TIP]
@@ -192,12 +193,12 @@ releases! 🌟
> All Docker images are built for x86 platforms. We don't currently offer Docker images for ARM64.
> If you are on an ARM64 platform, follow [this guide](https://ragflow.io/docs/dev/build_docker_image) to build a Docker image compatible with your system.
> The command below downloads the `v0.25.5` edition of the RAGFlow Docker image. See the following table for descriptions of different RAGFlow editions. To download a RAGFlow edition different from `v0.25.5`, update the `RAGFLOW_IMAGE` variable accordingly in **docker/.env** before using `docker compose` to start the server.
> The command below downloads the `v0.26.4` edition of the RAGFlow Docker image. See the following table for descriptions of different RAGFlow editions. To download a RAGFlow edition different from `v0.26.4`, update the `RAGFLOW_IMAGE` variable accordingly in **docker/.env** before using `docker compose` to start the server.
```bash
$ cd ragflow/docker
# git checkout v0.25.5
git checkout v0.26.4
# Optional: use a stable tag (see releases: https://github.com/infiniflow/ragflow/releases)
# This step ensures the **entrypoint.sh** file in the code matches the Docker image version.
@@ -318,19 +319,24 @@ docker build --platform linux/amd64 \
## 🔨 Launch service from source for development
1. Install `uv` and `pre-commit`, or skip this step if they are already installed:
> [!IMPORTANT]
> After cloning the repository for the first time, run `git config --local --unset core.hooksPath`, `uv tool install lefthook` and `lefthook install` once from the repo root to enable local Git hooks.
1. Install `uv`, or skip this step if it is already installed:
```bash
pipx install uv pre-commit
pipx install uv
```
2. Clone the source code and install Python dependencies:
```bash
git clone https://github.com/infiniflow/ragflow.git
cd ragflow/
uv sync --python 3.12 # install RAGFlow dependent python modules
uv run python3 download_deps.py
pre-commit install
uv sync --python 3.13 # install RAGFlow dependent python modules
uv run python3 ragflow_deps/download_deps.py
git config --local --unset core.hooksPath
uv tool install lefthook
lefthook install
```
3. Launch the dependent services (MinIO, Elasticsearch, Redis, and MySQL) using Docker Compose:

View File

@@ -1,6 +1,6 @@
<div align="center">
<a href="https://cloud.ragflow.io/">
<img src="web/src/assets/logo-with-text.svg" width="520" alt="ragflow logo">
<img src="https://raw.githubusercontent.com/infiniflow/ragflow/main/web/src/assets/logo-with-text.svg" width="520" alt="ragflow logo">
</a>
</div>
@@ -25,7 +25,7 @@
<img alt="Static Badge" src="https://img.shields.io/badge/Get-Started-4e6b99">
</a>
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
<img src="https://img.shields.io/docker/pulls/infiniflow/ragflow?label=Docker%20Pulls&color=0db7ed&logo=docker&logoColor=white&style=flat-square" alt="docker pull infiniflow/ragflow:v0.25.5">
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/infiniflow/ragflow-stats/main/badges/docker-pulls.json&style=flat-square&logo=docker&logoColor=white" alt="docker pull infiniflow/ragflow:v0.26.4">
</a>
<a href="https://github.com/infiniflow/ragflow/releases/latest">
<img src="https://img.shields.io/github/v/release/infiniflow/ragflow?color=blue&label=Latest%20Release" alt="Latest Release">
@@ -87,6 +87,7 @@
## 🔥 آخر التحديثات
- 15-06-2026 يدعم قنوات دردشة متعددة مثل Feishu و Discord و Telegram و Line وما إلى ذلك.
- 24-04-2026 يدعم DeepSeek v4.
- 24-03-2026 [RAGFlow Skill on OpenClaw](https://clawhub.ai/yingfeng/ragflow-skill) — توفر مهارة رسمية للوصول إلى مجموعات بيانات RAGFlow عبر OpenClaw.
- 26-12-2025 يدعم ميزة "Memory" لوكلاء الذكاء الاصطناعي.
@@ -97,7 +98,6 @@
- 08-08-2025 يدعم أحدث موديلات سلسلة OpenAI.
- 01-08-2025 يدعم سير العمل الوكيل وMCP.
- 23-05-2025 تمت إضافة مكون منفذ كود Python/JavaScript إلى Agent.
- 05-05-2025 يدعم الاستعلام بين اللغات.
- 19-03-2025 يدعم استخدام نموذج متعدد الوسائط لفهم الصور داخل ملفات PDF أو DOCX.
## 🎉 تابعونا
@@ -152,6 +152,7 @@
- الرام >= 16 جيجا
- القرص >= 50 جيجا بايت
- Docker >= 24.0.0 & Docker Compose >= v2.26.1
- بايثون >= 3.13
- [gVisor](https://gvisor.dev/docs/user_guide/install/): مطلوب فقط إذا كنت تنوي استخدام ميزة منفذ التعليمات البرمجية (وضع الحماية) لـ RAGFlow.
> [!TIP]
@@ -192,12 +193,12 @@
> جميع الصور Docker مصممة لمنصات x86. لا نعرض حاليًا صور Docker لـ ARM64.
> إذا كنت تستخدم نظامًا أساسيًا ARM64، فاتبع [هذا الدليل](https://ragflow.io/docs/dev/build_docker_image) لإنشاء صورة Docker متوافقة مع نظامك.
> يقوم الأمر أدناه بتنزيل إصدار `v0.25.5` من الصورة RAGFlow Docker. راجع الجدول التالي للحصول على أوصاف لإصدارات RAGFlow المختلفة. لتنزيل إصدار RAGFlow مختلف عن `v0.25.5`، قم بتحديث المتغير `RAGFLOW_IMAGE` وفقًا لذلك في **docker/.env** قبل استخدام `docker compose` لبدء تشغيل الخادم.
> يقوم الأمر أدناه بتنزيل إصدار `v0.26.4` من الصورة RAGFlow Docker. راجع الجدول التالي للحصول على أوصاف لإصدارات RAGFlow المختلفة. لتنزيل إصدار RAGFlow مختلف عن `v0.26.4`، قم بتحديث المتغير `RAGFLOW_IMAGE` وفقًا لذلك في **docker/.env** قبل استخدام `docker compose` لبدء تشغيل الخادم.
```bash
$ cd ragflow/docker
# git checkout v0.25.5
git checkout v0.26.4
# Optional: use a stable tag (see releases: https://github.com/infiniflow/ragflow/releases)
# This step ensures the **entrypoint.sh** file in the code matches the Docker image version.
@@ -318,19 +319,21 @@ docker build --platform linux/amd64 \
## 🔨 إطلاق الخدمة من المصدر للتطوير
1. قم بتثبيت `uv` و`pre-commit`، أو قم بتخطي هذه الخطوة إذا كانا مثبتين بالفعل:
1. قم بتثبيت `uv`، أو قم بتخطي هذه الخطوة إذا كان مثبتًا بالفعل:
```bash
pipx install uv pre-commit
pipx install uv
```
2. استنساخ الكود المصدري وتثبيت تبعيات بايثون:
```bash
git clone https://github.com/infiniflow/ragflow.git
cd ragflow/
uv sync --python 3.12 # install RAGFlow dependent python modules
uv run python3 download_deps.py
pre-commit install
uv sync --python 3.13 # install RAGFlow dependent python modules
uv run python3 ragflow_deps/download_deps.py
git config --local --unset core.hooksPath
uv tool install lefthook
lefthook install
```
3. قم بتشغيل الخدمات التابعة (MinIO وElasticsearch وRedis وMySQL) باستخدام Docker Compose:

View File

@@ -1,6 +1,6 @@
<div align="center">
<a href="https://cloud.ragflow.io/">
<img src="web/src/assets/logo-with-text.svg" width="520" alt="ragflow logo">
<img src="https://raw.githubusercontent.com/infiniflow/ragflow/main/web/src/assets/logo-with-text.svg" width="520" alt="ragflow logo">
</a>
</div>
@@ -25,10 +25,10 @@
<img alt="Badge statique" src="https://img.shields.io/badge/Get-Started-4e6b99">
</a>
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
<img src="https://img.shields.io/docker/pulls/infiniflow/ragflow?label=Docker%20Pulls&color=0db7ed&logo=docker&logoColor=white&style=flat-square" alt="docker pull infiniflow/ragflow:v0.25.5">
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/infiniflow/ragflow-stats/main/badges/docker-pulls.json&style=flat-square&logo=docker&logoColor=white" alt="docker pull infiniflow/ragflow:v0.26.4">
</a>
<a href="https://github.com/infiniflow/ragflow/releases/latest">
<img src="https://img.shields.io/github/v/release/infiniflow/ragflow?color=blue&label=Dernière%20version" alt="Dernière version">
<img src="https://img.shields.io/github/v/release/infiniflow/ragflow?color=blue&label=Derniere%20version" alt="Dernière version">
</a>
<a href="https://github.com/infiniflow/ragflow/blob/main/LICENSE">
<img height="21" src="https://img.shields.io/badge/License-Apache--2.0-ffffff?labelColor=d4eaf7&color=2e6cc4" alt="licence">
@@ -87,6 +87,7 @@ Essayez notre service cloud sur [https://cloud.ragflow.io](https://cloud.ragflow
## 🔥 Dernières mises à jour
- 15-06-2026 Prise en charge de plusieurs canaux de discussion tels que Feishu, Discord, Telegram, Line, etc.
- 24-04-2026 Prise en charge de DeepSeek v4.
- 24-03-2026 [RAGFlow Skill on OpenClaw](https://clawhub.ai/yingfeng/ragflow-skill) — Fournit un skill officiel pour accéder aux datasets RAGFlow via OpenClaw.
- 26-12-2025 Prise en charge de la « Mémoire » pour l'agent IA.
@@ -97,7 +98,6 @@ Essayez notre service cloud sur [https://cloud.ragflow.io](https://cloud.ragflow
- 08-08-2025 Prise en charge des derniers modèles de la série GPT-5 d'OpenAI.
- 01-08-2025 Prise en charge du flux de travail agentique et de MCP.
- 23-05-2025 Ajout d'un composant exécuteur de code Python/JavaScript à l'Agent.
- 05-05-2025 Prise en charge des requêtes inter-langues.
- 19-03-2025 Prise en charge de l'utilisation d'un modèle multi-modal pour analyser les images dans les fichiers PDF ou DOCX.
## 🎉 Restez informé
@@ -150,6 +150,7 @@ Essayez notre service cloud sur [https://cloud.ragflow.io](https://cloud.ragflow
- RAM >= 16 Go
- Disque >= 50 Go
- Docker >= 24.0.0 & Docker Compose >= v2.26.1
- Python >= 3.13
- [gVisor](https://gvisor.dev/docs/user_guide/install/) : Requis uniquement si vous souhaitez utiliser la fonctionnalité d'exécuteur de code (sandbox) de RAGFlow.
> [!TIP]
@@ -189,12 +190,12 @@ Essayez notre service cloud sur [https://cloud.ragflow.io](https://cloud.ragflow
> Toutes les images Docker sont construites pour les plateformes x86. Nous ne proposons pas actuellement d'images Docker pour ARM64.
> Si vous êtes sur une plateforme ARM64, suivez [ce guide](https://ragflow.io/docs/dev/build_docker_image) pour construire une image Docker compatible avec votre système.
> La commande ci-dessous télécharge l'édition `v0.25.5` de l'image Docker RAGFlow. Consultez le tableau suivant pour les descriptions des différentes éditions de RAGFlow. Pour télécharger une édition de RAGFlow différente de `v0.25.5`, mettez à jour la variable `RAGFLOW_IMAGE` dans **docker/.env** avant d'utiliser `docker compose` pour démarrer le serveur.
> La commande ci-dessous télécharge l'édition `v0.26.4` de l'image Docker RAGFlow. Consultez le tableau suivant pour les descriptions des différentes éditions de RAGFlow. Pour télécharger une édition de RAGFlow différente de `v0.26.4`, mettez à jour la variable `RAGFLOW_IMAGE` dans **docker/.env** avant d'utiliser `docker compose` pour démarrer le serveur.
```bash
$ cd ragflow/docker
# git checkout v0.25.5
git checkout v0.26.4
# Optionnel : utiliser un tag stable (voir les versions : https://github.com/infiniflow/ragflow/releases)
# Cette étape garantit que le fichier **entrypoint.sh** dans le code correspond à la version de l'image Docker.
@@ -309,19 +310,21 @@ docker build --platform linux/amd64 \
## 🔨 Lancer le service depuis les sources pour le développement
1. Installez `uv` et `pre-commit`, ou ignorez cette étape s'ils sont déjà installés :
1. Installez `uv`, ou ignorez cette étape s'il est déjà installé :
```bash
pipx install uv pre-commit
pipx install uv
```
2. Clonez le code source et installez les dépendances Python :
```bash
git clone https://github.com/infiniflow/ragflow.git
cd ragflow/
uv sync --python 3.12 # install RAGFlow dependent python modules
uv run python3 download_deps.py
pre-commit install
uv sync --python 3.13 # install RAGFlow dependent python modules
uv run python3 ragflow_deps/download_deps.py
git config --local --unset core.hooksPath
uv tool install lefthook
lefthook install
```
3. Lancez les services dépendants (MinIO, Elasticsearch, Redis et MySQL) avec Docker Compose :

View File

@@ -1,6 +1,6 @@
<div align="center">
<a href="https://cloud.ragflow.io/">
<img src="web/src/assets/logo-with-text.svg" width="520" alt="Logo ragflow">
<img src="https://raw.githubusercontent.com/infiniflow/ragflow/main/web/src/assets/logo-with-text.svg" width="520" alt="Logo ragflow">
</a>
</div>
@@ -25,7 +25,7 @@
<img alt="Lencana Daring" src="https://img.shields.io/badge/Get-Started-4e6b99">
</a>
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
<img src="https://img.shields.io/docker/pulls/infiniflow/ragflow?label=Docker%20Pulls&color=0db7ed&logo=docker&logoColor=white&style=flat-square" alt="docker pull infiniflow/ragflow:v0.25.5">
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/infiniflow/ragflow-stats/main/badges/docker-pulls.json&style=flat-square&logo=docker&logoColor=white" alt="docker pull infiniflow/ragflow:v0.26.4">
</a>
<a href="https://github.com/infiniflow/ragflow/releases/latest">
<img src="https://img.shields.io/github/v/release/infiniflow/ragflow?color=blue&label=Rilis%20Terbaru" alt="Rilis Terbaru">
@@ -87,6 +87,7 @@ Coba layanan cloud kami di [https://cloud.ragflow.io](https://cloud.ragflow.io).
## 🔥 Pembaruan Terbaru
- 2026-06-15 Mendukung berbagai saluran obrolan seperti Feishu, Discord, Telegram, Line, dll.
- 2026-04-24 Mendukung DeepSeek v4.
- 2026-03-24 [RAGFlow Skill on OpenClaw](https://clawhub.ai/yingfeng/ragflow-skill) — Menyediakan skill resmi untuk mengakses dataset RAGFlow melalui OpenClaw.
- 2025-12-26 Mendukung 'Memori' untuk agen AI.
@@ -97,10 +98,7 @@ Coba layanan cloud kami di [https://cloud.ragflow.io](https://cloud.ragflow.io).
- 2025-08-08 Mendukung model seri GPT-5 terbaru dari OpenAI.
- 2025-08-01 Mendukung alur kerja agen dan MCP.
- 2025-05-23 Menambahkan komponen pelaksana kode Python/JS ke Agen.
- 2025-05-05 Mendukung kueri lintas bahasa.
- 2025-03-19 Mendukung penggunaan model multi-modal untuk memahami gambar di dalam file PDF atau DOCX.
- 2024-12-18 Meningkatkan model Analisis Tata Letak Dokumen di DeepDoc.
- 2024-08-22 Dukungan untuk teks ke pernyataan SQL melalui RAG.
## 🎉 Tetap Terkini
@@ -152,6 +150,7 @@ Coba layanan cloud kami di [https://cloud.ragflow.io](https://cloud.ragflow.io).
- RAM >= 16 GB
- Disk >= 50 GB
- Docker >= 24.0.0 & Docker Compose >= v2.26.1
- Python >= 3.13
- [gVisor](https://gvisor.dev/docs/user_guide/install/): Hanya diperlukan jika Anda ingin menggunakan fitur eksekutor kode (sandbox) dari RAGFlow.
> [!TIP]
@@ -192,12 +191,12 @@ Coba layanan cloud kami di [https://cloud.ragflow.io](https://cloud.ragflow.io).
> Semua gambar Docker dibangun untuk platform x86. Saat ini, kami tidak menawarkan gambar Docker untuk ARM64.
> Jika Anda menggunakan platform ARM64, [silakan gunakan panduan ini untuk membangun gambar Docker yang kompatibel dengan sistem Anda](https://ragflow.io/docs/dev/build_docker_image).
> Perintah di bawah ini mengunduh edisi v0.25.5 dari gambar Docker RAGFlow. Silakan merujuk ke tabel berikut untuk deskripsi berbagai edisi RAGFlow. Untuk mengunduh edisi RAGFlow yang berbeda dari v0.25.5, perbarui variabel RAGFLOW_IMAGE di docker/.env sebelum menggunakan docker compose untuk memulai server.
> Perintah di bawah ini mengunduh edisi v0.26.4 dari gambar Docker RAGFlow. Silakan merujuk ke tabel berikut untuk deskripsi berbagai edisi RAGFlow. Untuk mengunduh edisi RAGFlow yang berbeda dari v0.26.4, perbarui variabel RAGFLOW_IMAGE di docker/.env sebelum menggunakan docker compose untuk memulai server.
```bash
$ cd ragflow/docker
# git checkout v0.25.5
git checkout v0.26.4
# Opsional: gunakan tag stabil (lihat releases: https://github.com/infiniflow/ragflow/releases)
# This steps ensures the **entrypoint.sh** file in the code matches the Docker image version.
@@ -292,19 +291,21 @@ docker build --platform linux/amd64 \
## 🔨 Menjalankan Aplikasi dari untuk Pengembangan
1. Instal `uv` dan `pre-commit`, atau lewati langkah ini jika sudah terinstal:
1. Instal `uv`, atau lewati langkah ini jika sudah terinstal:
```bash
pipx install uv pre-commit
pipx install uv
```
2. Clone kode sumber dan instal dependensi Python:
```bash
git clone https://github.com/infiniflow/ragflow.git
cd ragflow/
uv sync --python 3.12 # install RAGFlow dependent python modules
uv run python3 download_deps.py
pre-commit install
uv sync --python 3.13 # install RAGFlow dependent python modules
uv run python3 ragflow_deps/download_deps.py
git config --local --unset core.hooksPath
uv tool install lefthook
lefthook install
```
3. Jalankan aplikasi yang diperlukan (MinIO, Elasticsearch, Redis, dan MySQL) menggunakan Docker Compose:

View File

@@ -1,6 +1,6 @@
<div align="center">
<a href="https://cloud.ragflow.io/">
<img src="web/src/assets/logo-with-text.svg" width="350" alt="ragflow logo">
<img src="https://raw.githubusercontent.com/infiniflow/ragflow/main/web/src/assets/logo-with-text.svg" width="520" alt="ragflow logo">
</a>
</div>
@@ -25,7 +25,7 @@
<img alt="Static Badge" src="https://img.shields.io/badge/Get-Started-4e6b99">
</a>
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
<img src="https://img.shields.io/docker/pulls/infiniflow/ragflow?label=Docker%20Pulls&color=0db7ed&logo=docker&logoColor=white&style=flat-square" alt="docker pull infiniflow/ragflow:v0.25.5">
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/infiniflow/ragflow-stats/main/badges/docker-pulls.json&style=flat-square&logo=docker&logoColor=white" alt="docker pull infiniflow/ragflow:v0.26.4">
</a>
<a href="https://github.com/infiniflow/ragflow/releases/latest">
<img src="https://img.shields.io/github/v/release/infiniflow/ragflow?color=blue&label=Latest%20Release" alt="Latest Release">
@@ -68,6 +68,7 @@
## 🔥 最新情報
- 2026-06-15 Feishu、Discord、Telegram、Lineなどの複数のチャットチャンネルをサポートします。
- 2026-04-24 DeepSeek v4 をサポート。
- 2026-03-24 [RAGFlow Skill on OpenClaw](https://clawhub.ai/yingfeng/ragflow-skill) — OpenClaw経由でRAGFlowデータセットにアクセスする公式スキルを提供。
- 2025-12-26 AIエージェントの「メモリ」機能をサポート。
@@ -78,10 +79,8 @@
- 2025-08-08 OpenAI の最新 GPT-5 シリーズモデルをサポートします。
- 2025-08-01 エージェントワークフローとMCPをサポート。
- 2025-05-23 エージェントに Python/JS コードエグゼキュータコンポーネントを追加しました。
- 2025-05-05 言語間クエリをサポートしました。
- 2025-03-19 PDFまたはDOCXファイル内の画像を理解するために、多モーダルモデルを使用することをサポートします。
- 2024-12-18 DeepDoc のドキュメント レイアウト分析モデルをアップグレードします。
- 2024-08-22 RAG を介して SQL ステートメントへのテキストをサポートします。
## 🎉 続きを楽しみに
@@ -133,6 +132,7 @@
- RAM >= 16 GB
- Disk >= 50 GB
- Docker >= 24.0.0 & Docker Compose >= v2.26.1
- Python >= 3.13
- [gVisor](https://gvisor.dev/docs/user_guide/install/): RAGFlowのコード実行サンドボックス機能を利用する場合のみ必要です。
> [!TIP]
@@ -172,12 +172,12 @@
> 現在、公式に提供されているすべての Docker イメージは x86 アーキテクチャ向けにビルドされており、ARM64 用の Docker イメージは提供されていません。
> ARM64 アーキテクチャのオペレーティングシステムを使用している場合は、[このドキュメント](https://ragflow.io/docs/dev/build_docker_image)を参照して Docker イメージを自分でビルドしてください。
> 以下のコマンドは、RAGFlow Docker イメージの v0.25.5 エディションをダウンロードします。異なる RAGFlow エディションの説明については、以下の表を参照してください。v0.25.5 とは異なるエディションをダウンロードするには、docker/.env ファイルの RAGFLOW_IMAGE 変数を適宜更新し、docker compose を使用してサーバーを起動してください。
> 以下のコマンドは、RAGFlow Docker イメージの v0.26.4 エディションをダウンロードします。異なる RAGFlow エディションの説明については、以下の表を参照してください。v0.26.4 とは異なるエディションをダウンロードするには、docker/.env ファイルの RAGFLOW_IMAGE 変数を適宜更新し、docker compose を使用してサーバーを起動してください。
```bash
$ cd ragflow/docker
# git checkout v0.25.5
git checkout v0.26.4
# 任意: 安定版タグを利用 (一覧: https://github.com/infiniflow/ragflow/releases)
# この手順は、コード内の entrypoint.sh ファイルが Docker イメージのバージョンと一致していることを確認します。
@@ -292,19 +292,21 @@ docker build --platform linux/amd64 \
## 🔨 ソースコードからサービスを起動する方法
1. `uv` と `pre-commit` をインストールする。すでにインストールされている場合は、このステップをスキップしてください:
1. `uv` をインストールする。すでにインストールされている場合は、このステップをスキップしてください:
```bash
pipx install uv pre-commit
pipx install uv
```
2. ソースコードをクローンし、Python の依存関係をインストールする:
```bash
git clone https://github.com/infiniflow/ragflow.git
cd ragflow/
uv sync --python 3.12 # install RAGFlow dependent python modules
uv run python3 download_deps.py
pre-commit install
uv sync --python 3.13 # install RAGFlow dependent python modules
uv run python3 ragflow_deps/download_deps.py
git config --local --unset core.hooksPath
uv tool install lefthook
lefthook install
```
3. Docker Compose を使用して依存サービスMinIO、Elasticsearch、Redis、MySQLを起動する:

View File

@@ -1,6 +1,6 @@
<div align="center">
<a href="https://cloud.ragflow.io/">
<img src="web/src/assets/logo-with-text.svg" width="520" alt="ragflow logo">
<img src="https://raw.githubusercontent.com/infiniflow/ragflow/main/web/src/assets/logo-with-text.svg" width="520" alt="ragflow logo">
</a>
</div>
@@ -25,7 +25,7 @@
<img alt="Static Badge" src="https://img.shields.io/badge/Get-Started-4e6b99">
</a>
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
<img src="https://img.shields.io/docker/pulls/infiniflow/ragflow?label=Docker%20Pulls&color=0db7ed&logo=docker&logoColor=white&style=flat-square" alt="docker pull infiniflow/ragflow:v0.25.5">
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/infiniflow/ragflow-stats/main/badges/docker-pulls.json&style=flat-square&logo=docker&logoColor=white" alt="docker pull infiniflow/ragflow:v0.26.4">
</a>
<a href="https://github.com/infiniflow/ragflow/releases/latest">
<img src="https://img.shields.io/github/v/release/infiniflow/ragflow?color=blue&label=Latest%20Release" alt="Latest Release">
@@ -69,6 +69,7 @@
## 🔥 업데이트
- 2026-06-15 Feishu, Discord, Telegram, Line 등 다양한 채팅 채널을 지원합니다.
- 2026-04-24 DeepSeek v4를 지원합니다.
- 2026-03-24 [RAGFlow Skill on OpenClaw](https://clawhub.ai/yingfeng/ragflow-skill) — OpenClaw를 통해 RAGFlow 데이터셋에 접근하는 공식 스킬 제공.
- 2025-12-26 AI 에이전트의 '메모리' 기능 지원.
@@ -79,10 +80,8 @@
- 2025-08-08 OpenAI의 최신 GPT-5 시리즈 모델을 지원합니다.
- 2025-08-01 에이전트 워크플로우와 MCP를 지원합니다.
- 2025-05-23 Agent에 Python/JS 코드 실행기 구성 요소를 추가합니다.
- 2025-05-05 언어 간 쿼리를 지원합니다.
- 2025-03-19 PDF 또는 DOCX 파일 내의 이미지를 이해하기 위해 다중 모드 모델을 사용하는 것을 지원합니다.
- 2024-12-18 DeepDoc의 문서 레이아웃 분석 모델 업그레이드.
- 2024-08-22 RAG를 통해 SQL 문에 텍스트를 지원합니다.
## 🎉 계속 지켜봐 주세요
@@ -134,6 +133,7 @@
- RAM >= 16 GB
- Disk >= 50 GB
- Docker >= 24.0.0 & Docker Compose >= v2.26.1
- Python >= 3.13
- [gVisor](https://gvisor.dev/docs/user_guide/install/): RAGFlow의 코드 실행기(샌드박스) 기능을 사용하려는 경우에만 필요합니다.
> [!TIP]
@@ -174,12 +174,12 @@
> 모든 Docker 이미지는 x86 플랫폼을 위해 빌드되었습니다. 우리는 현재 ARM64 플랫폼을 위한 Docker 이미지를 제공하지 않습니다.
> ARM64 플랫폼을 사용 중이라면, [시스템과 호환되는 Docker 이미지를 빌드하려면 이 가이드를 사용해 주세요](https://ragflow.io/docs/dev/build_docker_image).
> 아래 명령어는 RAGFlow Docker 이미지의 v0.25.5 버전을 다운로드합니다. 다양한 RAGFlow 버전에 대한 설명은 다음 표를 참조하십시오. v0.25.5와 다른 RAGFlow 버전을 다운로드하려면, docker/.env 파일에서 RAGFLOW_IMAGE 변수를 적절히 업데이트한 후 docker compose를 사용하여 서버를 시작하십시오.
> 아래 명령어는 RAGFlow Docker 이미지의 v0.26.4 버전을 다운로드합니다. 다양한 RAGFlow 버전에 대한 설명은 다음 표를 참조하십시오. v0.26.4와 다른 RAGFlow 버전을 다운로드하려면, docker/.env 파일에서 RAGFLOW_IMAGE 변수를 적절히 업데이트한 후 docker compose를 사용하여 서버를 시작하십시오.
```bash
$ cd ragflow/docker
# git checkout v0.25.5
git checkout v0.26.4
# Optional: use a stable tag (see releases: https://github.com/infiniflow/ragflow/releases)
# 이 단계는 코드의 entrypoint.sh 파일이 Docker 이미지 버전과 일치하도록 보장합니다.
@@ -289,7 +289,7 @@ docker build --platform linux/amd64 \
1. `uv` 와 `pre-commit` 을 설치하거나, 이미 설치된 경우 이 단계를 건너뜁니다:
```bash
pipx install uv pre-commit
pipx install uv
```
2. 소스 코드를 클론하고 Python 의존성을 설치합니다:
@@ -297,9 +297,11 @@ docker build --platform linux/amd64 \
```bash
git clone https://github.com/infiniflow/ragflow.git
cd ragflow/
uv sync --python 3.12 # install RAGFlow dependent python modules
uv run python3 download_deps.py
pre-commit install
uv sync --python 3.13 # install RAGFlow dependent python modules
uv run python3 ragflow_deps/download_deps.py
git config --local --unset core.hooksPath
uv tool install lefthook
lefthook install
```
3. Docker Compose를 사용하여 의존 서비스(MinIO, Elasticsearch, Redis 및 MySQL)를 시작합니다:

View File

@@ -1,6 +1,6 @@
<div align="center">
<a href="https://cloud.ragflow.io/">
<img src="web/src/assets/logo-with-text.svg" width="520" alt="ragflow logo">
<img src="https://raw.githubusercontent.com/infiniflow/ragflow/main/web/src/assets/logo-with-text.svg" width="520" alt="ragflow logo">
</a>
</div>
@@ -25,10 +25,10 @@
<img alt="Badge Estático" src="https://img.shields.io/badge/Get-Started-4e6b99">
</a>
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
<img src="https://img.shields.io/docker/pulls/infiniflow/ragflow?label=Docker%20Pulls&color=0db7ed&logo=docker&logoColor=white&style=flat-square" alt="docker pull infiniflow/ragflow:v0.25.5">
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/infiniflow/ragflow-stats/main/badges/docker-pulls.json&style=flat-square&logo=docker&logoColor=white" alt="docker pull infiniflow/ragflow:v0.26.4">
</a>
<a href="https://github.com/infiniflow/ragflow/releases/latest">
<img src="https://img.shields.io/github/v/release/infiniflow/ragflow?color=blue&label=Última%20Relese" alt="Última Versão">
<img src="https://img.shields.io/github/v/release/infiniflow/ragflow?color=blue&label=%C3%9Altima%20Release" alt="Última Release">
</a>
<a href="https://github.com/infiniflow/ragflow/blob/main/LICENSE">
<img height="21" src="https://img.shields.io/badge/License-Apache--2.0-ffffff?labelColor=d4eaf7&color=2e6cc4" alt="licença">
@@ -88,6 +88,7 @@ Experimente o nosso serviço na nuvem em [https://cloud.ragflow.io](https://clou
## 🔥 Últimas Atualizações
- 15-06-2026 Suporte a múltiplos canais de chat, como Feishu, Discord, Telegram, Line, etc..
- 24-04-2026 Suporta DeepSeek v4.
- 24-03-2026 [RAGFlow Skill on OpenClaw](https://clawhub.ai/yingfeng/ragflow-skill) — Fornece um skill oficial para acessar datasets do RAGFlow via OpenClaw.
- 26-12-2025 Suporte à função 'Memória' para agentes de IA.
@@ -98,10 +99,7 @@ Experimente o nosso serviço na nuvem em [https://cloud.ragflow.io](https://clou
- 08-08-2025 Suporta a mais recente série GPT-5 da OpenAI.
- 01-08-2025 Suporta fluxo de trabalho agente e MCP.
- 23-05-2025 Adicione o componente executor de código Python/JS ao Agente.
- 05-05-2025 Suporte a consultas entre idiomas.
- 19-03-2025 Suporta o uso de um modelo multi-modal para entender imagens dentro de arquivos PDF ou DOCX.
- 18-12-2024 Atualiza o modelo de Análise de Layout de Documentos no DeepDoc.
- 22-08-2024 Suporta conversão de texto para comandos SQL via RAG.
## 🎉 Fique Ligado
@@ -153,6 +151,7 @@ Experimente o nosso serviço na nuvem em [https://cloud.ragflow.io](https://clou
- RAM >= 16 GB
- Disco >= 50 GB
- Docker >= 24.0.0 & Docker Compose >= v2.26.1
- Python >= 3.13
- [gVisor](https://gvisor.dev/docs/user_guide/install/): Necessário apenas se você pretende usar o recurso de executor de código (sandbox) do RAGFlow.
> [!TIP]
@@ -192,12 +191,12 @@ Experimente o nosso serviço na nuvem em [https://cloud.ragflow.io](https://clou
> Todas as imagens Docker são construídas para plataformas x86. Atualmente, não oferecemos imagens Docker para ARM64.
> Se você estiver usando uma plataforma ARM64, por favor, utilize [este guia](https://ragflow.io/docs/dev/build_docker_image) para construir uma imagem Docker compatível com o seu sistema.
> O comando abaixo baixa a edição`v0.25.5` da imagem Docker do RAGFlow. Consulte a tabela a seguir para descrições de diferentes edições do RAGFlow. Para baixar uma edição do RAGFlow diferente da `v0.25.5`, atualize a variável `RAGFLOW_IMAGE` conforme necessário no **docker/.env** antes de usar `docker compose` para iniciar o servidor.
> O comando abaixo baixa a edição`v0.26.4` da imagem Docker do RAGFlow. Consulte a tabela a seguir para descrições de diferentes edições do RAGFlow. Para baixar uma edição do RAGFlow diferente da `v0.26.4`, atualize a variável `RAGFLOW_IMAGE` conforme necessário no **docker/.env** antes de usar `docker compose` para iniciar o servidor.
```bash
$ cd ragflow/docker
# git checkout v0.25.5
git checkout v0.26.4
# Opcional: use uma tag estável (veja releases: https://github.com/infiniflow/ragflow/releases)
# Esta etapa garante que o arquivo entrypoint.sh no código corresponda à versão da imagem do Docker.
@@ -312,16 +311,18 @@ docker build --platform linux/amd64 \
1. Instale o `uv` e o `pre-commit`, ou pule esta etapa se eles já estiverem instalados:
```bash
pipx install uv pre-commit
pipx install uv
```
2. Clone o código-fonte e instale as dependências Python:
```bash
git clone https://github.com/infiniflow/ragflow.git
cd ragflow/
uv sync --python 3.12 # instala os módulos Python dependentes do RAGFlow
uv run python3 download_deps.py
pre-commit install
uv sync --python 3.13 # instala os módulos Python dependentes do RAGFlow
uv run python3 ragflow_deps/download_deps.py
git config --local --unset core.hooksPath
uv tool install lefthook
lefthook install
```
3. Inicie os serviços dependentes (MinIO, Elasticsearch, Redis e MySQL) usando Docker Compose:

View File

@@ -1,6 +1,6 @@
<div align="center">
<a href="https://cloud.ragflow.io/">
<img src="web/src/assets/logo-with-text.svg" width="520" alt="ragflow logo">
<img src="https://raw.githubusercontent.com/infiniflow/ragflow/main/web/src/assets/logo-with-text.svg" width="520" alt="ragflow logo">
</a>
</div>
@@ -25,10 +25,10 @@
<img alt="Çevrimiçi Demo" src="https://img.shields.io/badge/Get-Started-4e6b99">
</a>
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
<img src="https://img.shields.io/docker/pulls/infiniflow/ragflow?label=Docker%20Pulls&color=0db7ed&logo=docker&logoColor=white&style=flat-square" alt="docker pull infiniflow/ragflow:v0.25.5">
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/infiniflow/ragflow-stats/main/badges/docker-pulls.json&style=flat-square&logo=docker&logoColor=white" alt="docker pull infiniflow/ragflow:v0.26.4">
</a>
<a href="https://github.com/infiniflow/ragflow/releases/latest">
<img src="https://img.shields.io/github/v/release/infiniflow/ragflow?color=blue&label=Son%20Sürüm" alt="Son Sürüm">
<img src="https://img.shields.io/github/v/release/infiniflow/ragflow?color=blue&label=Son%20S%C3%BCr%C3%BCm" alt="Son Sürüm">
</a>
<a href="https://github.com/infiniflow/ragflow/blob/main/LICENSE">
<img height="21" src="https://img.shields.io/badge/Lisans-Apache--2.0-ffffff?labelColor=d4eaf7&color=2e6cc4" alt="lisans">
@@ -42,7 +42,7 @@
<a href="https://cloud.ragflow.io">Cloud</a> |
<a href="https://ragflow.io/docs/dev/">Dokümantasyon</a> |
<a href="https://github.com/infiniflow/ragflow/issues/12241">Yol Haritası</a> |
<a href="https://discord.gg/NjYzJD3GM3">Discord</a>
<a href="https://discord.gg/NjYzJD3GM3">Discord</a>
</h4>
<div align="center" style="margin-top:20px;margin-bottom:20px;">
@@ -87,6 +87,7 @@ Bulut hizmetimizi [https://cloud.ragflow.io](https://cloud.ragflow.io) adresinde
## 🔥 Son Güncellemeler
- 2026-06-15 Feishu, Discord, Telegram, Line vb. gibi birden fazla sohbet kanalını destekleyin.
- 2026-04-24 DeepSeek v4 desteği.
- 2026-03-24 [RAGFlow Skill on OpenClaw](https://clawhub.ai/yingfeng/ragflow-skill) — OpenClaw üzerinden RAGFlow veri setlerine erişmek için resmi bir skill sağlar.
- 2025-12-26 Yapay zeka ajanı için 'Bellek' desteği eklendi.
@@ -97,7 +98,6 @@ Bulut hizmetimizi [https://cloud.ragflow.io](https://cloud.ragflow.io) adresinde
- 2025-08-08 OpenAI'ın en yeni GPT-5 serisi modelleri için destek eklendi.
- 2025-08-01 Ajanlı iş akışı ve MCP desteği eklendi.
- 2025-05-23 Ajana Python/JavaScript kod çalıştırıcı bileşeni eklendi.
- 2025-05-05 Diller arası sorgu desteği eklendi.
- 2025-03-19 PDF veya DOCX dosyalarındaki görselleri yorumlamak için çok modlu model desteği eklendi.
## 🎉 Bizi Takip Edin
@@ -150,6 +150,7 @@ Bulut hizmetimizi [https://cloud.ragflow.io](https://cloud.ragflow.io) adresinde
- RAM >= 16 GB
- Disk >= 50 GB
- Docker >= 24.0.0 & Docker Compose >= v2.26.1
- Python >= 3.13
- [gVisor](https://gvisor.dev/docs/user_guide/install/): Yalnızca RAGFlow'un kod çalıştırıcı (sandbox) özelliğini kullanmayı planlıyorsanız gereklidir.
> [!TIP]
@@ -190,12 +191,12 @@ Bulut hizmetimizi [https://cloud.ragflow.io](https://cloud.ragflow.io) adresinde
> Tüm Docker imajları x86 platformları için oluşturulmuştur. Şu anda ARM64 için Docker imajı sunmuyoruz.
> ARM64 platformundaysanız, sisteminizle uyumlu bir Docker imajı oluşturmak için [bu kılavuzu](https://ragflow.io/docs/dev/build_docker_image) takip edin.
> Aşağıdaki komut RAGFlow Docker imajının `v0.25.5` sürümünü indirir. Farklı RAGFlow sürümleri için aşağıdaki tabloya bakın. `v0.25.5` dışında bir sürüm indirmek için, `docker compose` ile sunucuyu başlatmadan önce **docker/.env** dosyasındaki `RAGFLOW_IMAGE` değişkenini güncelleyin.
> Aşağıdaki komut RAGFlow Docker imajının `v0.26.4` sürümünü indirir. Farklı RAGFlow sürümleri için aşağıdaki tabloya bakın. `v0.26.4` dışında bir sürüm indirmek için, `docker compose` ile sunucuyu başlatmadan önce **docker/.env** dosyasındaki `RAGFLOW_IMAGE` değişkenini güncelleyin.
```bash
$ cd ragflow/docker
# git checkout v0.25.5
git checkout v0.26.4
# İsteğe bağlı: Kararlı bir etiket kullanın (sürümler: https://github.com/infiniflow/ragflow/releases)
# Bu adım, koddaki **entrypoint.sh** dosyasının Docker imaj sürümüyle eşleşmesini sağlar.
@@ -313,19 +314,21 @@ docker build --platform linux/amd64 \
## 🔨 Geliştirme İçin Kaynaktan Hizmet Başlatma
1. `uv` ve `pre-commit` yükleyin veya zaten yüklüyse bu adımı atlayın:
1. `uv` yükleyin veya zaten yüklüyse bu adımı atlayın:
```bash
pipx install uv pre-commit
pipx install uv
```
2. Kaynak kodunu klonlayın ve Python bağımlılıklarını yükleyin:
```bash
git clone https://github.com/infiniflow/ragflow.git
cd ragflow/
uv sync --python 3.12 # RAGFlow'un bağımlı Python modüllerini yükler
uv run python3 download_deps.py
pre-commit install
uv sync --python 3.13 # RAGFlow'un bağımlı Python modüllerini yükler
uv run python3 ragflow_deps/download_deps.py
git config --local --unset core.hooksPath
uv tool install lefthook
lefthook install
```
3. Bağımlı hizmetleri (MinIO, Elasticsearch, Redis ve MySQL) Docker Compose kullanarak başlatın:

View File

@@ -1,6 +1,6 @@
<div align="center">
<a href="https://cloud.ragflow.io/">
<img src="web/src/assets/logo-with-text.svg" width="350" alt="ragflow logo">
<img src="https://raw.githubusercontent.com/infiniflow/ragflow/main/web/src/assets/logo-with-text.svg" width="520" alt="ragflow logo">
</a>
</div>
@@ -25,7 +25,7 @@
<img alt="Static Badge" src="https://img.shields.io/badge/Get-Started-4e6b99">
</a>
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
<img src="https://img.shields.io/docker/pulls/infiniflow/ragflow?label=Docker%20Pulls&color=0db7ed&logo=docker&logoColor=white&style=flat-square" alt="docker pull infiniflow/ragflow:v0.25.5">
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/infiniflow/ragflow-stats/main/badges/docker-pulls.json&style=flat-square&logo=docker&logoColor=white" alt="docker pull infiniflow/ragflow:v0.26.4">
</a>
<a href="https://github.com/infiniflow/ragflow/releases/latest">
<img src="https://img.shields.io/github/v/release/infiniflow/ragflow?color=blue&label=Latest%20Release" alt="Latest Release">
@@ -87,6 +87,7 @@
## 🔥 近期更新
- 2026-06-15 支援飛書、Discord、Telegram、Line 等多種聊天管道。
- 2026-04-24 支援 DeepSeek v4 版本。
- 2026-03-24 發布 [RAGFlow 官方 Skill](https://clawhub.ai/yingfeng/ragflow-skill) — 提供官方 Skill 以透過 OpenClaw 訪問 RAGFlow 數據集。
- 2025-12-26 支援AI代理的「記憶」功能。
@@ -97,10 +98,8 @@
- 2025-08-08 支援 OpenAI 最新的 GPT-5 系列模型。
- 2025-08-01 支援 agentic workflow 和 MCP。
- 2025-05-23 為 Agent 新增 Python/JS 程式碼執行器元件。
- 2025-05-05 支援跨語言查詢。
- 2025-03-19 PDF和DOCX中的圖支持用多模態大模型去解析得到描述。
- 2024-12-18 升級了 DeepDoc 的文檔佈局分析模型。
- 2024-08-22 支援用 RAG 技術實現從自然語言到 SQL 語句的轉換。
## 🎉 關注項目
@@ -152,6 +151,7 @@
- RAM >= 16 GB
- Disk >= 50 GB
- Docker >= 24.0.0 & Docker Compose >= v2.26.1
- Python >= 3.13
- [gVisor](https://gvisor.dev/docs/user_guide/install/): 僅在您打算使用 RAGFlow 的代碼執行器(沙箱)功能時才需要安裝。
> [!TIP]
@@ -191,12 +191,12 @@
> 所有 Docker 映像檔都是為 x86 平台建置的。目前,我們不提供 ARM64 平台的 Docker 映像檔。
> 如果您使用的是 ARM64 平台,請使用 [這份指南](https://ragflow.io/docs/dev/build_docker_image) 來建置適合您系統的 Docker 映像檔。
> 執行以下指令會自動下載 RAGFlow Docker 映像 `v0.25.5`。請參考下表查看不同 Docker 發行版的說明。如需下載不同於 `v0.25.5` 的 Docker 映像,請在執行 `docker compose` 啟動服務之前先更新 **docker/.env** 檔案內的 `RAGFLOW_IMAGE` 變數。
> 執行以下指令會自動下載 RAGFlow Docker 映像 `v0.26.4`。請參考下表查看不同 Docker 發行版的說明。如需下載不同於 `v0.26.4` 的 Docker 映像,請在執行 `docker compose` 啟動服務之前先更新 **docker/.env** 檔案內的 `RAGFLOW_IMAGE` 變數。
```bash
$ cd ragflow/docker
# git checkout v0.25.5
git checkout v0.26.4
# 可選使用穩定版標籤查看發佈https://github.com/infiniflow/ragflow/releases
# 此步驟確保程式碼中的 entrypoint.sh 檔案與 Docker 映像版本一致。
@@ -318,10 +318,10 @@ docker build --platform linux/amd64 \
## 🔨 以原始碼啟動服務
1. 安裝 `uv` 和 `pre-commit`。如已安裝,可跳過此步驟:
1. 安裝 `uv`。如已安裝,可跳過此步驟:
```bash
pipx install uv pre-commit
pipx install uv
export UV_INDEX=https://mirrors.aliyun.com/pypi/simple
```
2. 下載原始碼並安裝 Python 依賴:
@@ -329,9 +329,11 @@ docker build --platform linux/amd64 \
```bash
git clone https://github.com/infiniflow/ragflow.git
cd ragflow/
uv sync --python 3.12 # install RAGFlow dependent python modules
uv run python3 download_deps.py
pre-commit install
uv sync --python 3.13 # install RAGFlow dependent python modules
uv run python3 ragflow_deps/download_deps.py
git config --local --unset core.hooksPath
uv tool install lefthook
lefthook install
```
3. 透過 Docker Compose 啟動依賴的服務MinIO, Elasticsearch, Redis, and MySQL

View File

@@ -1,6 +1,6 @@
<div align="center">
<a href="https://cloud.ragflow.io/">
<img src="web/src/assets/logo-with-text.svg" width="350" alt="ragflow logo">
<img src="https://raw.githubusercontent.com/infiniflow/ragflow/main/web/src/assets/logo-with-text.svg" width="520" alt="ragflow logo">
</a>
</div>
@@ -25,7 +25,7 @@
<img alt="Static Badge" src="https://img.shields.io/badge/Get-Started-4e6b99">
</a>
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
<img src="https://img.shields.io/docker/pulls/infiniflow/ragflow?label=Docker%20Pulls&color=0db7ed&logo=docker&logoColor=white&style=flat-square" alt="docker pull infiniflow/ragflow:v0.25.5">
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/infiniflow/ragflow-stats/main/badges/docker-pulls.json&style=flat-square&logo=docker&logoColor=white" alt="docker pull infiniflow/ragflow:v0.26.4">
</a>
<a href="https://github.com/infiniflow/ragflow/releases/latest">
<img src="https://img.shields.io/github/v/release/infiniflow/ragflow?color=blue&label=Latest%20Release" alt="Latest Release">
@@ -87,6 +87,7 @@
## 🔥 近期更新
- 2026-06-15 支持飞书、Discord、Telegram、Line 等多种聊天渠道。
- 2026-04-24 支持 DeepSeek v4.
- 2026-03-24 发布 [RAGFlow 官方 Skill](https://clawhub.ai/yingfeng/ragflow-skill) — 提供官方 Skill 以通过 OpenClaw 访问 RAGFlow 数据集。
- 2025-12-26 支持 AI 代理的"记忆"功能。
@@ -97,10 +98,8 @@
- 2025-08-08 支持 OpenAI 最新的 GPT-5 系列模型。
- 2025-08-01 支持 agentic workflow 和 MCP。
- 2025-05-23 Agent 新增 Python/JS 代码执行器组件。
- 2025-05-05 支持跨语言查询。
- 2025-03-19 PDF 和 DOCX 中的图支持用多模态大模型去解析得到描述。
- 2024-12-18 升级了 DeepDoc 的文档布局分析模型。
- 2024-08-22 支持用 RAG 技术实现从自然语言到 SQL 语句的转换。
## 🎉 关注项目
@@ -152,6 +151,7 @@
- RAM >= 16 GB
- Disk >= 50 GB
- Docker >= 24.0.0 & Docker Compose >= v2.26.1
- Python >= 3.13
- [gVisor](https://gvisor.dev/docs/user_guide/install/): 仅在你打算使用 RAGFlow 的代码执行器(沙箱)功能时才需要安装。
> [!TIP]
@@ -192,12 +192,12 @@
> 请注意,目前官方提供的所有 Docker 镜像均基于 x86 架构构建,并不提供基于 ARM64 的 Docker 镜像。
> 如果你的操作系统是 ARM64 架构,请参考[这篇文档](https://ragflow.io/docs/dev/build_docker_image)自行构建 Docker 镜像。
> 运行以下命令会自动下载 RAGFlow Docker 镜像 `v0.25.5`。请参考下表查看不同 Docker 发行版的描述。如需下载不同于 `v0.25.5` 的 Docker 镜像,请在运行 `docker compose` 启动服务之前先更新 **docker/.env** 文件内的 `RAGFLOW_IMAGE` 变量。
> 运行以下命令会自动下载 RAGFlow Docker 镜像 `v0.26.4`。请参考下表查看不同 Docker 发行版的描述。如需下载不同于 `v0.26.4` 的 Docker 镜像,请在运行 `docker compose` 启动服务之前先更新 **docker/.env** 文件内的 `RAGFLOW_IMAGE` 变量。
```bash
$ cd ragflow/docker
# git checkout v0.25.5
git checkout v0.26.4
# 可选使用稳定版本标签查看发布https://github.com/infiniflow/ragflow/releases
# 这一步确保代码中的 entrypoint.sh 文件与 Docker 镜像的版本保持一致。
@@ -317,10 +317,10 @@ docker build --platform linux/amd64 \
## 🔨 以源代码启动服务
1. 安装 `uv` 和 `pre-commit`。如已经安装,可跳过本步骤:
1. 安装 `uv`。如已经安装,可跳过本步骤:
```bash
pipx install uv pre-commit
pipx install uv
export UV_INDEX=https://mirrors.aliyun.com/pypi/simple
```
@@ -329,9 +329,11 @@ docker build --platform linux/amd64 \
```bash
git clone https://github.com/infiniflow/ragflow.git
cd ragflow/
uv sync --python 3.12 # install RAGFlow dependent python modules
uv run python3 download_deps.py
pre-commit install
uv sync --python 3.13 # install RAGFlow dependent python modules
uv run python3 ragflow_deps/download_deps.py
git config --local --unset core.hooksPath
uv tool install lefthook
lefthook install
```
3. 通过 Docker Compose 启动依赖的服务MinIO, Elasticsearch, Redis, and MySQL

View File

@@ -28,7 +28,7 @@ It consists of a server-side Service and a command-line client (CLI), both imple
```bash
python admin/server/admin_server.py
```
The service will start and listen for incoming connections from the CLI on the configured port.
The service will start and listen for incoming connections from the CLI on the configured port.
#### Using docker image
@@ -48,7 +48,7 @@ It consists of a server-side Service and a command-line client (CLI), both imple
1. Ensure the Admin Service is running.
2. Install ragflow-cli.
```bash
pip install ragflow-cli==0.25.5
pip install ragflow-cli==0.26.4
```
3. Launch the CLI client:
```bash
@@ -58,9 +58,9 @@ It consists of a server-side Service and a command-line client (CLI), both imple
The default password is admin.
**Parameters:**
- -h: RAGFlow admin server host address
- -p: RAGFlow admin server port

View File

@@ -25,14 +25,14 @@ import requests
class HttpClient:
def __init__(
self,
host: str = "127.0.0.1",
port: int = 9381,
api_version: str = "v1",
api_key: Optional[str] = None,
connect_timeout: float = 5.0,
read_timeout: float = 60.0,
verify_ssl: bool = False,
self,
host: str = "127.0.0.1",
port: int = 9381,
api_version: str = "v1",
api_key: Optional[str] = None,
connect_timeout: float = 5.0,
read_timeout: float = 60.0,
verify_ssl: bool = False,
) -> None:
self.host = host
self.port = port
@@ -71,19 +71,19 @@ class HttpClient:
return headers
def request(
self,
method: str,
path: str,
*,
use_api_base: bool = True,
auth_kind: Optional[str] = "api",
headers: Optional[Dict[str, str]] = None,
json_body: Optional[Dict[str, Any]] = None,
data: Any = None,
files: Any = None,
params: Optional[Dict[str, Any]] = None,
stream: bool = False,
iterations: int = 1,
self,
method: str,
path: str,
*,
use_api_base: bool = True,
auth_kind: Optional[str] = "api",
headers: Optional[Dict[str, str]] = None,
json_body: Optional[Dict[str, Any]] = None,
data: Any = None,
files: Any = None,
params: Optional[Dict[str, Any]] = None,
stream: bool = False,
iterations: int = 1,
) -> requests.Response | dict:
url = self.build_url(path, use_api_base=use_api_base)
merged_headers = self._headers(auth_kind, headers)
@@ -144,18 +144,18 @@ class HttpClient:
# )
def request_json(
self,
method: str,
path: str,
*,
use_api_base: bool = True,
auth_kind: Optional[str] = "api",
headers: Optional[Dict[str, str]] = None,
json_body: Optional[Dict[str, Any]] = None,
data: Any = None,
files: Any = None,
params: Optional[Dict[str, Any]] = None,
stream: bool = False,
self,
method: str,
path: str,
*,
use_api_base: bool = True,
auth_kind: Optional[str] = "api",
headers: Optional[Dict[str, str]] = None,
json_body: Optional[Dict[str, Any]] = None,
data: Any = None,
files: Any = None,
params: Optional[Dict[str, Any]] = None,
stream: bool = False,
) -> Dict[str, Any]:
response = self.request(
method,

View File

@@ -336,8 +336,8 @@ reset_default_asr: RESET DEFAULT ASR ";"
reset_default_tts: RESET DEFAULT TTS ";"
list_user_datasets: LIST DATASETS ";"
create_user_dataset_with_parser: CREATE DATASET quoted_string WITH EMBEDDING quoted_string PARSER quoted_string ";"
create_user_dataset_with_pipeline: CREATE DATASET quoted_string WITH EMBEDDING quoted_string PIPELINE quoted_string ";"
create_user_dataset_with_parser: CREATE DATASET quoted_string WITH EMBEDDING quoted_string PARSER quoted_string ";"
create_user_dataset_with_pipeline: CREATE DATASET quoted_string WITH EMBEDDING quoted_string PIPELINE quoted_string ";"
drop_user_dataset: DROP DATASET quoted_string ";"
list_user_dataset_files: LIST FILES OF DATASET quoted_string ";"
list_user_dataset_documents: LIST DOCUMENTS OF DATASET quoted_string ";"
@@ -640,15 +640,13 @@ class RAGFlowCLITransformer(Transformer):
dataset_name = items[2].children[0].strip("'\"")
embedding = items[5].children[0].strip("'\"")
parser_type = items[7].children[0].strip("'\"")
return {"type": "create_user_dataset", "dataset_name": dataset_name, "embedding": embedding,
"parser_type": parser_type}
return {"type": "create_user_dataset", "dataset_name": dataset_name, "embedding": embedding, "parser_type": parser_type}
def create_user_dataset_with_pipeline(self, items):
dataset_name = items[2].children[0].strip("'\"")
embedding = items[5].children[0].strip("'\"")
pipeline = items[7].children[0].strip("'\"")
return {"type": "create_user_dataset", "dataset_name": dataset_name, "embedding": embedding,
"pipeline": pipeline}
return {"type": "create_user_dataset", "dataset_name": dataset_name, "embedding": embedding, "pipeline": pipeline}
def drop_user_dataset(self, items):
dataset_name = items[2].children[0].strip("'\"")
@@ -666,7 +664,7 @@ class RAGFlowCLITransformer(Transformer):
dataset_names = []
dataset_names.append(items[4].children[0].strip("'\""))
for i in range(5, len(items)):
if items[i] and hasattr(items[i], 'children') and items[i].children:
if items[i] and hasattr(items[i], "children") and items[i].children:
dataset_names.append(items[i].children[0].strip("'\""))
return {"type": "list_user_datasets_metadata", "dataset_names": dataset_names}
@@ -675,7 +673,7 @@ class RAGFlowCLITransformer(Transformer):
doc_ids = []
if len(items) > 6 and items[6] == "DOCUMENTS":
for i in range(7, len(items)):
if items[i] and hasattr(items[i], 'children') and items[i].children:
if items[i] and hasattr(items[i], "children") and items[i].children:
doc_id = items[i].children[0].strip("'\"")
doc_ids.append(doc_id)
return {"type": "list_user_documents_metadata_summary", "dataset_name": dataset_name, "document_ids": doc_ids}
@@ -698,17 +696,17 @@ class RAGFlowCLITransformer(Transformer):
dataset_name = None
vector_size = None
for i, item in enumerate(items):
if hasattr(item, 'data') and item.data == 'quoted_string':
if hasattr(item, "data") and item.data == "quoted_string":
dataset_name = item.children[0].strip("'\"")
if hasattr(item, 'type') and item.type == 'NUMBER':
if i > 0 and items[i-1].type == 'SIZE' and items[i-2].type == 'VECTOR':
if hasattr(item, "type") and item.type == "NUMBER":
if i > 0 and items[i - 1].type == "SIZE" and items[i - 2].type == "VECTOR":
vector_size = int(item)
return {"type": "create_dataset_table", "dataset_name": dataset_name, "vector_size": vector_size}
def drop_dataset_table(self, items):
dataset_name = None
for item in items:
if hasattr(item, 'data') and item.data == 'quoted_string':
if hasattr(item, "data") and item.data == "quoted_string":
dataset_name = item.children[0].strip("'\"")
return {"type": "drop_dataset_table", "dataset_name": dataset_name}
@@ -792,7 +790,7 @@ class RAGFlowCLITransformer(Transformer):
def update_chunk(self, items):
def get_quoted_value(item):
if hasattr(item, 'children') and item.children:
if hasattr(item, "children") and item.children:
return item.children[0].strip("'\"")
return str(item).strip("'\"")
@@ -813,16 +811,16 @@ class RAGFlowCLITransformer(Transformer):
for i in range(2, len(items)):
item = items[i]
# Check for FROM token to stop
if hasattr(item, 'type') and item.type == 'FROM':
if hasattr(item, "type") and item.type == "FROM":
break
if hasattr(item, 'children') and item.children:
if hasattr(item, "children") and item.children:
tag = item.children[0].strip("'\"")
tags.append(tag)
# Find dataset_name: quoted_string after DATASET
dataset_name = None
for i, item in enumerate(items):
# Check if item is a DATASET token
if hasattr(item, 'type') and item.type == 'DATASET':
if hasattr(item, "type") and item.type == "DATASET":
# Next item should be quoted_string
dataset_name = items[i + 1].children[0].strip("'\"")
break
@@ -835,10 +833,10 @@ class RAGFlowCLITransformer(Transformer):
# Check if it's "REMOVE ALL CHUNKS"
for item in items:
if hasattr(item, 'type') and item.type == 'ALL':
if hasattr(item, "type") and item.type == "ALL":
# Find doc_id
for j, inner_item in enumerate(items):
if hasattr(inner_item, 'type') and inner_item.type == 'DOCUMENT':
if hasattr(inner_item, "type") and inner_item.type == "DOCUMENT":
doc_id = items[j + 1].children[0].strip("'\"")
return {"type": "remove_chunks", "doc_id": doc_id, "delete_all": True}
@@ -846,12 +844,12 @@ class RAGFlowCLITransformer(Transformer):
chunk_ids = []
doc_id = None
for i, item in enumerate(items):
if hasattr(item, 'type') and item.type == 'DOCUMENT':
if hasattr(item, "type") and item.type == "DOCUMENT":
doc_id = items[i + 1].children[0].strip("'\"")
elif hasattr(item, 'children') and item.children:
elif hasattr(item, "children") and item.children:
val = item.children[0].strip("'\"")
# Skip if it's "FROM" or "DOCUMENT"
if val.upper() in ['FROM', 'DOCUMENT']:
if val.upper() in ["FROM", "DOCUMENT"]:
continue
chunk_ids.append(val)

View File

@@ -1,14 +1,14 @@
[project]
name = "ragflow-cli"
version = "0.25.5"
version = "0.26.4"
description = "Admin Service's client of [RAGFlow](https://github.com/infiniflow/ragflow). The Admin Service provides user management and system monitoring. "
authors = [{ name = "Lynn", email = "lynn_inf@hotmail.com" }]
license = { text = "Apache License, Version 2.0" }
readme = "README.md"
requires-python = ">=3.12,<3.15"
requires-python = ">=3.13,<3.14"
dependencies = [
"requests>=2.30.0,<3.0.0",
"beartype>=0.20.0,<1.0.0",
"beartype>=0.22.9,<1.0.0",
"pycryptodomex>=3.10.0",
"lark>=1.1.0",
"requests-toolbelt>=1.0.0",

View File

@@ -36,6 +36,7 @@ from user import login_user
warnings.filterwarnings("ignore", category=getpass.GetPassWarning)
def encrypt(input_string):
pub = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArq9XTUSeYr2+N1h3Afl/z8Dse/2yD0ZGrKwx+EEEcdsBLca9Ynmx3nIB5obmLlSfmskLpBo0UACBmB5rEjBp2Q2f3AG3Hjd4B+gNCG6BDaawuDlgANIhGnaTLrIqWrrcm4EMzJOnAOI1fgzJRsOOUEfaS318Eq9OVO3apEyCCt0lOQK6PuksduOjVxtltDav+guVAA068NrPYmRNabVKRNLJpL8w4D44sfth5RvZ3q9t+6RTArpEtc5sh5ChzvqPOzKGMXW83C95TxmXqpbK6olN4RevSfVjEAgCydH6HN6OhtOQEcnrU97r9H0iZOWwbw3pVrZiUkuRD1R56Wzs2wIDAQAB\n-----END PUBLIC KEY-----"
pub_key = RSA.importKey(pub)
@@ -49,9 +50,6 @@ def encode_to_base64(input_string):
return base64_encoded.decode("utf-8")
class RAGFlowCLI(Cmd):
def __init__(self):
super().__init__()
@@ -240,9 +238,9 @@ class RAGFlowCLI(Cmd):
print(r"""
____ ___ ______________ ________ ____
/ __ \/ | / ____/ ____/ /___ _ __ / ____/ / / _/
/ /_/ / /| |/ / __/ /_ / / __ \ | /| / / / / / / / /
/ _, _/ ___ / /_/ / __/ / / /_/ / |/ |/ / / /___/ /____/ /
/_/ |_/_/ |_\____/_/ /_/\____/|__/|__/ \____/_____/___/
/ /_/ / /| |/ / __/ /_ / / __ \ | /| / / / / / / / /
/ _, _/ ___ / /_/ / __/ / / /_/ / |/ |/ / / /___/ /____/ /
/_/ |_/_/ |_\____/_/ /_/\____/|__/|__/ \____/_____/___/
""")
self.cmdloop()
@@ -254,15 +252,13 @@ class RAGFlowCLI(Cmd):
result = self.parse_command(command)
self.execute_command(result)
def parse_connection_args(self, args: List[str]) -> Dict[str, Any]:
parser = argparse.ArgumentParser(description="RAGFlow CLI Client", add_help=False)
parser.add_argument("-h", "--host", default="127.0.0.1", help="Admin or RAGFlow service host")
parser.add_argument("-p", "--port", type=int, default=9381, help="Admin or RAGFlow service port")
parser.add_argument("-w", "--password", default="admin", type=str, help="Superuser password")
parser.add_argument("-t", "--type", default="admin", type=str, help="CLI mode, admin or user")
parser.add_argument("-u", "--username", default=None,
help="Username (email). In admin mode defaults to admin@ragflow.io, in user mode required.")
parser.add_argument("-u", "--username", default=None, help="Username (email). In admin mode defaults to admin@ragflow.io, in user mode required.")
parser.add_argument("command", nargs="?", help="Single command")
try:
parsed_args, remaining_args = parser.parse_known_args(args)
@@ -274,7 +270,7 @@ class RAGFlowCLI(Cmd):
if remaining_args:
if remaining_args[0] == "command":
command_str = ' '.join(remaining_args[1:]) + ';'
command_str = " ".join(remaining_args[1:]) + ";"
auth = True
if remaining_args[1] == "register":
auth = False
@@ -282,28 +278,14 @@ class RAGFlowCLI(Cmd):
if username is None:
print("Error: username (-u) is required in user mode")
return {"error": "Username required"}
return {
"host": parsed_args.host,
"port": parsed_args.port,
"password": parsed_args.password,
"type": parsed_args.type,
"username": username,
"command": command_str,
"auth": auth
}
return {"host": parsed_args.host, "port": parsed_args.port, "password": parsed_args.password, "type": parsed_args.type, "username": username, "command": command_str, "auth": auth}
else:
return {"error": "Invalid command"}
else:
auth = True
if username is None:
auth = False
return {
"host": parsed_args.host,
"port": parsed_args.port,
"type": parsed_args.type,
"username": username,
"auth": auth
}
return {"host": parsed_args.host, "port": parsed_args.port, "type": parsed_args.type, "username": username, "auth": auth}
except SystemExit:
return {"error": "Invalid connection arguments"}
@@ -321,6 +303,7 @@ class RAGFlowCLI(Cmd):
# print(f"Parsed command: {command_dict}")
run_command(self.ragflow_client, command_dict)
def main():
cli = RAGFlowCLI()

View File

@@ -56,7 +56,7 @@ class RAGFlowClient:
def login_user(self, command):
try:
response = self.http_client.request("GET", "/system/ping", use_api_base=False, auth_kind="web")
response = self.http_client.request("GET", "/system/ping", use_api_base=True, auth_kind="web")
if response.status_code == 200 and response.content == b"pong":
pass
else:
@@ -71,6 +71,7 @@ class RAGFlowClient:
user_password: str = command.get("password")
if not user_password:
import getpass
user_password = getpass.getpass("Password: ")
try:
token = login_user(self.http_client, self.server_type, email, user_password)
@@ -86,11 +87,10 @@ class RAGFlowClient:
def ping_server(self, command):
iterations = command.get("iterations", 1)
if iterations > 1:
response = self.http_client.request("GET", "/system/ping", use_api_base=False, auth_kind="web",
iterations=iterations)
response = self.http_client.request("GET", "/system/ping", use_api_base=True, auth_kind="web", iterations=iterations)
return response
else:
response = self.http_client.request("GET", "/system/ping", use_api_base=False, auth_kind="web")
response = self.http_client.request("GET", "/system/ping", use_api_base=True, auth_kind="web")
if response.status_code == 200 and response.content == b"pong":
print("Server is alive")
else:
@@ -106,8 +106,7 @@ class RAGFlowClient:
enc_password = encrypt_password(password)
print(f"Register user: {nickname}, email: {username}, password: ******")
payload = {"email": username, "nickname": nickname, "password": enc_password}
response = self.http_client.request(method="POST", path="/user/register",
json_body=payload, use_api_base=False, auth_kind="web")
response = self.http_client.request(method="POST", path="/users", json_body=payload, use_api_base=True, auth_kind="web")
res_json = response.json()
if response.status_code == 200:
if res_json["code"] == 0:
@@ -135,8 +134,7 @@ class RAGFlowClient:
service_id: int = command["number"]
response = self.http_client.request("GET", f"/admin/services/{service_id}", use_api_base=True,
auth_kind="admin")
response = self.http_client.request("GET", f"/admin/services/{service_id}", use_api_base=True, auth_kind="admin")
res_json = response.json()
if response.status_code == 200:
res_data = res_json["data"]
@@ -226,9 +224,7 @@ class RAGFlowClient:
password_tree: Tree = command["password"]
password: str = password_tree.children[0].strip("'\"")
print(f"Alter user: {user_name}, password: ******")
response = self.http_client.request("PUT", f"/admin/users/{user_name}/password",
json_body={"new_password": encrypt_password(password)}, use_api_base=True,
auth_kind="admin")
response = self.http_client.request("PUT", f"/admin/users/{user_name}/password", json_body={"new_password": encrypt_password(password)}, use_api_base=True, auth_kind="admin")
res_json = response.json()
if response.status_code == 200:
print(res_json["message"])
@@ -247,9 +243,7 @@ class RAGFlowClient:
print(f"Create user: {user_name}, password: ******, role: {role}")
# enpass1 = encrypt(password)
enc_password = encrypt_password(password)
response = self.http_client.request(method="POST", path="/admin/users",
json_body={"username": user_name, "password": enc_password, "role": role},
use_api_base=True, auth_kind="admin")
response = self.http_client.request(method="POST", path="/admin/users", json_body={"username": user_name, "password": enc_password, "role": role}, use_api_base=True, auth_kind="admin")
res_json = response.json()
if response.status_code == 200:
self._print_table_simple(res_json["data"])
@@ -266,9 +260,7 @@ class RAGFlowClient:
activate_status: str = activate_tree.children[0].strip("'\"")
if activate_status.lower() in ["on", "off"]:
print(f"Alter user {user_name} activate status, turn {activate_status.lower()}.")
response = self.http_client.request("PUT", f"/admin/users/{user_name}/activate",
json_body={"activate_status": activate_status}, use_api_base=True,
auth_kind="admin")
response = self.http_client.request("PUT", f"/admin/users/{user_name}/activate", json_body={"activate_status": activate_status}, use_api_base=True, auth_kind="admin")
res_json = response.json()
if response.status_code == 200:
print(res_json["message"])
@@ -283,14 +275,12 @@ class RAGFlowClient:
user_name_tree: Tree = command["user_name"]
user_name: str = user_name_tree.children[0].strip("'\"")
response = self.http_client.request("PUT", f"/admin/users/{user_name}/admin", use_api_base=True,
auth_kind="admin")
response = self.http_client.request("PUT", f"/admin/users/{user_name}/admin", use_api_base=True, auth_kind="admin")
res_json = response.json()
if response.status_code == 200:
print(res_json["message"])
else:
print(
f"Fail to grant {user_name} admin authorization, code: {res_json['code']}, message: {res_json['message']}")
print(f"Fail to grant {user_name} admin authorization, code: {res_json['code']}, message: {res_json['message']}")
def revoke_admin(self, command):
if self.server_type != "admin":
@@ -298,14 +288,12 @@ class RAGFlowClient:
user_name_tree: Tree = command["user_name"]
user_name: str = user_name_tree.children[0].strip("'\"")
response = self.http_client.request("DELETE", f"/admin/users/{user_name}/admin", use_api_base=True,
auth_kind="admin")
response = self.http_client.request("DELETE", f"/admin/users/{user_name}/admin", use_api_base=True, auth_kind="admin")
res_json = response.json()
if response.status_code == 200:
print(res_json["message"])
else:
print(
f"Fail to revoke {user_name} admin authorization, code: {res_json['code']}, message: {res_json['message']}")
print(f"Fail to revoke {user_name} admin authorization, code: {res_json['code']}, message: {res_json['message']}")
def create_role(self, command):
if self.server_type != "admin":
@@ -319,10 +307,7 @@ class RAGFlowClient:
desc_str = desc_tree.children[0].strip("'\"")
print(f"create role name: {role_name}, description: {desc_str}")
response = self.http_client.request("POST", "/admin/roles",
json_body={"role_name": role_name, "description": desc_str},
use_api_base=True,
auth_kind="admin")
response = self.http_client.request("POST", "/admin/roles", json_body={"role_name": role_name, "description": desc_str}, use_api_base=True, auth_kind="admin")
res_json = response.json()
if response.status_code == 200:
self._print_table_simple(res_json["data"])
@@ -336,9 +321,7 @@ class RAGFlowClient:
role_name_tree: Tree = command["role_name"]
role_name: str = role_name_tree.children[0].strip("'\"")
print(f"drop role name: {role_name}")
response = self.http_client.request("DELETE", f"/admin/roles/{role_name}",
use_api_base=True,
auth_kind="admin")
response = self.http_client.request("DELETE", f"/admin/roles/{role_name}", use_api_base=True, auth_kind="admin")
res_json = response.json()
if response.status_code == 200:
self._print_table_simple(res_json["data"])
@@ -355,24 +338,18 @@ class RAGFlowClient:
desc_str: str = desc_tree.children[0].strip("'\"")
print(f"alter role name: {role_name}, description: {desc_str}")
response = self.http_client.request("PUT", f"/admin/roles/{role_name}",
json_body={"description": desc_str},
use_api_base=True,
auth_kind="admin")
response = self.http_client.request("PUT", f"/admin/roles/{role_name}", json_body={"description": desc_str}, use_api_base=True, auth_kind="admin")
res_json = response.json()
if response.status_code == 200:
self._print_table_simple(res_json["data"])
else:
print(
f"Fail to update role {role_name} with description: {desc_str}, code: {res_json['code']}, message: {res_json['message']}")
print(f"Fail to update role {role_name} with description: {desc_str}, code: {res_json['code']}, message: {res_json['message']}")
def list_roles(self, command):
if self.server_type != "admin":
print("This command is only allowed in ADMIN mode")
response = self.http_client.request("GET", "/admin/roles",
use_api_base=True,
auth_kind="admin")
response = self.http_client.request("GET", "/admin/roles", use_api_base=True, auth_kind="admin")
res_json = response.json()
if response.status_code == 200:
self._print_table_simple(res_json["data"])
@@ -386,9 +363,7 @@ class RAGFlowClient:
role_name_tree: Tree = command["role_name"]
role_name: str = role_name_tree.children[0].strip("'\"")
print(f"show role: {role_name}")
response = self.http_client.request("GET", f"/admin/roles/{role_name}/permission",
use_api_base=True,
auth_kind="admin")
response = self.http_client.request("GET", f"/admin/roles/{role_name}/permission", use_api_base=True, auth_kind="admin")
res_json = response.json()
if response.status_code == 200:
self._print_table_simple(res_json["data"])
@@ -409,15 +384,12 @@ class RAGFlowClient:
action_str: str = action_tree.children[0].strip("'\"")
actions.append(action_str)
print(f"grant role_name: {role_name_str}, resource: {resource_str}, actions: {actions}")
response = self.http_client.request("POST", f"/admin/roles/{role_name_str}/permission",
json_body={"actions": actions, "resource": resource_str}, use_api_base=True,
auth_kind="admin")
response = self.http_client.request("POST", f"/admin/roles/{role_name_str}/permission", json_body={"actions": actions, "resource": resource_str}, use_api_base=True, auth_kind="admin")
res_json = response.json()
if response.status_code == 200:
self._print_table_simple(res_json["data"])
else:
print(
f"Fail to grant role {role_name_str} with {actions} on {resource_str}, code: {res_json['code']}, message: {res_json['message']}")
print(f"Fail to grant role {role_name_str} with {actions} on {resource_str}, code: {res_json['code']}, message: {res_json['message']}")
def revoke_permission(self, command):
if self.server_type != "admin":
@@ -433,15 +405,12 @@ class RAGFlowClient:
action_str: str = action_tree.children[0].strip("'\"")
actions.append(action_str)
print(f"revoke role_name: {role_name_str}, resource: {resource_str}, actions: {actions}")
response = self.http_client.request("DELETE", f"/admin/roles/{role_name_str}/permission",
json_body={"actions": actions, "resource": resource_str}, use_api_base=True,
auth_kind="admin")
response = self.http_client.request("DELETE", f"/admin/roles/{role_name_str}/permission", json_body={"actions": actions, "resource": resource_str}, use_api_base=True, auth_kind="admin")
res_json = response.json()
if response.status_code == 200:
self._print_table_simple(res_json["data"])
else:
print(
f"Fail to revoke role {role_name_str} with {actions} on {resource_str}, code: {res_json['code']}, message: {res_json['message']}")
print(f"Fail to revoke role {role_name_str} with {actions} on {resource_str}, code: {res_json['code']}, message: {res_json['message']}")
def alter_user_role(self, command):
if self.server_type != "admin":
@@ -452,15 +421,12 @@ class RAGFlowClient:
user_name_tree: Tree = command["user_name"]
user_name_str: str = user_name_tree.children[0].strip("'\"")
print(f"alter_user_role user_name: {user_name_str}, role_name: {role_name_str}")
response = self.http_client.request("PUT", f"/admin/users/{user_name_str}/role",
json_body={"role_name": role_name_str}, use_api_base=True,
auth_kind="admin")
response = self.http_client.request("PUT", f"/admin/users/{user_name_str}/role", json_body={"role_name": role_name_str}, use_api_base=True, auth_kind="admin")
res_json = response.json()
if response.status_code == 200:
self._print_table_simple(res_json["data"])
else:
print(
f"Fail to alter user: {user_name_str} to role {role_name_str}, code: {res_json['code']}, message: {res_json['message']}")
print(f"Fail to alter user: {user_name_str} to role {role_name_str}, code: {res_json['code']}, message: {res_json['message']}")
def show_user_permission(self, command):
if self.server_type != "admin":
@@ -469,14 +435,12 @@ class RAGFlowClient:
user_name_tree: Tree = command["user_name"]
user_name_str: str = user_name_tree.children[0].strip("'\"")
print(f"show_user_permission user_name: {user_name_str}")
response = self.http_client.request("GET", f"/admin/users/{user_name_str}/permission", use_api_base=True,
auth_kind="admin")
response = self.http_client.request("GET", f"/admin/users/{user_name_str}/permission", use_api_base=True, auth_kind="admin")
res_json = response.json()
if response.status_code == 200:
self._print_table_simple(res_json["data"])
else:
print(
f"Fail to show user: {user_name_str} permission, code: {res_json['code']}, message: {res_json['message']}")
print(f"Fail to show user: {user_name_str} permission, code: {res_json['code']}, message: {res_json['message']}")
def generate_key(self, command: dict[str, Any]) -> None:
if self.server_type != "admin":
@@ -485,14 +449,12 @@ class RAGFlowClient:
username_tree: Tree = command["user_name"]
user_name: str = username_tree.children[0].strip("'\"")
print(f"Generating API key for user: {user_name}")
response = self.http_client.request("POST", f"/admin/users/{user_name}/keys", use_api_base=True,
auth_kind="admin")
response = self.http_client.request("POST", f"/admin/users/{user_name}/keys", use_api_base=True, auth_kind="admin")
res_json: dict[str, Any] = response.json()
if response.status_code == 200:
self._print_table_simple(res_json["data"])
else:
print(
f"Failed to generate key for user {user_name}, code: {res_json['code']}, message: {res_json['message']}")
print(f"Failed to generate key for user {user_name}, code: {res_json['code']}, message: {res_json['message']}")
def list_keys(self, command: dict[str, Any]) -> None:
if self.server_type != "admin":
@@ -501,8 +463,7 @@ class RAGFlowClient:
username_tree: Tree = command["user_name"]
user_name: str = username_tree.children[0].strip("'\"")
print(f"Listing API keys for user: {user_name}")
response = self.http_client.request("GET", f"/admin/users/{user_name}/keys", use_api_base=True,
auth_kind="admin")
response = self.http_client.request("GET", f"/admin/users/{user_name}/keys", use_api_base=True, auth_kind="admin")
res_json: dict[str, Any] = response.json()
if response.status_code == 200:
self._print_table_simple(res_json["data"])
@@ -520,8 +481,7 @@ class RAGFlowClient:
print(f"Dropping API key for user: {user_name}")
# URL encode the key to handle special characters
encoded_key: str = urllib.parse.quote(key, safe="")
response = self.http_client.request("DELETE", f"/admin/users/{user_name}/keys/{encoded_key}", use_api_base=True,
auth_kind="admin")
response = self.http_client.request("DELETE", f"/admin/users/{user_name}/keys/{encoded_key}", use_api_base=True, auth_kind="admin")
res_json: dict[str, Any] = response.json()
if response.status_code == 200:
print(res_json["message"])
@@ -534,23 +494,19 @@ class RAGFlowClient:
var_name = _strip_tree_value(command["var_name"])
var_value = _strip_tree_value(command["var_value"])
response = self.http_client.request("PUT", "/admin/variables",
json_body={"var_name": var_name, "var_value": var_value}, use_api_base=True,
auth_kind="admin")
response = self.http_client.request("PUT", "/admin/variables", json_body={"var_name": var_name, "var_value": var_value}, use_api_base=True, auth_kind="admin")
res_json = response.json()
if response.status_code == 200:
print(res_json["message"])
else:
print(
f"Fail to set variable {var_name} to {var_value}, code: {res_json['code']}, message: {res_json['message']}")
print(f"Fail to set variable {var_name} to {var_value}, code: {res_json['code']}, message: {res_json['message']}")
def show_variable(self, command):
if self.server_type != "admin":
print("This command is only allowed in ADMIN mode")
var_name = _strip_tree_value(command["var_name"])
response = self.http_client.request(method="GET", path="/admin/variables", json_body={"var_name": var_name},
use_api_base=True, auth_kind="admin")
response = self.http_client.request(method="GET", path="/admin/variables", json_body={"var_name": var_name}, use_api_base=True, auth_kind="admin")
res_json = response.json()
if response.status_code == 200:
self._print_table_simple(res_json["data"])
@@ -604,8 +560,7 @@ class RAGFlowClient:
if self.server_type != "admin":
print("This command is only allowed in ADMIN mode")
license = command["license"]
response = self.http_client.request("POST", "/admin/license", json_body={"license": license}, use_api_base=True,
auth_kind="admin")
response = self.http_client.request("POST", "/admin/license", json_body={"license": license}, use_api_base=True, auth_kind="admin")
res_json = response.json()
if response.status_code == 200:
print("Set license successfully")
@@ -617,9 +572,7 @@ class RAGFlowClient:
print("This command is only allowed in ADMIN mode")
value1 = command["value1"]
value2 = command["value2"]
response = self.http_client.request("POST", "/admin/license/config",
json_body={"value1": value1, "value2": value2}, use_api_base=True,
auth_kind="admin")
response = self.http_client.request("POST", "/admin/license/config", json_body={"value1": value1, "value2": value2}, use_api_base=True, auth_kind="admin")
res_json = response.json()
if response.status_code == 200:
print("Set license successfully")
@@ -690,8 +643,7 @@ class RAGFlowClient:
user_name: str = username_tree.children[0].strip("'\"")
print(f"Listing all datasets of user: {user_name}")
response = self.http_client.request("GET", f"/admin/users/{user_name}/datasets", use_api_base=True,
auth_kind="admin")
response = self.http_client.request("GET", f"/admin/users/{user_name}/datasets", use_api_base=True, auth_kind="admin")
res_json = response.json()
if response.status_code == 200:
table_data = res_json["data"]
@@ -708,8 +660,7 @@ class RAGFlowClient:
username_tree: Tree = command["user_name"]
user_name: str = username_tree.children[0].strip("'\"")
print(f"Listing all agents of user: {user_name}")
response = self.http_client.request("GET", f"/admin/users/{user_name}/agents", use_api_base=True,
auth_kind="admin")
response = self.http_client.request("GET", f"/admin/users/{user_name}/agents", use_api_base=True, auth_kind="admin")
res_json = response.json()
if response.status_code == 200:
table_data = res_json["data"]
@@ -727,45 +678,122 @@ class RAGFlowClient:
def create_model_provider(self, command):
if self.server_type != "user":
print("This command is only allowed in USER mode")
llm_factory: str = command["provider_name"]
return
provider_name: str = command["provider_name"]
api_key: str = command["provider_key"]
payload = {"api_key": api_key, "llm_factory": llm_factory}
response = self.http_client.request("POST", "/llm/set_api_key", json_body=payload, use_api_base=False,
auth_kind="web")
res_json = response.json()
if response.status_code == 200 and res_json["code"] == 0:
print(f"Success to add model provider {llm_factory}")
# Step 1: Add provider
provider_payload = {"provider_name": provider_name}
provider_response = self.http_client.request("PUT", "/providers", json_body=provider_payload, use_api_base=True, auth_kind="web")
provider_res = provider_response.json()
if provider_response.status_code == 200 and provider_res.get("code") == 0:
print(f"Success to add provider {provider_name}")
else:
print(f"Fail to add model provider {llm_factory}, code: {res_json['code']}, message: {res_json['message']}")
msg = provider_res.get("message", "")
if "duplicated" in msg.lower() or "already exist" in msg.lower():
print(f"Note: provider {provider_name} already exists, continuing to add instance")
else:
print(f"Fail to add provider {provider_name}, code: {provider_res.get('code')}, message: {msg}")
return
# Step 2: Add instance
instance_payload = {"instance_name": "default", "api_key": api_key, "region": "default", "base_url": ""}
instance_response = self.http_client.request("POST", f"/providers/{provider_name}/instances", json_body=instance_payload, use_api_base=True, auth_kind="web")
instance_res = instance_response.json()
if instance_response.status_code == 200 and instance_res.get("code") == 0:
print(f"Success to add instance for provider {provider_name}")
else:
msg = instance_res.get("message", "")
if "already exist" in msg.lower():
print(f"Note: instance for provider {provider_name} already exists, skipping")
else:
print(f"Fail to add instance for provider {provider_name}, code: {instance_res.get('code')}, message: {msg}")
def drop_model_provider(self, command):
if self.server_type != "user":
print("This command is only allowed in USER mode")
llm_factory: str = command["provider_name"]
payload = {"llm_factory": llm_factory}
response = self.http_client.request("POST", "/llm/delete_factory", json_body=payload, use_api_base=False,
auth_kind="web")
return
provider_name: str = command["provider_name"]
response = self.http_client.request("DELETE", f"/providers/{provider_name}", use_api_base=True, auth_kind="web")
res_json = response.json()
if response.status_code == 200 and res_json["code"] == 0:
print(f"Success to drop model provider {llm_factory}")
if response.status_code == 200 and res_json.get("code") == 0:
print(f"Success to drop model provider {provider_name}")
else:
print(
f"Fail to drop model provider {llm_factory}, code: {res_json['code']}, message: {res_json['message']}")
print(f"Fail to drop model provider {provider_name}, code: {res_json.get('code')}, message: {res_json.get('message')}")
# Mapping from legacy model_type keys to API model_type values
_MODEL_TYPE_MAP = {
"llm_id": "chat",
"embd_id": "embedding",
"img2txt_id": "vision",
"reranker_id": "rerank",
"asr_id": "asr",
"tts_id": "tts",
}
def set_default_model(self, command):
if self.server_type != "user":
print("This command is only allowed in USER mode")
return
model_type: str = command["model_type"]
model_type_key: str = command["model_type"]
model_id: str = command["model_id"]
self._set_default_models(model_type, model_id)
model_type = self._MODEL_TYPE_MAP.get(model_type_key)
if model_type is None:
print(f"Unknown model type: {model_type_key}")
return
model_name, model_instance, model_provider = self._parse_model_id(model_id)
payload = {
"model_provider": model_provider,
"model_instance": model_instance,
"model_type": model_type,
"model_name": model_name,
}
response = self.http_client.request("PATCH", "/models/default", json_body=payload, use_api_base=True, auth_kind="web")
res_json = response.json()
if response.status_code == 200 and res_json.get("code") == 0:
print(f"Success to set default {model_type} to {model_id}")
else:
print(f"Fail to set default {model_type}, code: {res_json.get('code')}, message: {res_json.get('message')}")
def reset_default_model(self, command):
if self.server_type != "user":
print("This command is only allowed in USER mode")
return
model_type: str = command["model_type"]
self._set_default_models(model_type, "")
model_type_key: str = command["model_type"]
model_type = self._MODEL_TYPE_MAP.get(model_type_key)
if model_type is None:
print(f"Unknown model type: {model_type_key}")
return
payload = {"model_type": model_type}
response = self.http_client.request("PATCH", "/models/default", json_body=payload, use_api_base=True, auth_kind="web")
res_json = response.json()
if response.status_code == 200 and res_json.get("code") == 0:
print(f"Success to reset default {model_type}")
else:
print(f"Fail to reset default {model_type}, code: {res_json.get('code')}, message: {res_json.get('message')}")
@staticmethod
def _parse_model_id(model_id: str):
"""Parse model_id into (model_name, model_instance, model_provider).
Accepted formats:
- model_name@instance@provider -> (model_name, instance, provider)
- model_name@provider -> (model_name, "default", provider)
- model_name -> (model_name, "default", "")
"""
parts = model_id.split("@")
if len(parts) >= 3:
return parts[0], parts[1], parts[-1]
elif len(parts) == 2:
return parts[0], "default", parts[1]
else:
return model_id, "default", ""
def list_user_datasets(self, command):
if self.server_type != "user":
@@ -773,8 +801,7 @@ class RAGFlowClient:
iterations = command.get("iterations", 1)
if iterations > 1:
response = self.http_client.request("GET", "/datasets", use_api_base=True, auth_kind="web",
iterations=iterations)
response = self.http_client.request("GET", "/datasets", use_api_base=True, auth_kind="web", iterations=iterations)
return response
else:
response = self.http_client.request("GET", "/datasets", use_api_base=True, auth_kind="web")
@@ -788,16 +815,12 @@ class RAGFlowClient:
def create_user_dataset(self, command):
if self.server_type != "user":
print("This command is only allowed in USER mode")
payload = {
"name": command["dataset_name"],
"embedding_model": command["embedding"]
}
payload = {"name": command["dataset_name"], "embedding_model": command["embedding"]}
if "parser_id" in command:
payload["chunk_method"] = command["parser"]
if "pipeline" in command:
payload["pipeline_id"] = command["pipeline"]
response = self.http_client.request("POST", "/datasets", json_body=payload, use_api_base=True,
auth_kind="web")
response = self.http_client.request("POST", "/datasets", json_body=payload, use_api_base=True, auth_kind="web")
res_json = response.json()
if response.status_code == 200:
self._print_table_simple(res_json["data"])
@@ -893,8 +916,7 @@ class RAGFlowClient:
dataset_ids = [dataset_id for _, dataset_id in valid_datasets]
kb_ids_param = ",".join(dataset_ids)
response = self.http_client.request("GET", f"/kb/get_meta?kb_ids={kb_ids_param}",
use_api_base=False, auth_kind="web")
response = self.http_client.request("GET", f"/kb/get_meta?kb_ids={kb_ids_param}", use_api_base=False, auth_kind="web")
res_json = response.json()
if response.status_code != 200:
print(f"Fail to get metadata, code: {res_json.get('code')}, message: {res_json.get('message')}")
@@ -908,11 +930,7 @@ class RAGFlowClient:
table_data = []
for field_name, values_dict in meta.items():
for value, docs in values_dict.items():
table_data.append({
"field": field_name,
"value": value,
"doc_ids": ", ".join(docs)
})
table_data.append({"field": field_name, "value": value, "doc_ids": ", ".join(docs)})
self._print_table_simple(table_data)
def list_user_documents_metadata_summary(self, command_dict):
@@ -930,8 +948,7 @@ class RAGFlowClient:
payload = {"kb_id": kb_id}
if doc_ids:
payload["doc_ids"] = doc_ids
response = self.http_client.request("POST", "/document/metadata/summary", json_body=payload,
use_api_base=False, auth_kind="web")
response = self.http_client.request("POST", "/document/metadata/summary", json_body=payload, use_api_base=False, auth_kind="web")
res_json = response.json()
if response.status_code == 200:
summary = res_json.get("data", {}).get("summary", {})
@@ -998,16 +1015,11 @@ class RAGFlowClient:
"quote": True,
"keyword": False,
"tts": False,
"system": "You are an intelligent assistant. Your primary function is to answer questions based strictly on the provided knowledge base.\n\n **Essential Rules:**\n - Your answer must be derived **solely** from this knowledge base: `{knowledge}`.\n - **When information is available**: Summarize the content to give a detailed answer.\n - **When information is unavailable**: Your response must contain this exact sentence: \"The answer you are looking for is not found in the knowledge base!\"\n - **Always consider** the entire conversation history.",
"system": 'You are an intelligent assistant. Your primary function is to answer questions based strictly on the provided knowledge base.\n\n **Essential Rules:**\n - Your answer must be derived **solely** from this knowledge base: `{knowledge}`.\n - **When information is available**: Summarize the content to give a detailed answer.\n - **When information is unavailable**: Your response must contain this exact sentence: "The answer you are looking for is not found in the knowledge base!"\n - **Always consider** the entire conversation history.',
"refine_multiturn": False,
"use_kg": False,
"reasoning": False,
"parameters": [
{
"key": "knowledge",
"optional": False
}
],
"parameters": [{"key": "knowledge", "optional": False}],
"toc_enhance": False,
},
"similarity_threshold": 0.2,
@@ -1048,8 +1060,7 @@ class RAGFlowClient:
# Build payload
payload = {"kb_id": dataset_id, "vector_size": vector_size}
# Call API
response = self.http_client.request("POST", "/kb/doc_engine_table", json_body=payload,
use_api_base=False, auth_kind="web")
response = self.http_client.request("POST", "/kb/doc_engine_table", json_body=payload, use_api_base=False, auth_kind="web")
res_json = response.json()
if response.status_code == 200 and res_json.get("code") == 0:
print(f"Success to create table for dataset: {dataset_name}")
@@ -1067,8 +1078,7 @@ class RAGFlowClient:
return
# Call API to delete table
payload = {"kb_id": dataset_id}
response = self.http_client.request("DELETE", "/kb/doc_engine_table", json_body=payload,
use_api_base=False, auth_kind="web")
response = self.http_client.request("DELETE", "/kb/doc_engine_table", json_body=payload, use_api_base=False, auth_kind="web")
res_json = response.json()
if response.status_code == 200 and res_json.get("code") == 0:
print(f"Success to drop table for dataset: {dataset_name}")
@@ -1080,8 +1090,7 @@ class RAGFlowClient:
print("This command is only allowed in USER mode")
return
# Call API to create metadata table
response = self.http_client.request("POST", "/tenant/doc_engine_metadata_table",
use_api_base=False, auth_kind="web")
response = self.http_client.request("POST", "/tenant/doc_engine_metadata_table", use_api_base=False, auth_kind="web")
res_json = response.json()
if response.status_code == 200 and res_json.get("code") == 0:
print("Success to create metadata table")
@@ -1093,8 +1102,7 @@ class RAGFlowClient:
print("This command is only allowed in USER mode")
return
# Call API to delete metadata table
response = self.http_client.request("DELETE", "/tenant/doc_engine_metadata_table",
use_api_base=False, auth_kind="web")
response = self.http_client.request("DELETE", "/tenant/doc_engine_metadata_table", use_api_base=False, auth_kind="web")
res_json = response.json()
if response.status_code == 200 and res_json.get("code") == 0:
print("Success to drop metadata table")
@@ -1137,8 +1145,7 @@ class RAGFlowClient:
def _list_chat_sessions(self, dialog_id):
"""List all sessions (conversations) for a given dialog."""
response = self.http_client.request("GET", f"/chats/{dialog_id}/conversations", use_api_base=True,
auth_kind="web")
response = self.http_client.request("GET", f"/chats/{dialog_id}/conversations", use_api_base=True, auth_kind="web")
res_json = response.json()
if response.status_code == 200 and res_json["code"] == 0:
return res_json["data"]
@@ -1154,14 +1161,12 @@ class RAGFlowClient:
if dialog_id is None:
return
payload = {"name": "New conversation"}
response = self.http_client.request("POST", f"/chats/{dialog_id}/conversations", json_body=payload,
use_api_base=True, auth_kind="web")
response = self.http_client.request("POST", f"/chats/{dialog_id}/conversations", json_body=payload, use_api_base=True, auth_kind="web")
res_json = response.json()
if response.status_code == 200 and res_json["code"] == 0:
print(f"Success to create chat session for chat: {chat_name}")
else:
print(
f"Fail to create chat session for chat {chat_name}, code: {res_json['code']}, message: {res_json['message']}")
print(f"Fail to create chat session for chat {chat_name}, code: {res_json['code']}, message: {res_json['message']}")
def drop_chat_session(self, command):
if self.server_type != "user":
@@ -1182,14 +1187,12 @@ class RAGFlowClient:
print(f"Chat session '{session_id}' not found in chat '{chat_name}'")
return
payload = {"ids": to_drop_session_ids}
response = self.http_client.request("DELETE", f"/chats/{dialog_id}/conversations", json_body=payload,
use_api_base=True, auth_kind="web")
response = self.http_client.request("DELETE", f"/chats/{dialog_id}/conversations", json_body=payload, use_api_base=True, auth_kind="web")
res_json = response.json()
if response.status_code == 200 and res_json["code"] == 0:
print(f"Success to drop chat session '{session_id}' from chat: {chat_name}")
else:
print(
f"Fail to drop chat session '{session_id}' from chat {chat_name}, code: {res_json['code']}, message: {res_json['message']}")
print(f"Fail to drop chat session '{session_id}' from chat {chat_name}, code: {res_json['code']}, message: {res_json['message']}")
def list_chat_sessions(self, command):
if self.server_type != "user":
@@ -1217,13 +1220,9 @@ class RAGFlowClient:
# Prepare payload for completion API
# Note: stream parameter is not sent, server defaults to stream=True
payload = {
"session_id": session_id,
"messages": [{"role": "user", "content": message}]
}
payload = {"session_id": session_id, "messages": [{"role": "user", "content": message}]}
response = self.http_client.request("POST", "/chat/completions", json_body=payload,
use_api_base=True, auth_kind="web", stream=True)
response = self.http_client.request("POST", "/chat/completions", json_body=payload, use_api_base=True, auth_kind="web", stream=True)
if response.status_code != 200:
print(f"Fail to chat on session, status code: {response.status_code}")
@@ -1234,17 +1233,16 @@ class RAGFlowClient:
for line in response.iter_lines():
if not line:
continue
line_str = line.decode('utf-8')
if not line_str.startswith('data:'):
line_str = line.decode("utf-8")
if not line_str.startswith("data:"):
continue
data_str = line_str[5:].strip()
if data_str == '[DONE]':
if data_str == "[DONE]":
break
try:
data_json = json.loads(data_str)
if data_json.get("code") != 0:
print(
f"\nFail to chat on session, code: {data_json.get('code')}, message: {data_json.get('message', '')}")
print(f"\nFail to chat on session, code: {data_json.get('code')}, message: {data_json.get('message', '')}")
return
# Check if it's the final message
if data_json.get("data") is True:
@@ -1328,14 +1326,12 @@ class RAGFlowClient:
print(f"Documents {document_names} not found in {dataset_name}")
payload = {"doc_ids": document_ids, "run": 1}
response = self.http_client.request("POST", "/documents/ingest", json_body=payload, use_api_base=True,
auth_kind="web")
response = self.http_client.request("POST", "/documents/ingest", json_body=payload, use_api_base=True, auth_kind="web")
res_json = response.json()
if response.status_code == 200 and res_json["code"] == 0:
print(f"Success to parse {to_parse_doc_names} of {dataset_name}")
else:
print(
f"Fail to parse documents {res_json["data"]["docs"]}, code: {res_json['code']}, message: {res_json['message']}")
print(f"Fail to parse documents {res_json['data']['docs']}, code: {res_json['code']}, message: {res_json['message']}")
def parse_dataset(self, command_dict):
if self.server_type != "user":
@@ -1354,8 +1350,7 @@ class RAGFlowClient:
document_ids.append(doc["id"])
payload = {"doc_ids": document_ids, "run": 1}
response = self.http_client.request("POST", "/documents/ingest", json_body=payload, use_api_base=True,
auth_kind="web")
response = self.http_client.request("POST", "/documents/ingest", json_body=payload, use_api_base=True, auth_kind="web")
res_json = response.json()
if response.status_code == 200 and res_json["code"] == 0:
pass
@@ -1395,15 +1390,7 @@ class RAGFlowClient:
encoder = MultipartEncoder(fields=fields)
headers = {"Content-Type": encoder.content_type}
response = self.http_client.request(
"POST",
f"/datasets/{dataset_id}/documents?return_raw_files=true",
headers=headers,
data=encoder,
json_body=None,
params=None,
stream=False,
auth_kind="web",
use_api_base=True
"POST", f"/datasets/{dataset_id}/documents?return_raw_files=true", headers=headers, data=encoder, json_body=None, params=None, stream=False, auth_kind="web", use_api_base=True
)
res = response.json()
if res.get("code") == 0:
@@ -1430,7 +1417,7 @@ class RAGFlowClient:
payload = {
"question": command_dict["question"],
"kb_id": dataset_ids,
"dataset_ids": dataset_ids,
"similarity_threshold": 0.2,
"vector_similarity_weight": 0.3,
# "top_k": 1024,
@@ -1438,22 +1425,18 @@ class RAGFlowClient:
}
iterations = command_dict.get("iterations", 1)
if iterations > 1:
response = self.http_client.request("POST", "/chunk/retrieval_test", json_body=payload, use_api_base=False,
auth_kind="web", iterations=iterations)
response = self.http_client.request("POST", "/retrieval", json_body=payload, use_api_base=True, auth_kind="web", iterations=iterations)
return response
else:
response = self.http_client.request("POST", "/chunk/retrieval_test", json_body=payload, use_api_base=False,
auth_kind="web")
response = self.http_client.request("POST", "/retrieval", json_body=payload, use_api_base=True, auth_kind="web")
res_json = response.json()
if response.status_code == 200:
if res_json["code"] == 0:
self._print_table_simple(res_json["data"]["chunks"])
else:
print(
f"Fail to search datasets: {dataset_names}, code: {res_json['code']}, message: {res_json['message']}")
print(f"Fail to search datasets: {dataset_names}, code: {res_json['code']}, message: {res_json['message']}")
else:
print(
f"Fail to search datasets: {dataset_names}, code: {res_json['code']}, message: {res_json['message']}")
print(f"Fail to search datasets: {dataset_names}, code: {res_json['code']}, message: {res_json['message']}")
def get_chunk(self, command_dict):
if self.server_type != "user":
@@ -1461,8 +1444,7 @@ class RAGFlowClient:
return
chunk_id = command_dict["chunk_id"]
response = self.http_client.request("GET", f"/chunk/get?chunk_id={chunk_id}", use_api_base=False,
auth_kind="web")
response = self.http_client.request("GET", f"/chunk/get?chunk_id={chunk_id}", use_api_base=False, auth_kind="web")
res_json = response.json()
if response.status_code == 200:
if res_json["code"] == 0:
@@ -1480,8 +1462,7 @@ class RAGFlowClient:
file_path = command_dict["file_path"]
payload = {"file_path": file_path}
response = self.http_client.request("POST", "/kb/insert_from_file", json_body=payload,
use_api_base=False, auth_kind="web")
response = self.http_client.request("POST", "/kb/insert_from_file", json_body=payload, use_api_base=False, auth_kind="web")
res_json = response.json()
if response.status_code == 200:
if res_json["code"] == 0:
@@ -1501,8 +1482,7 @@ class RAGFlowClient:
file_path = command_dict["file_path"]
payload = {"file_path": file_path}
response = self.http_client.request("POST", "/tenant/insert_metadata_from_file", json_body=payload,
use_api_base=False, auth_kind="web")
response = self.http_client.request("POST", "/tenant/insert_metadata_from_file", json_body=payload, use_api_base=False, auth_kind="web")
res_json = response.json()
if response.status_code == 200:
if res_json["code"] == 0:
@@ -1529,8 +1509,7 @@ class RAGFlowClient:
return
# Get doc_id from chunk_id via GET /chunk/get
response = self.http_client.request("GET", f"/chunk/get?chunk_id={chunk_id}", use_api_base=False,
auth_kind="web")
response = self.http_client.request("GET", f"/chunk/get?chunk_id={chunk_id}", use_api_base=False, auth_kind="web")
res_json = response.json()
if response.status_code != 200:
print(f"Fail to get chunk info, code: {res_json.get('code')}, message: {res_json.get('message')}")
@@ -1567,14 +1546,8 @@ class RAGFlowClient:
else:
print(f"Fail to update chunk, HTTP {response.status_code}")
def _get_documents_by_ids(self, ids:list[str]):
response = self.http_client.request(
"POST",
"/document/infos",
json_body={"doc_ids": ids},
use_api_base=False,
auth_kind="web"
)
def _get_documents_by_ids(self, ids: list[str]):
response = self.http_client.request("POST", "/document/infos", json_body={"doc_ids": ids}, use_api_base=False, auth_kind="web")
if response.status_code != 200:
return f"Fail to get document info, HTTP {response.status_code}", None
@@ -1599,6 +1572,7 @@ class RAGFlowClient:
# Parse JSON string to dict
import json
try:
meta_fields = json.loads(meta_json_str)
except json.JSONDecodeError as e:
@@ -1625,13 +1599,7 @@ class RAGFlowClient:
"meta_fields": meta_fields,
}
response = self.http_client.request(
"PATCH",
f"/datasets/{dataset_id}/documents/{doc_id}",
json_body=payload,
use_api_base=True,
auth_kind="web"
)
response = self.http_client.request("PATCH", f"/datasets/{dataset_id}/documents/{doc_id}", json_body=payload, use_api_base=True, auth_kind="web")
res_json = response.json()
if response.status_code == 200:
@@ -1659,8 +1627,7 @@ class RAGFlowClient:
"tags": tags,
}
response = self.http_client.request("POST", f"/kb/{dataset_id}/rm_tags", json_body=payload,
use_api_base=False, auth_kind="web")
response = self.http_client.request("POST", f"/kb/{dataset_id}/rm_tags", json_body=payload, use_api_base=False, auth_kind="web")
res_json = response.json()
if response.status_code == 200:
if res_json.get("code") == 0:
@@ -1683,8 +1650,7 @@ class RAGFlowClient:
elif command_dict.get("chunk_ids"):
payload["chunk_ids"] = command_dict["chunk_ids"]
response = self.http_client.request("POST", "/chunk/rm", json_body=payload,
use_api_base=False, auth_kind="web")
response = self.http_client.request("POST", "/chunk/rm", json_body=payload, use_api_base=False, auth_kind="web")
res_json = response.json()
if response.status_code == 200:
if res_json.get("code") == 0:
@@ -1715,15 +1681,14 @@ class RAGFlowClient:
if "available_int" in command_dict:
payload["available_int"] = command_dict["available_int"]
response = self.http_client.request("POST", "/chunk/list", json_body=payload, use_api_base=False,
auth_kind="web")
response = self.http_client.request("POST", "/chunk/list", json_body=payload, use_api_base=False, auth_kind="web")
res_json = response.json()
if response.status_code == 200:
if res_json["code"] == 0:
chunks = res_json["data"]["chunks"]
if chunks:
for i, chunk in enumerate(chunks):
print(f"\n--- Chunk {i+1} ---")
print(f"\n--- Chunk {i + 1} ---")
for key, value in chunk.items():
print(f" {key}: {value}")
else:
@@ -1757,7 +1722,7 @@ class RAGFlowClient:
all_done = True
for doc in docs:
if doc.get("run") != "DONE":
print(f"Document {doc["name"]} is not done, status: {doc.get("run")}")
print(f"Document {doc['name']} is not done, status: {doc.get('run')}")
all_done = False
break
if all_done:
@@ -1768,16 +1733,10 @@ class RAGFlowClient:
def _list_documents(self, dataset_name: str, dataset_id: str):
# Use the new RESTful API: GET /api/v1/datasets/<dataset_id>/documents
response = self.http_client.request(
"GET",
f"/datasets/{dataset_id}/documents",
use_api_base=True,
auth_kind="web"
)
response = self.http_client.request("GET", f"/datasets/{dataset_id}/documents", use_api_base=True, auth_kind="web")
res_json = response.json()
if response.status_code != 200:
print(
f"Fail to list files from dataset {dataset_name}, code: {res_json['code']}, message: {res_json['message']}")
print(f"Fail to list files from dataset {dataset_name}, code: {res_json['code']}, message: {res_json['message']}")
return None
return res_json["data"]["docs"]
@@ -1825,41 +1784,6 @@ class RAGFlowClient:
print(f"Fail to list chats, code: {res_json['code']}, message: {res_json['message']}")
return None
def _get_default_models(self):
response = self.http_client.request("GET", "/user/tenant_info", use_api_base=False, auth_kind="web")
res_json = response.json()
if response.status_code == 200:
if res_json["code"] == 0:
return res_json["data"]
else:
print(f"Fail to list user default models, code: {res_json['code']}, message: {res_json['message']}")
return None
else:
print(f"Fail to list user default models, HTTP code: {response.status_code}, message: {res_json}")
return None
def _set_default_models(self, model_type, model_id):
current_payload = self._get_default_models()
if current_payload is None:
return
else:
current_payload.update({model_type: model_id})
payload = {
"tenant_id": current_payload["tenant_id"],
"llm_id": current_payload["llm_id"],
"embd_id": current_payload["embd_id"],
"img2txt_id": current_payload["img2txt_id"],
"asr_id": current_payload["asr_id"],
"tts_id": current_payload["tts_id"],
}
response = self.http_client.request("POST", "/user/set_tenant_info", json_body=payload, use_api_base=False,
auth_kind="web")
res_json = response.json()
if response.status_code == 200 and res_json["code"] == 0:
print(f"Success to set default llm to {model_type}")
else:
print(f"Fail to set default llm to {model_type}, code: {res_json['code']}, message: {res_json['message']}")
def _format_service_detail_table(self, data):
if isinstance(data, list):
return data
@@ -2201,22 +2125,14 @@ def run_benchmark(client: RAGFlowClient, command_dict: dict):
total_duration = result["duration"]
qps = iterations / total_duration if total_duration > 0 else None
print(f"command: {command}, Concurrency: {concurrency}, iterations: {iterations}")
print(
f"total duration: {total_duration:.4f}s, QPS: {qps}, COMMAND_COUNT: {iterations}, SUCCESS: {success_count}, FAILURE: {iterations - success_count}")
print(f"total duration: {total_duration:.4f}s, QPS: {qps}, COMMAND_COUNT: {iterations}, SUCCESS: {success_count}, FAILURE: {iterations - success_count}")
pass
else:
results: List[Optional[dict]] = [None] * concurrency
mp_context = mp.get_context("spawn")
start_time = time.perf_counter()
with ProcessPoolExecutor(max_workers=concurrency, mp_context=mp_context) as executor:
future_map = {
executor.submit(
run_command,
client,
command
): idx
for idx in range(concurrency)
}
future_map = {executor.submit(run_command, client, command): idx for idx in range(concurrency)}
for future in as_completed(future_map):
idx = future_map[future]
results[idx] = future.result()
@@ -2238,7 +2154,6 @@ def run_benchmark(client: RAGFlowClient, command_dict: dict):
total_command_count = iterations * concurrency
qps = total_command_count / total_duration if total_duration > 0 else None
print(f"command: {command}, Concurrency: {concurrency} , iterations: {iterations}")
print(
f"total duration: {total_duration:.4f}s, QPS: {qps}, COMMAND_COUNT: {total_command_count}, SUCCESS: {success_count}, FAILURE: {total_command_count - success_count}")
print(f"total duration: {total_duration:.4f}s, QPS: {qps}, COMMAND_COUNT: {total_command_count}, SUCCESS: {success_count}, FAILURE: {total_command_count - success_count}")
pass

View File

@@ -29,6 +29,7 @@ def encrypt_password(password_plain: str) -> str:
import base64
from Cryptodome.PublicKey import RSA
from Cryptodome.Cipher import PKCS1_v1_5 as Cipher_pkcs1_v1_5
def crypt(line):
"""
decrypt(crypt(input_string)) == base64(input_string), which frontend and ragflow_cli use.
@@ -36,20 +37,18 @@ def encrypt_password(password_plain: str) -> str:
pub = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArq9XTUSeYr2+N1h3Afl/z8Dse/2yD0ZGrKwx+EEEcdsBLca9Ynmx3nIB5obmLlSfmskLpBo0UACBmB5rEjBp2Q2f3AG3Hjd4B+gNCG6BDaawuDlgANIhGnaTLrIqWrrcm4EMzJOnAOI1fgzJRsOOUEfaS318Eq9OVO3apEyCCt0lOQK6PuksduOjVxtltDav+guVAA068NrPYmRNabVKRNLJpL8w4D44sfth5RvZ3q9t+6RTArpEtc5sh5ChzvqPOzKGMXW83C95TxmXqpbK6olN4RevSfVjEAgCydH6HN6OhtOQEcnrU97r9H0iZOWwbw3pVrZiUkuRD1R56Wzs2wIDAQAB\n-----END PUBLIC KEY-----"
rsa_key = RSA.importKey(pub)
cipher = Cipher_pkcs1_v1_5.new(rsa_key)
password_base64 = base64.b64encode(line.encode('utf-8')).decode("utf-8")
password_base64 = base64.b64encode(line.encode("utf-8")).decode("utf-8")
encrypted_password = cipher.encrypt(password_base64.encode())
return base64.b64encode(encrypted_password).decode('utf-8')
return base64.b64encode(encrypted_password).decode("utf-8")
except Exception as exc:
raise AuthException(
"Password encryption unavailable; install pycryptodomex (uv sync --python 3.12 --group test)."
) from exc
raise AuthException("Password encryption unavailable; install pycryptodomex (uv sync --python 3.13 --group test).") from exc
return crypt(password_plain)
def register_user(client: HttpClient, email: str, nickname: str, password: str) -> None:
password_enc = encrypt_password(password)
payload = {"email": email, "nickname": nickname, "password": password_enc}
res = client.request_json("POST", "/user/register", use_api_base=False, auth_kind=None, json_body=payload)
res = client.request_json("POST", "/users", use_api_base=True, auth_kind=None, json_body=payload)
if res.get("code") == 0:
return
msg = res.get("message", "")
@@ -64,7 +63,7 @@ def login_user(client: HttpClient, server_type: str, email: str, password: str)
if server_type == "admin":
response = client.request("POST", "/admin/login", use_api_base=True, auth_kind=None, json_body=payload)
else:
response = client.request("POST", "/user/login", use_api_base=False, auth_kind=None, json_body=payload)
response = client.request("POST", "/auth/login", use_api_base=True, auth_kind=None, json_body=payload)
try:
res = response.json()
except Exception as exc:

40
admin/client/uv.lock generated
View File

@@ -1,6 +1,6 @@
version = 1
revision = 3
requires-python = ">=3.12, <3.15"
requires-python = "==3.13.*"
[[package]]
name = "beartype"
@@ -26,22 +26,6 @@ version = "3.4.4"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
@@ -58,22 +42,6 @@ wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
]
@@ -188,20 +156,20 @@ wheels = [
[[package]]
name = "ragflow-cli"
version = "0.25.5"
version = "0.26.4"
source = { virtual = "." }
dependencies = [
{ name = "beartype" },
{ name = "lark" },
{ name = "pycryptodomex" },
{ name = "requests" },
{ name = "requests-toolbelt" },
]
[package.dev-dependencies]
test = [
{ name = "pytest" },
{ name = "requests" },
{ name = "requests-toolbelt" },
]
[package.metadata]
@@ -210,13 +178,13 @@ requires-dist = [
{ name = "lark", specifier = ">=1.1.0" },
{ name = "pycryptodomex", specifier = ">=3.10.0" },
{ name = "requests", specifier = ">=2.30.0,<3.0.0" },
{ name = "requests-toolbelt", specifier = ">=1.0.0" },
]
[package.metadata.requires-dev]
test = [
{ name = "pytest", specifier = ">=8.3.5" },
{ name = "requests", specifier = ">=2.32.3" },
{ name = "requests-toolbelt", specifier = ">=1.0.0" },
]
[[package]]

View File

@@ -15,6 +15,7 @@
#
import time
start_ts = time.time()
import os
@@ -38,26 +39,24 @@ from common.versions import get_ragflow_version
stop_event = threading.Event()
if __name__ == '__main__':
if __name__ == "__main__":
faulthandler.enable()
init_root_logger("admin_service")
logging.info(r"""
____ ___ ______________ ___ __ _
/ __ \/ | / ____/ ____/ /___ _ __ / | ____/ /___ ___ (_)___
____ ___ ______________ ___ __ _
/ __ \/ | / ____/ ____/ /___ _ __ / | ____/ /___ ___ (_)___
/ /_/ / /| |/ / __/ /_ / / __ \ | /| / / / /| |/ __ / __ `__ \/ / __ \
/ _, _/ ___ / /_/ / __/ / / /_/ / |/ |/ / / ___ / /_/ / / / / / / / / / /
/_/ |_/_/ |_\____/_/ /_/\____/|__/|__/ /_/ |_\__,_/_/ /_/ /_/_/_/ /_/
/_/ |_/_/ |_\____/_/ /_/\____/|__/|__/ /_/ |_\__,_/_/ /_/ /_/_/_/ /_/
""")
app = Flask(__name__)
app.register_blueprint(admin_bp)
app.config["SESSION_PERMANENT"] = False
app.config["SESSION_TYPE"] = "filesystem"
app.config["MAX_CONTENT_LENGTH"] = int(
os.environ.get("MAX_CONTENT_LENGTH", 1024 * 1024 * 1024)
)
app.config["MAX_CONTENT_LENGTH"] = int(os.environ.get("MAX_CONTENT_LENGTH", 1024 * 1024 * 1024))
Session(app)
logging.info(f'RAGFlow admin version: {get_ragflow_version()}')
logging.info(f"RAGFlow admin version: {get_ragflow_version()}")
show_configs()
login_manager = LoginManager()
login_manager.init_app(app)

View File

@@ -70,9 +70,7 @@ def setup_auth(login_manager):
logging.warning(f"Authentication attempt with invalid token format: {len(access_token)} chars")
return None
user = UserService.query(
access_token=access_token, status=StatusEnum.VALID.value
)
user = UserService.query(access_token=access_token, status=StatusEnum.VALID.value)
if user:
if not user[0].access_token or not user[0].access_token.strip():
logging.warning(f"User {user[0].email} has empty access_token in database")
@@ -115,8 +113,6 @@ def init_default_admin():
def add_tenant_for_admin(user_info: dict, role: str):
from api.db.services.tenant_llm_service import TenantLLMService
from api.db.services.llm_service import get_init_tenant_llm
tenant = {
"id": user_info["id"],
@@ -125,22 +121,16 @@ def add_tenant_for_admin(user_info: dict, role: str):
"embd_id": settings.EMBEDDING_MDL,
"asr_id": settings.ASR_MDL,
"parser_ids": settings.PARSERS,
"img2txt_id": settings.IMAGE2TEXT_MDL,
"img2txt_id": settings.VISION_MDL,
"rerank_id": settings.RERANK_MDL,
}
usr_tenant = {
"tenant_id": user_info["id"],
"user_id": user_info["id"],
"invited_by": user_info["id"],
"role": role
}
usr_tenant = {"tenant_id": user_info["id"], "user_id": user_info["id"], "invited_by": user_info["id"], "role": role}
tenant_llm = get_init_tenant_llm(user_info["id"])
# tenant_llm = get_init_tenant_llm(user_info["id"])
TenantService.insert(**tenant)
UserTenantService.insert(**usr_tenant)
TenantLLMService.insert_many(tenant_llm)
logging.info(
f"Added tenant for email: {user_info['email']}, A default tenant has been set; changing the default models after login is strongly recommended.")
# TenantLLMService.insert_many(tenant_llm)
logging.info(f"Added tenant for email: {user_info['email']}, A default tenant has been set; changing the default models after login is strongly recommended.")
def check_admin_auth(func):
@@ -162,13 +152,13 @@ def check_admin_auth(func):
def login_admin(email: str, password: str):
"""
:param email: admin email
:param password: string before decrypt
:param password: string before decrypt (RSA encrypted + base64 encoded)
"""
users = UserService.query(email=email)
if not users:
raise UserNotFoundError(email)
psw = decrypt(password)
user = UserService.query_user(email, psw)
decrypted = decrypt(password)
user = UserService.query_user(email, decrypted)
if not user:
raise AdminException("Email and password do not match!")
if not user.is_superuser:
@@ -214,28 +204,17 @@ def login_verify(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
if not auth or 'username' not in auth.parameters or 'password' not in auth.parameters:
return jsonify({
"code": 401,
"message": "Authentication required",
"data": None
}), 200
if not auth or "username" not in auth.parameters or "password" not in auth.parameters:
return jsonify({"code": 401, "message": "Authentication required", "data": None}), 200
username = auth.parameters['username']
password = auth.parameters['password']
username = auth.parameters["username"]
password = auth.parameters["password"]
try:
if not check_admin(username, password):
return jsonify({
"code": 500,
"message": "Access denied",
"data": None
}), 200
return jsonify({"code": 500, "message": "Access denied", "data": None}), 200
except Exception:
logging.exception("An error occurred during admin login verification.")
return jsonify({
"code": 500,
"message": "An internal server error occurred."
}), 200
return jsonify({"code": 500, "message": "An internal server error occurred."}), 200
return f(*args, **kwargs)

View File

@@ -34,8 +34,7 @@ class BaseConfig(BaseModel):
detail_func_name: str
def to_dict(self) -> dict[str, Any]:
return {'id': self.id, 'name': self.name, 'host': self.host, 'port': self.port,
'service_type': self.service_type}
return {"id": self.id, "name": self.name, "host": self.host, "port": self.port, "service_type": self.service_type}
class ServiceConfigs:
@@ -63,11 +62,11 @@ class MetaConfig(BaseConfig):
def to_dict(self) -> dict[str, Any]:
result = super().to_dict()
if 'extra' not in result:
result['extra'] = dict()
extra_dict = result['extra'].copy()
extra_dict['meta_type'] = self.meta_type
result['extra'] = extra_dict
if "extra" not in result:
result["extra"] = dict()
extra_dict = result["extra"].copy()
extra_dict["meta_type"] = self.meta_type
result["extra"] = extra_dict
return result
@@ -77,21 +76,20 @@ class MySQLConfig(MetaConfig):
def to_dict(self) -> dict[str, Any]:
result = super().to_dict()
if 'extra' not in result:
result['extra'] = dict()
extra_dict = result['extra'].copy()
extra_dict['username'] = self.username
extra_dict['password'] = self.password
result['extra'] = extra_dict
if "extra" not in result:
result["extra"] = dict()
extra_dict = result["extra"].copy()
extra_dict["username"] = self.username
extra_dict["password"] = self.password
result["extra"] = extra_dict
return result
class PostgresConfig(MetaConfig):
def to_dict(self) -> dict[str, Any]:
result = super().to_dict()
if 'extra' not in result:
result['extra'] = dict()
if "extra" not in result:
result["extra"] = dict()
return result
@@ -100,11 +98,11 @@ class RetrievalConfig(BaseConfig):
def to_dict(self) -> dict[str, Any]:
result = super().to_dict()
if 'extra' not in result:
result['extra'] = dict()
extra_dict = result['extra'].copy()
extra_dict['retrieval_type'] = self.retrieval_type
result['extra'] = extra_dict
if "extra" not in result:
result["extra"] = dict()
extra_dict = result["extra"].copy()
extra_dict["retrieval_type"] = self.retrieval_type
result["extra"] = extra_dict
return result
@@ -113,11 +111,11 @@ class InfinityConfig(RetrievalConfig):
def to_dict(self) -> dict[str, Any]:
result = super().to_dict()
if 'extra' not in result:
result['extra'] = dict()
extra_dict = result['extra'].copy()
extra_dict['db_name'] = self.db_name
result['extra'] = extra_dict
if "extra" not in result:
result["extra"] = dict()
extra_dict = result["extra"].copy()
extra_dict["db_name"] = self.db_name
result["extra"] = extra_dict
return result
@@ -127,12 +125,12 @@ class ElasticsearchConfig(RetrievalConfig):
def to_dict(self) -> dict[str, Any]:
result = super().to_dict()
if 'extra' not in result:
result['extra'] = dict()
extra_dict = result['extra'].copy()
extra_dict['username'] = self.username
extra_dict['password'] = self.password
result['extra'] = extra_dict
if "extra" not in result:
result["extra"] = dict()
extra_dict = result["extra"].copy()
extra_dict["username"] = self.username
extra_dict["password"] = self.password
result["extra"] = extra_dict
return result
@@ -141,11 +139,11 @@ class MessageQueueConfig(BaseConfig):
def to_dict(self) -> dict[str, Any]:
result = super().to_dict()
if 'extra' not in result:
result['extra'] = dict()
extra_dict = result['extra'].copy()
extra_dict['mq_type'] = self.mq_type
result['extra'] = extra_dict
if "extra" not in result:
result["extra"] = dict()
extra_dict = result["extra"].copy()
extra_dict["mq_type"] = self.mq_type
result["extra"] = extra_dict
return result
@@ -155,30 +153,28 @@ class RedisConfig(MessageQueueConfig):
def to_dict(self) -> dict[str, Any]:
result = super().to_dict()
if 'extra' not in result:
result['extra'] = dict()
extra_dict = result['extra'].copy()
extra_dict['database'] = self.database
extra_dict['password'] = self.password
result['extra'] = extra_dict
if "extra" not in result:
result["extra"] = dict()
extra_dict = result["extra"].copy()
extra_dict["database"] = self.database
extra_dict["password"] = self.password
result["extra"] = extra_dict
return result
class RabbitMQConfig(MessageQueueConfig):
def to_dict(self) -> dict[str, Any]:
result = super().to_dict()
if 'extra' not in result:
result['extra'] = dict()
if "extra" not in result:
result["extra"] = dict()
return result
class RAGFlowServerConfig(BaseConfig):
def to_dict(self) -> dict[str, Any]:
result = super().to_dict()
if 'extra' not in result:
result['extra'] = dict()
if "extra" not in result:
result["extra"] = dict()
return result
@@ -187,9 +183,9 @@ class TaskExecutorConfig(BaseConfig):
def to_dict(self) -> dict[str, Any]:
result = super().to_dict()
if 'extra' not in result:
result['extra'] = dict()
result['extra']['message_queue_type'] = self.message_queue_type
if "extra" not in result:
result["extra"] = dict()
result["extra"]["message_queue_type"] = self.message_queue_type
return result
@@ -198,11 +194,11 @@ class FileStoreConfig(BaseConfig):
def to_dict(self) -> dict[str, Any]:
result = super().to_dict()
if 'extra' not in result:
result['extra'] = dict()
extra_dict = result['extra'].copy()
extra_dict['store_type'] = self.store_type
result['extra'] = extra_dict
if "extra" not in result:
result["extra"] = dict()
extra_dict = result["extra"].copy()
extra_dict["store_type"] = self.store_type
result["extra"] = extra_dict
return result
@@ -212,12 +208,12 @@ class MinioConfig(FileStoreConfig):
def to_dict(self) -> dict[str, Any]:
result = super().to_dict()
if 'extra' not in result:
result['extra'] = dict()
extra_dict = result['extra'].copy()
extra_dict['user'] = self.user
extra_dict['password'] = self.password
result['extra'] = extra_dict
if "extra" not in result:
result["extra"] = dict()
extra_dict = result["extra"].copy()
extra_dict["user"] = self.user
extra_dict["password"] = self.password
result["extra"] = extra_dict
return result
@@ -229,106 +225,105 @@ def load_configurations(config_path: str) -> list[BaseConfig]:
for k, v in raw_configs.items():
match k:
case "ragflow":
name: str = f'ragflow_{ragflow_count}'
host: str = v['host']
http_port: int = v['http_port']
config = RAGFlowServerConfig(id=id_count, name=name, host=host, port=http_port,
service_type="ragflow_server",
detail_func_name="check_ragflow_server_alive")
name: str = f"ragflow_{ragflow_count}"
host: str = v["host"]
http_port: int = v["http_port"]
config = RAGFlowServerConfig(id=id_count, name=name, host=host, port=http_port, service_type="ragflow_server", detail_func_name="check_ragflow_server_alive")
configurations.append(config)
id_count += 1
case "es":
name: str = 'elasticsearch'
url = v['hosts']
name: str = "elasticsearch"
url = v["hosts"]
parsed = urlparse(url)
host: str = parsed.hostname
port: int = parsed.port
username: str = v.get('username')
password: str = v.get('password')
config = ElasticsearchConfig(id=id_count, name=name, host=host, port=port, service_type="retrieval",
retrieval_type="elasticsearch",
username=username, password=password,
detail_func_name="get_es_cluster_stats")
username: str = v.get("username")
password: str = v.get("password")
config = ElasticsearchConfig(
id=id_count,
name=name,
host=host,
port=port,
service_type="retrieval",
retrieval_type="elasticsearch",
username=username,
password=password,
detail_func_name="get_es_cluster_stats",
)
configurations.append(config)
id_count += 1
case "infinity":
name: str = 'infinity'
url = v['uri']
parts = url.split(':', 1)
name: str = "infinity"
url = v["uri"]
parts = url.split(":", 1)
host = parts[0]
port = int(parts[1])
database: str = v.get('db_name', 'default_db')
config = InfinityConfig(id=id_count, name=name, host=host, port=port, service_type="retrieval",
retrieval_type="infinity",
db_name=database, detail_func_name="get_infinity_status")
database: str = v.get("db_name", "default_db")
config = InfinityConfig(id=id_count, name=name, host=host, port=port, service_type="retrieval", retrieval_type="infinity", db_name=database, detail_func_name="get_infinity_status")
configurations.append(config)
id_count += 1
case "minio_0":
name: str = 'minio_0'
url = v['host']
parts = url.split(':', 1)
name: str = "minio_0"
url = v["host"]
parts = url.split(":", 1)
host = parts[0]
port = int(parts[1])
user = v.get('user')
password = v.get('password')
config = MinioConfig(id=id_count, name=name, host=host, port=port, user=user, password=password,
service_type="file_store",
store_type="minio", detail_func_name="check_minio_alive")
user = v.get("user")
password = v.get("password")
config = MinioConfig(id=id_count, name=name, host=host, port=port, user=user, password=password, service_type="file_store", store_type="minio", detail_func_name="check_minio_alive")
configurations.append(config)
id_count += 1
case "minio":
name: str = 'minio'
url = v['host']
parts = url.split(':', 1)
name: str = "minio"
url = v["host"]
parts = url.split(":", 1)
host = parts[0]
port = int(parts[1])
user = v.get('user')
password = v.get('password')
config = MinioConfig(id=id_count, name=name, host=host, port=port, user=user, password=password,
service_type="file_store",
store_type="minio", detail_func_name="check_minio_alive")
user = v.get("user")
password = v.get("password")
config = MinioConfig(id=id_count, name=name, host=host, port=port, user=user, password=password, service_type="file_store", store_type="minio", detail_func_name="check_minio_alive")
configurations.append(config)
id_count += 1
case "redis":
name: str = 'redis'
url = v['host']
parts = url.split(':', 1)
name: str = "redis"
url = v["host"]
parts = url.split(":", 1)
host = parts[0]
port = int(parts[1])
password = v.get('password')
db: int = v.get('db')
config = RedisConfig(id=id_count, name=name, host=host, port=port, password=password, database=db,
service_type="message_queue", mq_type="redis", detail_func_name="get_redis_info")
password = v.get("password")
db: int = v.get("db")
config = RedisConfig(id=id_count, name=name, host=host, port=port, password=password, database=db, service_type="message_queue", mq_type="redis", detail_func_name="get_redis_info")
configurations.append(config)
id_count += 1
case "mysql":
name: str = 'mysql'
host: str = v.get('host')
port: int = v.get('port')
username = v.get('user')
password = v.get('password')
config = MySQLConfig(id=id_count, name=name, host=host, port=port, username=username, password=password,
service_type="meta_data", meta_type="mysql", detail_func_name="get_mysql_status")
name: str = "mysql"
host: str = v.get("host")
port: int = v.get("port")
username = v.get("user")
password = v.get("password")
config = MySQLConfig(
id=id_count, name=name, host=host, port=port, username=username, password=password, service_type="meta_data", meta_type="mysql", detail_func_name="get_mysql_status"
)
configurations.append(config)
id_count += 1
case "admin":
pass
case "task_executor":
name: str = 'task_executor'
host: str = v.get('host', '')
port: int = v.get('port', 0)
message_queue_type: str = v.get('message_queue_type')
config = TaskExecutorConfig(id=id_count, name=name, host=host, port=port, message_queue_type=message_queue_type,
service_type="task_executor", detail_func_name="check_task_executor_alive")
name: str = "task_executor"
host: str = v.get("host", "")
port: int = v.get("port", 0)
message_queue_type: str = v.get("message_queue_type")
config = TaskExecutorConfig(
id=id_count, name=name, host=host, port=port, message_queue_type=message_queue_type, service_type="task_executor", detail_func_name="check_task_executor_alive"
)
configurations.append(config)
id_count += 1
case "rabbitmq":
name: str = 'rabbitmq'
host: str = v.get('host')
port: int = v.get('port')
config = RabbitMQConfig(id=id_count, name=name, host=host, port=port,
service_type="message_queue", mq_type="rabbitmq", detail_func_name="check_rabbitmq_alive")
name: str = "rabbitmq"
host: str = v.get("host")
port: int = v.get("port")
config = RabbitMQConfig(id=id_count, name=name, host=host, port=port, service_type="message_queue", mq_type="rabbitmq", detail_func_name="check_rabbitmq_alive")
configurations.append(config)
id_count += 1
case _:

View File

@@ -4,14 +4,17 @@ class AdminException(Exception):
self.code = code
self.message = message
class UserNotFoundError(AdminException):
def __init__(self, username):
super().__init__(f"User '{username}' not found", 404)
class UserAlreadyExistsError(AdminException):
def __init__(self, username):
super().__init__(f"User '{username}' already exists", 409)
class CannotDeleteAdminError(AdminException):
def __init__(self):
super().__init__("Cannot delete admin account", 403)
super().__init__("Cannot delete admin account", 403)

View File

@@ -17,16 +17,8 @@ from flask import jsonify
def success_response(data=None, message="Success", code=0):
return jsonify({
"code": code,
"message": message,
"data": data
}), 200
return jsonify({"code": code, "message": message, "data": data}), 200
def error_response(message="Error", code=-1, data=None):
return jsonify({
"code": code,
"message": message,
"data": data
}), 400
return jsonify({"code": code, "message": message, "data": data}), 400

View File

@@ -153,6 +153,8 @@ def change_password(username):
def alter_user_activate_status(username):
try:
data = request.get_json()
if current_user.email == username:
return error_response(f"can't alter current user status: {username}", 409)
if not data or "activate_status" not in data:
return error_response("Activation status is required", 400)
activate_status = data["activate_status"]

View File

@@ -489,10 +489,7 @@ class SandboxMgr:
"""List all available sandbox providers."""
result = []
for provider_id, metadata in SandboxMgr.PROVIDER_REGISTRY.items():
result.append({
"id": provider_id,
**metadata
})
result.append({"id": provider_id, **metadata})
return result
@staticmethod
@@ -635,6 +632,7 @@ class SandboxMgr:
config_json = json.dumps(config)
SettingsMgr.update_by_name(f"sandbox.{provider_type}", config_json)
from agent.sandbox.client import reload_provider
reload_provider()
return {"provider_type": provider_type, "config": config}
@@ -693,17 +691,17 @@ class SandboxMgr:
raise AdminException("Failed to create sandbox instance.")
try:
# Simple test code that exercises provider wrapping via main().
# Keep the probe close to the original coverage, but avoid
# `sys` because the sandbox security analyzer blocks it.
test_code = """
import json
import math
import sys
def main() -> dict:
print("Python version:", sys.version)
print("Platform:", sys.platform)
print(f"2 + 2 = {2 + 2}")
left = 2
right = 2
print(f"2 + 2 = {left + right}")
print(f"JSON dump: {json.dumps({'test': 'data', 'value': 123})}")
print(f"Math.sqrt(16) = {math.sqrt(16)}")
print("TEST_PASSED")
@@ -727,11 +725,7 @@ def main() -> dict:
# Build detailed result message
success = execution_result.exit_code == 0 and "TEST_PASSED" in execution_result.stdout
message_parts = [
f"Test {success and 'PASSED' or 'FAILED'}",
f"Exit code: {execution_result.exit_code}",
f"Execution time: {execution_result.execution_time:.2f}s"
]
message_parts = [f"Test {success and 'PASSED' or 'FAILED'}", f"Exit code: {execution_result.exit_code}", f"Execution time: {execution_result.execution_time:.2f}s"]
if execution_result.stdout.strip():
stdout_preview = execution_result.stdout.strip()[:200]
@@ -751,12 +745,13 @@ def main() -> dict:
"execution_time": execution_result.execution_time,
"stdout": execution_result.stdout,
"stderr": execution_result.stderr,
}
},
}
except AdminException:
raise
except Exception as e:
import traceback
error_details = traceback.format_exc()
raise AdminException(f"Connection test failed: {str(e)}\\n\\nStack trace:\\n{error_details}")

View File

@@ -15,6 +15,7 @@
#
import asyncio
import base64
import contextvars
import datetime
import inspect
import json
@@ -24,62 +25,67 @@ import time
from concurrent.futures import ThreadPoolExecutor
from copy import deepcopy
from functools import partial
from typing import Any, Union, Tuple
from typing import Any, Tuple, Union
from agent.component import component_class
from agent.component.base import ComponentBase
from agent.dsl_migration import normalize_chunker_dsl
from api.db.joint_services.tenant_model_service import get_tenant_default_model_by_type
from api.db.services.file_service import FileService
from api.db.services.llm_service import LLMBundle
from api.db.services.task_service import has_canceled
from api.db.joint_services.tenant_model_service import get_tenant_default_model_by_type
from common.constants import LLMType
from common.misc_utils import get_uuid, hash_str2int
from common.llm_request_context import set_llm_request_context, reset_llm_request_context
from common.exceptions import TaskCanceledException
from common.misc_utils import get_uuid, hash_str2int
from common.token_utils import token_usage_sink, langfuse_run_attrs
from rag.prompts.generator import chunks_format
from rag.utils.redis_conn import REDIS_CONN
from rag.utils.tts_cache import synthesize_with_cache
_logger = logging.getLogger(__name__)
class Graph:
"""
dsl = {
"components": {
"begin": {
"obj":{
"component_name": "Begin",
"params": {},
},
"downstream": ["answer_0"],
"upstream": [],
dsl = {
"components": {
"begin": {
"obj":{
"component_name": "Begin",
"params": {},
},
"retrieval_0": {
"obj": {
"component_name": "Retrieval",
"params": {}
},
"downstream": ["generate_0"],
"upstream": ["answer_0"],
},
"generate_0": {
"obj": {
"component_name": "Generate",
"params": {}
},
"downstream": ["answer_0"],
"upstream": ["retrieval_0"],
}
"downstream": ["answer_0"],
"upstream": [],
},
"history": [],
"path": ["begin"],
"retrieval": {"chunks": [], "doc_aggs": []},
"globals": {
"sys.query": "",
"sys.user_id": tenant_id,
"sys.conversation_turns": 0,
"sys.files": []
"retrieval_0": {
"obj": {
"component_name": "Retrieval",
"params": {}
},
"downstream": ["generate_0"],
"upstream": ["answer_0"],
},
"generate_0": {
"obj": {
"component_name": "Generate",
"params": {}
},
"downstream": ["answer_0"],
"upstream": ["retrieval_0"],
}
},
"history": [],
"path": ["begin"],
"retrieval": {"chunks": [], "doc_aggs": []},
"globals": {
"sys.query": "",
"sys.user_id": tenant_id,
"sys.conversation_turns": 0,
"sys.files": []
}
"""
}
"""
def __init__(self, dsl: str, tenant_id=None, task_id=None, custom_header=None):
self.path = []
@@ -113,13 +119,15 @@ class Graph:
def __str__(self):
self.dsl["path"] = self.path
self.dsl["task_id"] = self.task_id
dsl = {
"components": {}
}
dsl = {"components": {}}
for k in self.dsl.keys():
if k in ["components"]:
continue
dsl[k] = deepcopy(self.dsl[k])
try:
dsl[k] = deepcopy(self.dsl[k])
except Exception as e:
logging.warning("Graph.__str__: deepcopy failed for dsl key '%s' (type=%s): %s. Using shallow reference.", k, type(self.dsl[k]).__name__, e)
dsl[k] = self.dsl[k]
for k, cpn in self.components.items():
if k not in dsl["components"]:
@@ -128,8 +136,19 @@ class Graph:
if c == "obj":
dsl["components"][k][c] = json.loads(str(cpn["obj"]))
continue
dsl["components"][k][c] = deepcopy(cpn[c])
return json.dumps(dsl, ensure_ascii=False)
try:
dsl["components"][k][c] = deepcopy(cpn[c])
except Exception as e:
logging.warning("Graph.__str__: deepcopy failed for component '%s' key '%s' (type=%s): %s. Using shallow reference.", k, c, type(cpn[c]).__name__, e)
dsl["components"][k][c] = cpn[c]
def _serialize_default(obj):
if callable(obj):
return None
logging.warning("Graph.__str__: JSON fallback via str() for type=%s", type(obj).__name__)
return str(obj)
return json.dumps(dsl, ensure_ascii=False, default=_serialize_default)
def reset(self):
self.path = []
@@ -141,6 +160,21 @@ class Graph:
except Exception as e:
logging.exception(e)
def close(self):
from common.mcp_tool_call_conn import MCPToolCallSession
seen = set()
for cpn in self.components.values():
obj = cpn.get("obj")
if obj and hasattr(obj, "tools"):
for tool in obj.tools.values():
if isinstance(tool, MCPToolCallSession) and id(tool) not in seen:
seen.add(id(tool))
try:
tool.close_sync(timeout=3)
except Exception:
pass
def get_component_name(self, cid):
for n in self.dsl.get("graph", {}).get("nodes", []):
if cid == n["id"]:
@@ -165,13 +199,15 @@ class Graph:
def get_tenant_id(self):
return self._tenant_id
def get_value_with_variable(self,value: str) -> Any:
pat = re.compile(r"\{* *\{([a-zA-Z:0-9]+@[A-Za-z0-9_.-]+|sys\.[A-Za-z0-9_.]+|env\.[A-Za-z0-9_.]+)\} *\}*")
def get_value_with_variable(self, value: str) -> Any:
# Reference the canonical pre-compiled regex from ComponentBase so
# the source-pattern and the runtime-pattern can never drift apart.
pat = ComponentBase.variable_ref_patt_re
out_parts = []
last = 0
for m in pat.finditer(value):
out_parts.append(value[last:m.start()])
out_parts.append(value[last : m.start()])
key = m.group(1)
v = self.get_variable_value(key)
if v is None:
@@ -190,13 +226,19 @@ class Graph:
last = m.end()
out_parts.append(value[last:])
return("".join(out_parts))
return "".join(out_parts)
def get_variable_value(self, exp: str) -> Any:
exp = exp.strip("{").strip("}").strip(" ").strip("{").strip("}")
if exp.find("@") < 0:
return self.globals[exp]
cpn_id, var_nm = exp.split("@")
# Split from the left with maxsplit=1 so the trailing var_nm can
# legitimately contain '@' characters (defensive: although the
# upstream regex in `get_value_with_variable` constrains `var_nm`
# to `[A-Za-z0-9_.-]+`, direct callers of this method may pass
# any string and should not raise `ValueError: too many values
# to unpack`). `cpn_id` is system-generated and never contains '@'.
cpn_id, var_nm = exp.split("@", 1)
cpn = self.get_component(cpn_id)
if not cpn:
raise Exception(f"Can't find variable: '{cpn_id}@{var_nm}'")
@@ -207,13 +249,13 @@ class Graph:
if not rest:
return root_val
return self.get_variable_param_value(root_val,rest)
return self.get_variable_param_value(root_val, rest)
def get_variable_param_value(self, obj: Any, path: str) -> Any:
cur = obj
if not path:
return cur
for key in path.split('.'):
for key in path.split("."):
if cur is None:
return None
@@ -238,12 +280,15 @@ class Graph:
cur = getattr(cur, key, None)
return cur
def set_variable_value(self, exp: str,value):
def set_variable_value(self, exp: str, value):
exp = exp.strip("{").strip("}").strip(" ").strip("{").strip("}")
if exp.find("@") < 0:
self.globals[exp] = value
return
cpn_id, var_nm = exp.split("@")
# See `get_variable_value` above for rationale on `maxsplit=1`.
# Without it, a var_nm containing '@' would raise
# `ValueError: too many values to unpack` instead of being preserved.
cpn_id, var_nm = exp.split("@", 1)
cpn = self.get_component(cpn_id)
if not cpn:
raise Exception(f"Can't find variable: '{cpn_id}@{var_nm}'")
@@ -256,11 +301,11 @@ class Graph:
root_val = cpn["obj"].output(root_key)
if not root_val:
root_val = {}
cpn["obj"].set_output(root_key, self.set_variable_param_value(root_val,rest,value))
cpn["obj"].set_output(root_key, self.set_variable_param_value(root_val, rest, value))
def set_variable_param_value(self, obj: Any, path: str, value) -> Any:
cur = obj
keys = path.split('.')
keys = path.split(".")
if not path:
return value
for key in keys[:-1]:
@@ -283,7 +328,6 @@ class Graph:
class Canvas(Graph):
def __init__(self, dsl: str, tenant_id=None, task_id=None, canvas_id=None, custom_header=None):
self.globals = {
"sys.query": "",
@@ -291,9 +335,14 @@ class Canvas(Graph):
"sys.conversation_turns": 0,
"sys.files": [],
"sys.history": [],
"sys.date": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
"sys.date": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d %H:%M:%S"),
}
self.variables = {}
# Aggregated provider token usage (prompt/completion/total) across every LLM
# call in a single run — query rewriting, cross-language translation, tool
# reasoning and the final answer. Populated via the token_usage_sink context
# variable that each LLMBundle chat call writes to. Reset at run() start.
self._run_token_usage: dict = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0, "calls": 0}
super().__init__(dsl, tenant_id, task_id, custom_header=custom_header)
self._id = canvas_id
@@ -308,13 +357,13 @@ class Canvas(Graph):
self.globals["sys.date"] = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
else:
self.globals = {
"sys.query": "",
"sys.user_id": "",
"sys.conversation_turns": 0,
"sys.files": [],
"sys.history": [],
"sys.date": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
}
"sys.query": "",
"sys.user_id": "",
"sys.conversation_turns": 0,
"sys.files": [],
"sys.history": [],
"sys.date": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d %H:%M:%S"),
}
if "variables" in self.dsl:
self.variables = self.dsl["variables"]
else:
@@ -329,6 +378,11 @@ class Canvas(Graph):
self.dsl["memory"] = self.memory
return super().__str__()
def clear_history(self):
self.history = []
if isinstance(self.globals.get("sys.history"), list):
self.globals["sys.history"] = []
def reset(self, mem=False):
super().reset()
if not mem:
@@ -373,6 +427,47 @@ class Canvas(Graph):
self.globals[k] = ""
async def run(self, **kwargs):
# Install a fresh per-run token usage sink and Langfuse correlation context,
# and guarantee both are torn down when the run ends (even on early return or
# exception) so later LLM calls in the same task never inherit a previous
# run's sink or session/user attributes.
self._run_token_usage = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0, "calls": 0}
_lf_attrs = {}
_user_id = kwargs.get("user_id")
if _user_id:
_lf_attrs["user_id"] = str(_user_id)[:200]
_session_id = kwargs.get("session_id") or self._id
if _session_id:
_lf_attrs["session_id"] = str(_session_id)[:200]
sink_token = token_usage_sink.set(self._run_token_usage)
attrs_token = langfuse_run_attrs.set(_lf_attrs)
# Forward the originating session/user to upstream LLM providers (as the
# OpenAI `user` field) for the duration of this run, and reset afterwards so
# the value never leaks to later calls in the same task. Reuse the same
# session/user already derived above so both integrations stay consistent.
_req_ctx_token = set_llm_request_context(
session_id=_session_id,
user_id=_user_id,
)
try:
async for ev in self._run_impl(**kwargs):
yield ev
finally:
# reset() can raise if the generator is closed from a different context
# (e.g. client disconnect); fall back to clearing the values in that case.
try:
token_usage_sink.reset(sink_token)
except ValueError:
logging.debug("Failed to reset token usage ContextVar", exc_info=True)
token_usage_sink.set(None)
try:
langfuse_run_attrs.reset(attrs_token)
except ValueError:
logging.debug("Failed to reset Langfuse run attributes ContextVar", exc_info=True)
langfuse_run_attrs.set(None)
reset_llm_request_context(_req_ctx_token)
async def _run_impl(self, **kwargs):
self.globals["sys.date"] = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
st = time.perf_counter()
self._loop = asyncio.get_running_loop()
@@ -382,11 +477,19 @@ class Canvas(Graph):
path_set = set(self.path)
for k, cpn in self.components.items():
if k in path_set:
self.components[k]["obj"].reset(True)
# Begin is intentionally kept as `only_output=True` to preserve existing behavior.
# (Begin/UserFillUp may populate `_param.inputs` during invocation; we leave that unchanged here.)
# All other path components must clear both
# inputs and outputs so the next run resolves refs against
# this run's runtime values (e.g. Await-response capture
# propagating to a downstream Agent's user_prompt), not
# against stale values from the previous canvas run.
is_begin = self.components[k]["obj"].component_name.lower() == "begin"
self.components[k]["obj"].reset(only_output=is_begin)
if kwargs.get("webhook_payload"):
for k, cpn in self.components.items():
if self.components[k]["obj"].component_name.lower() == "begin" and self.components[k]["obj"]._param.mode == "Webhook":
if self.components[k]["obj"].component_name.lower() == "begin" and self.components[k]["obj"]._param.mode == "Webhook":
payload = kwargs.get("webhook_payload", {})
if "input" in payload:
self.components[k]["obj"].set_input_value("request", payload["input"])
@@ -402,36 +505,42 @@ class Canvas(Graph):
break
for k in kwargs.keys():
if k in ["query", "user_id", "files"] and kwargs[k]:
if k in ["query", "user_id", "files", "chat_template_kwargs"] and kwargs[k]:
if k == "files":
self.globals[f"sys.{k}"] = await self.get_files_async(kwargs[k], layout_recognize)
else:
self.globals[f"sys.{k}"] = kwargs[k]
if not self.globals["sys.conversation_turns"] :
if not self.globals["sys.conversation_turns"]:
self.globals["sys.conversation_turns"] = 0
self.globals["sys.conversation_turns"] += 1
is_resume = bool(self.path) and self.path[0].lower().find("userfillup") >= 0
def decorate(event, dt):
nonlocal created_at
return {
"event": event,
#"conversation_id": "f3cc152b-24b0-4258-a1a1-7d5e9fc8a115",
# "conversation_id": "f3cc152b-24b0-4258-a1a1-7d5e9fc8a115",
"message_id": self.message_id,
"created_at": created_at,
"task_id": self.task_id,
"data": dt
"data": dt,
}
if not self.path or self.path[-1].lower().find("userfillup") < 0:
if not is_resume:
self.path.append("begin")
self.retrieval.append({"chunks": [], "doc_aggs": []})
if self.is_canceled():
msg = f"Task {self.task_id} has been canceled before starting."
logging.info(msg)
raise TaskCanceledException(msg)
yield decorate("workflow_started", {"inputs": kwargs.get("inputs")})
if not is_resume:
yield decorate("workflow_started", {"inputs": kwargs.get("inputs")})
_logger.debug(
"[Canvas] Workflow started. Path: %s, Inputs: %s",
[self.get_component_name(c) for c in self.path],
json.dumps(kwargs.get("inputs", {}), ensure_ascii=False, default=str)[:500],
)
self.retrieval.append({"chunks": {}, "doc_aggs": {}})
async def _run_batch(f, t):
@@ -450,7 +559,13 @@ class Canvas(Graph):
if use_async:
await cpn_obj.invoke_async(**(call_kwargs or {}))
return
await loop.run_in_executor(self._thread_pool, partial(sync_fn, **(call_kwargs or {})))
# run_in_executor does not carry context variables into the worker
# thread; copy the current context so the LLM request context (the
# `user` forwarding), token usage sink, and Langfuse attributes set
# by run() remain visible to sync components.
bound_call = partial(sync_fn, **(call_kwargs or {}))
call_ctx = contextvars.copy_context()
await loop.run_in_executor(self._thread_pool, partial(call_ctx.run, bound_call))
i = f
while i < t:
@@ -476,6 +591,13 @@ class Canvas(Graph):
if task_fn is None:
continue
_logger.debug(
"[Canvas] Invoking component '%s' (%s) with inputs: %s",
self.get_component_name(self.path[i - 1]),
cpn.component_name,
json.dumps(call_kwargs, ensure_ascii=False, default=str)[:500],
)
fn_invoke_async = getattr(cpn, "_invoke_async", None)
use_async = (fn_invoke_async and asyncio.iscoroutinefunction(fn_invoke_async)) or asyncio.iscoroutinefunction(getattr(cpn, "_invoke", None))
tasks.append(asyncio.create_task(_invoke_one(cpn, task_fn, call_kwargs, use_async)))
@@ -484,31 +606,46 @@ class Canvas(Graph):
await asyncio.gather(*tasks)
def _node_finished(cpn_obj):
return decorate("node_finished",{
"inputs": cpn_obj.get_input_values(),
"outputs": cpn_obj.output(),
"component_id": cpn_obj._id,
"component_name": self.get_component_name(cpn_obj._id),
"component_type": self.get_component_type(cpn_obj._id),
"error": cpn_obj.error(),
"elapsed_time": time.perf_counter() - cpn_obj.output("_created_time"),
"created_at": cpn_obj.output("_created_time"),
})
outputs = cpn_obj.output()
_logger.debug(
"[Canvas] Component '%s' (%s) finished. Outputs: %s, Error: %s",
self.get_component_name(cpn_obj._id),
self.get_component_type(cpn_obj._id),
json.dumps(outputs, ensure_ascii=False, default=str)[:500],
cpn_obj.error(),
)
return decorate(
"node_finished",
{
"inputs": cpn_obj.get_input_values(),
"outputs": outputs,
"component_id": cpn_obj._id,
"component_name": self.get_component_name(cpn_obj._id),
"component_type": self.get_component_type(cpn_obj._id),
"error": cpn_obj.error(),
"elapsed_time": time.perf_counter() - cpn_obj.output("_created_time"),
"created_at": cpn_obj.output("_created_time"),
},
)
self.error = ""
idx = len(self.path) - 1
idx = 0 if is_resume else len(self.path) - 1
partials = []
tts_mdl = None
while idx < len(self.path):
to = len(self.path)
for i in range(idx, to):
yield decorate("node_started", {
"inputs": None, "created_at": int(time.time()),
"component_id": self.path[i],
"component_name": self.get_component_name(self.path[i]),
"component_type": self.get_component_type(self.path[i]),
"thoughts": self.get_component_thoughts(self.path[i])
})
yield decorate(
"node_started",
{
"inputs": None,
"created_at": int(time.time()),
"component_id": self.path[i],
"component_name": self.get_component_name(self.path[i]),
"component_type": self.get_component_type(self.path[i]),
"thoughts": self.get_component_thoughts(self.path[i]),
},
)
await _run_batch(idx, to)
to = len(self.path)
# post-processing of components invocation
@@ -523,6 +660,7 @@ class Canvas(Graph):
_m = ""
buff_m = ""
stream = cpn_obj.output("content")()
async def _process_stream(m):
nonlocal buff_m, _m, tts_mdl
if not m:
@@ -537,13 +675,7 @@ class Canvas(Graph):
_m += m
if len(buff_m) > 16:
ev = decorate(
"message",
{
"content": m,
"audio_binary": self.tts(tts_mdl, buff_m)
}
)
ev = decorate("message", {"content": m, "audio_binary": self.tts(tts_mdl, buff_m)})
buff_m = ""
return ev
@@ -551,12 +683,12 @@ class Canvas(Graph):
if inspect.isasyncgen(stream):
async for m in stream:
ev= await _process_stream(m)
ev = await _process_stream(m)
if ev:
yield ev
else:
for m in stream:
ev= await _process_stream(m)
ev = await _process_stream(m)
if ev:
yield ev
if buff_m:
@@ -588,7 +720,7 @@ class Canvas(Graph):
else:
self.error = cpn_obj.error()
if cpn_obj.component_name.lower() not in ("iteration","loop"):
if cpn_obj.component_name.lower() not in ("iteration", "loop"):
if isinstance(cpn_obj.output("content"), partial):
if self.error:
cpn_obj.set_output("content", None)
@@ -613,7 +745,7 @@ class Canvas(Graph):
for cpn_id in cpn_ids:
_append_path(cpn_id)
if cpn_obj.component_name.lower() in ("iterationitem","loopitem") and cpn_obj.end():
if cpn_obj.component_name.lower() in ("iterationitem", "loopitem") and cpn_obj.end():
iter = cpn_obj.get_parent()
yield _node_finished(iter)
_extend_path(self.get_component(cpn["parent_id"])["downstream"])
@@ -633,40 +765,49 @@ class Canvas(Graph):
break
idx = to
if any([self.get_component_obj(c).component_name.lower() == "userfillup" for c in self.path[idx:]]):
path = [c for c in self.path[idx:] if self.get_component(c)["obj"].component_name.lower() == "userfillup"]
path.extend([c for c in self.path[idx:] if self.get_component(c)["obj"].component_name.lower() != "userfillup"])
if any([self.components.get(c) is not None and self.get_component_obj(c).component_name.lower() == "userfillup" for c in self.path[idx:]]):
path = [c for c in self.path[idx:] if self.components.get(c) is not None and self.get_component(c)["obj"].component_name.lower() == "userfillup"]
path.extend([c for c in self.path[idx:] if self.components.get(c) is not None and self.get_component(c)["obj"].component_name.lower() != "userfillup"])
another_inputs = {}
tips = ""
for c in path:
o = self.get_component_obj(c)
if o.component_name.lower() == "userfillup":
o.invoke()
another_inputs.update(o.get_input_elements())
another_inputs.update({k: v for k, v in o.get_input_elements().items() if not self._is_input_field_satisfied(v)})
if o.get_param("enable_tips"):
tips = o.output("tips")
if not another_inputs:
continue
self.path = path
yield decorate("user_inputs", {"inputs": another_inputs, "tips": tips})
return
self.path = self.path[:idx]
if not self.error:
yield decorate("workflow_finished",
{
"inputs": kwargs.get("inputs"),
"outputs": self.get_component_obj(self.path[-1]).output(),
"elapsed_time": time.perf_counter() - st,
"created_at": st,
})
yield decorate(
"workflow_finished",
{
"inputs": kwargs.get("inputs"),
"outputs": self.get_component_obj(self.path[-1]).output(),
"elapsed_time": time.perf_counter() - st,
"created_at": st,
# Run-level total of all LLM calls — emitted once here.
"usage": self._run_usage_payload(),
},
)
self.history.append(("assistant", self.get_component_obj(self.path[-1]).output()))
self.globals["sys.history"].append(f"{self.history[-1][0]}: {self.history[-1][1]}")
elif "Task has been canceled" in self.error:
yield decorate("workflow_finished",
{
"inputs": kwargs.get("inputs"),
"outputs": "Task has been canceled",
"elapsed_time": time.perf_counter() - st,
"created_at": st,
})
yield decorate(
"workflow_finished",
{
"inputs": kwargs.get("inputs"),
"outputs": "Task has been canceled",
"elapsed_time": time.perf_counter() - st,
"created_at": st,
"usage": self._run_usage_payload(),
},
)
def is_reff(self, exp: str) -> bool:
exp = exp.strip("{").strip("}")
@@ -679,8 +820,7 @@ class Canvas(Graph):
return False
return True
def tts(self,tts_mdl, text):
def tts(self, tts_mdl, text):
def clean_tts_text(text: str) -> str:
if not text:
return ""
@@ -690,15 +830,8 @@ class Canvas(Graph):
text = re.sub(r"[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]", "", text)
emoji_pattern = re.compile(
"[\U0001F600-\U0001F64F"
"\U0001F300-\U0001F5FF"
"\U0001F680-\U0001F6FF"
"\U0001F1E0-\U0001F1FF"
"\U00002700-\U000027BF"
"\U0001F900-\U0001F9FF"
"\U0001FA70-\U0001FAFF"
"\U0001FAD0-\U0001FAFF]+",
flags=re.UNICODE
"[\U0001f600-\U0001f64f\U0001f300-\U0001f5ff\U0001f680-\U0001f6ff\U0001f1e0-\U0001f1ff\U00002700-\U000027bf\U0001f900-\U0001f9ff\U0001fa70-\U0001faff\U0001fad0-\U0001faff]+",
flags=re.UNICODE,
)
text = emoji_pattern.sub("", text)
@@ -709,6 +842,7 @@ class Canvas(Graph):
text = text[:MAX_LEN]
return text
if not tts_mdl or not text:
return None
text = clean_tts_text(text)
@@ -720,7 +854,7 @@ class Canvas(Graph):
convs = []
if window_size <= 0:
return convs
for role, obj in self.history[window_size * -2:]:
for role, obj in self.history[window_size * -2 :]:
if isinstance(obj, dict):
convs.append({"role": role, "content": obj.get("content", "")})
else:
@@ -729,7 +863,25 @@ class Canvas(Graph):
def add_user_input(self, question):
self.history.append(("user", question))
self.globals["sys.history"].append(f"{self.history[-1][0]}: {self.history[-1][1]}")
rendered = json.dumps(question, ensure_ascii=False) if isinstance(question, dict) else question
self.globals["sys.history"].append(f"{self.history[-1][0]}: {rendered}")
@staticmethod
def _is_input_field_satisfied(field: Any) -> bool:
if not isinstance(field, dict):
return field is not None
value = field.get("value")
field_type = str(field.get("type", "")).lower()
if field_type.find("file") >= 0:
if field.get("optional") and value is None:
return True
return value not in (None, [], "")
if value is None:
return False
return True
def get_prologue(self):
return self.components["begin"]["obj"]._param.prologue
@@ -751,17 +903,19 @@ class Canvas(Graph):
async def get_files_async(self, files: Union[None, list[dict]], layout_recognize: str = None) -> list[str]:
if not files:
return []
return []
def image_to_base64(file):
return "data:{};base64,{}".format(file["mime_type"],
base64.b64encode(FileService.get_blob(file["created_by"], file["id"])).decode("utf-8"))
return "data:{};base64,{}".format(file["mime_type"], base64.b64encode(FileService.get_blob(file["created_by"], file["id"])).decode("utf-8"))
def parse_file(file):
blob = FileService.get_blob(file["created_by"], file["id"])
return FileService.parse(file["name"], blob, True, file["created_by"], layout_recognize)
loop = asyncio.get_running_loop()
tasks = []
for file in files:
if file["mime_type"].find("image") >=0:
if file["mime_type"].find("image") >= 0:
tasks.append(loop.run_in_executor(self._thread_pool, image_to_base64, file))
continue
tasks.append(loop.run_in_executor(self._thread_pool, parse_file, file))
@@ -780,7 +934,7 @@ class Canvas(Graph):
def tool_use_callback(self, agent_id: str, func_name: str, params: dict, result: Any, elapsed_time=None):
agent_ids = agent_id.split("-->")
agent_name = self.get_component_name(agent_ids[0])
path = agent_name if len(agent_ids) < 2 else agent_name+"-->"+"-->".join(agent_ids[1:])
path = agent_name if len(agent_ids) < 2 else agent_name + "-->" + "-->".join(agent_ids[1:])
try:
bin = REDIS_CONN.get(f"{self.task_id}-{self.message_id}-logs")
if bin:
@@ -788,16 +942,10 @@ class Canvas(Graph):
if obj[-1]["component_id"] == agent_ids[0]:
obj[-1]["trace"].append({"path": path, "tool_name": func_name, "arguments": params, "result": result, "elapsed_time": elapsed_time})
else:
obj.append({
"component_id": agent_ids[0],
"trace": [{"path": path, "tool_name": func_name, "arguments": params, "result": result, "elapsed_time": elapsed_time}]
})
obj.append({"component_id": agent_ids[0], "trace": [{"path": path, "tool_name": func_name, "arguments": params, "result": result, "elapsed_time": elapsed_time}]})
else:
obj = [{
"component_id": agent_ids[0],
"trace": [{"path": path, "tool_name": func_name, "arguments": params, "result": result, "elapsed_time": elapsed_time}]
}]
REDIS_CONN.set_obj(f"{self.task_id}-{self.message_id}-logs", obj, 60*10)
obj = [{"component_id": agent_ids[0], "trace": [{"path": path, "tool_name": func_name, "arguments": params, "result": result, "elapsed_time": elapsed_time}]}]
REDIS_CONN.set_obj(f"{self.task_id}-{self.message_id}-logs", obj, 60 * 10)
except Exception as e:
logging.exception(e)
@@ -835,9 +983,22 @@ class Canvas(Graph):
message_end["attachment"] = cpn_obj.output("attachment")
if self._has_reference():
message_end["reference"] = self.get_reference()
# NOTE: aggregated run token usage is intentionally NOT attached here.
# _build_message_end runs once per Message component, so a multi-Message graph
# would emit cumulative usage repeatedly and double count. The run total is
# emitted exactly once on the terminal workflow_finished event instead.
return message_end
def add_memory(self, user:str, assist:str, summ: str):
def _run_usage_payload(self) -> dict:
usage = getattr(self, "_run_token_usage", None) or {}
return {
"prompt_tokens": usage.get("prompt_tokens", 0),
"completion_tokens": usage.get("completion_tokens", 0),
"total_tokens": usage.get("total_tokens", 0),
"calls": usage.get("calls", 0),
}
def add_memory(self, user: str, assist: str, summ: str):
self.memory.append((user, assist, summ))
def get_memory(self) -> list[Tuple]:

View File

@@ -22,8 +22,9 @@ from typing import Dict, Type
_package_path = os.path.dirname(__file__)
__all_classes: Dict[str, Type] = {}
def _import_submodules() -> None:
for filename in os.listdir(_package_path): # noqa: F821
for filename in os.listdir(_package_path): # noqa: F821
if filename.startswith("__") or not filename.endswith(".py") or filename.startswith("base"):
continue
module_name = filename[:-3]
@@ -34,13 +35,14 @@ def _import_submodules() -> None:
except ImportError as e:
print(f"Warning: Failed to import module {module_name}: {str(e)}")
def _extract_classes_from_module(module: ModuleType) -> None:
for name, obj in inspect.getmembers(module):
if (inspect.isclass(obj) and
obj.__module__ == module.__name__ and not name.startswith("_")):
if inspect.isclass(obj) and obj.__module__ == module.__name__ and not name.startswith("_"):
__all_classes[name] = obj
globals()[name] = obj
_import_submodules()
__all__ = list(__all_classes.keys()) + ["__all_classes"]

View File

@@ -27,14 +27,15 @@ import json_repair
from agent.component.llm import LLM, LLMParam
from agent.tools.base import LLMToolPluginCallSession, ToolBase, ToolMeta, ToolParamBase
from api.db.joint_services.tenant_model_service import get_model_config_by_type_and_name
from api.db.joint_services.tenant_model_service import resolve_model_config, resolve_model_type
from api.db.services.llm_service import LLMBundle
from api.db.services.mcp_server_service import MCPServerService
from api.db.services.tenant_llm_service import TenantLLMService
from common.connection_utils import timeout
from common.mcp_tool_call_conn import MCPToolBinding, MCPToolCallSession, mcp_tool_metadata_to_openai_tool
from rag.prompts.generator import citation_plus, citation_prompt, full_question, kb_prompt, message_fit_in, structured_output_prompt
_logger = logging.getLogger(__name__)
class AgentParam(LLMParam, ToolParamBase):
"""
@@ -81,7 +82,9 @@ class Agent(LLM, ToolBase):
original_name = cpn.get_meta()["function"]["name"]
indexed_name = f"{original_name}_{idx}"
self.tools[indexed_name] = cpn
chat_model_config = get_model_config_by_type_and_name(self._canvas.get_tenant_id(), TenantLLMService.llm_id2llm_type(self._param.llm_id), self._param.llm_id)
model_types = resolve_model_type(self._canvas.get_tenant_id(), self._param.llm_id)
model_type = "chat" if "chat" in model_types else model_types[0]
chat_model_config = resolve_model_config(self._canvas.get_tenant_id(), model_type, self._param.llm_id)
self.chat_mdl = LLMBundle(
self._canvas.get_tenant_id(),
chat_model_config,
@@ -112,12 +115,12 @@ class Agent(LLM, ToolBase):
if self.tool_meta:
self.chat_mdl.bind_tools(self.toolcall_session, self.tool_meta)
def _fit_messages(self, prompt: str, msg: list[dict]) -> list[dict]:
_, fitted_messages = message_fit_in(
[{"role": "system", "content": prompt}, *msg],
int(self.chat_mdl.max_length * 0.97),
)
return fitted_messages
def _fit_messages(self, prompt: str, msg: list[dict]) -> tuple[list[dict] | None, str | None]:
msg_fit, fit_error = LLM.fit_messages(prompt, msg, self.chat_mdl.max_length)
if fit_error:
logging.error("Agent prompt fit error: %s", fit_error)
return None, fit_error
return msg_fit, None
@staticmethod
def _append_system_prompt(msg: list[dict], extra_prompt: str) -> None:
@@ -182,7 +185,7 @@ class Agent(LLM, ToolBase):
{"role": "system", "content": schema_prompt + "\nIMPORTANT: Output ONLY valid JSON. No markdown, no extra text."},
{"role": "user", "content": text},
]
_, fmt_msgs = message_fit_in(fmt_msgs, int(self.chat_mdl.max_length * 0.97))
_, fmt_msgs = message_fit_in(fmt_msgs, LLM.context_fit_budget(self.chat_mdl.max_length))
return await self._generate_async(fmt_msgs)
def _invoke(self, **kwargs):
@@ -193,6 +196,17 @@ class Agent(LLM, ToolBase):
if self.check_if_canceled("Agent processing"):
return
user_prompt = kwargs.get("user_prompt")
user_prompt_text = "" if user_prompt is None else str(user_prompt)
_logger.debug(
"[Agent] _invoke_async called. Component: %s, Keys in kwargs: %s, user_prompt_present: %s, user_prompt_length: %d, tools count: %d",
self._id,
list(kwargs.keys()),
bool(user_prompt_text.strip()),
len(user_prompt_text),
len(self.tools) if self.tools else 0,
)
if kwargs.get("user_prompt"):
usr_pmt = ""
if kwargs.get("reasoning"):
@@ -204,10 +218,12 @@ class Agent(LLM, ToolBase):
else:
usr_pmt = str(kwargs["user_prompt"])
self._param.prompts = [{"role": "user", "content": usr_pmt}]
_logger.debug("[Agent] Built user prompt with length=%d, reasoning=%s, context=%s", len(usr_pmt), bool(kwargs.get("reasoning")), bool(kwargs.get("context")))
if not self.tools:
if self.check_if_canceled("Agent processing"):
return
_logger.debug("[Agent] No tools configured. Delegating to LLM._invoke_async. prompt_count=%d", len(self._param.prompts) if self._param.prompts else 0)
return await LLM._invoke_async(self, **kwargs)
prompt, msg, user_defined_prompt = self._prepare_prompt_variables()
@@ -222,11 +238,20 @@ class Agent(LLM, ToolBase):
ex = self.exception_handler()
has_message_downstream = any(self._canvas.get_component_obj(cid).component_name.lower() == "message" for cid in downstreams)
if has_message_downstream and not (ex and ex["goto"]) and not output_schema:
_logger.debug("[Agent] Entering streaming mode (has message downstream)")
self.set_output("content", partial(self.stream_output_with_tools_async, prompt, deepcopy(msg), user_defined_prompt))
return
msg = self._fit_messages(prompt, msg)
msg, fit_error = self._fit_messages(prompt, msg)
if fit_error:
if self.get_exception_default_value():
self.set_output("content", self.get_exception_default_value())
else:
self.set_output("_ERROR", fit_error)
return
self._append_system_prompt(msg, schema_prompt)
_logger.debug("[Agent] Calling LLM with %d messages, has_schema=%s", len(msg), bool(schema_prompt))
ans = await self._generate_async(msg)
if ans.find("**ERROR**") >= 0:
@@ -256,6 +281,7 @@ class Agent(LLM, ToolBase):
artifact_md = self._collect_tool_artifact_markdown(existing_text=ans)
if artifact_md:
ans += "\n\n" + artifact_md
_logger.debug("[Agent] Final output. content_length=%d, has_artifact=%s", len(ans), bool(artifact_md))
self.set_output("content", ans)
return ans
@@ -266,7 +292,17 @@ class Agent(LLM, ToolBase):
self.callback("Multi-turn conversation optimization", {}, user_request, elapsed_time=timer() - st)
msg = [*msg[:-1], {"role": "user", "content": user_request}]
msg = self._fit_messages(prompt, msg)
msg, fit_error = self._fit_messages(prompt, msg)
if fit_error:
if self.get_exception_default_value():
fallback = self.get_exception_default_value()
self.set_output("content", fallback)
yield fallback
else:
self.set_output("_ERROR", fit_error)
self.set_output("content", fit_error)
yield fit_error
return
need2cite = self._param.cite and self._canvas.get_reference()["chunks"] and self._id.find("-->") < 0
cited = False

View File

@@ -15,22 +15,25 @@
#
import asyncio
import builtins
import json
import logging
import os
import re
import time
from abc import ABC
import builtins
import json
import os
import logging
from typing import Any, List, Union
import pandas as pd
from agent import settings
from common.connection_utils import timeout
from common.misc_utils import thread_pool_exec
_logger = logging.getLogger(__name__)
_FEEDED_DEPRECATED_PARAMS = "_feeded_deprecated_params"
_DEPRECATED_PARAMS = "_deprecated_params"
_USER_FEEDED_PARAMS = "_user_feeded_params"
@@ -94,7 +97,13 @@ class ComponentParamBase(ABC):
return {name: True for name in self.get_feeded_deprecated_params()}
def __str__(self):
return json.dumps(self.as_dict(), ensure_ascii=False)
def _serialize_default(obj):
if callable(obj):
return None
logging.warning("ComponentParamBase.__str__: JSON fallback via str() for type=%s", type(obj).__name__)
return str(obj)
return json.dumps(self.as_dict(), ensure_ascii=False, default=_serialize_default)
def as_dict(self):
def _recursive_convert_obj_to_dict(obj):
@@ -128,15 +137,11 @@ class ComponentParamBase(ABC):
update_from_raw_conf = conf.get(_IS_RAW_CONF, True)
if update_from_raw_conf:
deprecated_params_set = self._get_or_init_deprecated_params_set()
feeded_deprecated_params_set = (
self._get_or_init_feeded_deprecated_params_set()
)
feeded_deprecated_params_set = self._get_or_init_feeded_deprecated_params_set()
user_feeded_params_set = self._get_or_init_user_feeded_params_set()
setattr(self, _IS_RAW_CONF, False)
else:
feeded_deprecated_params_set = (
self._get_or_init_feeded_deprecated_params_set(conf)
)
feeded_deprecated_params_set = self._get_or_init_feeded_deprecated_params_set(conf)
user_feeded_params_set = self._get_or_init_user_feeded_params_set(conf)
def _recursive_update_param(param, config, depth, prefix):
@@ -172,15 +177,11 @@ class ComponentParamBase(ABC):
else:
# recursive set obj attr
sub_params = _recursive_update_param(
attr, config_value, depth + 1, prefix=f"{prefix}{config_key}."
)
sub_params = _recursive_update_param(attr, config_value, depth + 1, prefix=f"{prefix}{config_key}.")
setattr(param, config_key, sub_params)
if not allow_redundant and redundant_attrs:
raise ValueError(
f"cpn `{getattr(self, '_name', type(self))}` has redundant parameters: `{[redundant_attrs]}`"
)
raise ValueError(f"cpn `{getattr(self, '_name', type(self))}` has redundant parameters: `{[redundant_attrs]}`")
return param
@@ -211,9 +212,7 @@ class ComponentParamBase(ABC):
param_validation_path_prefix = home_dir + "/param_validation/"
param_name = type(self).__name__
param_validation_path = "/".join(
[param_validation_path_prefix, param_name + ".json"]
)
param_validation_path = "/".join([param_validation_path_prefix, param_name + ".json"])
validation_json = None
@@ -246,11 +245,7 @@ class ComponentParamBase(ABC):
break
if not value_legal:
raise ValueError(
"Please check runtime conf, {} = {} does not match user-parameter restriction".format(
variable, value
)
)
raise ValueError("Please check runtime conf, {} = {} does not match user-parameter restriction".format(variable, value))
elif variable in validation_json:
self._validate_param(attr, validation_json)
@@ -328,11 +323,7 @@ class ComponentParamBase(ABC):
def _range(value, ranges):
in_range = False
for left_limit, right_limit in ranges:
if (
left_limit - settings.FLOAT_ZERO
<= value
<= right_limit + settings.FLOAT_ZERO
):
if left_limit - settings.FLOAT_ZERO <= value <= right_limit + settings.FLOAT_ZERO:
in_range = True
break
@@ -348,16 +339,11 @@ class ComponentParamBase(ABC):
def _warn_deprecated_param(self, param_name, description):
if self._deprecated_params_set.get(param_name):
logging.warning(
f"{description} {param_name} is deprecated and ignored in this version."
)
logging.warning(f"{description} {param_name} is deprecated and ignored in this version.")
def _warn_to_deprecate_param(self, param_name, description, new_param):
if self._deprecated_params_set.get(param_name):
logging.warning(
f"{description} {param_name} will be deprecated in future release; "
f"please use {new_param} instead."
)
logging.warning(f"{description} {param_name} will be deprecated in future release; please use {new_param} instead.")
return True
return False
@@ -365,8 +351,14 @@ class ComponentParamBase(ABC):
class ComponentBase(ABC):
component_name: str
thread_limiter = asyncio.Semaphore(int(os.environ.get("MAX_CONCURRENT_CHATS", 10)))
variable_ref_patt = r"\{* *\{([a-zA-Z:0-9]+@[A-Za-z0-9_.-]+|sys\.[A-Za-z0-9_.]+|env\.[A-Za-z0-9_.]+)\} *\}*"
# Match `cpn_id@var_nm` / `sys.var_nm` / `env.var_nm` style template refs.
# `cpn_id` allows underscores (frontend ids like `userfillup_abc`,
# `retrieval_xyz`) and colons (legacy DSL ids like `UserFillUp:CateInput`,
# `Retrieval:KBSearch`).
variable_ref_patt = r"\{* *\{([a-zA-Z0-9_:]+@[A-Za-z0-9_.-]+|sys\.[A-Za-z0-9_.]+|env\.[A-Za-z0-9_.]+)\} *\}*"
variable_ref_patt_re = re.compile(variable_ref_patt, flags=re.IGNORECASE | re.DOTALL)
iteration_alias_patt = r"\{* *\{(item|index|result)\} *\}*"
iteration_alias_patt_re = re.compile(iteration_alias_patt, flags=re.IGNORECASE | re.DOTALL)
def __str__(self):
"""
@@ -378,9 +370,7 @@ class ComponentBase(ABC):
return """{{
"component_name": "{}",
"params": {}
}}""".format(self.component_name,
self._param
)
}}""".format(self.component_name, self._param)
def __init__(self, canvas, id, param: ComponentParamBase):
from agent.canvas import Graph # Local import to avoid cyclic dependency
@@ -396,7 +386,7 @@ class ComponentBase(ABC):
def check_if_canceled(self, message: str = "") -> bool:
if self.is_canceled():
task_id = getattr(self._canvas, 'task_id', 'unknown')
task_id = getattr(self._canvas, "task_id", "unknown")
log_message = f"Task {task_id} has been canceled"
if message:
log_message += f" during {message}"
@@ -481,18 +471,30 @@ class ComponentBase(ABC):
return self._param.inputs.get(key, {}).get("value")
res = {}
for var, o in self.get_input_elements().items():
input_elements = self.get_input_elements()
_logger.debug(
"[Base] Component '%s' (%s) resolving inputs. Input element keys: %s",
self._id,
self.component_name,
list(input_elements.keys()),
)
for var, o in input_elements.items():
v = self.get_param(var)
if v is None:
_logger.debug("[Base] var '%s': param is None, skipping", var)
continue
if isinstance(v, str) and self._canvas.is_reff(v):
self.set_input_value(var, self._canvas.get_variable_value(v))
elif isinstance(v, str) and re.search(self.variable_ref_patt, v):
resolved = self._canvas.get_variable_value(v)
self.set_input_value(var, resolved)
_logger.debug("[Base] var '%s': resolved ref '%s' -> %s", var, v, json.dumps(resolved, ensure_ascii=False, default=str)[:200])
elif isinstance(v, str) and self.variable_ref_patt_re.search(v):
elements = self.get_input_elements_from_text(v)
kv = {k: e.get('value', '') for k, e in elements.items()}
kv = {k: e.get("value", "") for k, e in elements.items()}
self.set_input_value(var, self.string_format(v, kv))
_logger.debug("[Base] var '%s': resolved text refs '%s' -> %s", var, v, json.dumps(kv, ensure_ascii=False, default=str)[:200])
else:
self.set_input_value(var, v)
_logger.debug("[Base] var '%s': literal value -> %s", var, json.dumps(v, ensure_ascii=False, default=str)[:200])
res[var] = self.get_input_value(var)
return res
@@ -521,16 +523,21 @@ class ComponentBase(ABC):
def get_input_elements_from_text(self, txt: str) -> dict[str, dict[str, str]]:
res = {}
for r in re.finditer(self.variable_ref_patt, txt, flags=re.IGNORECASE | re.DOTALL):
for r in self.variable_ref_patt_re.finditer(txt):
exp = r.group(1)
cpn_id, var_nm = exp.split("@") if exp.find("@") > 0 else ("", exp)
# Use maxsplit=1 to be defensive: although `exp` here comes
# from `variable_ref_patt` (which constrains `var_nm` to
# `[A-Za-z0-9_.-]+`), a future regex relaxation or a non-
# pattern caller should not raise `ValueError: too many values
# to unpack` if the trailing part happens to contain '@'.
cpn_id, var_nm = exp.split("@", 1) if exp.find("@") > 0 else ("", exp)
res[exp] = {
"name": (self._canvas.get_component_name(cpn_id) + f"@{var_nm}") if cpn_id else exp,
"value": self._canvas.get_variable_value(exp),
"_retrieval": self._canvas.get_variable_value(f"{cpn_id}@_references") if cpn_id else None,
"_cpn_id": cpn_id
"_cpn_id": cpn_id,
}
for r in re.finditer(self.iteration_alias_patt, txt, flags=re.IGNORECASE | re.DOTALL):
for r in self.iteration_alias_patt_re.finditer(txt):
exp = r.group(1)
if exp in res:
continue
@@ -542,7 +549,7 @@ class ComponentBase(ABC):
"name": (self._canvas.get_component_name(cpn_id) + f"@{var_nm}"),
"value": self._canvas.get_variable_value(ref),
"_retrieval": self._canvas.get_variable_value(f"{cpn_id}@_references"),
"_cpn_id": cpn_id
"_cpn_id": cpn_id,
}
return res
@@ -562,6 +569,10 @@ class ComponentBase(ABC):
return None
return self._param.inputs[key].get("value")
@staticmethod
def be_output(v):
return pd.DataFrame([{"content": v}])
def get_component_name(self, cpn_id) -> str:
return self._canvas.get_component(cpn_id)["obj"].component_name.lower()
@@ -580,33 +591,27 @@ class ComponentBase(ABC):
return self._canvas.get_component(pid)["obj"]
def get_upstream(self) -> List[str]:
cpn_nms = self._canvas.get_component(self._id)['upstream']
cpn_nms = self._canvas.get_component(self._id)["upstream"]
return cpn_nms
def get_downstream(self) -> List[str]:
cpn_nms = self._canvas.get_component(self._id)['downstream']
cpn_nms = self._canvas.get_component(self._id)["downstream"]
return cpn_nms
@staticmethod
def string_format(content: str, kv: dict[str, str]) -> str:
for n, v in kv.items():
def repl(_match, val=v):
return str(val) if val is not None else ""
content = re.sub(
r"\{%s\}" % re.escape(n),
repl,
content
)
content = re.sub(r"\{%s\}" % re.escape(n), repl, content)
return content
def exception_handler(self):
if not self._param.exception_method:
return None
return {
"goto": self._param.exception_goto,
"default_value": self._param.exception_default_value
}
return {"goto": self._param.exception_goto, "default_value": self._param.exception_default_value}
def get_exception_default_value(self):
if self._param.exception_method != "comment":

View File

@@ -14,21 +14,20 @@
# limitations under the License.
#
from agent.component.fillup import UserFillUpParam, UserFillUp
from api.db.services.file_service import FileService
class BeginParam(UserFillUpParam):
"""
Define the Begin component parameters.
"""
def __init__(self):
super().__init__()
self.mode = "conversational"
self.prologue = "Hi! I'm your smart assistant. What can I do for you?"
def check(self):
self.check_valid_value(self.mode, "The 'mode' should be either `conversational` or `task`", ["conversational", "task","Webhook"])
self.check_valid_value(self.mode, "The 'mode' should be either `conversational` or `task`", ["conversational", "task", "Webhook"])
def get_input_form(self) -> dict[str, dict]:
return getattr(self, "inputs")
@@ -42,20 +41,11 @@ class Begin(UserFillUp):
return
layout_recognize = self._param.layout_recognize or None
for k, v in kwargs.get("inputs", {}).items():
merged_inputs = self._merge_runtime_inputs(kwargs.get("inputs", {}))
for k, v in merged_inputs.items():
if self.check_if_canceled("Begin processing"):
return
if isinstance(v, dict) and v.get("type", "").lower().find("file") >= 0:
if v.get("optional") and v.get("value", None) is None:
v = None
else:
file_value = v["value"]
# Support both single file (backward compatibility) and multiple files
files = file_value if isinstance(file_value, list) else [file_value]
v = FileService.get_files(files, layout_recognize=layout_recognize)
else:
v = v.get("value")
v = self._resolve_input_value(v, layout_recognize)
self.set_output(k, v)
self.set_input_value(k, v)

713
agent/component/browser.py Normal file
View File

@@ -0,0 +1,713 @@
#
# 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.
#
import asyncio
import hashlib
import inspect
import json
import logging
import os
import re
import shutil
import tempfile
from abc import ABC
from pathlib import Path
from typing import Any
from urllib.error import HTTPError, URLError
from urllib.parse import unquote, urlparse
from urllib.request import Request, urlopen
from agent.component.base import ComponentBase
from agent.component.llm import LLMParam
from api.db import FileType
from api.db.joint_services.tenant_model_service import resolve_model_config, resolve_model_type
from api.db.services import duplicate_name
from api.db.services.file_service import FileService
from api.utils.file_utils import filename_type
from common import settings
from common.connection_utils import timeout
from common.misc_utils import get_uuid
from rag.llm import FACTORY_DEFAULT_BASE_URL
class BrowserParam(LLMParam):
"""
Parameters for Browser node.
"""
def __init__(self):
super().__init__()
self.prompts = "{sys.query}"
self.max_steps = 30
self.headless = True
self.enable_default_extensions = False
self.chromium_sandbox = False
# Reuse browser profile across runs of the same agent node by default.
self.persist_session = True
self.upload_sources = []
self.outputs = {
"content": {"type": "string", "value": ""},
"downloaded_files": {"type": "Array<Object>", "value": []},
}
def check(self):
self.check_empty(self.llm_id, "[Browser] LLM")
self.check_positive_integer(self.max_steps, "[Browser] Max steps")
self.check_boolean(self.headless, "[Browser] Headless")
self.check_boolean(self.enable_default_extensions, "[Browser] Enable default extensions")
self.check_boolean(self.chromium_sandbox, "[Browser] Chromium sandbox")
self.check_boolean(self.persist_session, "[Browser] Persist session")
self.check_empty(self.prompts, "[Browser] Prompts")
return True
def get_input_form(self) -> dict[str, dict]:
return {
"prompts": {"type": "text", "name": "Prompts"},
"upload_sources": {"type": "line", "name": "Upload sources"},
}
class Browser(ComponentBase, ABC):
component_name = "Browser"
def _prepare_input_values(self):
for key, meta in self.get_input_elements().items():
val = meta.get("value")
if val is None:
val = ""
elif not isinstance(val, str):
val = json.dumps(val, ensure_ascii=False)
self.set_input_value(key, val)
def get_input_elements(self) -> dict[str, dict]:
text_parts = [
str(self._param.prompts or ""),
json.dumps(self._param.upload_sources, ensure_ascii=False),
]
return self.get_input_elements_from_text("\n".join(text_parts))
def _resolve_param_value(self, value: Any) -> Any:
if isinstance(value, str):
direct_ref = value.strip()
if direct_ref.startswith("{") and direct_ref.endswith("}") and self._canvas.is_reff(direct_ref):
return self._canvas.get_variable_value(direct_ref)
return value
return value
def _extract_ids(self, value: Any) -> list[str]:
ids: list[str] = []
value = self._resolve_param_value(value)
def collect(item: Any):
if item is None:
return
if isinstance(item, str):
token = item.strip()
if not token:
return
if token.startswith("{") and token.endswith("}") and self._canvas.is_reff(token):
collect(self._canvas.get_variable_value(token))
return
if token.startswith("[") and token.endswith("]"):
try:
parsed = json.loads(token)
collect(parsed)
return
except Exception:
pass
if self._is_http_url(token):
ids.append(token)
return
if "," in token:
for part in token.split(","):
collect(part)
return
ids.append(token)
return
if isinstance(item, dict):
for k in ("file_id", "id", "url", "value"):
if k in item:
collect(item[k])
return
for v in item.values():
collect(v)
return
if isinstance(item, (list, tuple, set)):
for v in item:
collect(v)
return
token = str(item).strip()
if token:
ids.append(token)
collect(value)
deduped: list[str] = []
visited = set()
for item in ids:
if item in visited:
continue
visited.add(item)
deduped.append(item)
return deduped
@staticmethod
def _is_http_url(value: str) -> bool:
token = str(value or "").strip()
if not token:
return False
parsed = urlparse(token)
return parsed.scheme in {"http", "https"} and bool(parsed.netloc)
@staticmethod
def _extract_url_filename(url: str, headers: Any) -> str:
content_disposition = str(getattr(headers, "get", lambda *_args, **_kwargs: "")("Content-Disposition", "") or "")
if content_disposition:
# Prefer RFC 5987 encoded filename*=UTF-8''... when present.
m = re.search(r"filename\*\s*=\s*(?:UTF-8''|utf-8'')?([^;]+)", content_disposition)
if m:
name = unquote(m.group(1).strip().strip('"'))
if name:
return os.path.basename(name)
m = re.search(r'filename\s*=\s*"([^"]+)"', content_disposition)
if m:
name = m.group(1).strip()
if name:
return os.path.basename(name)
m = re.search(r"filename\s*=\s*([^;]+)", content_disposition)
if m:
name = m.group(1).strip().strip('"')
if name:
return os.path.basename(name)
parsed = urlparse(url)
raw_name = os.path.basename(parsed.path or "")
name = unquote(raw_name).strip()
if name:
return name
return f"url_file_{get_uuid()[:8]}.bin"
@staticmethod
def _resolve_upload_url_max_bytes() -> int:
raw = str(os.getenv("RAGFLOW_BROWSER_UPLOAD_URL_MAX_BYTES", "") or "").strip()
default_max_bytes = 100 * 1024 * 1024
if not raw:
return default_max_bytes
try:
parsed = int(raw)
return parsed if parsed > 0 else default_max_bytes
except (TypeError, ValueError):
return default_max_bytes
@staticmethod
def _restore_env_var(key: str, value: str | None):
if value is None:
os.environ.pop(key, None)
return
os.environ[key] = value
def _prepare_upload_url_file(self, url: str, upload_dir: str) -> dict[str, Any] | None:
max_bytes = self._resolve_upload_url_max_bytes()
local_path = ""
local_name = ""
total_size = 0
try:
req = Request(url, headers={"User-Agent": "RAGFlow-Browser-Node/1.0"})
with urlopen(req, timeout=30) as response:
local_name = self._extract_url_filename(url, response.headers)
local_path = os.path.join(upload_dir, local_name)
index = 1
while os.path.exists(local_path):
stem, ext = os.path.splitext(local_name)
local_path = os.path.join(upload_dir, f"{stem}_{index}{ext}")
index += 1
with open(local_path, "wb") as f:
while True:
chunk = response.read(1024 * 1024)
if not chunk:
break
total_size += len(chunk)
if total_size > max_bytes:
raise ValueError(f"upload url file exceeds max size limit: {max_bytes}")
f.write(chunk)
except (HTTPError, URLError, OSError, TimeoutError, ValueError) as e:
if local_path and os.path.exists(local_path):
try:
os.remove(local_path)
except OSError:
pass
logging.warning("Browser failed to fetch upload url. url=%s, error=%s", url, e)
return None
if total_size <= 0:
if local_path and os.path.exists(local_path):
try:
os.remove(local_path)
except OSError:
pass
logging.warning("Browser upload url returned empty content: %s", url)
return None
return {
"file_id": "",
"name": local_name,
"size": total_size,
"local_path": local_path,
"source_url": url,
}
def _resolve_text(self, raw_text: Any) -> str:
text = str(self._resolve_param_value(raw_text) or "")
vars_map = self.get_input_elements_from_text(text)
kv = {}
for key, meta in vars_map.items():
val = meta.get("value", "")
if isinstance(val, str):
kv[key] = val
else:
kv[key] = json.dumps(val, ensure_ascii=False)
return self.string_format(text, kv)
@staticmethod
def _as_model_config_dict(cfg_obj: Any) -> dict[str, Any]:
if cfg_obj is None:
return {}
if isinstance(cfg_obj, dict):
return cfg_obj
if hasattr(cfg_obj, "to_dict") and callable(cfg_obj.to_dict):
try:
result = cfg_obj.to_dict()
return result if isinstance(result, dict) else {}
except (AttributeError, TypeError, ValueError):
return {}
result = {}
for key in ("model", "model_name", "llm_name", "llm_factory", "api_key", "base_url", "api_base", "temperature"):
val = getattr(cfg_obj, key, None)
if val not in (None, ""):
result[key] = val
return result
@staticmethod
def _error_chain(exc: Exception) -> str:
parts = []
cur = exc
depth = 0
while cur is not None and depth < 6:
parts.append(f"{type(cur).__name__}: {cur}")
cur = cur.__cause__ or cur.__context__
depth += 1
return " <- ".join(parts)
@staticmethod
def _resolve_browser_executable() -> str:
explicit_candidates = [
os.getenv("BROWSER_USE_EXECUTABLE_PATH", "").strip(),
os.getenv("BROWSER_USE_BROWSER_BINARY_PATH", "").strip(),
os.getenv("BROWSER_USE_CHROME_BINARY_PATH", "").strip(),
]
for explicit in explicit_candidates:
if explicit and os.path.isfile(explicit) and os.access(explicit, os.X_OK):
return explicit
candidates = [
"/opt/chrome/chrome",
"/usr/local/bin/chrome",
"/usr/local/bin/google-chrome",
"/usr/bin/google-chrome",
"/usr/bin/google-chrome-stable",
"/usr/bin/chromium",
"/usr/bin/chromium-browser",
]
for path in candidates:
if os.path.isfile(path) and os.access(path, os.X_OK):
return path
for cmd in ("chrome", "google-chrome", "google-chrome-stable", "chromium", "chromium-browser"):
path = shutil.which(cmd)
if path and os.path.isfile(path) and os.access(path, os.X_OK):
return path
return ""
@staticmethod
def _normalize_model_name(model: Any) -> str:
name = str(model or "").strip()
if not name:
return ""
if name.startswith("bu-") or name.startswith("browser-use/"):
return name
if "@" in name:
# RAGFlow model aliases may include provider suffix, e.g. qwen3.5-flash@Tongyi-Qianwen.
# browser-use OpenAI-compatible adapters need the pure model name.
name = name.split("@", 1)[0].strip()
return name
@staticmethod
def _safe_path_segment(value: Any) -> str:
token = str(value or "").strip()
if not token:
return "unknown"
token = re.sub(r"[^A-Za-z0-9._-]+", "_", token)
return token.strip("._-") or "unknown"
def _resolve_persistent_profile_dir(self) -> str:
root = os.path.join(tempfile.gettempdir(), "ragflow_browser_use_profiles")
tenant = self._safe_path_segment(self._canvas.get_tenant_id())
raw_canvas_id = getattr(self._canvas, "_id", "")
if not raw_canvas_id:
graph_text = json.dumps(
self._canvas.dsl.get("graph", {}),
sort_keys=True,
ensure_ascii=False,
)
raw_canvas_id = f"dsl_{hashlib.sha1(graph_text.encode('utf-8')).hexdigest()[:12]}"
canvas_id = self._safe_path_segment(raw_canvas_id)
node_id = self._safe_path_segment(self._id)
return os.path.join(root, tenant, canvas_id, node_id)
def _should_persist_session(self) -> bool:
return bool(self._param.persist_session)
def _infer_provider_name(self, cfg: dict[str, Any]) -> str:
provider = str(cfg.get("llm_factory") or "").strip()
if provider:
return provider
llm_id = str(self._param.llm_id or "")
if "@" in llm_id:
return llm_id.split("@", 1)[1].strip()
return ""
def _resolve_openai_compatible_base_url(self, cfg: dict[str, Any]) -> str:
explicit = str(cfg.get("base_url") or cfg.get("api_base") or "").strip()
if explicit:
return explicit
provider = self._infer_provider_name(cfg)
fallback = str(FACTORY_DEFAULT_BASE_URL.get(provider, "")).strip()
return fallback if fallback else ""
def _build_browser_llm(self):
from browser_use.llm import ChatBrowserUse, ChatOpenAI
chat_model_config = resolve_model_config(
self._canvas.get_tenant_id(),
resolve_model_type(self._canvas.get_tenant_id(), self._param.llm_id),
self._param.llm_id,
)
cfg = self._as_model_config_dict(chat_model_config)
model_name = self._normalize_model_name(cfg.get("model_name") or cfg.get("model") or self._param.llm_id)
if not model_name:
raise ValueError(f"Invalid model config for Browser llm_id={self._param.llm_id}")
base_url = self._resolve_openai_compatible_base_url(cfg)
# ChatBrowserUse only supports bu-* models. For tenant models, use OpenAI-compatible adapter.
if model_name.startswith("bu-") or model_name.startswith("browser-use/"):
llm_kwargs = {
"model": model_name,
"api_key": cfg.get("api_key"),
"base_url": base_url,
"temperature": self._param.temperature,
"max_retries": self._param.max_retries,
}
llm_kwargs = {k: v for k, v in llm_kwargs.items() if v not in (None, "")}
return ChatBrowserUse(**llm_kwargs)
# browser-use Agent defaults to json_schema response_format and may use tool_choice via
# ChatDeepSeek. Many providers (e.g. DeepSeek thinking models) reject both. Use ChatOpenAI
# with schema-in-prompt and without forced structured output on the first run.
llm_kwargs = {
"model": model_name,
"api_key": cfg.get("api_key"),
"base_url": base_url,
"temperature": self._param.temperature,
"max_retries": self._param.max_retries,
"add_schema_to_system_prompt": True,
"dont_force_structured_output": True,
}
llm_kwargs = {k: v for k, v in llm_kwargs.items() if v not in (None, "")}
return ChatOpenAI(**llm_kwargs)
async def _run_browser_use_async(
self,
task_text: str,
download_dir: str,
available_file_paths: list[str] | None = None,
profile_dir: str | None = None,
):
from browser_use import Agent as BrowserUseAgent, Browser as BrowserUseBrowser
llm = self._build_browser_llm()
# NOTE:
# _invoke() uses asyncio.run(), which creates a fresh event loop per task run.
# Reusing a Browser object created by a previous loop can deadlock/timestamp out
# in browser-use watchdog handlers on subsequent runs.
# We keep persistent user_data_dir for session continuity, but we do not keep
# browser instances alive across runs.
available_file_paths = available_file_paths or []
agent_kwargs: dict[str, Any] = {
"task": task_text,
"llm": llm,
"available_file_paths": available_file_paths,
}
browser_obj = None
previous_disable_extensions = os.environ.get("BROWSER_USE_DISABLE_EXTENSIONS")
previous_browser_binary_path = os.environ.get("BROWSER_USE_BROWSER_BINARY_PATH")
try:
enable_default_extensions = bool(self._param.enable_default_extensions)
if not enable_default_extensions:
os.environ["BROWSER_USE_DISABLE_EXTENSIONS"] = "1"
else:
os.environ.pop("BROWSER_USE_DISABLE_EXTENSIONS", None)
executable_path = self._resolve_browser_executable()
browser_kwargs = {
"headless": self._param.headless,
"downloads_path": download_dir,
# Docker often runs as root without user namespaces; disable sandbox by default.
"chromium_sandbox": bool(self._param.chromium_sandbox),
# Disable runtime extension download by default for intranet/offline environments.
# Enable only when explicitly required and extensions are pre-cached.
"enable_default_extensions": enable_default_extensions,
}
if executable_path:
browser_kwargs["executable_path"] = executable_path
# Keep browser-use watchdog fallback in sync with our resolved path.
os.environ["BROWSER_USE_BROWSER_BINARY_PATH"] = executable_path
else:
logging.warning("Browser no local browser executable found. Set BROWSER_USE_EXECUTABLE_PATH or preinstall chromium in image to avoid runtime playwright install.")
if profile_dir:
browser_kwargs["user_data_dir"] = profile_dir
# browser-use expects profile_directory to be a profile name
# such as "Default" / "Profile 1", not an absolute path.
browser_kwargs["profile_directory"] = "Default"
browser_obj = BrowserUseBrowser(**browser_kwargs)
agent_kwargs["browser"] = browser_obj
except (OSError, RuntimeError, TypeError, ValueError) as e:
logging.warning("Browser browser context customization skipped: %s", e)
agent = BrowserUseAgent(**agent_kwargs)
history = None
run_fn = getattr(agent, "run", None)
if run_fn is None:
raise RuntimeError("browser-use Agent does not provide run().")
run_kwargs = {"max_steps": self._param.max_steps}
try:
if inspect.iscoroutinefunction(run_fn):
history = await run_fn(**run_kwargs)
else:
history = await asyncio.to_thread(run_fn, **run_kwargs)
except Exception as e:
logging.error("Browser agent.run failed. error_chain=%s", self._error_chain(e))
logging.exception("Browser agent.run traceback")
raise
finally:
if browser_obj:
close_fn = getattr(browser_obj, "close", None)
if close_fn:
try:
if inspect.iscoroutinefunction(close_fn):
await close_fn()
else:
await asyncio.to_thread(close_fn)
except Exception as close_err:
logging.warning("Browser failed to close browser object cleanly: %s", close_err)
self._restore_env_var("BROWSER_USE_DISABLE_EXTENSIONS", previous_disable_extensions)
self._restore_env_var("BROWSER_USE_BROWSER_BINARY_PATH", previous_browser_binary_path)
return history
def _prepare_upload_files(self, upload_dir: str) -> list[dict[str, Any]]:
upload_refs = self._extract_ids(self._param.upload_sources)
prepared = []
for file_ref in upload_refs:
if self._is_http_url(file_ref):
prepared_url_file = self._prepare_upload_url_file(file_ref, upload_dir)
if prepared_url_file:
prepared.append(prepared_url_file)
continue
file_id = file_ref
exists, file = FileService.get_by_id(file_id)
if not exists:
logging.warning("Browser upload file_id not found: %s", file_id)
continue
try:
blob = settings.STORAGE_IMPL.get(file.parent_id, file.location)
if not blob:
logging.warning("Browser upload blob not found: %s", file_id)
continue
local_name = os.path.basename(file.location) if file.location else (file.name or f"{file_id}.bin")
local_path = os.path.join(upload_dir, local_name)
index = 1
while os.path.exists(local_path):
stem, ext = os.path.splitext(local_name)
local_path = os.path.join(upload_dir, f"{stem}_{index}{ext}")
index += 1
with open(local_path, "wb") as f:
f.write(blob)
except OSError as e:
logging.warning("Browser failed to prepare upload file. file_id=%s, error=%s", file_id, e)
continue
except Exception as e:
logging.warning("Browser failed to fetch upload blob. file_id=%s, error=%s", file_id, e)
continue
prepared.append(
{
"file_id": file.id,
"name": file.name,
"size": file.size,
"local_path": local_path,
}
)
return prepared
def _save_downloads(self, download_dir: str, parent_id: str) -> list[dict[str, Any]]:
downloaded_files: list[dict[str, Any]] = []
exists, folder = FileService.get_by_id(parent_id)
if not exists or folder.type != FileType.FOLDER.value:
raise ValueError(f"RAGFlow target folder does not exist or is not a folder: {parent_id}")
tenant_id = self._canvas.get_tenant_id()
storage_put = settings.STORAGE_IMPL.put
storage_rm = getattr(settings.STORAGE_IMPL, "rm", None)
insert_file = FileService.insert
for path in Path(download_dir).rglob("*"):
if not path.is_file():
continue
try:
if path.stat().st_size <= 0:
continue
blob = path.read_bytes()
except OSError as e:
logging.warning("Browser failed to read downloaded file. path=%s, error=%s", path, e)
continue
if not blob:
continue
display_name = ""
blob_stored = False
try:
display_name = duplicate_name(FileService.query, name=path.name, parent_id=parent_id)
storage_put(parent_id, display_name, blob)
blob_stored = True
file_data = {
"id": get_uuid(),
"parent_id": parent_id,
"tenant_id": tenant_id,
"created_by": tenant_id,
"type": filename_type(display_name),
"name": display_name,
"location": display_name,
"size": len(blob),
}
inserted = insert_file(file_data)
downloaded_files.append(
{
"file_id": inserted.id,
"name": inserted.name,
"size": inserted.size,
"parent_id": inserted.parent_id,
}
)
except Exception as e:
if blob_stored and callable(storage_rm):
try:
storage_rm(parent_id, display_name)
except Exception as rollback_err:
logging.warning(
"Browser rollback stored download failed. path=%s, parent_id=%s, display_name=%s, error=%s",
path,
parent_id,
display_name,
rollback_err,
)
logging.error(
"Browser failed to save download. path=%s, tenant_id=%s, parent_id=%s, display_name=%s, error=%s",
path,
tenant_id,
parent_id,
display_name,
e,
)
continue
return downloaded_files
@staticmethod
def _extract_history_text(history: Any) -> str:
if history is None:
return ""
def pick_final_result(value: Any) -> str:
if value is None:
return ""
if isinstance(value, str):
return value.strip()
if isinstance(value, (int, float, bool)):
return str(value)
return ""
# Only trust browser-use's explicit final_result API/property.
final_result_fn = getattr(history, "final_result", None)
if callable(final_result_fn):
try:
final_result_value = final_result_fn()
return pick_final_result(final_result_value)
except Exception:
return ""
return pick_final_result(final_result_fn)
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 20 * 60)))
def _invoke(self, **kwargs):
profile_dir = None
persist_session = self._should_persist_session()
try:
self._prepare_input_values()
user_prompt = self._resolve_text(kwargs.get("prompts", self._param.prompts))
with tempfile.TemporaryDirectory(prefix="browser_use_upload_") as upload_dir, tempfile.TemporaryDirectory(prefix="browser_use_download_") as download_dir:
uploaded_files = self._prepare_upload_files(upload_dir)
upload_lines = [f"- file_id={item['file_id']}, name={item['name']}, local_path={item['local_path']}" for item in uploaded_files]
task_text = user_prompt
if upload_lines:
task_text += "\n\nYou can upload files from these local paths when operating web pages:\n" + "\n".join(upload_lines)
upload_local_paths = [item.get("local_path", "") for item in uploaded_files if item.get("local_path")]
if persist_session:
profile_dir = self._resolve_persistent_profile_dir()
os.makedirs(profile_dir, exist_ok=True)
else:
try:
profile_dir = tempfile.mkdtemp(prefix="browser_use_profile_")
except OSError:
profile_dir = None
history = asyncio.run(self._run_browser_use_async(task_text, download_dir, upload_local_paths, profile_dir))
target_dir_id = FileService.get_root_folder(self._canvas.get_tenant_id())["id"]
downloaded_files = self._save_downloads(download_dir, target_dir_id)
self.set_output("content", self._extract_history_text(history))
self.set_output("downloaded_files", downloaded_files)
return self.output()
except Exception as e:
logging.exception("Browser invoke failed")
self.set_output("_ERROR", str(e))
return self.output()
finally:
if profile_dir and not persist_session:
shutil.rmtree(profile_dir, ignore_errors=True)
def thoughts(self) -> str:
return "Planning and executing browser actions..."

View File

@@ -21,17 +21,17 @@ from abc import ABC
from common.constants import LLMType
from api.db.services.llm_service import LLMBundle
from api.db.joint_services.tenant_model_service import get_model_config_by_type_and_name
from api.db.joint_services.tenant_model_service import resolve_model_config
from agent.component.llm import LLMParam, LLM
from common.connection_utils import timeout
from rag.llm.chat_model import ERROR_PREFIX
class CategorizeParam(LLMParam):
"""
Define the categorize component parameters.
"""
def __init__(self):
super().__init__()
self.category_description = {}
@@ -40,7 +40,8 @@ class CategorizeParam(LLMParam):
self.update_prompt()
def check(self):
self.check_positive_integer(self.message_history_window_size, "[Categorize] Message window size > 0")
if not isinstance(self.message_history_window_size, int) or self.message_history_window_size < 0:
raise ValueError("[Categorize] Message window size cannot be negative")
self.check_empty(self.category_description, "[Categorize] Category examples")
for k, v in self.category_description.items():
if not k:
@@ -49,12 +50,7 @@ class CategorizeParam(LLMParam):
raise ValueError(f"[Categorize] 'To' of category {k} can not be empty!")
def get_input_form(self) -> dict[str, dict]:
return {
"query": {
"type": "line",
"name": "Query"
}
}
return {"query": {"type": "line", "name": "Query"}}
def update_prompt(self):
cate_lines = []
@@ -62,13 +58,12 @@ class CategorizeParam(LLMParam):
for line in desc.get("examples", []):
if not line:
continue
cate_lines.append("USER: \"" + re.sub(r"\n", " ", line, flags=re.DOTALL) + "\""+c)
cate_lines.append('USER: "' + re.sub(r"\n", " ", line, flags=re.DOTALL) + '"' + c)
descriptions = []
for c, desc in self.category_description.items():
if desc.get("description"):
descriptions.append(
"\n------\nCategory: {}\nDescription: {}".format(c, desc["description"]))
descriptions.append("\n------\nCategory: {}\nDescription: {}".format(c, desc["description"]))
self.sys_prompt = """
You are an advanced classification system that categorizes user questions into specific types. Analyze the input question and classify it into ONE of the following categories:
@@ -83,10 +78,7 @@ Here's description of each category:
- Return only the category name without explanations
- Use "Other" only when no other category fits
""".format(
"\n - ".join(list(self.category_description.keys())),
"\n".join(descriptions)
)
""".format("\n - ".join(list(self.category_description.keys())), "\n".join(descriptions))
if cate_lines:
self.sys_prompt += """
@@ -105,7 +97,7 @@ class Categorize(LLM, ABC):
logging.warning(f"[Categorize] input element not detected for query key: {query_key}")
return elements
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10 * 60)))
async def _invoke_async(self, **kwargs):
if self.check_if_canceled("Categorize processing"):
return
@@ -123,13 +115,13 @@ class Categorize(LLM, ABC):
msg[-1]["content"] = query_value
self.set_input_value(query_key, msg[-1]["content"])
self._param.update_prompt()
chat_model_config = get_model_config_by_type_and_name(self._canvas.get_tenant_id(), LLMType.CHAT, self._param.llm_id)
chat_model_config = resolve_model_config(self._canvas.get_tenant_id(), LLMType.CHAT, self._param.llm_id)
chat_mdl = LLMBundle(self._canvas.get_tenant_id(), chat_model_config)
user_prompt = """
---- Real Data ----
{}
""".format(" | ".join(["{}: \"{}\"".format(c["role"].upper(), re.sub(r"\n", "", c["content"], flags=re.DOTALL)) for c in msg]))
""".format(" | ".join(['{}: "{}"'.format(c["role"].upper(), re.sub(r"\n", "", c["content"], flags=re.DOTALL)) for c in msg]))
if self.check_if_canceled("Categorize processing"):
return
@@ -157,7 +149,7 @@ class Categorize(LLM, ABC):
self.set_output("category_name", max_category)
self.set_output("_next", cpn_ids)
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10 * 60)))
def _invoke(self, **kwargs):
return asyncio.run(self._invoke_async(**kwargs))

View File

@@ -19,55 +19,47 @@ import os
from agent.component.base import ComponentBase, ComponentParamBase
from api.utils.api_utils import timeout
class DataOperationsParam(ComponentParamBase):
"""
Define the Data Operations component parameters.
"""
def __init__(self):
super().__init__()
self.query = []
self.operations = "literal_eval"
self.select_keys = []
self.filter_values=[]
self.updates=[]
self.remove_keys=[]
self.rename_keys=[]
self.outputs = {
"result": {
"value": [],
"type": "Array of Object"
}
}
def check(self):
self.check_valid_value(self.operations, "Support operations", ["select_keys", "literal_eval","combine","filter_values","append_or_update","remove_keys","rename_keys"])
self.filter_values = []
self.updates = []
self.remove_keys = []
self.rename_keys = []
self.outputs = {"result": {"value": [], "type": "Array of Object"}}
class DataOperations(ComponentBase,ABC):
def check(self):
self.check_valid_value(self.operations, "Support operations", ["select_keys", "literal_eval", "combine", "filter_values", "append_or_update", "remove_keys", "rename_keys"])
class DataOperations(ComponentBase, ABC):
component_name = "DataOperations"
def get_input_form(self) -> dict[str, dict]:
return {
k: {"name": o.get("name", ""), "type": "line"}
for input_item in (self._param.query or [])
for k, o in self.get_input_elements_from_text(input_item).items()
}
return {k: {"name": o.get("name", ""), "type": "line"} for input_item in (self._param.query or []) for k, o in self.get_input_elements_from_text(input_item).items()}
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10 * 60)))
def _invoke(self, **kwargs):
self.input_objects=[]
self.input_objects = []
inputs = getattr(self._param, "query", None)
if not isinstance(inputs, (list, tuple)):
inputs = [inputs]
for input_ref in inputs:
input_object=self._canvas.get_variable_value(input_ref)
input_object = self._canvas.get_variable_value(input_ref)
self.set_input_value(input_ref, input_object)
if input_object is None:
continue
if isinstance(input_object,dict):
if isinstance(input_object, dict):
self.input_objects.append(input_object)
elif isinstance(input_object,list):
elif isinstance(input_object, list):
self.input_objects.extend(x for x in input_object if isinstance(x, dict))
else:
continue
@@ -85,13 +77,12 @@ class DataOperations(ComponentBase,ABC):
self._remove_keys()
else:
self._rename_keys()
def _select_keys(self):
filter_criteria: list[str] = self._param.select_keys
results = [{key: value for key, value in data_dict.items() if key in filter_criteria} for data_dict in self.input_objects]
self.set_output("result", results)
def _recursive_eval(self, data):
if isinstance(data, dict):
return {k: self._recursive_eval(v) for k, v in data.items()}
@@ -99,23 +90,19 @@ class DataOperations(ComponentBase,ABC):
return [self._recursive_eval(item) for item in data]
if isinstance(data, str):
try:
if (
data.strip().startswith(("{", "[", "(", "'", '"'))
or data.strip().lower() in ("true", "false", "none")
or data.strip().replace(".", "").isdigit()
):
if data.strip().startswith(("{", "[", "(", "'", '"')) or data.strip().lower() in ("true", "false", "none") or data.strip().replace(".", "").isdigit():
return ast.literal_eval(data)
except (ValueError, SyntaxError, TypeError, MemoryError):
return data
else:
return data
return data
def _literal_eval(self):
self.set_output("result", self._recursive_eval(self.input_objects))
def _combine(self):
result={}
result = {}
for obj in self.input_objects:
for key, value in obj.items():
if key not in result:
@@ -126,15 +113,13 @@ class DataOperations(ComponentBase,ABC):
else:
result[key].append(value)
else:
result[key] = (
[result[key], value] if not isinstance(value, list) else [result[key], *value]
)
result[key] = [result[key], value] if not isinstance(value, list) else [result[key], *value]
self.set_output("result", result)
def norm(self,v):
def norm(self, v):
s = "" if v is None else str(v)
return s
def match_rule(self, obj, rule):
key = rule.get("key")
op = (rule.get("operator") or "equals").lower()
@@ -155,10 +140,10 @@ class DataOperations(ComponentBase,ABC):
if op == "end with":
return v.endswith(target)
return False
def _filter_values(self):
results=[]
rules = (getattr(self._param, "filter_values", None) or [])
results = []
rules = getattr(self._param, "filter_values", None) or []
for obj in self.input_objects:
if not rules:
results.append(obj)
@@ -166,11 +151,10 @@ class DataOperations(ComponentBase,ABC):
if all(self.match_rule(obj, r) for r in rules):
results.append(obj)
self.set_output("result", results)
def _append_or_update(self):
results=[]
updates = getattr(self._param, "updates", []) or []
results = []
updates = getattr(self._param, "updates", []) or []
for obj in self.input_objects:
new_obj = dict(obj)
for item in updates:
@@ -187,7 +171,7 @@ class DataOperations(ComponentBase,ABC):
results = []
remove_keys = getattr(self._param, "remove_keys", []) or []
for obj in (self.input_objects or []):
for obj in self.input_objects or []:
new_obj = dict(obj)
for k in remove_keys:
if not isinstance(k, str):
@@ -200,7 +184,7 @@ class DataOperations(ComponentBase,ABC):
results = []
rename_pairs = getattr(self._param, "rename_keys", []) or []
for obj in (self.input_objects or []):
for obj in self.input_objects or []:
new_obj = dict(obj)
for pair in rename_pairs:
if not isinstance(pair, dict):

View File

@@ -13,6 +13,7 @@ from xml.sax.saxutils import escape
from agent.component.base import ComponentParamBase
from api.utils.api_utils import timeout
from api.utils.file_response import agent_attachment_preview_path
from common import settings
from common.misc_utils import get_uuid
from .message import Message
@@ -52,6 +53,10 @@ class DocGeneratorParam(ComponentParamBase):
self.include_download_info_in_content = False
self.font_size = 12
self.outputs = {
"doc_id": {"value": "", "type": "string"},
"filename": {"value": "", "type": "string"},
"mime_type": {"value": "", "type": "string"},
"size": {"value": 0, "type": "number"},
"download": {"value": "", "type": "string"},
}
@@ -132,8 +137,13 @@ class DocGenerator(Message, ABC):
"mime_type": mime_type,
"size": file_size,
"base64": file_base64,
"preview_url": agent_attachment_preview_path(doc_id, ext=output_format, mime_type=mime_type),
"include_download_info_in_content": self._param.include_download_info_in_content,
}
self.set_output("doc_id", doc_id)
self.set_output("filename", filename)
self.set_output("mime_type", mime_type)
self.set_output("size", file_size)
self.set_output("download", json.dumps(download_info))
return download_info
@@ -155,6 +165,7 @@ class DocGenerator(Message, ABC):
logging.info("Starting document generation, content length: %s chars", len(content))
if content:
def _replace_variable(match_obj: re.Match[str]) -> str:
match = match_obj.group(1)
try:
@@ -178,7 +189,7 @@ class DocGenerator(Message, ABC):
flags=re.DOTALL,
)
return content
return self._strip_thinking(content)
def _get_output_directory(self) -> str:
os.makedirs(self._default_output_directory, exist_ok=True)

View File

@@ -39,71 +39,52 @@ class ExcelProcessorParam(ComponentParamBase):
"""
Define the ExcelProcessor component parameters.
"""
def __init__(self):
super().__init__()
# Input configuration
self.input_files = [] # Variable references to uploaded files
self.operation = "read" # read, merge, transform, output
# Processing options
self.sheet_selection = "all" # all, first, or comma-separated sheet names
self.merge_strategy = "concat" # concat, join
self.join_on = "" # Column name for join operations
# Transform options (for LLM-guided transformations)
self.transform_instructions = ""
self.transform_data = "" # Variable reference to transformation data
# Output options
self.output_format = "xlsx" # xlsx, csv
self.output_filename = "output"
# Component outputs
self.outputs = {
"data": {
"type": "object",
"value": {}
},
"summary": {
"type": "str",
"value": ""
},
"markdown": {
"type": "str",
"value": ""
}
}
self.outputs = {"data": {"type": "object", "value": {}}, "summary": {"type": "str", "value": ""}, "markdown": {"type": "str", "value": ""}}
def check(self):
self.check_valid_value(
self.operation,
"[ExcelProcessor] Operation",
["read", "merge", "transform", "output"]
)
self.check_valid_value(
self.output_format,
"[ExcelProcessor] Output format",
["xlsx", "csv"]
)
self.check_valid_value(self.operation, "[ExcelProcessor] Operation", ["read", "merge", "transform", "output"])
self.check_valid_value(self.output_format, "[ExcelProcessor] Output format", ["xlsx", "csv"])
return True
class ExcelProcessor(ComponentBase, ABC):
"""
Excel processing component for RAGFlow agents.
Operations:
- read: Parse Excel files into structured data
- merge: Combine multiple Excel files
- transform: Apply data transformations based on instructions
- output: Generate Excel file output
"""
component_name = "ExcelProcessor"
def get_input_form(self) -> dict[str, dict]:
"""Define input form for the component."""
res = {}
for ref in (self._param.input_files or []):
for ref in self._param.input_files or []:
for k, o in self.get_input_elements_from_text(ref).items():
res[k] = {"name": o.get("name", ""), "type": "file"}
if self._param.transform_data:
@@ -111,13 +92,13 @@ class ExcelProcessor(ComponentBase, ABC):
res[k] = {"name": o.get("name", ""), "type": "object"}
return res
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10 * 60)))
def _invoke(self, **kwargs):
if self.check_if_canceled("ExcelProcessor processing"):
return
operation = self._param.operation.lower()
if operation == "read":
self._read_excels()
elif operation == "merge":
@@ -137,7 +118,7 @@ class ExcelProcessor(ComponentBase, ABC):
value = self._canvas.get_variable_value(file_ref)
if value is None:
return None, None
# Handle different value formats
if isinstance(value, dict):
# File reference from Begin/UserFillUp component
@@ -154,12 +135,13 @@ class ExcelProcessor(ComponentBase, ABC):
# Could be base64 encoded or a path
if value.startswith("data:"):
import base64
# Extract base64 content
_, encoded = value.split(",", 1)
return base64.b64decode(encoded), "uploaded.xlsx"
return None, None
def _get_file_content_from_list(self, item) -> tuple[bytes, str]:
"""Extract file content from a list item."""
if isinstance(item, dict):
@@ -170,15 +152,15 @@ class ExcelProcessor(ComponentBase, ABC):
"""Parse Excel content into a dictionary of DataFrames (one per sheet)."""
try:
excel_file = BytesIO(content)
if filename.lower().endswith(".csv"):
df = pd.read_csv(excel_file)
return {"Sheet1": df}
else:
# Read all sheets
xlsx = pd.ExcelFile(excel_file, engine='openpyxl')
xlsx = pd.ExcelFile(excel_file, engine="openpyxl")
sheet_selection = self._param.sheet_selection
if sheet_selection == "all":
sheets_to_read = xlsx.sheet_names
elif sheet_selection == "first":
@@ -187,12 +169,12 @@ class ExcelProcessor(ComponentBase, ABC):
# Comma-separated sheet names
requested = [s.strip() for s in sheet_selection.split(",")]
sheets_to_read = [s for s in requested if s in xlsx.sheet_names]
dfs = {}
for sheet in sheets_to_read:
dfs[sheet] = pd.read_excel(xlsx, sheet_name=sheet)
return dfs
except Exception as e:
logging.error(f"Error parsing Excel file {filename}: {e}")
return {}
@@ -202,36 +184,36 @@ class ExcelProcessor(ComponentBase, ABC):
all_data = {}
summaries = []
markdown_parts = []
for file_ref in (self._param.input_files or []):
for file_ref in self._param.input_files or []:
if self.check_if_canceled("ExcelProcessor reading"):
return
# Get variable value
value = self._canvas.get_variable_value(file_ref)
self.set_input_value(file_ref, str(value)[:200] if value else "")
if value is None:
continue
# Handle file content
content, filename = self._get_file_content(file_ref)
if content is None:
continue
# Parse Excel
dfs = self._parse_excel_to_dataframes(content, filename)
for sheet_name, df in dfs.items():
key = f"{filename}_{sheet_name}" if len(dfs) > 1 else filename
all_data[key] = df.to_dict(orient="records")
# Build summary
summaries.append(f"**{key}**: {len(df)} rows, {len(df.columns)} columns ({', '.join(df.columns.tolist()[:5])}{'...' if len(df.columns) > 5 else ''})")
# Build markdown table
markdown_parts.append(f"### {key}\n\n{df.head(10).to_markdown(index=False)}\n")
# Set outputs
self.set_output("data", all_data)
self.set_output("summary", "\n".join(summaries) if summaries else "No Excel files found")
@@ -240,29 +222,29 @@ class ExcelProcessor(ComponentBase, ABC):
def _merge_excels(self):
"""Merge multiple Excel files/sheets into one."""
all_dfs = []
for file_ref in (self._param.input_files or []):
for file_ref in self._param.input_files or []:
if self.check_if_canceled("ExcelProcessor merging"):
return
value = self._canvas.get_variable_value(file_ref)
self.set_input_value(file_ref, str(value)[:200] if value else "")
if value is None:
continue
content, filename = self._get_file_content(file_ref)
if content is None:
continue
dfs = self._parse_excel_to_dataframes(content, filename)
all_dfs.extend(dfs.values())
if not all_dfs:
self.set_output("data", {})
self.set_output("summary", "No data to merge")
return
# Merge strategy
if self._param.merge_strategy == "concat":
merged_df = pd.concat(all_dfs, ignore_index=True)
@@ -273,7 +255,7 @@ class ExcelProcessor(ComponentBase, ABC):
merged_df = merged_df.merge(df, on=self._param.join_on, how="outer")
else:
merged_df = pd.concat(all_dfs, ignore_index=True)
self.set_output("data", {"merged": merged_df.to_dict(orient="records")})
self.set_output("summary", f"Merged {len(all_dfs)} sources into {len(merged_df)} rows, {len(merged_df.columns)} columns")
self.set_output("markdown", merged_df.head(20).to_markdown(index=False))
@@ -285,14 +267,14 @@ class ExcelProcessor(ComponentBase, ABC):
if not transform_ref:
self.set_output("summary", "No transform data reference provided")
return
data = self._canvas.get_variable_value(transform_ref)
self.set_input_value(transform_ref, str(data)[:300] if data else "")
if data is None:
self.set_output("summary", "Transform data is empty")
return
# Convert to DataFrame
if isinstance(data, dict):
# Could be {"sheet": [rows]} format
@@ -315,7 +297,7 @@ class ExcelProcessor(ComponentBase, ABC):
else:
self.set_output("data", {"raw": str(data)})
self.set_output("markdown", str(data))
self.set_output("summary", "Transformed data ready for processing")
def _output_excel(self):
@@ -325,14 +307,14 @@ class ExcelProcessor(ComponentBase, ABC):
if not transform_ref:
self.set_output("summary", "No data reference for output")
return
data = self._canvas.get_variable_value(transform_ref)
self.set_input_value(transform_ref, str(data)[:300] if data else "")
if data is None:
self.set_output("summary", "No data to output")
return
try:
# Prepare DataFrames
if isinstance(data, dict):
@@ -346,10 +328,10 @@ class ExcelProcessor(ComponentBase, ABC):
else:
self.set_output("summary", "Invalid data format for Excel output")
return
# Generate output
doc_id = get_uuid()
if self._param.output_format == "csv":
# For CSV, only output first sheet
first_df = list(dfs.values())[0]
@@ -358,7 +340,7 @@ class ExcelProcessor(ComponentBase, ABC):
else:
# Excel output
excel_io = BytesIO()
with pd.ExcelWriter(excel_io, engine='openpyxl') as writer:
with pd.ExcelWriter(excel_io, engine="openpyxl") as writer:
for sheet_name, df in dfs.items():
# Sanitize sheet name (max 31 chars, no special chars)
safe_name = sheet_name[:31].replace("/", "_").replace("\\", "_")
@@ -366,23 +348,19 @@ class ExcelProcessor(ComponentBase, ABC):
excel_io.seek(0)
binary_content = excel_io.read()
filename = f"{self._param.output_filename}.xlsx"
# Store file
settings.STORAGE_IMPL.put(self._canvas._tenant_id, doc_id, binary_content)
# Set attachment output
self.set_output("attachment", {
"doc_id": doc_id,
"format": self._param.output_format,
"file_name": filename
})
self.set_output("attachment", {"doc_id": doc_id, "format": self._param.output_format, "file_name": filename})
total_rows = sum(len(df) for df in dfs.values())
self.set_output("summary", f"Generated {filename} with {len(dfs)} sheet(s), {total_rows} total rows")
self.set_output("data", {k: v.to_dict(orient="records") for k, v in dfs.items()})
logging.info(f"ExcelProcessor: Generated {filename} as {doc_id}")
except Exception as e:
logging.error(f"ExcelProcessor output error: {e}")
self.set_output("summary", f"Error generating output: {str(e)}")

View File

@@ -29,4 +29,4 @@ class ExitLoop(ComponentBase, ABC):
pass
def thoughts(self) -> str:
return ""
return ""

View File

@@ -21,8 +21,10 @@ from agent.component.base import ComponentParamBase, ComponentBase
from api.db.services.file_service import FileService
class UserFillUpParam(ComponentParamBase):
_INITIAL_USER_INPUT_CONSUMED_KEY = "sys.__initial_user_input_consumed__"
class UserFillUpParam(ComponentParamBase):
def __init__(self):
super().__init__()
self.enable_tips = True
@@ -36,6 +38,54 @@ class UserFillUpParam(ComponentParamBase):
class UserFillUp(ComponentBase):
component_name = "UserFillUp"
def _merge_runtime_inputs(self, runtime_inputs):
if runtime_inputs:
return runtime_inputs
fields = self.get_input_elements()
if not fields:
return {}
if self._canvas.globals.get(_INITIAL_USER_INPUT_CONSUMED_KEY):
return {}
query = self._canvas.globals.get("sys.query")
if query is None or query == "":
return {}
if isinstance(query, dict):
matched = {key: value if isinstance(value, dict) else {"value": value} for key, value in query.items() if key in fields}
if matched:
self._canvas.globals[_INITIAL_USER_INPUT_CONSUMED_KEY] = True
return matched
if len(fields) == 1:
field_name = next(iter(fields))
self._canvas.globals[_INITIAL_USER_INPUT_CONSUMED_KEY] = True
return {field_name: {"value": query}}
return {}
def _resolve_input_value(self, value, layout_recognize):
if isinstance(value, dict) and value.get("type", "").lower().find("file") >= 0:
if value.get("optional") and value.get("value", None) is None:
return None
file_value = value["value"]
files = file_value if isinstance(file_value, list) else [file_value]
return FileService.get_files(files, layout_recognize=layout_recognize)
if isinstance(value, dict):
raw = value.get("value")
if value.get("type") == "object" and isinstance(raw, str) and raw.strip():
try:
return json.loads(raw)
except Exception:
return raw
return raw
return value
def _invoke(self, **kwargs):
if self.check_if_canceled("UserFillUp processing"):
return
@@ -59,24 +109,37 @@ class UserFillUp(ComponentBase):
ans = v
if not ans:
ans = ""
content = re.sub(r"\{%s\}"%k, ans, content)
content = re.sub(r"\{%s\}" % k, ans, content)
self.set_output("tips", content)
layout_recognize = self._param.layout_recognize or None
for k, v in kwargs.get("inputs", {}).items():
merged_inputs = self._merge_runtime_inputs(kwargs.get("inputs", {}))
if not merged_inputs:
# No fresh user answer was supplied on this entry. Clear any values
# retained from a previous response so the canvas wait-check treats
# the form as unsatisfied and pauses for input again. Without this,
# an Await Response node inside a Loop would only pause on the first
# iteration and silently reuse the earlier answer afterwards.
self._clear_form_values()
for k, v in merged_inputs.items():
if self.check_if_canceled("UserFillUp processing"):
return
if isinstance(v, dict) and v.get("type", "").lower().find("file") >= 0:
if v.get("optional") and v.get("value", None) is None:
v = None
else:
file_value = v["value"]
# Support both single file (backward compatibility) and multiple files
files = file_value if isinstance(file_value, list) else [file_value]
v = FileService.get_files(files, layout_recognize=layout_recognize)
else:
v = v.get("value")
self.set_output(k, v)
resolved = self._resolve_input_value(v, layout_recognize)
self.set_output(k, resolved)
self.set_input_value(k, resolved)
def _clear_form_values(self):
for field in self.get_input_elements().values():
if not isinstance(field, dict):
continue
field_type = str(field.get("type", "")).lower()
# An optional file input is already treated as satisfied when empty
# (see Canvas._is_input_field_satisfied), so clearing it would not
# force a re-prompt and would only drop a previously uploaded file.
# Leave it untouched to avoid unexpected data loss.
if "file" in field_type and field.get("optional"):
continue
field["value"] = None
def thoughts(self) -> str:
return "Waiting for your input..."

View File

@@ -20,11 +20,13 @@ import re
import time
from abc import ABC
from functools import partial
from urllib.parse import urlparse
import requests
from agent.component.base import ComponentBase, ComponentParamBase
from common.connection_utils import timeout
from common.ssrf_guard import assert_url_is_safe, pin_dns
from deepdoc.parser import HtmlParser
@@ -56,6 +58,11 @@ class Invoke(ComponentBase, ABC):
component_name = "Invoke"
header_variable_ref_patt = r"\{([a-zA-Z_][a-zA-Z0-9_.@-]*)\}"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._pinned_hostname: str | None = None
self._pinned_ip: str | None = None
@staticmethod
def _coerce_json_arg_if_possible(key, value):
raw_value = value
@@ -169,6 +176,9 @@ class Invoke(ComponentBase, ABC):
url = self._resolve_template_text(self._param.url.strip(), kwargs)
if not url.startswith(("http://", "https://")):
url = "http://" + url
hostname, ip = assert_url_is_safe(url)
self._pinned_hostname = hostname
self._pinned_ip = ip
return url
def _build_headers(self, kwargs: dict) -> dict:
@@ -181,8 +191,24 @@ class Invoke(ComponentBase, ABC):
return {key: self._resolve_header_text(value, kwargs) if isinstance(value, str) else value for key, value in headers.items()}
@staticmethod
def _ssrf_log_target(url: str) -> str:
parsed = urlparse(url)
if not parsed.scheme or not parsed.hostname:
return "invalid-url"
return f"{parsed.scheme}://{parsed.hostname}"
def _normalize_proxy_url(self) -> str | None:
proxy = (self._param.proxy or "").strip()
if not re.sub(r"https?:?/?/?", "", proxy):
return None
if not proxy.startswith(("http://", "https://")):
proxy = "http://" + proxy
return proxy
def _build_proxies(self) -> dict | None:
if not re.sub(r"https?:?/?/?", "", self._param.proxy):
proxy_url = self._normalize_proxy_url()
if not proxy_url:
return None
return {"http": self._param.proxy, "https": self._param.proxy}
@@ -194,6 +220,7 @@ class Invoke(ComponentBase, ABC):
"headers": headers,
"proxies": proxies,
"timeout": self._param.timeout,
"allow_redirects": False,
}
# GET sends query params; POST/PUT send either JSON or form data based on datatype.
@@ -219,9 +246,22 @@ class Invoke(ComponentBase, ABC):
return
args = self._build_request_args(kwargs)
url = self._build_url(kwargs)
headers = self._build_headers(kwargs)
proxies = self._build_proxies()
proxy_hostname = proxy_ip = None
if proxies:
proxy_url = self._normalize_proxy_url()
try:
proxy_hostname, proxy_ip = assert_url_is_safe(proxy_url)
except ValueError as exc:
logging.warning(
"Invoke SSRF guard blocked proxy=%s: %s",
self._ssrf_log_target(proxy_url),
exc,
)
self.set_output("_ERROR", "URL not valid")
return "Http request error: URL not valid"
last_error = None
for _ in range(self._param.max_retries + 1):
@@ -229,10 +269,26 @@ class Invoke(ComponentBase, ABC):
return
try:
response = self._send_request(url, args, headers, proxies)
url = self._build_url(kwargs)
if not self._pinned_hostname or not self._pinned_ip:
raise ValueError("Invoke URL was not validated before request.")
with pin_dns(self._pinned_hostname, self._pinned_ip):
if proxy_hostname and proxy_ip:
with pin_dns(proxy_hostname, proxy_ip):
response = self._send_request(url, args, headers, proxies)
else:
response = self._send_request(url, args, headers, proxies)
result = self._format_response(response)
self.set_output("result", result)
return result
except ValueError as e:
logging.warning(
"Invoke SSRF guard blocked url=%s: %s",
self._ssrf_log_target(locals().get("url", self._param.url)),
e,
)
self.set_output("_ERROR", "URL not valid")
return "Http request error: URL not valid"
except Exception as e:
if self.check_if_canceled("Invoke processing"):
return

View File

@@ -24,6 +24,7 @@ class VariableModel(BaseModel):
model_config = ConfigDict(extra="forbid")
"""
class IterationParam(ComponentParamBase):
"""
Define the Iteration component parameters.
@@ -32,15 +33,10 @@ class IterationParam(ComponentParamBase):
def __init__(self):
super().__init__()
self.items_ref = ""
self.variable={}
self.variable = {}
def get_input_form(self) -> dict[str, dict]:
return {
"items": {
"type": "json",
"name": "Items"
}
}
return {"items": {"type": "json", "name": "Items"}}
def check(self):
return True
@@ -62,10 +58,7 @@ class Iteration(ComponentBase, ABC):
arr = self._canvas.get_variable_value(self._param.items_ref)
if not isinstance(arr, list):
self.set_output("_ERROR", self._param.items_ref + " must be an array, but its type is "+str(type(arr)))
self.set_output("_ERROR", self._param.items_ref + " must be an array, but its type is " + str(type(arr)))
def thoughts(self) -> str:
return "Need to process {} items.".format(len(self._canvas.get_variable_value(self._param.items_ref)))

View File

@@ -14,6 +14,7 @@
# limitations under the License.
#
from abc import ABC
from agent.component.base import ComponentBase, ComponentParamBase
@@ -21,6 +22,7 @@ class IterationItemParam(ComponentParamBase):
"""
Define the IterationItem component parameters.
"""
def check(self):
return True
@@ -40,7 +42,7 @@ class IterationItem(ComponentBase, ABC):
arr = self._canvas.get_variable_value(parent._param.items_ref)
if not isinstance(arr, list):
self._idx = -1
raise Exception(parent._param.items_ref + " must be an array, but its type is "+str(type(arr)))
raise Exception(parent._param.items_ref + " must be an array, but its type is " + str(type(arr)))
if self._idx > 0:
if self.check_if_canceled("IterationItem processing"):
@@ -79,7 +81,11 @@ class IterationItem(ComponentBase, ABC):
for k, o in p._param.outputs.items():
if "ref" not in o:
continue
_cid, var = o["ref"].split("@")
# Use maxsplit=1 so an `@` legitimately embedded in `var`
# (e.g. a user-defined output key that happens to contain
# '@') does not raise `ValueError: too many values to unpack`.
# `_cid` is system-generated and never contains '@'.
_cid, var = o["ref"].split("@", 1)
if _cid != cid:
continue
res = p.output(k)

View File

@@ -3,10 +3,12 @@ import os
from agent.component.base import ComponentBase, ComponentParamBase
from api.utils.api_utils import timeout
class ListOperationsParam(ComponentParamBase):
"""
Define the List Operations component parameters.
"""
def __init__(self):
super().__init__()
self.query = ""
@@ -14,27 +16,25 @@ class ListOperationsParam(ComponentParamBase):
self.n = 0
self.strict = False
self.sort_method = "asc"
self.filter = {
"operator": "=",
"value": ""
}
self.outputs = {
"result": {
"value": [],
"type": "Array of ?"
},
"first": {
"value": "",
"type": "?"
},
"last": {
"value": "",
"type": "?"
}
}
# Comma-separated list of map keys to sort by (primary,
# tiebreak, ...). Empty / unset falls back to the legacy
# full-hashable-key behaviour (sort by the lexicographically
# first field). Mirrors internal/agent/component/list_operations.go
# parseSortByFieldList + opSort's SortBy path.
self.sort_by = ""
self.filter = {"operator": "=", "value": ""}
self.outputs = {"result": {"value": [], "type": "Array of ?"}, "first": {"value": "", "type": "?"}, "last": {"value": "", "type": "?"}}
@staticmethod
def _normalize_operation_name(operation):
op = "" if operation is None else str(operation).strip()
if op.lower() == "topn":
return "head"
return op or "nth"
def check(self):
self.check_empty(self.query, "query")
self.operations = self._normalize_operation_name(self.operations)
self.check_valid_value(
self.operations,
"Support operations",
@@ -43,14 +43,14 @@ class ListOperationsParam(ComponentParamBase):
def get_input_form(self) -> dict[str, dict]:
return {}
class ListOperations(ComponentBase,ABC):
class ListOperations(ComponentBase, ABC):
component_name = "ListOperations"
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10 * 60)))
def _invoke(self, **kwargs):
self.input_objects=[]
self.input_objects = []
inputs = getattr(self._param, "query", None)
self.inputs = self._canvas.get_variable_value(inputs)
if not isinstance(self.inputs, list):
@@ -69,7 +69,6 @@ class ListOperations(ComponentBase,ABC):
elif self._param.operations == "drop_duplicates":
self._drop_duplicates()
def _coerce_n(self):
try:
return int(getattr(self._param, "n", 0))
@@ -85,12 +84,10 @@ class ListOperations(ComponentBase,ABC):
def _set_outputs(self, outputs):
self._param.outputs["result"]["value"] = outputs
self._param.outputs["first"]["value"] = outputs[0] if outputs else None
self._param.outputs["last"]["value"] = outputs[-1] if outputs else None
self._param.outputs["last"]["value"] = outputs[-1] if outputs else None
def _raise_strict_range_error(self, operation, n):
raise ValueError(
f"{operation} requires n to be within the valid range in strict mode, got {n}."
)
raise ValueError(f"{operation} requires n to be within the valid range in strict mode, got {n}.")
def _nth(self):
n = self._coerce_n()
@@ -146,9 +143,9 @@ class ListOperations(ComponentBase,ABC):
self._set_outputs(outputs)
def _filter(self):
self._set_outputs([i for i in self.inputs if self._eval(self._norm(i),self._param.filter["operator"],self._param.filter["value"])])
self._set_outputs([i for i in self.inputs if self._eval(self._norm(i), self._param.filter["operator"], self._param.filter["value"])])
def _norm(self,v):
def _norm(self, v):
s = "" if v is None else str(v)
return s
@@ -178,11 +175,20 @@ class ListOperations(ComponentBase,ABC):
first = items[0]
if isinstance(first, dict):
outputs = sorted(
items,
key=lambda x: self._hashable(x),
reverse=reverse,
)
sort_by_raw = getattr(self._param, "sort_by", "") or ""
sort_by = [k.strip() for k in sort_by_raw.split(",") if k.strip()]
if sort_by:
outputs = sorted(
items,
key=lambda x: tuple(x.get(k) for k in sort_by),
reverse=reverse,
)
else:
outputs = sorted(
items,
key=lambda x: self._hashable(x),
reverse=reverse,
)
else:
outputs = sorted(items, reverse=reverse)
@@ -199,7 +205,7 @@ class ListOperations(ComponentBase,ABC):
outs.append(item)
self._set_outputs(outs)
def _hashable(self,x):
def _hashable(self, x):
if isinstance(x, dict):
return tuple(sorted((k, self._hashable(v)) for k, v in x.items()))
if isinstance(x, (list, tuple)):

View File

@@ -23,9 +23,9 @@ from typing import Any, AsyncGenerator
import json_repair
from functools import partial
from common.constants import LLMType
from api.db.services.dialog_service import _stream_with_think_delta
from api.db.services.llm_service import LLMBundle
from api.db.services.tenant_llm_service import TenantLLMService
from api.db.joint_services.tenant_model_service import get_model_config_by_type_and_name
from api.db.joint_services.tenant_model_service import resolve_model_config, resolve_model_type
from agent.component.base import ComponentBase, ComponentParamBase
from common.connection_utils import timeout
from rag.prompts.generator import tool_call_summary, message_fit_in, citation_prompt, structured_output_prompt
@@ -61,6 +61,7 @@ class LLMParam(ComponentParamBase):
def gen_conf(self):
conf = {}
def get_attr(nm):
try:
return getattr(self, nm)
@@ -77,6 +78,8 @@ class LLMParam(ComponentParamBase):
conf["presence_penalty"] = float(self.presence_penalty)
if float(self.frequency_penalty) > 0 and get_attr("frequencyPenaltyEnabled"):
conf["frequency_penalty"] = float(self.frequency_penalty)
if hasattr(self, "thinking") and self.thinking and self.thinking != "default":
conf["thinking"] = self.thinking
return conf
@@ -85,19 +88,16 @@ class LLM(ComponentBase):
def __init__(self, canvas, component_id, param: ComponentParamBase):
super().__init__(canvas, component_id, param)
chat_model_config = get_model_config_by_type_and_name(self._canvas.get_tenant_id(), TenantLLMService.llm_id2llm_type(self._param.llm_id), self._param.llm_id)
self.chat_mdl = LLMBundle(self._canvas.get_tenant_id(), chat_model_config,
max_retries=self._param.max_retries,
retry_interval=self._param.delay_after_error)
model_types = resolve_model_type(self._canvas.get_tenant_id(), self._param.llm_id)
model_type = "chat" if "chat" in model_types else model_types[0]
chat_model_config = resolve_model_config(self._canvas.get_tenant_id(), model_type, self._param.llm_id)
self.chat_mdl = LLMBundle(self._canvas.get_tenant_id(), chat_model_config, max_retries=self._param.max_retries, retry_interval=self._param.delay_after_error)
self.imgs = []
def get_input_form(self) -> dict[str, dict]:
res = {}
for k, v in self.get_input_elements().items():
res[k] = {
"type": "line",
"name": v["name"]
}
res[k] = {"type": "line", "name": v["name"]}
return res
def get_input_elements(self) -> dict[str, Any]:
@@ -118,14 +118,41 @@ class LLM(ComponentBase):
def _sys_prompt_and_msg(self, msg, args):
if isinstance(self._param.prompts, str):
self._param.prompts = [{"role": "user", "content": self._param.prompts}]
history_size = len(msg)
for p in self._param.prompts:
if msg and msg[-1]["role"] == p["role"]:
continue
p = deepcopy(p)
p["content"] = self.string_format(p["content"], args)
msg.append(p)
formatted = deepcopy(p)
formatted["content"] = self.string_format(formatted["content"], args)
if len(msg) == history_size and msg and msg[-1]["role"] == formatted["role"]:
msg[-1] = formatted
else:
msg.append(formatted)
return msg, self.string_format(self._param.sys_prompt, args)
@staticmethod
def effective_context_length(max_length) -> int:
return max_length or 8192
@classmethod
def context_fit_budget(cls, max_length) -> int:
return int(cls.effective_context_length(max_length) * 0.97)
@staticmethod
def validate_fitted_messages(msg_fit: list[dict]) -> str | None:
if len(msg_fit) < 2:
return "**ERROR**: message_fit_in produced insufficient messages for LLM"
last = msg_fit[-1]
if last.get("role") != "user" or not str(last.get("content") or "").strip():
return "**ERROR**: LLM user message is empty after prompt fitting; check model max_tokens context setting"
return None
@classmethod
def fit_messages(cls, system_prompt: str, msg: list[dict], max_length) -> tuple[list[dict], str | None]:
_, msg_fit = message_fit_in(
[{"role": "system", "content": system_prompt}, *deepcopy(msg)],
cls.context_fit_budget(max_length),
)
return msg_fit, cls.validate_fitted_messages(msg_fit)
@staticmethod
def _extract_data_images(value) -> list[str]:
imgs = []
@@ -224,6 +251,42 @@ class LLM(ComponentBase):
return value
def _collect_sys_files(self) -> tuple[list[str], list[str]]:
files = self._canvas.globals.get("sys.files") or []
if not files:
logging.debug("[LLM] sys.files empty; skipping attachment injection")
return [], []
logging.info("[LLM] sys.files present: count=%d", len(files))
explicit = "{sys.files}" in (self._param.sys_prompt or "")
if not explicit and isinstance(self._param.prompts, list):
for p in self._param.prompts:
if isinstance(p, dict) and "{sys.files}" in (p.get("content") or ""):
explicit = True
break
if explicit:
logging.info("[LLM] prompt template references {sys.files}; skipping auto-injection (explicit=%s)", explicit)
return [], []
text_parts: list[str] = []
image_data_uris: list[str] = []
for f in files:
if not isinstance(f, str):
logging.debug("[LLM] skipping non-str sys.files entry: type=%s", type(f).__name__)
continue
if f.startswith("data:image/"):
image_data_uris.append(f)
else:
text_parts.append(f)
logging.info(
"[LLM] sys.files split: text_parts=%d image_data_uris=%d (explicit=%s)",
len(text_parts),
len(image_data_uris),
explicit,
)
return text_parts, image_data_uris
def _prepare_prompt_variables(self):
self.imgs = []
if self._param.visual_files_var:
@@ -246,14 +309,47 @@ class LLM(ComponentBase):
args[k] = str(args[k])
self.set_input_value(k, args[k])
self.imgs = self._uniq_images(self.imgs + extracted_imgs)
if self.imgs and TenantLLMService.llm_id2llm_type(self._param.llm_id) == LLMType.CHAT.value:
self.chat_mdl = LLMBundle(self._canvas.get_tenant_id(), LLMType.IMAGE2TEXT.value,
self._param.llm_id, max_retries=self._param.max_retries,
retry_interval=self._param.delay_after_error
)
sys_file_texts, sys_file_imgs = self._collect_sys_files()
prev_img_count = len(self.imgs) + len(extracted_imgs)
self.imgs = self._uniq_images(self.imgs + extracted_imgs + sys_file_imgs)
logging.debug(
"[LLM] imgs rebuilt: total=%d sys_files_added=%d unique_dropped=%d",
len(self.imgs),
len(sys_file_imgs),
max(0, prev_img_count + len(sys_file_imgs) - len(self.imgs)),
)
model_types = resolve_model_type(self._canvas.get_tenant_id(), self._param.llm_id)
if self.imgs and LLMType.VISION.value in model_types:
model_type = LLMType.VISION.value
elif LLMType.CHAT.value in model_types:
model_type = LLMType.CHAT.value
else:
model_type = model_types[0]
model_config = resolve_model_config(self._canvas.get_tenant_id(), model_type, self._param.llm_id)
if self.imgs:
self.chat_mdl = LLMBundle(self._canvas.get_tenant_id(), model_config, max_retries=self._param.max_retries, retry_interval=self._param.delay_after_error)
msg, sys_prompt = self._sys_prompt_and_msg(self._canvas.get_history(self._param.message_history_window_size)[:-1], args)
if sys_file_texts:
joined = "\n\n".join(sys_file_texts)
merged_idx = -1
for i in range(len(msg) - 1, -1, -1):
if msg[i].get("role") == "user":
msg[i]["content"] = (msg[i].get("content") or "") + "\n\n" + joined
merged_idx = i
break
else:
msg.append({"role": "user", "content": joined})
merged_idx = len(msg) - 1
logging.info(
"[LLM] sys.files text merged into msg: parts=%d total_chars=%d msg_index=%d action=%s",
len(sys_file_texts),
len(joined),
merged_idx,
"merged_into_existing_user" if merged_idx < len(msg) - 1 or msg[merged_idx].get("content", "") != joined else "appended_new_user",
)
user_defined_prompt, sys_prompt = self._extract_prompts(sys_prompt)
if self._param.cite and self._canvas.get_reference()["chunks"]:
sys_prompt += citation_prompt(user_defined_prompt)
@@ -263,11 +359,11 @@ class LLM(ComponentBase):
def _extract_prompts(self, sys_prompt):
pts = {}
for tag in ["TASK_ANALYSIS", "PLAN_GENERATION", "REFLECTION", "CONTEXT_SUMMARY", "CONTEXT_RANKING", "CITATION_GUIDELINES"]:
r = re.search(rf"<{tag}>(.*?)</{tag}>", sys_prompt, flags=re.DOTALL|re.IGNORECASE)
r = re.search(rf"<{tag}>(.*?)</{tag}>", sys_prompt, flags=re.DOTALL | re.IGNORECASE)
if not r:
continue
pts[tag.lower()] = r.group(1)
sys_prompt = re.sub(rf"<{tag}>(.*?)</{tag}>", "", sys_prompt, flags=re.DOTALL|re.IGNORECASE)
sys_prompt = re.sub(rf"<{tag}>(.*?)</{tag}>", "", sys_prompt, flags=re.DOTALL | re.IGNORECASE)
return pts, sys_prompt
async def _generate_async(self, msg: list[dict], **kwargs) -> str:
@@ -276,82 +372,33 @@ class LLM(ComponentBase):
return await self.chat_mdl.async_chat(msg[0]["content"], msg[1:], self._param.gen_conf(), images=self.imgs, **kwargs)
async def _generate_streamly(self, msg: list[dict], **kwargs) -> AsyncGenerator[str, None]:
async def delta_wrapper(txt_iter):
ans = ""
last_idx = 0
endswith_think = False
def delta(txt):
nonlocal ans, last_idx, endswith_think
delta_ans = txt[last_idx:]
ans = txt
if delta_ans.find("<think>") == 0:
last_idx += len("<think>")
return "<think>"
elif delta_ans.find("<think>") > 0:
delta_ans = txt[last_idx:last_idx + delta_ans.find("<think>")]
last_idx += delta_ans.find("<think>")
return delta_ans
elif delta_ans.endswith("</think>"):
endswith_think = True
elif endswith_think:
endswith_think = False
return "</think>"
last_idx = len(ans)
if ans.endswith("</think>"):
last_idx -= len("</think>")
return re.sub(r"(<think>|</think>)", "", delta_ans)
async for t in txt_iter:
yield delta(t)
if not self.imgs:
async for t in delta_wrapper(self.chat_mdl.async_chat_streamly(msg[0]["content"], msg[1:], self._param.gen_conf(), **kwargs)):
yield t
return
async for t in delta_wrapper(self.chat_mdl.async_chat_streamly(msg[0]["content"], msg[1:], self._param.gen_conf(), images=self.imgs, **kwargs)):
yield t
stream_kwargs = {"images": self.imgs} if self.imgs else {}
stream_kwargs.update(kwargs)
stream = self.chat_mdl.async_chat_streamly_delta(msg[0]["content"], msg[1:], self._param.gen_conf(), **stream_kwargs)
async for _, value, _ in _stream_with_think_delta(stream, min_tokens=0):
yield value
async def _stream_output_async(self, prompt, msg):
_, msg = message_fit_in([{"role": "system", "content": prompt}, *msg], int(self.chat_mdl.max_length * 0.97))
msg_fit, fit_error = self.fit_messages(prompt, msg, self.chat_mdl.max_length)
if fit_error:
logging.error("LLM streaming prompt fit error: %s", fit_error)
if self.get_exception_default_value():
fallback = self.get_exception_default_value()
self.set_output("content", fallback)
yield fallback
else:
self.set_output("_ERROR", fit_error)
return
answer = ""
last_idx = 0
endswith_think = False
def delta(txt):
nonlocal answer, last_idx, endswith_think
delta_ans = txt[last_idx:]
answer = txt
if delta_ans.find("<think>") == 0:
last_idx += len("<think>")
return "<think>"
elif delta_ans.find("<think>") > 0:
delta_ans = txt[last_idx:last_idx + delta_ans.find("<think>")]
last_idx += delta_ans.find("<think>")
return delta_ans
elif delta_ans.endswith("</think>"):
endswith_think = True
elif endswith_think:
endswith_think = False
return "</think>"
last_idx = len(answer)
if answer.endswith("</think>"):
last_idx -= len("</think>")
return re.sub(r"(<think>|</think>)", "", delta_ans)
stream_kwargs = {"images": self.imgs} if self.imgs else {}
async for ans in self.chat_mdl.async_chat_streamly(msg[0]["content"], msg[1:], self._param.gen_conf(), **stream_kwargs):
extra_chat_kwargs = self._get_chat_template_kwargs()
stream_kwargs.update(extra_chat_kwargs)
stream = self.chat_mdl.async_chat_streamly_delta(msg_fit[0]["content"], msg_fit[1:], self._param.gen_conf(), **stream_kwargs)
async for _, ans, _ in _stream_with_think_delta(stream, min_tokens=0):
if self.check_if_canceled("LLM streaming"):
return
if isinstance(ans, int):
continue
if ans.find("**ERROR**") >= 0:
if self.get_exception_default_value():
self.set_output("content", self.get_exception_default_value())
@@ -360,11 +407,12 @@ class LLM(ComponentBase):
self.set_output("_ERROR", ans)
return
yield delta(ans)
answer += ans
yield ans
self.set_output("content", answer)
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10 * 60)))
async def _invoke_async(self, **kwargs):
if self.check_if_canceled("LLM processing"):
return
@@ -375,6 +423,7 @@ class LLM(ComponentBase):
return re.sub(r"```\n*$", "", ans, flags=re.DOTALL)
prompt, msg, _ = self._prepare_prompt_variables()
extra_chat_kwargs = self._get_chat_template_kwargs()
error: str = ""
output_structure = None
try:
@@ -388,12 +437,13 @@ class LLM(ComponentBase):
if self.check_if_canceled("LLM processing"):
return
_, msg_fit = message_fit_in(
[{"role": "system", "content": prompt_with_schema}, *deepcopy(msg)],
int(self.chat_mdl.max_length * 0.97),
)
msg_fit, fit_error = self.fit_messages(prompt_with_schema, msg, self.chat_mdl.max_length)
if fit_error:
logging.error("LLM structured prompt fit error: %s", fit_error)
self.set_output("_ERROR", fit_error)
return
error = ""
ans = await self._generate_async(msg_fit)
ans = await self._generate_async(msg_fit, **extra_chat_kwargs)
msg_fit.pop(0)
if ans.find("**ERROR**") >= 0:
logging.error(f"LLM response error: {ans}")
@@ -411,9 +461,7 @@ class LLM(ComponentBase):
downstreams = self._canvas.get_component(self._id)["downstream"] if self._canvas.get_component(self._id) else []
ex = self.exception_handler()
if any([self._canvas.get_component_obj(cid).component_name.lower() == "message" for cid in downstreams]) and not (
ex and ex["goto"]
):
if any([self._canvas.get_component_obj(cid).component_name.lower() == "message" for cid in downstreams]) and not (ex and ex["goto"]):
self.set_output("content", partial(self._stream_output_async, prompt, deepcopy(msg)))
return
@@ -422,11 +470,13 @@ class LLM(ComponentBase):
if self.check_if_canceled("LLM processing"):
return
_, msg_fit = message_fit_in(
[{"role": "system", "content": prompt}, *deepcopy(msg)], int(self.chat_mdl.max_length * 0.97)
)
msg_fit, fit_error = self.fit_messages(prompt, msg, self.chat_mdl.max_length)
if fit_error:
logging.error("LLM prompt fit error: %s", fit_error)
error = fit_error
break
error = ""
ans = await self._generate_async(msg_fit)
ans = await self._generate_async(msg_fit, **extra_chat_kwargs)
msg_fit.pop(0)
if ans.find("**ERROR**") >= 0:
logging.error(f"LLM response error: {ans}")
@@ -441,15 +491,33 @@ class LLM(ComponentBase):
else:
self.set_output("_ERROR", error)
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10 * 60)))
def _invoke(self, **kwargs):
return asyncio.run(self._invoke_async(**kwargs))
async def add_memory(self, user:str, assist:str, func_name: str, params: dict, results: str, user_defined_prompt:dict={}):
def _get_chat_template_kwargs(self) -> dict[str, Any]:
chat_template_kwargs = self._canvas.globals.get("sys.chat_template_kwargs")
if chat_template_kwargs is None:
return {}
# The API should pass this as a JSON object, but accept a JSON string for compatibility.
if isinstance(chat_template_kwargs, str):
try:
chat_template_kwargs = json_repair.loads(chat_template_kwargs)
except Exception:
logging.warning("Ignore invalid sys.chat_template_kwargs: expected JSON object or JSON string object.")
return {}
if not isinstance(chat_template_kwargs, dict):
logging.warning("Ignore invalid sys.chat_template_kwargs type: %s", type(chat_template_kwargs).__name__)
return {}
return {"chat_template_kwargs": chat_template_kwargs}
async def add_memory(self, user: str, assist: str, func_name: str, params: dict, results: str, user_defined_prompt: dict = {}):
summ = await tool_call_summary(self.chat_mdl, func_name, params, results, user_defined_prompt)
logging.info(f"[MEMORY]: {summ}")
self._canvas.add_memory(user, assist, summ)
def thoughts(self) -> str:
_, msg,_ = self._prepare_prompt_variables()
return "⌛Give me a moment—starting from: \n\n" + re.sub(r"(User's query:|[\\]+)", '', msg[-1]['content'], flags=re.DOTALL) + "\n\nIll figure out our best next move."
_, msg, _ = self._prepare_prompt_variables()
return "⌛Give me a moment—starting from: \n\n" + re.sub(r"(User's query:|[\\]+)", "", msg[-1]["content"], flags=re.DOTALL) + "\n\nIll figure out our best next move."

View File

@@ -25,16 +25,11 @@ class LoopParam(ComponentParamBase):
def __init__(self):
super().__init__()
self.loop_variables = []
self.loop_termination_condition=[]
self.loop_termination_condition = []
self.maximum_loop_count = 0
def get_input_form(self) -> dict[str, dict]:
return {
"items": {
"type": "json",
"name": "Items"
}
}
return {"items": {"type": "json", "name": "Items"}}
def check(self):
return True
@@ -43,6 +38,32 @@ class LoopParam(ComponentParamBase):
class Loop(ComponentBase, ABC):
component_name = "Loop"
@staticmethod
def _is_missing_required_field(value):
if value is None:
return True
if isinstance(value, str):
return value == ""
return False
@classmethod
def _is_incomplete_loop_variable(cls, item):
if any(
[
cls._is_missing_required_field(item.get("variable")),
cls._is_missing_required_field(item.get("input_mode")),
cls._is_missing_required_field(item.get("type")),
]
):
return True
input_mode = item.get("input_mode")
if input_mode == "variable":
return cls._is_missing_required_field(item.get("value"))
if input_mode == "constant":
return item.get("value") is None
return True
def get_start(self):
for cid in self._canvas.components.keys():
if self._canvas.get_component(cid)["obj"].component_name.lower() != "loopitem":
@@ -55,12 +76,12 @@ class Loop(ComponentBase, ABC):
return
for item in self._param.loop_variables:
if any([not item.get("variable"), not item.get("input_mode"), not item.get("value"),not item.get("type")]):
if self._is_incomplete_loop_variable(item):
raise ValueError("Loop Variable is not complete.")
if item["input_mode"]=="variable":
self.set_output(item["variable"],self._canvas.get_variable_value(item["value"]))
elif item["input_mode"]=="constant":
self.set_output(item["variable"],item["value"])
if item["input_mode"] == "variable":
self.set_output(item["variable"], self._canvas.get_variable_value(item["value"]))
elif item["input_mode"] == "constant":
self.set_output(item["variable"], item["value"])
else:
if item["type"] == "number":
self.set_output(item["variable"], 0)
@@ -75,6 +96,5 @@ class Loop(ComponentBase, ABC):
else:
self.set_output(item["variable"], "")
def thoughts(self) -> str:
return "Loop from canvas."
return "Loop from canvas."

View File

@@ -21,9 +21,11 @@ class LoopItemParam(ComponentParamBase):
"""
Define the LoopItem component parameters.
"""
def check(self):
return True
class LoopItem(ComponentBase, ABC):
component_name = "LoopItem"
@@ -31,7 +33,6 @@ class LoopItem(ComponentBase, ABC):
super().__init__(canvas, id, param)
self._idx = 0
def _invoke(self, **kwargs):
if self.check_if_canceled("LoopItem processing"):
return
@@ -45,7 +46,7 @@ class LoopItem(ComponentBase, ABC):
return
self._idx += 1
def evaluate_condition(self,var, operator, value):
def evaluate_condition(self, var, operator, value):
if isinstance(var, str):
if operator == "contains":
return value in var
@@ -140,11 +141,7 @@ class LoopItem(ComponentBase, ABC):
else:
raise ValueError("Invalid input mode.")
conditions.append(self.evaluate_condition(var, operator, value))
should_end = (
all(conditions) if logical_operator == "and"
else any(conditions) if logical_operator == "or"
else None
)
should_end = all(conditions) if logical_operator == "and" else any(conditions) if logical_operator == "or" else None
if should_end is None:
raise ValueError("Invalid logical operator,should be 'and' or 'or'.")
@@ -164,4 +161,4 @@ class LoopItem(ComponentBase, ABC):
return False
def thoughts(self) -> str:
return "Next turn..."
return "Next turn..."

View File

@@ -14,8 +14,10 @@
# limitations under the License.
#
import asyncio
try:
import nest_asyncio
nest_asyncio.apply()
except Exception:
pass
@@ -45,20 +47,14 @@ class MessageParam(ComponentParamBase):
"""
Define the Message component parameters.
"""
def __init__(self):
super().__init__()
self.content = []
self.stream = True
self.output_format = None # default output format
self.auto_play = False
self.outputs = {
"content": {
"type": "str"
},
"downloads": {
"type": "list"
}
}
self.outputs = {"content": {"type": "str"}, "downloads": {"type": "list"}}
def check(self):
self.check_empty(self.content, "[Message] Content")
@@ -71,9 +67,7 @@ class Message(ComponentBase):
@staticmethod
def _is_download_info(value: Any) -> bool:
return isinstance(value, dict) and all(
key in value for key in ("doc_id", "filename", "mime_type")
)
return isinstance(value, dict) and all(key in value for key in ("doc_id", "filename", "mime_type"))
@staticmethod
def _download_info_includes_content(value: Any) -> bool:
@@ -157,7 +151,7 @@ class Message(ComponentBase):
delimiter: str = None,
downloads: list[dict[str, Any]] | None = None,
) -> tuple[str, dict[str, str | list | Any]]:
for k,v in self.get_input_elements_from_text(script).items():
for k, v in self.get_input_elements_from_text(script).items():
if k in kwargs:
continue
v = v["value"]
@@ -191,7 +185,7 @@ class Message(ComponentBase):
buf += t
return buf
async def _stream(self, rand_cnt:str):
async def _stream(self, rand_cnt: str):
s = 0
all_content = ""
cache = {}
@@ -200,8 +194,8 @@ class Message(ComponentBase):
if self.check_if_canceled("Message streaming"):
return
all_content += rand_cnt[s: r.start()]
yield rand_cnt[s: r.start()]
all_content += rand_cnt[s : r.start()]
yield rand_cnt[s : r.start()]
s = r.end()
exp = r.group(1)
if exp in cache:
@@ -235,9 +229,7 @@ class Message(ComponentBase):
continue
elif inspect.isawaitable(v):
v = await v
v = self._stringify_message_value(
v, downloads=downloads, fallback_to_str=True
)
v = self._stringify_message_value(v, downloads=downloads, fallback_to_str=True)
yield v
self.set_input_value(exp, v)
all_content += v
@@ -247,21 +239,19 @@ class Message(ComponentBase):
if self.check_if_canceled("Message streaming"):
return
all_content += rand_cnt[s: ]
yield rand_cnt[s: ]
all_content += rand_cnt[s:]
yield rand_cnt[s:]
self.set_output("downloads", downloads)
self.set_output("content", all_content)
self._convert_content(all_content)
await self._save_to_memory(all_content)
def _is_jinjia2(self, content:str) -> bool:
patt = [
r"\{%.*%\}", "{{", "}}"
]
def _is_jinjia2(self, content: str) -> bool:
patt = [r"\{%.*%\}", "{{", "}}"]
return any([re.search(p, content) for p in patt])
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10 * 60)))
def _invoke(self, **kwargs):
if self.check_if_canceled("Message processing"):
return
@@ -284,12 +274,18 @@ class Message(ComponentBase):
return
for n, v in kwargs.items():
content = re.sub(n, v, content)
if v is not None:
content = re.sub(n, str(v), content)
self.set_output("downloads", downloads)
self.set_output("content", content)
self._convert_content(content)
self._save_to_memory(content)
try:
loop = asyncio.get_running_loop()
except RuntimeError:
asyncio.run(self._save_to_memory(content))
else:
asyncio.run_coroutine_threadsafe(self._save_to_memory(content), loop)
def thoughts(self) -> str:
return ""
@@ -297,19 +293,19 @@ class Message(ComponentBase):
def _parse_markdown_table_lines(self, table_lines: list):
"""
Parse a list of Markdown table lines into a pandas DataFrame.
Args:
table_lines: List of strings, each representing a row in the Markdown table
(excluding separator lines like |---|---|)
Returns:
pandas DataFrame with the table data, or None if parsing fails
"""
import pandas as pd
if not table_lines:
return None
rows = []
headers = None
@@ -344,36 +340,58 @@ class Message(ComponentBase):
return cell
return cell
for line in table_lines:
# Split by | and clean up
cells = [cell.strip() for cell in line.split('|')]
cells = [cell.strip() for cell in line.split("|")]
# Remove empty first and last elements from split (caused by leading/trailing |)
cells = [c for c in cells if c]
if headers is None:
headers = cells
else:
cells = [_coerce_excel_cell_type(c) for c in cells]
rows.append(cells)
if headers and rows:
# Ensure all rows have same number of columns as headers
normalized_rows = []
for row in rows:
while len(row) < len(headers):
row.append('')
normalized_rows.append(row[:len(headers)])
row.append("")
normalized_rows.append(row[: len(headers)])
return pd.DataFrame(normalized_rows, columns=headers)
return None
@staticmethod
def _strip_thinking(content: str) -> str:
"""Remove <think>...</think> reasoning blocks before document export.
Reasoning models (e.g. DeepSeek-R1, OpenAI o1) embed chain-of-thought
inside ``<think>`` tags. These blocks must not leak into exported
Word/PDF/Excel documents.
"""
if not isinstance(content, str) or not content:
return content
# Remove complete think blocks (DOTALL so newlines are matched)
cleaned = re.sub(r"<think>.*?</think>", "", content, flags=re.DOTALL)
# Remove any dangling unclosed <think> opening tag + trailing content
cleaned = re.sub(r"<think>.*$", "", cleaned, flags=re.DOTALL)
# Remove leftover standalone tags
cleaned = re.sub(r"</?think>", "", cleaned)
# Collapse 3+ consecutive newlines left behind by removed blocks
cleaned = re.sub(r"\n{3,}", "\n\n", cleaned)
return cleaned.strip()
def _convert_content(self, content):
if not self._param.output_format:
return
content = self._strip_thinking(content)
import pypandoc
doc_id = get_uuid()
if self._param.output_format.lower() not in {"markdown", "html", "pdf", "docx", "xlsx"}:
@@ -402,49 +420,47 @@ class Message(ComponentBase):
# Debug: log the content being parsed
logging.info(f"XLSX Parser: Content length={len(content) if content else 0}, first 500 chars: {content[:500] if content else 'None'}")
# Try to parse ALL Markdown tables from the content
# Each table will be written to a separate sheet
tables = [] # List of (sheet_name, dataframe)
if isinstance(content, str):
lines = content.strip().split('\n')
lines = content.strip().split("\n")
logging.info(f"XLSX Parser: Total lines={len(lines)}, lines starting with '|': {sum(1 for line in lines if line.strip().startswith('|'))}")
current_table_lines = []
current_table_title = None
pending_title = None
in_table = False
table_count = 0
for i, line in enumerate(lines):
stripped = line.strip()
# Check for potential table title (lines before a table)
# Look for patterns like "Table 1:", "## Table", or markdown headers
if not in_table and stripped and not stripped.startswith('|'):
if not in_table and stripped and not stripped.startswith("|"):
# Check if this could be a table title
lower_stripped = stripped.lower()
if (lower_stripped.startswith('table') or
stripped.startswith('#') or
':' in stripped):
pending_title = stripped.lstrip('#').strip()
if stripped.startswith('|') and '|' in stripped[1:]:
if lower_stripped.startswith("table") or stripped.startswith("#") or ":" in stripped:
pending_title = stripped.lstrip("#").strip()
if stripped.startswith("|") and "|" in stripped[1:]:
# Check if this is a separator line (|---|---|)
cleaned = stripped.replace(' ', '').replace('|', '').replace('-', '').replace(':', '')
if cleaned == '':
cleaned = stripped.replace(" ", "").replace("|", "").replace("-", "").replace(":", "")
if cleaned == "":
continue # Skip separator line
if not in_table:
# Starting a new table
in_table = True
current_table_lines = []
current_table_title = pending_title
pending_title = None
current_table_lines.append(stripped)
elif in_table and not stripped.startswith('|'):
elif in_table and not stripped.startswith("|"):
# End of current table - save it
if current_table_lines:
df = self._parse_markdown_table_lines(current_table_lines)
@@ -454,24 +470,22 @@ class Message(ComponentBase):
if current_table_title:
# Clean and truncate title for sheet name
sheet_name = current_table_title[:31]
sheet_name = sheet_name.replace('/', '_').replace('\\', '_').replace('*', '').replace('?', '').replace('[', '').replace(']', '').replace(':', '')
sheet_name = sheet_name.replace("/", "_").replace("\\", "_").replace("*", "").replace("?", "").replace("[", "").replace("]", "").replace(":", "")
else:
sheet_name = f"Table_{table_count}"
tables.append((sheet_name, df))
# Reset for next table
in_table = False
current_table_lines = []
current_table_title = None
# Check if this line could be a title for the next table
if stripped:
lower_stripped = stripped.lower()
if (lower_stripped.startswith('table') or
stripped.startswith('#') or
':' in stripped):
pending_title = stripped.lstrip('#').strip()
if lower_stripped.startswith("table") or stripped.startswith("#") or ":" in stripped:
pending_title = stripped.lstrip("#").strip()
# Don't forget the last table if content ends with a table
if in_table and current_table_lines:
df = self._parse_markdown_table_lines(current_table_lines)
@@ -479,11 +493,11 @@ class Message(ComponentBase):
table_count += 1
if current_table_title:
sheet_name = current_table_title[:31]
sheet_name = sheet_name.replace('/', '_').replace('\\', '_').replace('*', '').replace('?', '').replace('[', '').replace(']', '').replace(':', '')
sheet_name = sheet_name.replace("/", "_").replace("\\", "_").replace("*", "").replace("?", "").replace("[", "").replace("]", "").replace(":", "")
else:
sheet_name = f"Table_{table_count}"
tables.append((sheet_name, df))
# Fallback: if no tables found, create single sheet with content
if not tables:
df = pd.DataFrame({"Content": [content if content else ""]})
@@ -491,7 +505,7 @@ class Message(ComponentBase):
# Write all tables to Excel, each in a separate sheet
excel_io = BytesIO()
with pd.ExcelWriter(excel_io, engine='openpyxl') as writer:
with pd.ExcelWriter(excel_io, engine="openpyxl") as writer:
used_names = set()
for sheet_name, df in tables:
# Ensure unique sheet names
@@ -499,14 +513,14 @@ class Message(ComponentBase):
counter = 1
while sheet_name in used_names:
suffix = f"_{counter}"
sheet_name = original_name[:31-len(suffix)] + suffix
sheet_name = original_name[: 31 - len(suffix)] + suffix
counter += 1
used_names.add(sheet_name)
df.to_excel(writer, sheet_name=sheet_name, index=False)
excel_io.seek(0)
binary_content = excel_io.read()
logging.info(f"Generated Excel with {len(tables)} sheet(s): {[t[0] for t in tables]}")
else: # pdf, docx
@@ -537,10 +551,7 @@ class Message(ComponentBase):
os.remove(tmp_name)
settings.STORAGE_IMPL.put(self._canvas._tenant_id, doc_id, binary_content)
self.set_output("attachment", {
"doc_id":doc_id,
"format":self._param.output_format,
"file_name":f"{doc_id[:8]}.{self._param.output_format}"})
self.set_output("attachment", {"doc_id": doc_id, "format": self._param.output_format, "file_name": f"{doc_id[:8]}.{self._param.output_format}"})
logging.info(f"Converted content uploaded as {doc_id} (format={self._param.output_format})")
@@ -554,15 +565,10 @@ class Message(ComponentBase):
user_id = self._param.user_id if hasattr(self._param, "user_id") else ""
if user_id:
import re
# is variable
if re.match(r"^{.*}$", user_id):
user_id = self._canvas.get_variable_value(user_id)
message_dict = {
"user_id": user_id,
"agent_id": self._canvas._id,
"session_id": self._canvas.task_id,
"user_input": self._canvas.get_sys_query(),
"agent_response": content
}
message_dict = {"user_id": user_id, "agent_id": self._canvas._id, "session_id": self._canvas.task_id, "user_input": self._canvas.get_sys_query(), "agent_response": content}
return await queue_save_to_memory_task(self._param.memory_ids, message_dict)

View File

@@ -52,18 +52,10 @@ class StringTransform(Message, ABC):
def get_input_form(self) -> dict[str, dict]:
if self._param.method == "split":
return {
"line": {
"name": "String",
"type": "line"
}
}
return {k: {
"name": o["name"],
"type": "line"
} for k, o in self.get_input_elements_from_text(self._param.script).items()}
return {"line": {"name": "String", "type": "line"}}
return {k: {"name": o["name"], "type": "line"} for k, o in self.get_input_elements_from_text(self._param.script).items()}
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10 * 60)))
def _invoke(self, **kwargs):
if self.check_if_canceled("StringTransform processing"):
return
@@ -73,7 +65,7 @@ class StringTransform(Message, ABC):
else:
self._merge(kwargs)
def _split(self, line:str|None = None):
def _split(self, line: str | None = None):
if self.check_if_canceled("StringTransform split processing"):
return
@@ -84,13 +76,13 @@ class StringTransform(Message, ABC):
self.set_input_value(self._param.split_ref, var)
res = []
for i,s in enumerate(re.split(r"(%s)"%("|".join([re.escape(d) for d in self._param.delimiters])), var, flags=re.DOTALL)):
for i, s in enumerate(re.split(r"(%s)" % ("|".join([re.escape(d) for d in self._param.delimiters])), var, flags=re.DOTALL)):
if i % 2 == 1:
continue
res.append(s)
self.set_output("result", res)
def _merge(self, kwargs:dict[str, str] = {}):
def _merge(self, kwargs: dict[str, str] = {}):
if self.check_if_canceled("StringTransform merge processing"):
return
@@ -104,7 +96,7 @@ class StringTransform(Message, ABC):
except Exception:
pass
for k,v in kwargs.items():
for k, v in kwargs.items():
if v is None:
v = ""
script = re.sub(k, lambda match: v, script)
@@ -113,5 +105,3 @@ class StringTransform(Message, ABC):
def thoughts(self) -> str:
return f"It's {self._param.method}ing."

View File

@@ -40,8 +40,7 @@ class SwitchParam(ComponentParamBase):
"""
self.conditions = []
self.end_cpn_ids = []
self.operators = ['contains', 'not contains', 'start with', 'end with', 'empty', 'not empty', '=', '', '>',
'<', '', '']
self.operators = ["contains", "not contains", "start with", "end with", "empty", "not empty", "=", "", ">", "<", "", ""]
def check(self):
self.check_empty(self.conditions, "[Switch] conditions")
@@ -51,12 +50,8 @@ class SwitchParam(ComponentParamBase):
self.check_empty(self.end_cpn_ids, "[Switch] the ELSE/Other destination can not be empty.")
def get_input_form(self) -> dict[str, dict]:
return {
"urls": {
"name": "URLs",
"type": "line"
}
}
return {"urls": {"name": "URLs", "type": "line"}}
class Switch(ComponentBase, ABC):
component_name = "Switch"
@@ -88,7 +83,7 @@ class Switch(ComponentBase, ABC):
self.set_output("_next", cond["to"])
return
if all(res):
if res and all(res):
self.set_output("next", [self._canvas.get_component_name(cpn_id) for cpn_id in cond["to"]])
self.set_output("_next", cond["to"])
return
@@ -97,6 +92,9 @@ class Switch(ComponentBase, ABC):
self.set_output("_next", self._param.end_cpn_ids)
def process_operator(self, input: Any, operator: str, value: Any) -> bool:
if operator in ("contains", "not contains", "start with", "end with"):
input = "" if input is None else str(input)
value = "" if value is None else str(value)
if operator == "contains":
return True if value.lower() in input.lower() else False
elif operator == "not contains":
@@ -134,7 +132,7 @@ class Switch(ComponentBase, ABC):
except Exception:
return True if input <= value else False
raise ValueError(f'Not supported operator: {operator}')
raise ValueError(f"Not supported operator: {operator}")
def thoughts(self) -> str:
return "Im weighing a few options and will pick the next step shortly."

View File

@@ -38,13 +38,9 @@ class VariableAggregatorParam(ComponentParamBase):
if not g.get("group_name"):
raise ValueError("[VariableAggregator] group_name can not be empty!")
if not g.get("variables"):
raise ValueError(
f"[VariableAggregator] variables of group `{g.get('group_name')}` can not be empty"
)
raise ValueError(f"[VariableAggregator] variables of group `{g.get('group_name')}` can not be empty")
if not isinstance(g.get("variables"), list):
raise ValueError(
f"[VariableAggregator] variables of group `{g.get('group_name')}` should be a list of strings"
)
raise ValueError(f"[VariableAggregator] variables of group `{g.get('group_name')}` should be a list of strings")
def get_input_form(self) -> dict[str, dict]:
return {
@@ -67,11 +63,11 @@ class VariableAggregator(ComponentBase):
# record candidate selectors within this group
self.set_input_value(f"{gname}.variables", list(group.get("variables", [])))
for selector in group.get("variables", []):
val = self._canvas.get_variable_value(selector['value'])
val = self._canvas.get_variable_value(selector["value"])
if val:
self.set_output(gname, val)
break
@staticmethod
def _to_object(value: Any) -> Any:
# Try to convert value to serializable object if it has to_object()

View File

@@ -19,138 +19,140 @@ import numbers
from agent.component.base import ComponentBase, ComponentParamBase
from api.utils.api_utils import timeout
class VariableAssignerParam(ComponentParamBase):
"""
Define the Variable Assigner component parameters.
"""
def __init__(self):
super().__init__()
self.variables=[]
self.variables = []
def check(self):
return True
def get_input_form(self) -> dict[str, dict]:
return {
"items": {
"type": "json",
"name": "Items"
}
}
return {"items": {"type": "json", "name": "Items"}}
class VariableAssigner(ComponentBase,ABC):
class VariableAssigner(ComponentBase, ABC):
component_name = "VariableAssigner"
_NO_PARAMETER_OPERATORS = {"clear", "remove_first", "remove_last"}
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10 * 60)))
def _invoke(self, **kwargs):
if not isinstance(self._param.variables,list):
if not isinstance(self._param.variables, list):
return
else:
for item in self._param.variables:
if any([not item.get("variable"), not item.get("operator"), not item.get("parameter")]):
variable = item.get("variable")
operator = item.get("operator")
parameter = item.get("parameter")
if any([not variable, not operator]):
raise ValueError("Variable is not complete.")
variable=item["variable"]
operator=item["operator"]
parameter=item["parameter"]
variable_value=self._canvas.get_variable_value(variable)
new_variable=self._operate(variable_value,operator,parameter)
if operator not in self._NO_PARAMETER_OPERATORS and parameter is None:
raise ValueError("Variable is not complete.")
variable_value = self._canvas.get_variable_value(variable)
new_variable = self._operate(variable_value, operator, parameter)
self._canvas.set_variable_value(variable, new_variable)
def _operate(self,variable,operator,parameter):
def _operate(self, variable, operator, parameter):
if operator == "overwrite":
return self._overwrite(parameter)
elif operator == "clear":
return self._clear(variable)
elif operator == "set":
return self._set(variable,parameter)
return self._set(variable, parameter)
elif operator == "append":
return self._append(variable,parameter)
return self._append(variable, parameter)
elif operator == "extend":
return self._extend(variable,parameter)
return self._extend(variable, parameter)
elif operator == "remove_first":
return self._remove_first(variable)
elif operator == "remove_last":
return self._remove_last(variable)
elif operator == "+=":
return self._add(variable,parameter)
return self._add(variable, parameter)
elif operator == "-=":
return self._subtract(variable,parameter)
return self._subtract(variable, parameter)
elif operator == "*=":
return self._multiply(variable,parameter)
return self._multiply(variable, parameter)
elif operator == "/=":
return self._divide(variable,parameter)
return self._divide(variable, parameter)
else:
return
def _overwrite(self,parameter):
def _overwrite(self, parameter):
return self._canvas.get_variable_value(parameter)
def _clear(self,variable):
if isinstance(variable,list):
def _clear(self, variable):
if isinstance(variable, list):
return []
elif isinstance(variable,str):
elif isinstance(variable, str):
return ""
elif isinstance(variable,dict):
elif isinstance(variable, dict):
return {}
elif isinstance(variable,bool):
elif isinstance(variable, bool):
return False
elif isinstance(variable,int):
elif isinstance(variable, int):
return 0
elif isinstance(variable,float):
elif isinstance(variable, float):
return 0.0
else:
return None
def _set(self,variable,parameter):
def _set(self, variable, parameter):
if variable is None:
return self._canvas.get_value_with_variable(parameter)
elif isinstance(variable,str):
elif isinstance(variable, str):
return self._canvas.get_value_with_variable(parameter)
elif isinstance(variable,bool):
elif isinstance(variable, bool):
return parameter
elif isinstance(variable,int):
elif isinstance(variable, int):
return parameter
elif isinstance(variable,float):
elif isinstance(variable, float):
return parameter
else:
return parameter
def _append(self,variable,parameter):
parameter=self._canvas.get_variable_value(parameter)
def _append(self, variable, parameter):
parameter = self._canvas.get_variable_value(parameter)
if variable is None:
variable=[]
if not isinstance(variable,list):
variable = []
if not isinstance(variable, list):
return "ERROR:VARIABLE_NOT_LIST"
elif len(variable)!=0 and not isinstance(parameter,type(variable[0])):
elif len(variable) != 0 and not isinstance(parameter, type(variable[0])):
return "ERROR:PARAMETER_NOT_LIST_ELEMENT_TYPE"
else:
variable.append(parameter)
return variable
def _extend(self,variable,parameter):
parameter=self._canvas.get_variable_value(parameter)
def _extend(self, variable, parameter):
parameter = self._canvas.get_variable_value(parameter)
if variable is None:
variable=[]
if not isinstance(variable,list):
variable = []
if not isinstance(variable, list):
return "ERROR:VARIABLE_NOT_LIST"
elif not isinstance(parameter,list):
elif not isinstance(parameter, list):
return "ERROR:PARAMETER_NOT_LIST"
elif len(variable)!=0 and len(parameter)!=0 and not isinstance(parameter[0],type(variable[0])):
elif len(variable) != 0 and len(parameter) != 0 and not isinstance(parameter[0], type(variable[0])):
return "ERROR:PARAMETER_NOT_LIST_ELEMENT_TYPE"
else:
return variable + parameter
def _remove_first(self,variable):
if not isinstance(variable,list):
def _remove_first(self, variable):
if not isinstance(variable, list):
return "ERROR:VARIABLE_NOT_LIST"
if len(variable)==0:
if len(variable) == 0:
return variable
return variable[1:]
def _remove_last(self,variable):
if not isinstance(variable,list):
def _remove_last(self, variable):
if not isinstance(variable, list):
return "ERROR:VARIABLE_NOT_LIST"
if len(variable)==0:
if len(variable) == 0:
return variable
return variable[:-1]
@@ -159,32 +161,32 @@ class VariableAssigner(ComponentBase,ABC):
return False
return isinstance(value, numbers.Number)
def _add(self,variable,parameter):
def _add(self, variable, parameter):
if self.is_number(variable) and self.is_number(parameter):
return variable + parameter
else:
return "ERROR:VARIABLE_NOT_NUMBER or PARAMETER_NOT_NUMBER"
def _subtract(self,variable,parameter):
def _subtract(self, variable, parameter):
if self.is_number(variable) and self.is_number(parameter):
return variable - parameter
else:
return "ERROR:VARIABLE_NOT_NUMBER or PARAMETER_NOT_NUMBER"
def _multiply(self,variable,parameter):
def _multiply(self, variable, parameter):
if self.is_number(variable) and self.is_number(parameter):
return variable * parameter
else:
return "ERROR:VARIABLE_NOT_NUMBER or PARAMETER_NOT_NUMBER"
def _divide(self,variable,parameter):
def _divide(self, variable, parameter):
if self.is_number(variable) and self.is_number(parameter):
if parameter==0:
if parameter == 0:
return "ERROR:DIVIDE_BY_ZERO"
else:
return variable/parameter
return variable / parameter
else:
return "ERROR:VARIABLE_NOT_NUMBER or PARAMETER_NOT_NUMBER"
return "ERROR:VARIABLE_NOT_NUMBER or PARAMETER_NOT_NUMBER"
def thoughts(self) -> str:
return "Assign variables from canvas."
return "Assign variables from canvas."

View File

@@ -56,7 +56,7 @@ def normalize_chunker_dsl(dsl: dict) -> dict:
for old_name, new_name in COMPONENT_RENAMES.items():
prefix = f"{old_name}:"
if component_id.startswith(prefix):
new_component_id = f"{new_name}:{component_id[len(prefix):]}"
new_component_id = f"{new_name}:{component_id[len(prefix) :]}"
break
component_id_map[component_id] = new_component_id
@@ -66,12 +66,7 @@ def normalize_chunker_dsl(dsl: dict) -> dict:
def repl(match: re.Match[str]) -> str:
component_id = match.group(2)
return (
match.group(1)
+ component_id_map.get(component_id, component_id)
+ match.group(3)
+ match.group(4)
)
return match.group(1) + component_id_map.get(component_id, component_id) + match.group(3) + match.group(4)
return VARIABLE_REF_PATTERN.sub(repl, text)
@@ -96,15 +91,9 @@ def normalize_chunker_dsl(dsl: dict) -> dict:
obj["component_name"] = COMPONENT_RENAMES.get(component_name, component_name)
if isinstance(new_component.get("downstream"), list):
new_component["downstream"] = [
component_id_map.get(component_id, component_id)
for component_id in new_component["downstream"]
]
new_component["downstream"] = [component_id_map.get(component_id, component_id) for component_id in new_component["downstream"]]
if isinstance(new_component.get("upstream"), list):
new_component["upstream"] = [
component_id_map.get(component_id, component_id)
for component_id in new_component["upstream"]
]
new_component["upstream"] = [component_id_map.get(component_id, component_id) for component_id in new_component["upstream"]]
parent_id = new_component.get("parent_id")
if isinstance(parent_id, str):
@@ -115,10 +104,7 @@ def normalize_chunker_dsl(dsl: dict) -> dict:
normalized["components"] = rewritten_components
if isinstance(normalized.get("path"), list):
normalized["path"] = [
component_id_map.get(component_id, component_id)
for component_id in normalized["path"]
]
normalized["path"] = [component_id_map.get(component_id, component_id) for component_id in normalized["path"]]
graph = normalized.get("graph")
if isinstance(graph, dict):

View File

@@ -1 +1 @@
PLUGIN_TYPE_LLM_TOOLS = "llm_tools"
PLUGIN_TYPE_LLM_TOOLS = "llm_tools"

View File

@@ -7,6 +7,7 @@ class BadCalculatorPlugin(LLMToolPlugin):
A sample LLM tool plugin, will add two numbers with 100.
It only presents for demo purpose. Do not use it in production.
"""
_version_ = "1.0.0"
@classmethod
@@ -17,19 +18,9 @@ class BadCalculatorPlugin(LLMToolPlugin):
"description": "A tool to calculate the sum of two numbers (will give wrong answer)",
"displayDescription": "$t:bad_calculator.description",
"parameters": {
"a": {
"type": "number",
"description": "The first number",
"displayDescription": "$t:bad_calculator.params.a",
"required": True
},
"b": {
"type": "number",
"description": "The second number",
"displayDescription": "$t:bad_calculator.params.b",
"required": True
}
}
"a": {"type": "number", "description": "The first number", "displayDescription": "$t:bad_calculator.params.a", "required": True},
"b": {"type": "number", "description": "The second number", "displayDescription": "$t:bad_calculator.params.b", "required": True},
},
}
def invoke(self, a: int, b: int) -> str:

View File

@@ -38,14 +38,8 @@ def llm_tool_metadata_to_openai_tool(llm_tool_metadata: LLMToolMetadata) -> dict
"description": llm_tool_metadata["description"],
"parameters": {
"type": "object",
"properties": {
k: {
"type": p["type"],
"description": p["description"]
}
for k, p in llm_tool_metadata["parameters"].items()
},
"required": [k for k, p in llm_tool_metadata["parameters"].items() if p["required"]]
}
}
"properties": {k: {"type": p["type"], "description": p["description"]} for k, p in llm_tool_metadata["parameters"].items()},
"required": [k for k, p in llm_tool_metadata["parameters"].items() if p["required"]],
},
},
}

View File

@@ -15,10 +15,8 @@ class PluginManager:
self._llm_tool_plugins = {}
def load_plugins(self) -> None:
loader = pluginlib.PluginLoader(
paths=[str(Path(os.path.dirname(__file__), "embedded_plugins"))]
)
loader = pluginlib.PluginLoader(paths=[str(Path(os.path.dirname(__file__), "embedded_plugins"))])
for type, plugins in loader.plugins.items():
for name, plugin in plugins.items():
logging.info(f"Loaded {type} plugin {name} version {plugin.version}")

View File

@@ -111,7 +111,10 @@ def _load_provider_from_settings() -> None:
except Exception as e:
logger.error(f"Failed to load sandbox provider from settings: {e}")
import traceback
traceback.print_exc()
def _load_provider_config_from_settings(provider_type: str) -> Dict[str, Any]:
provider_config_settings = SystemSettingsService.get_by_name(f"sandbox.{provider_type}")
if not provider_config_settings:
@@ -147,12 +150,7 @@ def reload_provider() -> None:
_load_provider_from_settings()
def execute_code(
code: str,
language: str = "python",
timeout: int = 30,
arguments: Optional[Dict[str, Any]] = None
) -> ExecutionResult:
def execute_code(code: str, language: str = "python", timeout: int = 30, arguments: Optional[Dict[str, Any]] = None) -> ExecutionResult:
"""
Execute code in the configured sandbox.
@@ -173,9 +171,7 @@ def execute_code(
provider_manager = get_provider_manager()
if not provider_manager.is_configured():
raise RuntimeError(
"No sandbox provider configured. Please configure sandbox settings in the admin panel."
)
raise RuntimeError("No sandbox provider configured. Please configure sandbox settings in the admin panel.")
provider = provider_manager.get_provider()
provider_name = provider_manager.get_provider_name() or getattr(provider, "__class__", type(provider)).__name__
@@ -192,13 +188,7 @@ def execute_code(
try:
# Execute the code
result = provider.execute_code(
instance_id=instance.instance_id,
code=code,
language=language,
timeout=timeout,
arguments=arguments
)
result = provider.execute_code(instance_id=instance.instance_id, code=code, language=language, timeout=timeout, arguments=arguments)
return result

View File

@@ -22,4 +22,3 @@ router = APIRouter()
router.get("/")(healthz_handler)
router.get("/healthz")(healthz_handler)
router.post("/run")(run_code_handler)

View File

@@ -230,9 +230,7 @@ async def execute_code(req: CodeExecutionRequest):
if returncode != 0:
raise RuntimeError(f"Directory creation failed: {stderr}")
tar_proc = await asyncio.create_subprocess_exec(
"tar", "czf", "-", "-C", workdir, code_name, runner_name, str(bundle["args_name"]), stdout=asyncio.subprocess.PIPE
)
tar_proc = await asyncio.create_subprocess_exec("tar", "czf", "-", "-C", workdir, code_name, runner_name, str(bundle["args_name"]), stdout=asyncio.subprocess.PIPE)
tar_stdout, _ = await tar_proc.communicate()
docker_proc = await asyncio.create_subprocess_exec(
@@ -334,8 +332,16 @@ async def _collect_artifacts(container: str, task_id: str, host_workdir: str) ->
# List files in the artifacts directory inside the container
returncode, stdout, _ = await async_run_command(
"docker", "exec", container, "find", artifacts_path,
"-maxdepth", "1", "-type", "f", timeout=5,
"docker",
"exec",
container,
"find",
artifacts_path,
"-maxdepth",
"1",
"-type",
"f",
timeout=5,
)
if returncode != 0 or not stdout.strip():
return []
@@ -359,7 +365,14 @@ async def _collect_artifacts(container: str, task_id: str, host_workdir: str) ->
# Check file size inside the container
returncode, size_str, _ = await async_run_command(
"docker", "exec", container, "stat", "-c", "%s", file_path, timeout=5,
"docker",
"exec",
container,
"stat",
"-c",
"%s",
file_path,
timeout=5,
)
if returncode != 0:
logger.warning(f"Failed to stat artifact {fname}")
@@ -374,7 +387,12 @@ async def _collect_artifacts(container: str, task_id: str, host_workdir: str) ->
# Read file content via docker exec (docker cp doesn't work with gVisor tmpfs)
returncode, content_b64, stderr = await async_run_command(
"docker", "exec", container, "base64", file_path, timeout=30,
"docker",
"exec",
container,
"base64",
file_path,
timeout=30,
)
if returncode != 0:
logger.warning(f"Failed to read artifact {fname}: {stderr}")
@@ -382,12 +400,14 @@ async def _collect_artifacts(container: str, task_id: str, host_workdir: str) ->
content_b64 = content_b64.replace("\n", "").strip()
items.append(ArtifactItem(
name=fname,
mime_type=mime_type,
size=file_size,
content_b64=content_b64,
))
items.append(
ArtifactItem(
name=fname,
mime_type=mime_type,
size=file_size,
content_b64=content_b64,
)
)
logger.info(f"Collected artifact: {fname} ({file_size} bytes, {mime_type})")
return items

View File

@@ -226,17 +226,13 @@ class AliyunCodeInterpreterProvider(SandboxProvider):
# Connect to existing sandbox instance
sandbox = Sandbox.connect(sandbox_id=instance_id, config=self._config)
# agentrun-sdk 0.0.26 only exposes CodeLanguage.PYTHON; keep JS as string fallback.
# CodeLanguage enum only exposes PYTHON across agentrun-sdk 0.0.26+; keep JS as string fallback.
code_language = CodeLanguage.PYTHON if normalized_lang == "python" else "javascript"
# Wrap code to call main() function
# Matches self_managed provider behavior: call main(**arguments)
args_json = json.dumps(arguments or {})
wrapped_code = (
build_python_wrapper(code, args_json)
if normalized_lang == "python"
else build_javascript_wrapper(code, args_json)
)
wrapped_code = build_python_wrapper(code, args_json) if normalized_lang == "python" else build_javascript_wrapper(code, args_json)
logger.debug(f"Aliyun Code Interpreter: Wrapped code (first 200 chars): {wrapped_code[:200]}")
start_time = time.time()
@@ -355,7 +351,7 @@ class AliyunCodeInterpreterProvider(SandboxProvider):
# Try to list templates to verify connection
from agentrun.sandbox import Template
templates = Template.list(config=self._config)
templates = Template.list_templates(config=self._config)
return templates is not None
except Exception as e:

View File

@@ -33,6 +33,7 @@ class SandboxProviderConfigError(Exception):
@dataclass
class SandboxInstance:
"""Represents a sandbox execution instance"""
instance_id: str
provider: str
status: str # running, stopped, error
@@ -46,6 +47,7 @@ class SandboxInstance:
@dataclass
class ExecutionResult:
"""Result of code execution in a sandbox"""
stdout: str
stderr: str
exit_code: int
@@ -96,14 +98,7 @@ class SandboxProvider(ABC):
pass
@abstractmethod
def execute_code(
self,
instance_id: str,
code: str,
language: str,
timeout: int = 10,
arguments: Optional[Dict[str, Any]] = None
) -> ExecutionResult:
def execute_code(self, instance_id: str, code: str, language: str, timeout: int = 10, arguments: Optional[Dict[str, Any]] = None) -> ExecutionResult:
"""
Execute code in a sandbox instance.

View File

@@ -97,16 +97,10 @@ class E2BProvider(SandboxProvider):
metadata={
"language": language,
"region": self.region,
}
},
)
def execute_code(
self,
instance_id: str,
code: str,
language: str,
timeout: int = 10
) -> ExecutionResult:
def execute_code(self, instance_id: str, code: str, language: str, timeout: int = 10) -> ExecutionResult:
"""
Execute code in the E2B instance.
@@ -130,9 +124,7 @@ class E2BProvider(SandboxProvider):
# POST /sandbox/{sandboxID}/execute
raise RuntimeError(
"E2B provider is not yet fully implemented. "
"Please use the self-managed provider or implement the E2B API integration. "
"See https://github.com/e2b-dev/e2b for API documentation."
"E2B provider is not yet fully implemented. Please use the self-managed provider or implement the E2B API integration. See https://github.com/e2b-dev/e2b for API documentation."
)
def destroy_instance(self, instance_id: str) -> bool:
@@ -208,7 +200,7 @@ class E2BProvider(SandboxProvider):
"min": 5,
"max": 300,
"description": "API request timeout for code execution",
}
},
}
def _normalize_language(self, language: str) -> str:

View File

@@ -49,6 +49,8 @@ LOCAL_PYTHON_THREAD_ENV_VARS = (
"BLIS_NUM_THREADS",
"VECLIB_MAXIMUM_THREADS",
)
class LocalProvider(SandboxProvider):
"""
Execute code as a local child process.

View File

@@ -108,17 +108,10 @@ class SelfManagedProvider(SandboxProvider):
"language": language,
"endpoint": self.endpoint,
"pool_size": self.pool_size,
}
},
)
def execute_code(
self,
instance_id: str,
code: str,
language: str,
timeout: int = 10,
arguments: Optional[Dict[str, Any]] = None
) -> ExecutionResult:
def execute_code(self, instance_id: str, code: str, language: str, timeout: int = 10, arguments: Optional[Dict[str, Any]] = None) -> ExecutionResult:
"""
Execute code in the sandbox.
@@ -144,11 +137,7 @@ class SelfManagedProvider(SandboxProvider):
# Prepare request
code_b64 = base64.b64encode(code.encode("utf-8")).decode("utf-8")
payload = {
"code_b64": code_b64,
"language": normalized_lang,
"arguments": arguments or {}
}
payload = {"code_b64": code_b64, "language": normalized_lang, "arguments": arguments or {}}
url = f"{self.endpoint}/run"
exec_timeout = timeout or self.timeout
@@ -156,19 +145,12 @@ class SelfManagedProvider(SandboxProvider):
start_time = time.time()
try:
response = requests.post(
url,
json=payload,
timeout=exec_timeout,
headers={"Content-Type": "application/json"}
)
response = requests.post(url, json=payload, timeout=exec_timeout, headers={"Content-Type": "application/json"})
execution_time = time.time() - start_time
if response.status_code != 200:
raise RuntimeError(
f"HTTP {response.status_code}: {response.text}"
)
raise RuntimeError(f"HTTP {response.status_code}: {response.text}")
result = response.json()
structured_result = result.get("result") or {}
@@ -188,14 +170,12 @@ class SelfManagedProvider(SandboxProvider):
"result_present": structured_result.get("present", False),
"result_value": structured_result.get("value"),
"result_type": structured_result.get("type"),
}
},
)
except requests.Timeout:
execution_time = time.time() - start_time
raise TimeoutError(
f"Execution timed out after {exec_timeout} seconds"
)
raise TimeoutError(f"Execution timed out after {exec_timeout} seconds")
except requests.RequestException as e:
raise RuntimeError(f"HTTP request failed: {str(e)}")
@@ -388,7 +368,8 @@ class SelfManagedProvider(SandboxProvider):
if endpoint:
# Check if it's a valid HTTP/HTTPS URL or localhost
import re
url_pattern = r'^(https?://|http://localhost|http://[\d\.]+:[a-z]+:[/]|http://[\w\.]+:)'
url_pattern = r"^(https?://|http://localhost|http://[\d\.]+:[a-z]+:[/]|http://[\w\.]+:)"
if not re.match(url_pattern, endpoint):
return False, f"Invalid endpoint format: {endpoint}. Must start with http:// or https://"

View File

@@ -19,6 +19,7 @@ from __future__ import annotations
import base64
import io
import json
import logging
import mimetypes
import os
import posixpath
@@ -73,6 +74,7 @@ class SSHProvider(SandboxProvider):
self.max_output_bytes = 1024 * 1024
self.max_artifacts = 20
self.max_artifact_bytes = 10 * 1024 * 1024
self.known_hosts = ""
self._initialized = False
self._instances: dict[str, dict[str, Any]] = {}
@@ -90,6 +92,7 @@ class SSHProvider(SandboxProvider):
self.max_output_bytes = int(config.get("max_output_bytes", 1024 * 1024) or 1024 * 1024)
self.max_artifacts = int(config.get("max_artifacts", 20) or 20)
self.max_artifact_bytes = int(config.get("max_artifact_bytes", 10 * 1024 * 1024) or 10 * 1024 * 1024)
self.known_hosts = str(config.get("known_hosts", "") or "").strip()
is_valid, error_message = self.validate_config(
{
@@ -132,9 +135,7 @@ class SSHProvider(SandboxProvider):
timeout=min(self.timeout, 10),
)
if exit_code != 0:
raise RuntimeError(
f"Failed to create remote artifacts directory: {stderr or stdout or 'unknown error'}"
)
raise RuntimeError(f"Failed to create remote artifacts directory: {stderr or stdout or 'unknown error'}")
except Exception:
sftp.close()
client.close()
@@ -208,9 +209,7 @@ class SSHProvider(SandboxProvider):
"status": "ok" if exit_code == 0 else "error",
"timeout": exec_timeout,
"command": command,
"artifacts": self._collect_artifacts(
sftp, posixpath.join(remote_work_dir, "artifacts")
),
"artifacts": self._collect_artifacts(sftp, posixpath.join(remote_work_dir, "artifacts")),
"result_present": structured_result.get("present", False),
"result_value": structured_result.get("value"),
"result_type": structured_result.get("type"),
@@ -266,18 +265,13 @@ class SSHProvider(SandboxProvider):
timeout=min(self.timeout, 10),
)
if exit_code != 0:
raise SandboxProviderConfigError(
f"SSH connectivity check failed on {self.username}@{self.host}:{self.port}: "
f"{stderr or 'remote command returned non-zero exit status'}"
)
raise SandboxProviderConfigError(f"SSH connectivity check failed on {self.username}@{self.host}:{self.port}: {stderr or 'remote command returned non-zero exit status'}")
finally:
client.close()
except SandboxProviderConfigError:
raise
except Exception as exc:
raise SandboxProviderConfigError(
f"Failed to connect to SSH host {self.username}@{self.host}:{self.port}: {exc}"
) from exc
raise SandboxProviderConfigError(f"Failed to connect to SSH host {self.username}@{self.host}:{self.port}: {exc}") from exc
def get_supported_languages(self) -> List[str]:
return ["python", "javascript", "nodejs"]
@@ -333,6 +327,18 @@ class SSHProvider(SandboxProvider):
"placeholder": "Optional",
"description": "Passphrase for the private key if it is encrypted.",
},
"known_hosts": {
"type": "string",
"required": False,
"label": "SSH known_hosts File",
"placeholder": "/etc/ragflow/ssh_known_hosts",
"description": (
"Path to an OpenSSH-format known_hosts file used to verify "
"the remote host's key. When set, the file is loaded on top "
"of the system host keys (~/.ssh/known_hosts). When unset, "
"only system keys are used and unknown hosts are rejected."
),
},
"python_bin": {
"type": "string",
"required": False,
@@ -435,7 +441,32 @@ class SSHProvider(SandboxProvider):
def _create_ssh_client(self) -> paramiko.SSHClient:
paramiko = _get_paramiko_module()
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# Load trusted host keys BEFORE setting the policy. Without
# load_system_host_keys() the in-memory store is empty and
# RejectPolicy would reject every host on first connect,
# breaking the provider for normal setups. The order matters:
# load_system_host_keys() populates the store from
# ~/.ssh/known_hosts (and the legacy /etc/ssh/ssh_known_hosts);
# an optional explicit known_hosts file from `known_hosts`
# config is then merged on top.
client.load_system_host_keys()
if self.known_hosts:
try:
client.load_host_keys(self.known_hosts)
except OSError as exc:
# Fail closed when the operator-configured trust store
# is unreadable: continuing with system keys could let
# the connection succeed against an unintended anchor
# (e.g. an attacker who can write ~/.ssh/known_hosts).
# Match the Go provider's fail-closed posture (see
# internal/agent/sandbox/ssh.go::hostKeyCallback).
logging.warning("SSH: failed to load configured known_hosts file; refusing connection")
raise SandboxProviderConfigError("Failed to load configured SSH known_hosts file.") from exc
# Reject unknown hosts: this is the default fail-closed posture
# to prevent silent MITM. Operators must either ship a populated
# known_hosts file or accept the warning (paramiko will fail the
# connect) on first encounter.
client.set_missing_host_key_policy(paramiko.RejectPolicy())
connect_kwargs: dict[str, Any] = {
"hostname": self.host,
@@ -480,9 +511,7 @@ class SSHProvider(SandboxProvider):
except Exception as exc:
errors.append(str(exc))
raise SandboxProviderConfigError(
"Failed to load SSH private key. " + "; ".join(error for error in errors if error)
)
raise SandboxProviderConfigError("Failed to load SSH private key. " + "; ".join(error for error in errors if error))
def _create_remote_workspace(self, client: paramiko.SSHClient) -> str:
base_dir = self.work_dir.rstrip("/") or "/tmp"
@@ -493,9 +522,7 @@ class SSHProvider(SandboxProvider):
timeout=min(self.timeout, 10),
)
if exit_code != 0:
raise RuntimeError(
f"Failed to create remote workspace on {self.host}: {stderr or stdout or 'unknown error'}"
)
raise RuntimeError(f"Failed to create remote workspace on {self.host}: {stderr or stdout or 'unknown error'}")
remote_work_dir = stdout.strip().splitlines()[-1] if stdout.strip() else ""
if not remote_work_dir:
@@ -535,10 +562,7 @@ class SSHProvider(SandboxProvider):
else:
raise RuntimeError(f"Unsupported language for SSH provider: {language}")
return (
f"cd {shlex.quote(remote_work_dir)} && "
f"{shlex.quote(executable)} {shlex.quote(remote_script_path)}"
)
return f"cd {shlex.quote(remote_work_dir)} && {shlex.quote(executable)} {shlex.quote(remote_script_path)}"
def _run_remote_command(
self,
@@ -658,7 +682,5 @@ def _get_paramiko_module():
try:
import paramiko
except ImportError as exc:
raise SandboxProviderConfigError(
"paramiko is required for the SSH sandbox provider. Install the project dependencies to enable it."
) from exc
raise SandboxProviderConfigError("paramiko is required for the SSH sandbox provider. Install the project dependencies to enable it.") from exc
return paramiko

View File

@@ -3,7 +3,7 @@ name = "gvisor-sandbox"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12,<3.15"
requires-python = ">=3.13,<3.14"
dependencies = [
"fastapi>=0.115.12",
"httpx>=0.28.1",

View File

@@ -36,7 +36,7 @@ if __name__ == "__main__":
def build_javascript_wrapper(code: str, args_json: str) -> str:
return f'''{code}
return f"""{code}
const __ragflowArgs = {args_json};
@@ -55,7 +55,7 @@ const __ragflowArgs = {args_json};
}}
console.log('{RESULT_MARKER_PREFIX}' + Buffer.from(payload, 'utf8').toString('base64'));
}})();
'''
"""
def extract_structured_result(stdout: str) -> tuple[str, dict[str, Any]]:

View File

@@ -45,11 +45,11 @@ class TestAliyunCodeInterpreterProvider:
assert provider.timeout == 30
assert not provider._initialized
@patch("agent.sandbox.providers.aliyun_codeinterpreter.Template")
@patch("agentrun.sandbox.Template")
def test_initialize_success(self, mock_template):
"""Test successful initialization."""
# Mock health check response
mock_template.list.return_value = []
mock_template.list_templates.return_value = []
provider = AliyunCodeInterpreterProvider()
result = provider.initialize(
@@ -89,10 +89,10 @@ class TestAliyunCodeInterpreterProvider:
result = provider2.initialize({"access_key_id": "LTAI5tXXXXXXXXXX", "access_key_secret": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"})
assert result is False
@patch("agent.sandbox.providers.aliyun_codeinterpreter.Template")
@patch("agentrun.sandbox.Template")
def test_initialize_default_config(self, mock_template):
"""Test initialization with default config."""
mock_template.list.return_value = []
mock_template.list_templates.return_value = []
provider = AliyunCodeInterpreterProvider()
result = provider.initialize({"access_key_id": "LTAI5tXXXXXXXXXX", "access_key_secret": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "account_id": "1234567890123456"})

View File

@@ -151,7 +151,7 @@ class TestAliyunCodeInterpreterIntegration:
""",
language="python",
timeout=30,
arguments={"name": "World", "count": 2}
arguments={"name": "World", "count": 2},
)
assert result.exit_code == 0
@@ -211,7 +211,7 @@ class TestAliyunCodeInterpreterIntegration:
}""",
language="javascript",
timeout=30,
arguments={"name": "World", "count": 2}
arguments={"name": "World", "count": 2},
)
assert result.exit_code == 0

View File

@@ -32,12 +32,7 @@ class TestSandboxDataclasses:
def test_sandbox_instance_creation(self):
"""Test SandboxInstance dataclass creation."""
instance = SandboxInstance(
instance_id="test-123",
provider="self_managed",
status="running",
metadata={"language": "python"}
)
instance = SandboxInstance(instance_id="test-123", provider="self_managed", status="running", metadata={"language": "python"})
assert instance.instance_id == "test-123"
assert instance.provider == "self_managed"
@@ -46,24 +41,13 @@ class TestSandboxDataclasses:
def test_sandbox_instance_default_metadata(self):
"""Test SandboxInstance with None metadata."""
instance = SandboxInstance(
instance_id="test-123",
provider="self_managed",
status="running",
metadata=None
)
instance = SandboxInstance(instance_id="test-123", provider="self_managed", status="running", metadata=None)
assert instance.metadata == {}
def test_execution_result_creation(self):
"""Test ExecutionResult dataclass creation."""
result = ExecutionResult(
stdout="Hello, World!",
stderr="",
exit_code=0,
execution_time=1.5,
metadata={"status": "success"}
)
result = ExecutionResult(stdout="Hello, World!", stderr="", exit_code=0, execution_time=1.5, metadata={"status": "success"})
assert result.stdout == "Hello, World!"
assert result.stderr == ""
@@ -73,13 +57,7 @@ class TestSandboxDataclasses:
def test_execution_result_default_metadata(self):
"""Test ExecutionResult with None metadata."""
result = ExecutionResult(
stdout="output",
stderr="error",
exit_code=1,
execution_time=0.5,
metadata=None
)
result = ExecutionResult(stdout="output", stderr="error", exit_code=1, execution_time=0.5, metadata=None)
assert result.metadata == {}
@@ -145,7 +123,7 @@ class TestSelfManagedProvider:
assert provider.pool_size == 10
assert not provider._initialized
@patch('requests.get')
@patch("requests.get")
def test_initialize_success(self, mock_get):
"""Test successful initialization."""
mock_response = Mock()
@@ -153,12 +131,7 @@ class TestSelfManagedProvider:
mock_get.return_value = mock_response
provider = SelfManagedProvider()
result = provider.initialize({
"endpoint": "http://test-endpoint:9385",
"timeout": 60,
"max_retries": 5,
"pool_size": 20
})
result = provider.initialize({"endpoint": "http://test-endpoint:9385", "timeout": 60, "max_retries": 5, "pool_size": 20})
assert result is True
assert provider.endpoint == "http://test-endpoint:9385"
@@ -168,7 +141,7 @@ class TestSelfManagedProvider:
assert provider._initialized
mock_get.assert_called_once_with("http://test-endpoint:9385/healthz", timeout=5)
@patch('requests.get')
@patch("requests.get")
def test_initialize_failure(self, mock_get):
"""Test initialization failure."""
mock_get.side_effect = Exception("Connection error")
@@ -181,7 +154,7 @@ class TestSelfManagedProvider:
def test_initialize_default_config(self):
"""Test initialization with default config."""
with patch('requests.get') as mock_get:
with patch("requests.get") as mock_get:
mock_response = Mock()
mock_response.status_code = 200
mock_get.return_value = mock_response
@@ -222,30 +195,18 @@ class TestSelfManagedProvider:
with pytest.raises(RuntimeError, match="Provider not initialized"):
provider.create_instance("python")
@patch('requests.post')
@patch("requests.post")
def test_execute_code_success(self, mock_post):
"""Test successful code execution."""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"status": "success",
"stdout": '{"result": 42}',
"stderr": "",
"exit_code": 0,
"time_used_ms": 100.0,
"memory_used_kb": 1024.0
}
mock_response.json.return_value = {"status": "success", "stdout": '{"result": 42}', "stderr": "", "exit_code": 0, "time_used_ms": 100.0, "memory_used_kb": 1024.0}
mock_post.return_value = mock_response
provider = SelfManagedProvider()
provider._initialized = True
result = provider.execute_code(
instance_id="test-123",
code="def main(): return {'result': 42}",
language="python",
timeout=10
)
result = provider.execute_code(instance_id="test-123", code="def main(): return {'result': 42}", language="python", timeout=10)
assert result.stdout == '{"result": 42}'
assert result.stderr == ""
@@ -254,7 +215,7 @@ class TestSelfManagedProvider:
assert result.metadata["status"] == "success"
assert result.metadata["instance_id"] == "test-123"
@patch('requests.post')
@patch("requests.post")
def test_execute_code_maps_structured_result_into_metadata(self, mock_post):
"""Test successful code execution with structured result envelope."""
mock_response = Mock()
@@ -277,19 +238,14 @@ class TestSelfManagedProvider:
provider = SelfManagedProvider()
provider._initialized = True
result = provider.execute_code(
instance_id="test-123",
code="def main(): return {'items': ['a', 'b']}",
language="python",
timeout=10
)
result = provider.execute_code(instance_id="test-123", code="def main(): return {'items': ['a', 'b']}", language="python", timeout=10)
assert result.stdout == "debug line\n"
assert result.metadata["result_present"] is True
assert result.metadata["result_value"] == {"items": ["a", "b"]}
assert result.metadata["result_type"] == "json"
@patch('requests.post')
@patch("requests.post")
def test_execute_code_timeout(self, mock_post):
"""Test code execution timeout."""
mock_post.side_effect = requests.Timeout()
@@ -298,14 +254,9 @@ class TestSelfManagedProvider:
provider._initialized = True
with pytest.raises(TimeoutError, match="Execution timed out"):
provider.execute_code(
instance_id="test-123",
code="while True: pass",
language="python",
timeout=5
)
provider.execute_code(instance_id="test-123", code="while True: pass", language="python", timeout=5)
@patch('requests.post')
@patch("requests.post")
def test_execute_code_http_error(self, mock_post):
"""Test code execution with HTTP error."""
mock_response = Mock()
@@ -317,22 +268,14 @@ class TestSelfManagedProvider:
provider._initialized = True
with pytest.raises(RuntimeError, match="HTTP 500"):
provider.execute_code(
instance_id="test-123",
code="invalid code",
language="python"
)
provider.execute_code(instance_id="test-123", code="invalid code", language="python")
def test_execute_code_not_initialized(self):
"""Test executing code when provider not initialized."""
provider = SelfManagedProvider()
with pytest.raises(RuntimeError, match="Provider not initialized"):
provider.execute_code(
instance_id="test-123",
code="print('hello')",
language="python"
)
provider.execute_code(instance_id="test-123", code="print('hello')", language="python")
def test_destroy_instance(self):
"""Test destroying an instance (no-op for self-managed)."""
@@ -344,7 +287,7 @@ class TestSelfManagedProvider:
assert result is True
@patch('requests.get')
@patch("requests.get")
def test_health_check_success(self, mock_get):
"""Test successful health check."""
mock_response = Mock()
@@ -358,7 +301,7 @@ class TestSelfManagedProvider:
assert result is True
mock_get.assert_called_once_with("http://localhost:9385/healthz", timeout=5)
@patch('requests.get')
@patch("requests.get")
def test_health_check_failure(self, mock_get):
"""Test health check failure."""
mock_get.side_effect = Exception("Connection error")
@@ -439,20 +382,20 @@ class TestProviderInterface:
provider = SelfManagedProvider()
# Check all abstract methods are implemented
assert hasattr(provider, 'initialize')
assert hasattr(provider, "initialize")
assert callable(provider.initialize)
assert hasattr(provider, 'create_instance')
assert hasattr(provider, "create_instance")
assert callable(provider.create_instance)
assert hasattr(provider, 'execute_code')
assert hasattr(provider, "execute_code")
assert callable(provider.execute_code)
assert hasattr(provider, 'destroy_instance')
assert hasattr(provider, "destroy_instance")
assert callable(provider.destroy_instance)
assert hasattr(provider, 'health_check')
assert hasattr(provider, "health_check")
assert callable(provider.health_check)
assert hasattr(provider, 'get_supported_languages')
assert hasattr(provider, "get_supported_languages")
assert callable(provider.get_supported_languages)

View File

@@ -76,9 +76,7 @@ def test_python_builtins_import_is_rejected():
assert is_safe is False
# Pin the specific reason: rejection must come from the new ``builtins``
# entry in ``DANGEROUS_IMPORTS``, not from some unrelated parse error.
assert any("builtins" in issue for issue, _ in issues), (
f"expected an issue mentioning 'builtins', got {issues!r}"
)
assert any("builtins" in issue for issue, _ in issues), f"expected an issue mentioning 'builtins', got {issues!r}"
def test_python_attribute_eval_call_is_rejected():
@@ -94,9 +92,7 @@ def test_python_attribute_eval_call_is_rejected():
# not from the ``import builtins`` line above. We assert ``exec`` is in at
# least one finding so the test fails if visit_Call's attribute branch is
# ever reverted.
assert any("exec" in issue for issue, _ in issues), (
f"expected an issue mentioning 'exec', got {issues!r}"
)
assert any("exec" in issue for issue, _ in issues), f"expected an issue mentioning 'exec', got {issues!r}"
def test_javascript_safe_code_still_passes():

View File

@@ -36,17 +36,14 @@ print("✓ Provider has all required methods")
print("\n[3/5] Testing SDK imports...")
try:
# Check if agentrun SDK is available using importlib
if (
importlib.util.find_spec("agentrun.sandbox") is None
or importlib.util.find_spec("agentrun.utils.config") is None
or importlib.util.find_spec("agentrun.utils.exception") is None
):
if importlib.util.find_spec("agentrun.sandbox") is None or importlib.util.find_spec("agentrun.utils.config") is None or importlib.util.find_spec("agentrun.utils.exception") is None:
raise ImportError("agentrun SDK not found")
# Verify imports work (assign to _ to indicate they're intentionally unused)
from agentrun.sandbox import CodeInterpreterSandbox, TemplateType, CodeLanguage
from agentrun.utils.config import Config
from agentrun.utils.exception import ServerError
_ = (CodeInterpreterSandbox, TemplateType, CodeLanguage, Config, ServerError)
print("✓ SDK modules imported successfully")

42
agent/sandbox/uv.lock generated
View File

@@ -1,6 +1,6 @@
version = 1
revision = 3
requires-python = ">=3.12, <3.15"
requires-python = "==3.13.*"
[[package]]
name = "annotated-doc"
@@ -27,7 +27,6 @@ source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
wheels = [
@@ -61,19 +60,6 @@ version = "3.4.2"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
@@ -278,20 +264,6 @@ dependencies = [
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" },
@@ -353,7 +325,6 @@ version = "0.49.1"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "anyio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1b/3f/507c21db33b66fb027a332f2cb3abbbe924cc3a79ced12f01ed8645955c9/starlette-0.49.1.tar.gz", hash = "sha256:481a43b71e24ed8c43b11ea02f5353d77840e01480881b8cb5a26b8cae64a8cb", size = 2654703, upload-time = "2025-10-28T17:34:10.928Z" }
wheels = [
@@ -409,17 +380,6 @@ version = "1.17.2"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531, upload-time = "2025-01-14T10:35:45.465Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799, upload-time = "2025-01-14T10:33:57.4Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821, upload-time = "2025-01-14T10:33:59.334Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919, upload-time = "2025-01-14T10:34:04.093Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721, upload-time = "2025-01-14T10:34:07.163Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899, upload-time = "2025-01-14T10:34:09.82Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222, upload-time = "2025-01-14T10:34:11.258Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707, upload-time = "2025-01-14T10:34:12.49Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685, upload-time = "2025-01-14T10:34:15.043Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567, upload-time = "2025-01-14T10:34:16.563Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672, upload-time = "2025-01-14T10:34:17.727Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865, upload-time = "2025-01-14T10:34:19.577Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800, upload-time = "2025-01-14T10:34:21.571Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824, upload-time = "2025-01-14T10:34:22.999Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920, upload-time = "2025-01-14T10:34:25.386Z" },

View File

@@ -0,0 +1,304 @@
{
"id": "41",
"title": {
"en": "CAJAL scientific paper agent",
"de": "CAJAL-Agent für wissenschaftliche Arbeiten",
"zh": "CAJAL 科学论文助手"
},
"description": {
"en": "A local-first scientific paper generation agent for RAGFlow. It is preconfigured for Agnuxo/CAJAL-4B-P2PCLAW through Ollama, retrieves knowledge-base evidence, and drafts citation-grounded LaTeX-ready academic sections.",
"de": "Ein lokal ausgerichteter Agent zur Erstellung wissenschaftlicher Arbeiten in RAGFlow. Er ist für Agnuxo/CAJAL-4B-P2PCLAW über Ollama vorkonfiguriert, ruft Evidenz aus der Wissensdatenbank ab und erstellt zitationsgestützte, LaTeX-fähige akademische Abschnitte.",
"zh": "面向 RAGFlow 的本地优先科学论文生成助手。该模板预配置 Agnuxo/CAJAL-4B-P2PCLAWOllama可检索知识库证据并生成带引用依据、适合 LaTeX 的学术章节。"
},
"canvas_type": "Agent",
"canvas_types": [
"Agent",
"Recommended"
],
"dsl": {
"components": {
"Agent:NewPumasLick": {
"downstream": [
"Message:OrangeYearsShine"
],
"obj": {
"component_name": "Agent",
"params": {
"delay_after_error": 1,
"description": "",
"exception_comment": "",
"exception_default_value": "",
"exception_goto": [],
"exception_method": null,
"frequencyPenaltyEnabled": false,
"frequency_penalty": 0.5,
"llm_id": "agnuxo/cajal-4b-p2pclaw@Ollama",
"maxTokensEnabled": true,
"max_retries": 3,
"max_rounds": 3,
"max_tokens": 32768,
"mcp": [],
"message_history_window_size": 12,
"outputs": {
"content": {
"type": "string",
"value": ""
}
},
"parameter": "Precise",
"presencePenaltyEnabled": false,
"presence_penalty": 0.5,
"prompts": [
{
"role": "user",
"content": "# Research request\n{sys.query}\n\nUse the configured knowledge base retrieval tool before drafting. If no evidence is retrieved, state that limitation clearly."
}
],
"sys_prompt": "## Role & Task\nYou are **CAJAL**, a local-first scientific writing agent specialized in citation-grounded paper generation.\n\nUse RAGFlow retrieval results as the source of truth. Write precise academic content that can be pasted into a paper draft, technical report, or LaTeX manuscript. You are optimized for the local GGUF model `Agnuxo/CAJAL-4B-P2PCLAW` served through Ollama as `agnuxo/cajal-4b-p2pclaw`.\n\n## Operating Rules\n1. Decompose the user request into research goals, target section type, expected citation style, and missing evidence.\n2. Retrieve relevant knowledge-base passages before drafting factual claims.\n3. Ground every substantive claim in retrieved evidence. If evidence is missing, mark it as a limitation instead of inventing a citation.\n4. Prefer structured scientific writing: abstract, introduction, related work, methodology, experiments, results, limitations, and conclusion.\n5. Use LaTeX formatting for equations, symbols, algorithms, tables, and section headings when it helps the manuscript.\n6. Preserve traceability: cite source titles, document names, page numbers, or chunk identifiers when available in retrieved context.\n7. Keep language technical, concise, and reproducible. Avoid marketing language and vague generalizations.\n\n## Output Contract\nReturn one of the following, depending on the user request:\n- A complete paper section with citation markers and a short evidence map.\n- A literature review organized by themes, methods, findings, and gaps.\n- A methodology or experiment section with reproducible steps, assumptions, and limitations.\n- A LaTeX-ready abstract, introduction, or conclusion.\n\nAlways include:\n- **Draft**: the requested scientific content.\n- **Evidence used**: concise bullets mapping claims to retrieved sources.\n- **Limitations**: missing evidence, weak support, or assumptions that require verification.\n",
"temperature": 0.2,
"temperatureEnabled": true,
"tools": [
{
"component_name": "Retrieval",
"name": "Retrieval",
"params": {
"cross_languages": [],
"description": "Retrieve papers, reports, datasets, and notes that ground CAJAL scientific writing outputs.",
"empty_response": "",
"kb_ids": [],
"keywords_similarity_weight": 0.7,
"outputs": {
"formalized_content": {
"type": "string",
"value": ""
}
},
"rerank_id": "",
"similarity_threshold": 0.2,
"top_k": 1024,
"top_n": 10,
"use_kg": false
}
}
],
"topPEnabled": false,
"top_p": 0.75,
"user_prompt": "",
"visual_files_var": ""
}
},
"upstream": [
"begin"
]
},
"Message:OrangeYearsShine": {
"downstream": [],
"obj": {
"component_name": "Message",
"params": {
"content": [
"{Agent:NewPumasLick@content}"
]
}
},
"upstream": [
"Agent:NewPumasLick"
]
},
"begin": {
"downstream": [
"Agent:NewPumasLick"
],
"obj": {
"component_name": "Begin",
"params": {
"enablePrologue": true,
"inputs": {},
"mode": "conversational",
"prologue": "Hi, I am CAJAL in RAGFlow. Add research papers or datasets to your knowledge base, then ask me to draft a citation-grounded paper section, literature review, methodology, or LaTeX-ready abstract."
}
},
"upstream": []
}
},
"globals": {
"sys.conversation_turns": 0,
"sys.files": [],
"sys.query": "",
"sys.user_id": ""
},
"graph": {
"edges": [
{
"data": {
"isHovered": false
},
"id": "xy-edge__beginstart-Agent:NewPumasLickend",
"source": "begin",
"sourceHandle": "start",
"target": "Agent:NewPumasLick",
"targetHandle": "end"
},
{
"data": {
"isHovered": false
},
"id": "xy-edge__Agent:NewPumasLickstart-Message:OrangeYearsShineend",
"markerEnd": "logo",
"source": "Agent:NewPumasLick",
"sourceHandle": "start",
"style": {
"stroke": "rgba(91, 93, 106, 1)",
"strokeWidth": 1
},
"target": "Message:OrangeYearsShine",
"targetHandle": "end",
"type": "buttonEdge",
"zIndex": 1001
}
],
"nodes": [
{
"data": {
"form": {
"enablePrologue": true,
"inputs": {},
"mode": "conversational",
"prologue": "Hi, I am CAJAL in RAGFlow. Add research papers or datasets to your knowledge base, then ask me to draft a citation-grounded paper section, literature review, methodology, or LaTeX-ready abstract."
},
"label": "Begin",
"name": "begin"
},
"dragging": false,
"id": "begin",
"measured": {
"height": 48,
"width": 200
},
"position": {
"x": -9.569875358221438,
"y": 205.84018385864917
},
"selected": false,
"sourcePosition": "left",
"targetPosition": "right",
"type": "beginNode"
},
{
"data": {
"form": {
"content": [
"{Agent:NewPumasLick@content}"
]
},
"label": "Scientific Draft",
"name": "Response"
},
"dragging": false,
"id": "Message:OrangeYearsShine",
"measured": {
"height": 56,
"width": 200
},
"position": {
"x": 734.4061285881053,
"y": 199.9706031723009
},
"selected": false,
"sourcePosition": "right",
"targetPosition": "left",
"type": "messageNode"
},
{
"data": {
"form": {
"delay_after_error": 1,
"description": "",
"exception_comment": "",
"exception_default_value": "",
"exception_goto": [],
"exception_method": null,
"frequencyPenaltyEnabled": false,
"frequency_penalty": 0.5,
"llm_id": "agnuxo/cajal-4b-p2pclaw@Ollama",
"maxTokensEnabled": true,
"max_retries": 3,
"max_rounds": 3,
"max_tokens": 32768,
"mcp": [],
"message_history_window_size": 12,
"outputs": {
"content": {
"type": "string",
"value": ""
}
},
"parameter": "Precise",
"presencePenaltyEnabled": false,
"presence_penalty": 0.5,
"prompts": [
{
"role": "user",
"content": "# Research request\n{sys.query}\n\nUse the configured knowledge base retrieval tool before drafting. If no evidence is retrieved, state that limitation clearly."
}
],
"sys_prompt": "## Role & Task\nYou are **CAJAL**, a local-first scientific writing agent specialized in citation-grounded paper generation.\n\nUse RAGFlow retrieval results as the source of truth. Write precise academic content that can be pasted into a paper draft, technical report, or LaTeX manuscript. You are optimized for the local GGUF model `Agnuxo/CAJAL-4B-P2PCLAW` served through Ollama as `agnuxo/cajal-4b-p2pclaw`.\n\n## Operating Rules\n1. Decompose the user request into research goals, target section type, expected citation style, and missing evidence.\n2. Retrieve relevant knowledge-base passages before drafting factual claims.\n3. Ground every substantive claim in retrieved evidence. If evidence is missing, mark it as a limitation instead of inventing a citation.\n4. Prefer structured scientific writing: abstract, introduction, related work, methodology, experiments, results, limitations, and conclusion.\n5. Use LaTeX formatting for equations, symbols, algorithms, tables, and section headings when it helps the manuscript.\n6. Preserve traceability: cite source titles, document names, page numbers, or chunk identifiers when available in retrieved context.\n7. Keep language technical, concise, and reproducible. Avoid marketing language and vague generalizations.\n\n## Output Contract\nReturn one of the following, depending on the user request:\n- A complete paper section with citation markers and a short evidence map.\n- A literature review organized by themes, methods, findings, and gaps.\n- A methodology or experiment section with reproducible steps, assumptions, and limitations.\n- A LaTeX-ready abstract, introduction, or conclusion.\n\nAlways include:\n- **Draft**: the requested scientific content.\n- **Evidence used**: concise bullets mapping claims to retrieved sources.\n- **Limitations**: missing evidence, weak support, or assumptions that require verification.\n",
"temperature": 0.2,
"temperatureEnabled": true,
"tools": [
{
"component_name": "Retrieval",
"name": "Retrieval",
"params": {
"cross_languages": [],
"description": "Retrieve papers, reports, datasets, and notes that ground CAJAL scientific writing outputs.",
"empty_response": "",
"kb_ids": [],
"keywords_similarity_weight": 0.7,
"outputs": {
"formalized_content": {
"type": "string",
"value": ""
}
},
"rerank_id": "",
"similarity_threshold": 0.2,
"top_k": 1024,
"top_n": 10,
"use_kg": false
}
}
],
"topPEnabled": false,
"top_p": 0.75,
"user_prompt": "",
"visual_files_var": ""
},
"label": "CAJAL Writer",
"name": "Knowledge Base Agent"
},
"dragging": false,
"id": "Agent:NewPumasLick",
"measured": {
"height": 84,
"width": 200
},
"position": {
"x": 347.00048227952215,
"y": 186.49109364794631
},
"selected": false,
"sourcePosition": "right",
"targetPosition": "left",
"type": "agentNode"
}
]
},
"history": [],
"memory": [],
"messages": [],
"path": [],
"retrieval": []
},
"avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAH0klEQVR4nO2ZC1BU1wGG/3uRp/IygG+DGK0GOjE1U6cxI4tT03Y0E+kENbaJbKpj60wzgNMwnTjuEtu0miGasY+0krI202kMVEnVxtoOLG00oVa0LajVBDcSEI0REFBgkZv/3GWXfdzdvctuHs7kmzmec9//d+45914XCXc4Xwjk1+59VJGGF7C5QAFSWBvgyWmWLl7IKiny6QNL173B5YjB84bOyrpKA4B1DLySdQpLKAiZGtZ7a/KMVoQJz6UfEZyhTWwaEBmssiLvCueu6BJg8EwFqGTTAC+uvNWC9w82sRWcux/JwaSHstjywcogRt4RG0KExwWG4QsVYCebKSwe3L5lR9OOWjyzfg2WL/0a1/jncO3b2FHxGnKeWYqo+Giu8UEMrWJKWBACPMY/DG+63txhvnKshUu+DF2/hayMDFRsL+VScDb++AVc6OjAuInxXPJl2tfnIikrzUyJMi7qQmLRhOEr2fOFbX/7P6STF7BqoWevfdij4NWGQfx+57OYO2sG1wSnsek8Nm15EU8sikF6ouelXz9ph7JwDqYt+5IIZaGEkauDIrH4wPBmhjexCSEws+VdVG1M4NIoj+2xYzBuJtavWcEl/VS8dggx/ZdQvcGzQwp+cxOXsu5RBQQMVkYJM4LA/Txh+ELFMWFVPARS5kFiabZdx8Olh7l17BzdvhzZmROhdJ3j6D/nIyBgOCMlLAgA9xmF4TMV4BSbrgnrLiBl5rOsRCRRbDUsBzQFiJjY91PCBj9w+yiP1lXWsTLAjc9YQGB9I8+Yx1oTiUWFvW9QgDo2PdASaDp/EQ8/sRnhcPTVcuTMncXwQQVESL9DidscaPW+QEtAICRu9PSxFTpJiePV8AI9AsTvXZBY/Pa+wJ9ApNApIILm8S5Y4QXXQwhYFH6csemDP4G3G5v579i5d04mknknQhDYS4HCrCVr/mC3D305KnbCEpvVIia5Onw6WaWw+KAl0Np+FUXbdiMcyoqfUoeRHoFrJ1uRtnBG1/9Mf/3LtElp+VwF2wcd7woJib1vUPwMH4GWQCQJJtBa/V9cPmFD8uQUpMdNGDhY8bNYrobh8acHu270/l0ImJWRt64Wn6WACN9z5gq2lXwPW8pfweT0icP/fH23vO9QLYq3/QKyLBmFQI3CUcT9NdESEEPItKsSN3r7MBaSJoxHWZERM6ZmMLy2gDP8/pd/og418dTL37hFSUpMUC5f+UiWZcnY9s5+ixCwUiCXx2iiJdDNx6f4pgkH8Q3lbxK7h8+enoHha1cRNdMp8axiHxo6+/5bVdk8DSROYIW1X7QEIom3wHD3gEf4vu1bVYEJZeWQ0zJQvmcfyiv2QZak6raG/QWfK4Ez9mTc5v8xPMJfuojoxXmIX/9DOMe+FCWbcHu4BJJ0YEwCx0824bFNW9HesB+CqYu+jepfPYcHF+aoPXS8sQl/+vU2bgmOU2C+qRc9/YrrPPbGBtzavd0nvCxLxui4pJrBm911PFwak4CYA80cj+JCAiGUzYkmxrSY4N2c3GLi6UEIFL/wRxxqkhmHnTEpDQcrfq6ea+hcE8bNy3GFzyq4H22HW1Kd4WMSkg1jmsSRpKj0Rzhy4gNUv/y8Gjrv8SJK3OWScA+fMn/ysVPPvTmeh6nh1TcxBUJ+jEaKYr7N36x7h+Edj0pB6+WrLokn87+BrTt/p4ZPzZ6MM7/8R2//h33vOcNzdwgBMwVMbGvySQmo4a0NqOZccU7YmGXLEfPQUlUid/XT6B8YdIU/99vjsPcOdEhDsfOd4QVCwKB8yp8SWuG1njbTl83DpMWz1PCKAswuWPDI0e8WebyAJBbxNdrF7cls+hBpAb3h3XtehL/3+4u7D35rQwpP4YFTwMJ91rHpQyQFQgmf9sAMNL9Ur4afv/FBjIuPVj+n4YVTwMD96tj0IVICoYYXv/q1VJ1Sl8UveQyaRwErvOB6B5SwKhqP00gI6A0vhsycJ7/KIzxhyHqGN0ADbnNAAYOicRfCFdAb/p50Gbfuc/wy5w1D5lOghk0fuG0USlgVr7sQjoDe8C8WxKGKPy2KjzlvAQb02/sCbh+FApngX1QUtyeSuwDi0hxFByV7L+LIf3r5kvpp4PBr07Hqvn71Y85bgOG6WS2ggA1+4D6eUKKQApVsqngI6KSkqh9HzsoM/3zg8Oz5VQ9E8wjf30YFDGdkeAsCwH18oYRZGXk7C4HuYxcwe6rjQsFovzaEvoFxqNkTOPzMjGikJso8wsF77XYkLx6dAwxWxvBmBIH7aUMJi8J3w0DnTVz7dyvX6KPzVBt+kL8cmzesRq9ps2Z48bRJmOIapS7E4zM2lXNt5CcU6ID7+ocSZkqY2NRN6ysnsHbJEpR8ZwV6t5Yg+iuLELf2KVd48VwXQf3BQGUMb4ZOuH9gKFEIYJfiNrEDcXZHHV4q3YRv5i7ikgM94RlETNgihrcgBHhccCiRCf7VhBK5rAPyr9I/Y/WKPEyfksH/9NjQ2dODhsYzwcLXsypkeBtCRGLRDUUMAMyKHxEx4dtrzyP97nQMygripiQiKi4aSbPvQmKW7+OXF69ntYvBa1iPCYklZEZECsGm4ja0Ops7EJsaj4SprlU+8IJiqIjAFga3Ikx4vvAYkTGALxyWFArlsnbBC9Sz6mI5zWKNRGh3JJY7mjte4GOz+r4tkRbxQQAAAABJRU5ErkJggg=="
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More