### What problem does this PR solve? #9082 #6365 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.20.0
| @@ -1,45 +0,0 @@ | |||
| English | [简体中文](./README_zh.md) | |||
| # *Graph* | |||
| ## Introduction | |||
| *Graph* is a mathematical concept which is composed of nodes and edges. | |||
| It is used to compose a complex work flow or agent. | |||
| And this graph is beyond the DAG that we can use circles to describe our agent or work flow. | |||
| Under this folder, we propose a test tool ./test/client.py which can test the DSLs such as json files in folder ./test/dsl_examples. | |||
| Please use this client at the same folder you start RAGFlow. If it's run by Docker, please go into the container before running the client. | |||
| Otherwise, correct configurations in service_conf.yaml is essential. | |||
| ```bash | |||
| PYTHONPATH=path/to/ragflow python graph/test/client.py -h | |||
| usage: client.py [-h] -s DSL -t TENANT_ID -m | |||
| options: | |||
| -h, --help show this help message and exit | |||
| -s DSL, --dsl DSL input dsl | |||
| -t TENANT_ID, --tenant_id TENANT_ID | |||
| Tenant ID | |||
| -m, --stream Stream output | |||
| ``` | |||
| <div align="center" style="margin-top:20px;margin-bottom:20px;"> | |||
| <img src="https://github.com/infiniflow/ragflow/assets/12318111/79179c5e-d4d6-464a-b6c4-5721cb329899" width="1000"/> | |||
| </div> | |||
| ## How to gain a TENANT_ID in command line? | |||
| <div align="center" style="margin-top:20px;margin-bottom:20px;"> | |||
| <img src="https://github.com/infiniflow/ragflow/assets/12318111/419d8588-87b1-4ab8-ac49-2d1f047a4b97" width="600"/> | |||
| </div> | |||
| 💡 We plan to display it here in the near future. | |||
| <div align="center" style="margin-top:20px;margin-bottom:20px;"> | |||
| <img src="https://github.com/infiniflow/ragflow/assets/12318111/c97915de-0091-46a5-afd9-e278946e5fe3" width="600"/> | |||
| </div> | |||
| ## How to set 'kb_ids' for component 'Retrieval' in DSL? | |||
| <div align="center" style="margin-top:20px;margin-bottom:20px;"> | |||
| <img src="https://github.com/infiniflow/ragflow/assets/12318111/0a731534-cac8-49fd-8a92-ca247eeef66d" width="600"/> | |||
| </div> | |||
| @@ -1,46 +0,0 @@ | |||
| [English](./README.md) | 简体中文 | |||
| # *Graph* | |||
| ## 简介 | |||
| "Graph"是一个由节点和边组成的数学概念。 | |||
| 它被用来构建复杂的工作流或代理。 | |||
| 这个图超越了有向无环图(DAG),我们可以使用循环来描述我们的代理或工作流。 | |||
| 在这个文件夹下,我们提出了一个测试工具 ./test/client.py, | |||
| 它可以测试像文件夹./test/dsl_examples下一样的DSL文件。 | |||
| 请在启动 RAGFlow 的同一文件夹中使用此客户端。如果它是通过 Docker 运行的,请在运行客户端之前进入容器。 | |||
| 否则,正确配置 service_conf.yaml 文件是必不可少的。 | |||
| ```bash | |||
| PYTHONPATH=path/to/ragflow python graph/test/client.py -h | |||
| usage: client.py [-h] -s DSL -t TENANT_ID -m | |||
| options: | |||
| -h, --help show this help message and exit | |||
| -s DSL, --dsl DSL input dsl | |||
| -t TENANT_ID, --tenant_id TENANT_ID | |||
| Tenant ID | |||
| -m, --stream Stream output | |||
| ``` | |||
| <div align="center" style="margin-top:20px;margin-bottom:20px;"> | |||
| <img src="https://github.com/infiniflow/ragflow/assets/12318111/05924730-c427-495b-8ee4-90b8b2250681" width="1000"/> | |||
| </div> | |||
| ## 命令行中的TENANT_ID如何获得? | |||
| <div align="center" style="margin-top:20px;margin-bottom:20px;"> | |||
| <img src="https://github.com/infiniflow/ragflow/assets/12318111/419d8588-87b1-4ab8-ac49-2d1f047a4b97" width="600"/> | |||
| </div> | |||
| 💡 后面会展示在这里: | |||
| <div align="center" style="margin-top:20px;margin-bottom:20px;"> | |||
| <img src="https://github.com/infiniflow/ragflow/assets/12318111/c97915de-0091-46a5-afd9-e278946e5fe3" width="600"/> | |||
| </div> | |||
| ## DSL里面的Retrieval组件的kb_ids怎么填? | |||
| <div align="center" style="margin-top:20px;margin-bottom:20px;"> | |||
| <img src="https://github.com/infiniflow/ragflow/assets/12318111/0a731534-cac8-49fd-8a92-ca247eeef66d" width="600"/> | |||
| </div> | |||
| @@ -272,6 +272,7 @@ class Canvas: | |||
| "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]) | |||
| }) | |||
| _run_batch(idx, to) | |||
| @@ -523,3 +524,6 @@ class Canvas: | |||
| def get_memory(self) -> list[Tuple]: | |||
| return self.memory | |||
| def get_component_thoughts(self, cpn_id) -> str: | |||
| return self.components.get(cpn_id)["obj"].thoughts() | |||
| @@ -48,10 +48,10 @@ __all__ = list(__all_classes.keys()) + ["__all_classes"] | |||
| del _package_path, _import_submodules, _extract_classes_from_module | |||
| def component_class(class_name): | |||
| m = importlib.import_module("agent.component") | |||
| try: | |||
| return getattr(m, class_name) | |||
| except Exception: | |||
| return getattr(importlib.import_module("agent.tools"), class_name) | |||
| @@ -23,7 +23,6 @@ from typing import Any | |||
| import json_repair | |||
| from agent.component.llm import LLMParam, LLM | |||
| from agent.tools.base import LLMToolPluginCallSession, ToolParamBase, ToolBase, ToolMeta | |||
| from api.db.services.llm_service import LLMBundle, TenantLLMService | |||
| from api.db.services.mcp_server_service import MCPServerService | |||
| @@ -32,6 +31,7 @@ from rag.prompts import message_fit_in | |||
| from rag.prompts.prompts import next_step, COMPLETE_TASK, analyze_task, \ | |||
| citation_prompt, reflect, rank_memories, kb_prompt, citation_plus, full_question | |||
| from rag.utils.mcp_tool_call_conn import MCPToolCallSession, mcp_tool_metadata_to_openai_tool | |||
| from agent.component.llm import LLMParam, LLM | |||
| class AgentParam(LLMParam, ToolParamBase): | |||
| @@ -330,3 +330,4 @@ Respond immediately with your final comprehensive answer. | |||
| logging.exception(e) | |||
| return "Error occurred." | |||
| @@ -13,9 +13,10 @@ | |||
| # See the License for the specific language governing permissions and | |||
| # limitations under the License. | |||
| # | |||
| import re | |||
| import time | |||
| from abc import ABC | |||
| from abc import ABC, abstractmethod | |||
| import builtins | |||
| import json | |||
| import os | |||
| @@ -535,3 +536,6 @@ class ComponentBase(ABC): | |||
| def get_exception_default_value(self): | |||
| return self._param.exception_default_value | |||
| @abstractmethod | |||
| def thoughts(self) -> str: | |||
| ... | |||
| @@ -44,3 +44,6 @@ class Begin(UserFillUp): | |||
| v = v.get("value") | |||
| self.set_output(k, v) | |||
| self.set_input_value(k, v) | |||
| def thoughts(self) -> str: | |||
| return "☕ Here we go..." | |||
| @@ -20,7 +20,7 @@ from abc import ABC | |||
| from api.db import LLMType | |||
| from api.db.services.llm_service import LLMBundle | |||
| from agent.component import LLMParam, LLM | |||
| from agent.component.llm import LLMParam, LLM | |||
| from api.utils.api_utils import timeout | |||
| from rag.llm.chat_model import ERROR_PREFIX | |||
| @@ -133,3 +133,5 @@ class Categorize(LLM, ABC): | |||
| self.set_output("category_name", max_category) | |||
| self.set_output("_next", cpn_ids) | |||
| def thoughts(self) -> str: | |||
| return "Which should it falls into {}? ...".format(",".join([f"`{c}`" for c, _ in self._param.category_description.items()])) | |||
| @@ -34,6 +34,7 @@ class UserFillUp(ComponentBase): | |||
| for k, v in kwargs.get("inputs", {}).items(): | |||
| self.set_output(k, v) | |||
| def thoughts(self) -> str: | |||
| return "Waiting for your input..." | |||
| @@ -137,3 +137,6 @@ class Invoke(ComponentBase, ABC): | |||
| return f"Http request error: {last_e}" | |||
| assert False, self.output() | |||
| def thoughts(self) -> str: | |||
| return "Waiting for the server respond..." | |||
| @@ -53,6 +53,8 @@ class Iteration(ComponentBase, ABC): | |||
| if not isinstance(arr, list): | |||
| 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))) | |||
| @@ -79,3 +79,5 @@ class IterationItem(ComponentBase, ABC): | |||
| def end(self): | |||
| return self._idx == -1 | |||
| def thoughts(self) -> str: | |||
| return "Next turn..." | |||
| @@ -240,3 +240,7 @@ class LLM(ComponentBase): | |||
| summ = tool_call_summary(self.chat_mdl, func_name, params, results) | |||
| logging.info(f"[MEMORY]: {summ}") | |||
| self._canvas.add_memory(user, assist, summ) | |||
| def thoughts(self) -> str: | |||
| _, msg = self._prepare_prompt_variables() | |||
| return f"I’m thinking and planning the next move, starting from the prompt:<br/>“{msg[-1]['content']}”<span class=\"collapse\"> (tap to see full text)</span>" | |||
| @@ -142,3 +142,5 @@ class Message(ComponentBase): | |||
| self.set_output("content", content) | |||
| def thoughts(self) -> str: | |||
| return "Thinking ..." | |||
| @@ -94,5 +94,7 @@ class StringTransform(Message, ABC): | |||
| self.set_output("result", script) | |||
| def thoughts(self) -> str: | |||
| return f"It's {self._param.method}ing." | |||
| @@ -125,4 +125,7 @@ class Switch(ComponentBase, ABC): | |||
| except Exception: | |||
| return True if input <= value else False | |||
| raise ValueError('Not supported operator' + operator) | |||
| raise ValueError('Not supported operator' + operator) | |||
| def thoughts(self) -> str: | |||
| return "I’m weighing a few options and will pick the next step shortly." | |||
| @@ -1,3 +1,18 @@ | |||
| # | |||
| # Copyright 2025 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 os | |||
| import importlib | |||
| import inspect | |||
| @@ -94,3 +94,9 @@ class ArXiv(ToolBase, ABC): | |||
| return f"ArXiv error: {last_e}" | |||
| assert False, self.output() | |||
| def thoughts(self) -> str: | |||
| return """ | |||
| Keywords: {} | |||
| Looking for the most relevant articles. | |||
| """.format(self.get_input().get("query", "-_-!")) | |||
| @@ -165,3 +165,6 @@ class ToolBase(ComponentBase): | |||
| }) | |||
| self._canvas.add_refernce(chunks, aggs) | |||
| self.set_output("formalized_content", "\n".join(kb_prompt({"chunks": chunks, "doc_aggs": aggs}, 200000, True))) | |||
| def thoughts(self) -> str: | |||
| return self._canvas.get_component_name(self._id) + " is running..." | |||
| @@ -189,4 +189,5 @@ class CodeExec(ToolBase, ABC): | |||
| def _encode_code(self, code: str) -> str: | |||
| return base64.b64encode(code.encode("utf-8")).decode("utf-8") | |||
| def thoughts(self) -> str: | |||
| return "Running a short script to process data." | |||
| @@ -112,3 +112,9 @@ class DuckDuckGo(ToolBase, ABC): | |||
| return f"DuckDuckGo error: {last_e}" | |||
| assert False, self.output() | |||
| def thoughts(self) -> str: | |||
| return """ | |||
| Keywords: {} | |||
| Looking for the most relevant articles. | |||
| """.format(self.get_input().get("query", "-_-!")) | |||
| @@ -204,4 +204,12 @@ class Email(ToolBase, ABC): | |||
| self.set_output("_ERROR", str(last_e)) | |||
| return False | |||
| assert False, self.output() | |||
| assert False, self.output() | |||
| def thoughts(self) -> str: | |||
| inputs = self.get_input() | |||
| return """ | |||
| To: {} | |||
| Subject: {} | |||
| Your email is on its way—sit tight! | |||
| """.format(inputs.get("to_email", "-_-!"), inputs.get("subject", "-_-!")) | |||
| @@ -128,6 +128,5 @@ class ExeSQL(ToolBase, ABC): | |||
| self.set_output("formalized_content", "\n\n".join(formalized_content)) | |||
| return self.output("formalized_content") | |||
| def debug(self, **kwargs): | |||
| return self._run([], **kwargs) | |||
| def thoughts(self) -> str: | |||
| return "Query sent—waiting for the data." | |||
| @@ -86,3 +86,6 @@ class GitHub(ToolBase, ABC): | |||
| return f"GitHub error: {last_e}" | |||
| assert False, self.output() | |||
| def thoughts(self) -> str: | |||
| return "Scanning GitHub repos related to `{}`.".format(self.get_input().get("query", "-_-!")) | |||
| @@ -152,3 +152,8 @@ class Google(ToolBase, ABC): | |||
| assert False, self.output() | |||
| def thoughts(self) -> str: | |||
| return """ | |||
| Keywords: {} | |||
| Looking for the most relevant articles. | |||
| """.format(self.get_input().get("query", "-_-!")) | |||
| @@ -91,3 +91,6 @@ class GoogleScholar(ToolBase, ABC): | |||
| return f"GoogleScholar error: {last_e}" | |||
| assert False, self.output() | |||
| def thoughts(self) -> str: | |||
| return "Looking for scholarly papers on `{}`,” prioritising reputable sources.".format(self.get_input().get("query", "-_-!")) | |||
| @@ -103,3 +103,6 @@ class PubMed(ToolBase, ABC): | |||
| return f"PubMed error: {last_e}" | |||
| assert False, self.output() | |||
| def thoughts(self) -> str: | |||
| return "Looking for scholarly papers on `{}`,” prioritising reputable sources.".format(self.get_input().get("query", "-_-!")) | |||
| @@ -159,3 +159,9 @@ class Retrieval(ToolBase, ABC): | |||
| form_cnt = "\n".join(kb_prompt(kbinfos, 200000, True)) | |||
| self.set_output("formalized_content", form_cnt) | |||
| return form_cnt | |||
| def thoughts(self) -> str: | |||
| return """ | |||
| Keywords: {} | |||
| Looking for the most relevant articles. | |||
| """.format(self.get_input().get("query", "-_-!")) | |||
| @@ -134,6 +134,12 @@ class TavilySearch(ToolBase, ABC): | |||
| assert False, self.output() | |||
| def thoughts(self) -> str: | |||
| return """ | |||
| Keywords: {} | |||
| Looking for the most relevant articles. | |||
| """.format(self.get_input().get("query", "-_-!")) | |||
| class TavilyExtractParam(ToolParamBase): | |||
| """ | |||
| @@ -216,3 +222,6 @@ class TavilyExtract(ToolBase, ABC): | |||
| return f"Tavily error: {last_e}" | |||
| assert False, self.output() | |||
| def thoughts(self) -> str: | |||
| return "Opened {}—pulling out the main text…".format(self.get_input().get("urls", "-_-!")) | |||
| @@ -109,3 +109,6 @@ class WenCai(ToolBase, ABC): | |||
| return f"WenCai error: {last_e}" | |||
| assert False, self.output() | |||
| def thoughts(self) -> str: | |||
| return "Pulling live financial data for `{}`.".format(self.get_input().get("query", "-_-!")) | |||
| @@ -96,3 +96,9 @@ class Wikipedia(ToolBase, ABC): | |||
| return f"Wikipedia error: {last_e}" | |||
| assert False, self.output() | |||
| def thoughts(self) -> str: | |||
| return """ | |||
| Keywords: {} | |||
| Looking for the most relevant articles. | |||
| """.format(self.get_input().get("query", "-_-!")) | |||
| @@ -109,3 +109,6 @@ class YahooFinance(ToolBase, ABC): | |||
| return f"YahooFinance error: {last_e}" | |||
| assert False, self.output() | |||
| def thoughts(self) -> str: | |||
| return "Pulling live financial data for `{}`.".format(self.get_input().get("stock_code", "-_-!")) | |||