diff --git a/api/apps/canvas_app.py b/api/apps/canvas_app.py index 8c896e36add..811d9870f91 100644 --- a/api/apps/canvas_app.py +++ b/api/apps/canvas_app.py @@ -13,330 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import copy -import inspect -import json import logging -from functools import partial -from quart import request, Response, make_response -from agent.component import LLM -from api.db import CanvasCategory -from api.db.services.canvas_service import CanvasTemplateService, UserCanvasService, API4ConversationService -from api.db.services.document_service import DocumentService -from api.db.services.file_service import FileService -from api.db.services.knowledgebase_service import KnowledgebaseService -from api.db.services.pipeline_operation_log_service import PipelineOperationLogService -from api.db.services.task_service import queue_dataflow, CANVAS_DEBUG_DOC_ID, TaskService -from api.db.services.user_service import TenantService -from api.db.services.user_canvas_version import UserCanvasVersionService -from common.constants import RetCode -from common.misc_utils import get_uuid, thread_pool_exec -from api.utils.api_utils import ( - get_json_result, - server_error_response, - validate_request, - get_data_error_result, - get_request_json, -) -from agent.canvas import Canvas -from agent.dsl_migration import normalize_chunker_dsl -from peewee import MySQLDatabase, PostgresqlDatabase -from api.db.db_models import APIToken, Task - -from rag.flow.pipeline import Pipeline -from rag.nlp import search +from api.utils.api_utils import get_json_result from rag.utils.redis_conn import REDIS_CONN -from common import settings -from api.apps import login_required, current_user -from api.apps.services.canvas_replica_service import CanvasReplicaService -from api.db.services.canvas_service import completion as agent_completion - - -@manager.route('/templates', methods=['GET']) # noqa: F821 -@login_required -def templates(): - return get_json_result(data=[c.to_dict() for c in CanvasTemplateService.get_all()]) - - -@manager.route('/rm', methods=['POST']) # noqa: F821 -@validate_request("canvas_ids") -@login_required -async def rm(): - req = await get_request_json() - for i in req["canvas_ids"]: - if not UserCanvasService.accessible(i, current_user.id): - return get_json_result( - data=False, message='Only owner of canvas authorized for this operation.', - code=RetCode.OPERATING_ERROR) - UserCanvasService.delete_by_id(i) - return get_json_result(data=True) - - -@manager.route('/set', methods=['POST']) # noqa: F821 -@validate_request("dsl", "title") -@login_required -async def save(): - req = await get_request_json() - req['release'] = bool(req.get("release", "")) - try: - req["dsl"] = CanvasReplicaService.normalize_dsl(req["dsl"]) - except ValueError as e: - return get_data_error_result(message=str(e)) - cate = req.get("canvas_category", CanvasCategory.Agent) - if "id" not in req: - req["user_id"] = current_user.id - if UserCanvasService.query(user_id=current_user.id, title=req["title"].strip(), canvas_category=cate): - return get_data_error_result(message=f"{req['title'].strip()} already exists.") - req["id"] = get_uuid() - if not UserCanvasService.save(**req): - return get_data_error_result(message="Fail to save canvas.") - else: - if not UserCanvasService.accessible(req["id"], current_user.id): - return get_json_result( - data=False, message='Only owner of canvas authorized for this operation.', - code=RetCode.OPERATING_ERROR) - UserCanvasService.update_by_id(req["id"], req) - # save version - UserCanvasVersionService.save_or_replace_latest( - user_canvas_id=req["id"], - dsl=req["dsl"], - title=UserCanvasVersionService.build_version_title(getattr(current_user, "nickname", current_user.id), req.get("title")), - release=req.get("release"), - ) - replica_ok = CanvasReplicaService.replace_for_set( - canvas_id=req["id"], - tenant_id=str(current_user.id), - runtime_user_id=str(current_user.id), - dsl=req["dsl"], - canvas_category=req.get("canvas_category", cate), - title=req.get("title", ""), - ) - if not replica_ok: - return get_data_error_result(message="canvas saved, but replica sync failed.") - return get_json_result(data=req) - - -@manager.route('/get/', methods=['GET']) # noqa: F821 -@login_required -def get(canvas_id): - if not UserCanvasService.accessible(canvas_id, current_user.id): - return get_data_error_result(message="canvas not found.") - e, c = UserCanvasService.get_by_canvas_id(canvas_id) - if not e: - return get_data_error_result(message="canvas not found.") - try: - # DELETE - CanvasReplicaService.bootstrap( - canvas_id=canvas_id, - tenant_id=str(current_user.id), - runtime_user_id=str(current_user.id), - dsl=c.get("dsl"), - canvas_category=c.get("canvas_category", CanvasCategory.Agent), - title=c.get("title", ""), - ) - except ValueError as e: - return get_data_error_result(message=str(e)) - - # Get the last publication time (latest released version's update_time) - last_publish_time = None - versions = UserCanvasVersionService.list_by_canvas_id(canvas_id) - if versions: - released_versions = [v for v in versions if v.release] - if released_versions: - # Sort by update_time descending and get the latest - released_versions.sort(key=lambda x: x.update_time, reverse=True) - last_publish_time = released_versions[0].update_time - - # Add last_publish_time to response data - if isinstance(c, dict): - c["dsl"] = normalize_chunker_dsl(c.get("dsl", {})) - c["last_publish_time"] = last_publish_time - else: - # If c is a model object, convert to dict first - c = c.to_dict() - c["dsl"] = normalize_chunker_dsl(c.get("dsl", {})) - c["last_publish_time"] = last_publish_time - - # For pipeline type, get associated datasets - if c.get("canvas_category") == CanvasCategory.DataFlow: - datasets = list(KnowledgebaseService.query(pipeline_id=canvas_id)) - c["datasets"] = [{"id": d.id, "name": d.name, "avatar": d.avatar} for d in datasets] - - return get_json_result(data=c) - - -@manager.route('/getsse/', methods=['GET']) # type: ignore # noqa: F821 -def getsse(canvas_id): - token = request.headers.get('Authorization').split() - if len(token) != 2: - return get_data_error_result(message='Authorization is not valid!') - token = token[1] - objs = APIToken.query(beta=token) - if not objs: - return get_data_error_result(message='Authentication error: API key is invalid!"') - tenant_id = objs[0].tenant_id - if not UserCanvasService.query(user_id=tenant_id, id=canvas_id): - return get_json_result( - data=False, - message='Only owner of canvas authorized for this operation.', - code=RetCode.OPERATING_ERROR - ) - e, c = UserCanvasService.get_by_id(canvas_id) - if not e or c.user_id != tenant_id: - return get_data_error_result(message="canvas not found.") - return get_json_result(data=c.to_dict()) - - -@manager.route('/completion', methods=['POST']) # noqa: F821 -@validate_request("id") -@login_required -async def run(): - req = await get_request_json() - query = req.get("query", "") - files = req.get("files", []) - inputs = req.get("inputs", {}) - tenant_id = str(current_user.id) - runtime_user_id = req.get("user_id") or tenant_id - user_id = str(runtime_user_id) - if not await thread_pool_exec(UserCanvasService.accessible, req["id"], tenant_id): - return get_json_result( - data=False, message='Only owner of canvas authorized for this operation.', - code=RetCode.OPERATING_ERROR) - - replica_payload = CanvasReplicaService.load_for_run( - canvas_id=req["id"], - tenant_id=tenant_id, - runtime_user_id=user_id, - ) - - if not replica_payload: - return get_data_error_result(message="canvas replica not found, please call /get/ first.") - - replica_dsl = replica_payload.get("dsl", {}) - canvas_title = replica_payload.get("title", "") - canvas_category = replica_payload.get("canvas_category", CanvasCategory.Agent) - dsl_str = json.dumps(replica_dsl, ensure_ascii=False) - - _, cvs = await thread_pool_exec(UserCanvasService.get_by_id, req["id"]) - if cvs.canvas_category == CanvasCategory.DataFlow: - task_id = get_uuid() - Pipeline(dsl_str, tenant_id=tenant_id, doc_id=CANVAS_DEBUG_DOC_ID, task_id=task_id, flow_id=req["id"]) - ok, error_message = await thread_pool_exec(queue_dataflow, user_id, req["id"], task_id, CANVAS_DEBUG_DOC_ID, files[0], 0) - if not ok: - return get_data_error_result(message=error_message) - return get_json_result(data={"message_id": task_id}) - - try: - canvas = Canvas(dsl_str, tenant_id, canvas_id=req["id"]) - except Exception as e: - return server_error_response(e) - - async def sse(): - nonlocal canvas, user_id - try: - async for ans in canvas.run(query=query, files=files, user_id=user_id, inputs=inputs): - yield "data:" + json.dumps(ans, ensure_ascii=False) + "\n\n" - - commit_ok = CanvasReplicaService.commit_after_run( - canvas_id=req["id"], - tenant_id=tenant_id, - runtime_user_id=user_id, - dsl=json.loads(str(canvas)), - canvas_category=canvas_category, - title=canvas_title, - ) - if not commit_ok: - logging.error( - "Canvas runtime replica commit failed: canvas_id=%s tenant_id=%s runtime_user_id=%s", - req["id"], - tenant_id, - user_id, - ) - - except Exception as e: - logging.exception(e) - canvas.cancel_task() - yield "data:" + json.dumps({"code": 500, "message": str(e), "data": False}, ensure_ascii=False) + "\n\n" - - resp = Response(sse(), mimetype="text/event-stream") - resp.headers.add_header("Cache-control", "no-cache") - resp.headers.add_header("Connection", "keep-alive") - resp.headers.add_header("X-Accel-Buffering", "no") - resp.headers.add_header("Content-Type", "text/event-stream; charset=utf-8") - #resp.call_on_close(lambda: canvas.cancel_task()) - return resp - - -@manager.route("//completion", methods=["POST"]) # noqa: F821 -@login_required -async def exp_agent_completion(canvas_id): - tenant_id = current_user.id - req = await get_request_json() - return_trace = bool(req.get("return_trace", False)) - async def generate(): - trace_items = [] - async for answer in agent_completion(tenant_id=tenant_id, agent_id=canvas_id, **req): - if isinstance(answer, str): - try: - ans = json.loads(answer[5:]) # remove "data:" - except Exception: - continue - - event = ans.get("event") - if event == "node_finished": - if return_trace: - data = ans.get("data", {}) - trace_items.append( - { - "component_id": data.get("component_id"), - "trace": [copy.deepcopy(data)], - } - ) - ans.setdefault("data", {})["trace"] = trace_items - answer = "data:" + json.dumps(ans, ensure_ascii=False) + "\n\n" - yield answer - - if event not in ["message", "message_end"]: - continue - - yield answer - - yield "data:[DONE]\n\n" - - resp = Response(generate(), mimetype="text/event-stream") - resp.headers.add_header("Cache-control", "no-cache") - resp.headers.add_header("Connection", "keep-alive") - resp.headers.add_header("X-Accel-Buffering", "no") - resp.headers.add_header("Content-Type", "text/event-stream; charset=utf-8") - return resp - - -@manager.route('/rerun', methods=['POST']) # noqa: F821 -@validate_request("id", "dsl", "component_id") -@login_required -async def rerun(): - req = await get_request_json() - doc = PipelineOperationLogService.get_documents_info(req["id"]) - if not doc: - return get_data_error_result(message="Document not found.") - doc = doc[0] - if 0 < doc["progress"] < 1: - return get_data_error_result(message=f"`{doc['name']}` is processing...") - - if settings.docStoreConn.index_exist(search.index_name(current_user.id), doc["kb_id"]): - settings.docStoreConn.delete({"doc_id": doc["id"]}, search.index_name(current_user.id), doc["kb_id"]) - doc["progress_msg"] = "" - doc["chunk_num"] = 0 - doc["token_num"] = 0 - DocumentService.clear_chunk_num_when_rerun(doc["id"]) - DocumentService.update_by_id(id, doc) - TaskService.filter_delete([Task.doc_id == id]) - - dsl = req["dsl"] - dsl["path"] = [req["component_id"]] - PipelineOperationLogService.update_by_id(req["id"], {"dsl": dsl}) - queue_dataflow(tenant_id=current_user.id, flow_id=req["id"], task_id=get_uuid(), doc_id=doc["id"], priority=0, rerun=True) - return get_json_result(data=True) +from api.apps import login_required @manager.route('/cancel/', methods=['PUT']) # noqa: F821 @@ -347,409 +27,3 @@ def cancel(task_id): except Exception as e: logging.exception(e) return get_json_result(data=True) - - -@manager.route('/reset', methods=['POST']) # noqa: F821 -@validate_request("id") -@login_required -async def reset(): - req = await get_request_json() - if not UserCanvasService.accessible(req["id"], current_user.id): - return get_json_result( - data=False, message='Only owner of canvas authorized for this operation.', - code=RetCode.OPERATING_ERROR) - try: - e, user_canvas = UserCanvasService.get_by_id(req["id"]) - if not e: - return get_data_error_result(message="canvas not found.") - - canvas = Canvas(json.dumps(user_canvas.dsl), current_user.id, canvas_id=user_canvas.id) - canvas.reset() - req["dsl"] = json.loads(str(canvas)) - UserCanvasService.update_by_id(req["id"], {"dsl": req["dsl"]}) - return get_json_result(data=req["dsl"]) - except Exception as e: - return server_error_response(e) - - -@manager.route("/upload/", methods=["POST"]) # noqa: F821 -async def upload(canvas_id): - e, cvs = UserCanvasService.get_by_canvas_id(canvas_id) - if not e: - return get_data_error_result(message="canvas not found.") - - user_id = cvs["user_id"] - files = await request.files - file_objs = files.getlist("file") if files and files.get("file") else [] - try: - if len(file_objs) == 1: - return get_json_result(data=FileService.upload_info(user_id, file_objs[0], request.args.get("url"))) - results = [FileService.upload_info(user_id, f) for f in file_objs] - return get_json_result(data=results) - except Exception as e: - return server_error_response(e) - - -@manager.route('/input_form', methods=['GET']) # noqa: F821 -@login_required -def input_form(): - cvs_id = request.args.get("id") - cpn_id = request.args.get("component_id") - try: - e, user_canvas = UserCanvasService.get_by_id(cvs_id) - if not e: - return get_data_error_result(message="canvas not found.") - if not UserCanvasService.query(user_id=current_user.id, id=cvs_id): - return get_json_result( - data=False, message='Only owner of canvas authorized for this operation.', - code=RetCode.OPERATING_ERROR) - - canvas = Canvas(json.dumps(user_canvas.dsl), current_user.id, canvas_id=user_canvas.id) - return get_json_result(data=canvas.get_component_input_form(cpn_id)) - except Exception as e: - return server_error_response(e) - - -@manager.route('/debug', methods=['POST']) # noqa: F821 -@validate_request("id", "component_id", "params") -@login_required -async def debug(): - req = await get_request_json() - if not UserCanvasService.accessible(req["id"], current_user.id): - return get_json_result( - data=False, message='Only owner of canvas authorized for this operation.', - code=RetCode.OPERATING_ERROR) - try: - e, user_canvas = UserCanvasService.get_by_id(req["id"]) - canvas = Canvas(json.dumps(user_canvas.dsl), current_user.id, canvas_id=user_canvas.id) - canvas.reset() - canvas.message_id = get_uuid() - component = canvas.get_component(req["component_id"])["obj"] - component.reset() - - if isinstance(component, LLM): - component.set_debug_inputs(req["params"]) - component.invoke(**{k: o["value"] for k,o in req["params"].items()}) - outputs = component.output() - for k in outputs.keys(): - if isinstance(outputs[k], partial): - txt = "" - iter_obj = outputs[k]() - if inspect.isasyncgen(iter_obj): - async for c in iter_obj: - txt += c - else: - for c in iter_obj: - txt += c - outputs[k] = txt - return get_json_result(data=outputs) - except Exception as e: - return server_error_response(e) - - -@manager.route('/test_db_connect', methods=['POST']) # noqa: F821 -@validate_request("db_type", "database", "username", "host", "port", "password") -@login_required -async def test_db_connect(): - req = await get_request_json() - try: - if req["db_type"] in ["mysql", "mariadb"]: - db = MySQLDatabase(req["database"], user=req["username"], host=req["host"], port=req["port"], - password=req["password"]) - elif req["db_type"] == "oceanbase": - db = MySQLDatabase(req["database"], user=req["username"], host=req["host"], port=req["port"], - password=req["password"], charset="utf8mb4") - elif req["db_type"] == 'postgres': - db = PostgresqlDatabase(req["database"], user=req["username"], host=req["host"], port=req["port"], - password=req["password"]) - elif req["db_type"] == 'mssql': - import pyodbc - connection_string = ( - f"DRIVER={{ODBC Driver 17 for SQL Server}};" - f"SERVER={req['host']},{req['port']};" - f"DATABASE={req['database']};" - f"UID={req['username']};" - f"PWD={req['password']};" - ) - db = pyodbc.connect(connection_string) - cursor = db.cursor() - cursor.execute("SELECT 1") - cursor.close() - elif req["db_type"] == 'IBM DB2': - import ibm_db - conn_str = ( - f"DATABASE={req['database']};" - f"HOSTNAME={req['host']};" - f"PORT={req['port']};" - f"PROTOCOL=TCPIP;" - f"UID={req['username']};" - f"PWD={req['password']};" - ) - redacted_conn_str = ( - f"DATABASE={req['database']};" - f"HOSTNAME={req['host']};" - f"PORT={req['port']};" - f"PROTOCOL=TCPIP;" - f"UID={req['username']};" - f"PWD=****;" - ) - logging.info(redacted_conn_str) - conn = ibm_db.connect(conn_str, "", "") - stmt = ibm_db.exec_immediate(conn, "SELECT 1 FROM sysibm.sysdummy1") - ibm_db.fetch_assoc(stmt) - ibm_db.close(conn) - return get_json_result(data="Database Connection Successful!") - elif req["db_type"] == 'trino': - def _parse_catalog_schema(db_name: str): - if not db_name: - return None, None - if "." in db_name: - catalog_name, schema_name = db_name.split(".", 1) - elif "/" in db_name: - catalog_name, schema_name = db_name.split("/", 1) - else: - catalog_name, schema_name = db_name, "default" - return catalog_name, schema_name - try: - import trino - import os - except Exception as e: - return server_error_response(f"Missing dependency 'trino'. Please install: pip install trino, detail: {e}") - - catalog, schema = _parse_catalog_schema(req["database"]) - if not catalog: - return server_error_response("For Trino, 'database' must be 'catalog.schema' or at least 'catalog'.") - - http_scheme = "https" if os.environ.get("TRINO_USE_TLS", "0") == "1" else "http" - - auth = None - if http_scheme == "https" and req.get("password"): - auth = trino.BasicAuthentication(req.get("username") or "ragflow", req["password"]) - - conn = trino.dbapi.connect( - host=req["host"], - port=int(req["port"] or 8080), - user=req["username"] or "ragflow", - catalog=catalog, - schema=schema or "default", - http_scheme=http_scheme, - auth=auth - ) - cur = conn.cursor() - cur.execute("SELECT 1") - cur.fetchall() - cur.close() - conn.close() - return get_json_result(data="Database Connection Successful!") - else: - return server_error_response("Unsupported database type.") - if req["db_type"] != 'mssql': - db.connect() - db.close() - - return get_json_result(data="Database Connection Successful!") - except Exception as e: - return server_error_response(e) - - -#api get list version dsl of canvas -@manager.route('/getlistversion/', methods=['GET']) # noqa: F821 -@login_required -def getlistversion(canvas_id): - try: - versions =sorted([c.to_dict() for c in UserCanvasVersionService.list_by_canvas_id(canvas_id)], key=lambda x: x["update_time"]*-1) - return get_json_result(data=versions) - except Exception as e: - return get_data_error_result(message=f"Error getting history files: {e}") - - -#api get version dsl of canvas -@manager.route('/getversion/', methods=['GET']) # noqa: F821 -@login_required -def getversion( version_id): - try: - e, version = UserCanvasVersionService.get_by_id(version_id) - if version: - return get_json_result(data=version.to_dict()) - except Exception as e: - return get_json_result(data=f"Error getting history file: {e}") - - -@manager.route('/list', methods=['GET']) # noqa: F821 -@login_required -def list_canvas(): - keywords = request.args.get("keywords", "") - page_number = int(request.args.get("page", 0)) - items_per_page = int(request.args.get("page_size", 0)) - orderby = request.args.get("orderby", "create_time") - canvas_category = request.args.get("canvas_category") - if request.args.get("desc", "true").lower() == "false": - desc = False - else: - desc = True - owner_ids = [id for id in request.args.get("owner_ids", "").strip().split(",") if id] - if not owner_ids: - tenants = TenantService.get_joined_tenants_by_user_id(current_user.id) - tenants = [m["tenant_id"] for m in tenants] - tenants.append(current_user.id) - canvas, total = UserCanvasService.get_by_tenant_ids( - tenants, current_user.id, page_number, - items_per_page, orderby, desc, keywords, canvas_category) - else: - tenants = owner_ids - canvas, total = UserCanvasService.get_by_tenant_ids( - tenants, current_user.id, 0, - 0, orderby, desc, keywords, canvas_category) - return get_json_result(data={"canvas": canvas, "total": total}) - - -@manager.route('/setting', methods=['POST']) # noqa: F821 -@validate_request("id", "title", "permission") -@login_required -async def setting(): - req = await get_request_json() - req["user_id"] = current_user.id - - if not UserCanvasService.accessible(req["id"], current_user.id): - return get_json_result( - data=False, message='Only owner of canvas authorized for this operation.', - code=RetCode.OPERATING_ERROR) - - e,flow = UserCanvasService.get_by_id(req["id"]) - if not e: - return get_data_error_result(message="canvas not found.") - flow = flow.to_dict() - flow["title"] = req["title"] - - for key in ["description", "permission", "avatar"]: - if value := req.get(key): - flow[key] = value - - num= UserCanvasService.update_by_id(req["id"], flow) - return get_json_result(data=num) - - -@manager.route('/trace', methods=['GET']) # noqa: F821 -def trace(): - cvs_id = request.args.get("canvas_id") - msg_id = request.args.get("message_id") - try: - binary = REDIS_CONN.get(f"{cvs_id}-{msg_id}-logs") - if not binary: - return get_json_result(data={}) - - return get_json_result(data=json.loads(binary.encode("utf-8"))) - except Exception as e: - logging.exception(e) - - -@manager.route('//sessions', methods=['GET']) # noqa: F821 -@login_required -def sessions(canvas_id): - tenant_id = current_user.id - if not UserCanvasService.accessible(canvas_id, tenant_id): - return get_json_result( - data=False, message='Only owner of canvas authorized for this operation.', - code=RetCode.OPERATING_ERROR) - - user_id = request.args.get("user_id") - page_number = int(request.args.get("page", 1)) - items_per_page = int(request.args.get("page_size", 30)) - keywords = request.args.get("keywords") - from_date = request.args.get("from_date") - to_date = request.args.get("to_date") - orderby = request.args.get("orderby", "update_time") - exp_user_id = request.args.get("exp_user_id") - if request.args.get("desc") == "False" or request.args.get("desc") == "false": - desc = False - else: - desc = True - - if exp_user_id: - sess = API4ConversationService.get_names(canvas_id, exp_user_id) - return get_json_result(data={"total": len(sess), "sessions": sess}) - - # dsl defaults to True in all cases except for False and false - include_dsl = request.args.get("dsl") != "False" and request.args.get("dsl") != "false" - total, sess = API4ConversationService.get_list(canvas_id, tenant_id, page_number, items_per_page, orderby, desc, - None, user_id, include_dsl, keywords, from_date, to_date, exp_user_id=exp_user_id) - try: - return get_json_result(data={"total": total, "sessions": sess}) - except Exception as e: - return server_error_response(e) - - -@manager.route('//sessions', methods=['PUT']) # noqa: F821 -@login_required -async def set_session(canvas_id): - req = await get_request_json() - tenant_id = current_user.id - e, cvs = UserCanvasService.get_by_id(canvas_id) - assert e, "Agent not found." - if not isinstance(cvs.dsl, str): - cvs.dsl = json.dumps(cvs.dsl, ensure_ascii=False) - session_id=get_uuid() - canvas = Canvas(cvs.dsl, tenant_id, canvas_id, canvas_id=cvs.id) - canvas.reset() - # Get the version title for this canvas (using latest, not necessarily released) - version_title = UserCanvasVersionService.get_latest_version_title(cvs.id, release_mode=False) - conv = { - "id": session_id, - "name": req.get("name", ""), - "dialog_id": cvs.id, - "user_id": tenant_id, - "exp_user_id": tenant_id, - "message": [], - "source": "agent", - "dsl": cvs.dsl, - "reference": [], - "version_title": version_title - } - API4ConversationService.save(**conv) - return get_json_result(data=conv) - - -@manager.route('//sessions/', methods=['GET']) # noqa: F821 -@login_required -def get_session(canvas_id, session_id): - tenant_id = current_user.id - if not UserCanvasService.accessible(canvas_id, tenant_id): - return get_json_result( - data=False, message='Only owner of canvas authorized for this operation.', - code=RetCode.OPERATING_ERROR) - _, conv = API4ConversationService.get_by_id(session_id) - return get_json_result(data=conv.to_dict()) - - -@manager.route('//sessions/', methods=['DELETE']) # noqa: F821 -@login_required -def del_session(canvas_id, session_id): - tenant_id = current_user.id - if not UserCanvasService.accessible(canvas_id, tenant_id): - return get_json_result( - data=False, message='Only owner of canvas authorized for this operation.', - code=RetCode.OPERATING_ERROR) - return get_json_result(data=API4ConversationService.delete_by_id(session_id)) - - -@manager.route('/prompts', methods=['GET']) # noqa: F821 -@login_required -def prompts(): - from rag.prompts.generator import ANALYZE_TASK_SYSTEM, ANALYZE_TASK_USER, NEXT_STEP, REFLECT, CITATION_PROMPT_TEMPLATE - - return get_json_result(data={ - "task_analysis": ANALYZE_TASK_SYSTEM +"\n\n"+ ANALYZE_TASK_USER, - "plan_generation": NEXT_STEP, - "reflection": REFLECT, - #"context_summary": SUMMARY4MEMORY, - #"context_ranking": RANK_MEMORY, - "citation_guidelines": CITATION_PROMPT_TEMPLATE - }) - - -@manager.route('/download', methods=['GET']) # noqa: F821 -async def download(): - id = request.args.get("id") - created_by = request.args.get("created_by") - blob = FileService.get_blob(created_by, id) - return await make_response(blob) diff --git a/api/apps/restful_apis/agent_api.py b/api/apps/restful_apis/agent_api.py new file mode 100644 index 00000000000..8cfc16c34b0 --- /dev/null +++ b/api/apps/restful_apis/agent_api.py @@ -0,0 +1,1047 @@ +# +# 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 inspect +import copy +import json +import logging +from functools import partial + +from quart import Response, jsonify, request + +from agent.component import LLM +from agent.canvas import Canvas +from agent.dsl_migration import normalize_chunker_dsl +from api.apps import login_required +from api.apps.services.canvas_replica_service import CanvasReplicaService +from api.db import CanvasCategory +from api.db.db_models import Task +from api.db.services.api_service import API4ConversationService +from api.db.services.canvas_service import ( + CanvasTemplateService, + UserCanvasService, + completion as agent_completion, + completion_openai, +) +from api.db.services.document_service import DocumentService +from api.db.services.file_service import FileService +from api.db.services.knowledgebase_service import KnowledgebaseService +from api.db.services.pipeline_operation_log_service import PipelineOperationLogService +from api.db.services.task_service import CANVAS_DEBUG_DOC_ID, TaskService, queue_dataflow +from api.db.services.user_service import TenantService, UserService +from api.db.services.user_canvas_version import UserCanvasVersionService +from api.utils.api_utils import ( + add_tenant_id_to_kwargs, + get_data_error_result, + get_json_result, + get_result, + get_request_json, + server_error_response, + validate_request, +) +from common.constants import RetCode +from common.misc_utils import get_uuid, thread_pool_exec +from common import settings +from peewee import MySQLDatabase, PostgresqlDatabase +from rag.flow.pipeline import Pipeline +from rag.nlp import search +from rag.utils.redis_conn import REDIS_CONN + + +def _get_user_nickname(user_id: str) -> str: + exists, user = UserService.get_by_id(user_id) + if not exists: + return user_id + return str(getattr(user, "nickname", "") or user_id) + + +def _build_sse_response(body): + resp = Response(body, mimetype="text/event-stream") + resp.headers.add_header("Cache-control", "no-cache") + resp.headers.add_header("Connection", "keep-alive") + resp.headers.add_header("X-Accel-Buffering", "no") + resp.headers.add_header("Content-Type", "text/event-stream; charset=utf-8") + return resp + + +def _normalize_agent_session(conv): + conv["messages"] = conv.pop("message") + for info in conv["messages"]: + if "prompt" in info: + info.pop("prompt") + conv["agent_id"] = conv.pop("dialog_id") + if isinstance(conv["reference"], dict): + if "chunks" in conv["reference"]: + conv["reference"] = [conv["reference"]] + else: + conv["reference"] = [value for _, value in sorted(conv["reference"].items(), key=lambda item: int(item[0]))] + + if conv["reference"]: + messages = [message for i, message in enumerate(conv["messages"]) if i != 0 and message["role"] != "user"] + for message, reference in zip(messages, conv["reference"]): + chunks = reference["chunks"] + message["reference"] = [ + { + "id": chunk.get("chunk_id", chunk.get("id")), + "content": chunk.get("content_with_weight", chunk.get("content")), + "document_id": chunk.get("doc_id", chunk.get("document_id")), + "document_name": chunk.get("docnm_kwd", chunk.get("document_name")), + "dataset_id": chunk.get("kb_id", chunk.get("dataset_id")), + "image_id": chunk.get("image_id", chunk.get("img_id")), + "positions": chunk.get("positions", chunk.get("position_int")), + } + for chunk in chunks + ] + del conv["reference"] + return conv + + +def _agent_session_list_result(data, total): + return jsonify({"code": RetCode.SUCCESS, "message": "success", "data": data, "total": total}) + + +@manager.route("/agents//sessions", methods=["GET"]) # noqa: F821 +@login_required +@add_tenant_id_to_kwargs +def list_agent_sessions(agent_id, tenant_id): + if not UserCanvasService.accessible(agent_id, tenant_id): + return get_json_result( + data=False, + message="Only owner of canvas authorized for this operation.", + code=RetCode.OPERATING_ERROR, + ) + + session_id = request.args.get("id") + user_id = request.args.get("user_id") + page_number = int(request.args.get("page", 1)) + items_per_page = int(request.args.get("page_size", 30)) + keywords = request.args.get("keywords") + from_date = request.args.get("from_date") + to_date = request.args.get("to_date") + orderby = request.args.get("orderby", "update_time") + exp_user_id = request.args.get("exp_user_id") + desc = request.args.get("desc") not in {"False", "false"} + + if exp_user_id: + sessions = API4ConversationService.get_names(agent_id, exp_user_id) + return _agent_session_list_result(sessions, len(sessions)) + + include_dsl = request.args.get("dsl") not in {"False", "false"} + total, sessions = API4ConversationService.get_list( + agent_id, + tenant_id, + page_number, + items_per_page, + orderby, + desc, + session_id, + user_id, + include_dsl, + keywords, + from_date, + to_date, + exp_user_id=exp_user_id, + ) + sessions = [_normalize_agent_session(session) for session in sessions] + return _agent_session_list_result(sessions, total) + + +@manager.route("/agents//sessions", methods=["POST"]) # noqa: F821 +@login_required +@add_tenant_id_to_kwargs +async def create_agent_session(agent_id, tenant_id): + req = await get_request_json() + user_id = req.get("user_id") or request.args.get("user_id", tenant_id) + release_mode = bool(req.get("release", request.args.get("release", False))) + + try: + cvs, dsl = UserCanvasService.get_agent_dsl_with_release(agent_id, release_mode, tenant_id) + except LookupError: + return get_data_error_result(message="Agent not found.") + except PermissionError as e: + return get_data_error_result(message=str(e)) + + session_id = get_uuid() + canvas = Canvas(dsl, tenant_id, agent_id, canvas_id=cvs.id) + canvas.reset() + + cvs.dsl = json.loads(str(canvas)) + version_title = UserCanvasVersionService.get_latest_version_title(cvs.id, release_mode=release_mode) + conv = { + "id": session_id, + "name": req.get("name", ""), + "dialog_id": cvs.id, + "user_id": user_id, + "exp_user_id": user_id, + "message": [{"role": "assistant", "content": canvas.get_prologue()}], + "source": "agent", + "dsl": cvs.dsl, + "reference": [], + "version_title": version_title, + } + API4ConversationService.save(**conv) + return get_result(data=_normalize_agent_session(conv)) + + +@manager.route("/agents//sessions/", methods=["GET"]) # noqa: F821 +@login_required +@add_tenant_id_to_kwargs +def get_agent_session(agent_id, session_id, tenant_id): + if not UserCanvasService.accessible(agent_id, tenant_id): + return get_json_result( + data=False, + message="Only owner of canvas authorized for this operation.", + code=RetCode.OPERATING_ERROR, + ) + _, conv = API4ConversationService.get_by_id(session_id) + return get_json_result(data=conv.to_dict()) + + +@manager.route("/agents//sessions/", methods=["DELETE"]) # noqa: F821 +@login_required +@add_tenant_id_to_kwargs +def delete_agent_session_item(agent_id, session_id, tenant_id): + if not UserCanvasService.accessible(agent_id, tenant_id): + return get_json_result( + data=False, + message="Only owner of canvas authorized for this operation.", + code=RetCode.OPERATING_ERROR, + ) + return get_json_result(data=API4ConversationService.delete_by_id(session_id)) + + +@manager.route("/agents/download", methods=["GET"]) # noqa: F821 +async def download_agent_file(): + id = request.args.get("id") + created_by = request.args.get("created_by") + blob = FileService.get_blob(created_by, id) + return Response(blob) + + +async def _iter_session_completion_events(tenant_id, agent_id, req, return_trace): + # Stream and non-stream session completions share the same event parsing and trace injection. + trace_items = [] + async for answer in agent_completion(tenant_id=tenant_id, agent_id=agent_id, **req): + if isinstance(answer, str): + try: + ans = json.loads(answer[5:]) + except Exception: + continue + else: + ans = answer + + event = ans.get("event") + if event == "node_finished": + if return_trace: + data = ans.get("data", {}) + trace_items.append( + { + "component_id": data.get("component_id"), + "trace": [copy.deepcopy(data)], + } + ) + ans.setdefault("data", {})["trace"] = trace_items + yield ans + continue + + if event in ["message", "message_end"]: + yield ans + + +@manager.route("/agents/templates", methods=["GET"]) # noqa: F821 +@login_required +def list_agent_template(): + return get_json_result(data=[item.to_dict() for item in CanvasTemplateService.get_all()]) + + +@manager.route("/agents/prompts", methods=["GET"]) # noqa: F821 +@login_required +def prompts(): + from rag.prompts.generator import ( + ANALYZE_TASK_SYSTEM, + ANALYZE_TASK_USER, + CITATION_PROMPT_TEMPLATE, + NEXT_STEP, + REFLECT, + ) + + return get_json_result( + data={ + "task_analysis": f"{ANALYZE_TASK_SYSTEM}\n\n{ANALYZE_TASK_USER}", + "plan_generation": NEXT_STEP, + "reflection": REFLECT, + "citation_guidelines": CITATION_PROMPT_TEMPLATE, + } + ) + + +@manager.route("/agents", methods=["GET"]) # noqa: F821 +@login_required +@add_tenant_id_to_kwargs +def list_agents(tenant_id): + keywords = request.args.get("keywords", "") + canvas_category = request.args.get("canvas_category") + owner_ids = [item for item in request.args.get("owner_ids", "").strip().split(",") if item] + + page_number = int(request.args.get("page", 0)) + items_per_page = int(request.args.get("page_size", 0)) + order_by = request.args.get("orderby", "create_time") + desc = str(request.args.get("desc", "true")).lower() != "false" + tenants = TenantService.get_joined_tenants_by_user_id(tenant_id) + authorized_owner_ids = {member["tenant_id"] for member in tenants} + authorized_owner_ids.add(tenant_id) + + if owner_ids: + requested_owner_ids = set(owner_ids) + unauthorized_owner_ids = requested_owner_ids - authorized_owner_ids + if unauthorized_owner_ids: + return get_json_result( + data=False, + message="Only authorized owner_ids can be queried.", + code=RetCode.OPERATING_ERROR, + ) + effective_owner_ids = list(requested_owner_ids) + else: + effective_owner_ids = list(authorized_owner_ids) + + canvas, total = UserCanvasService.get_by_tenant_ids( + effective_owner_ids, + tenant_id, + page_number, + items_per_page, + order_by, + desc, + keywords, + canvas_category, + ) + + return get_json_result(data={"canvas": canvas, "total": total}) + + +@manager.route("/agents", methods=["POST"]) # noqa: F821 +@login_required +@add_tenant_id_to_kwargs +async def create_agent(tenant_id): + req = {k: v for k, v in (await get_request_json()).items() if v is not None} + req["user_id"] = tenant_id + req["canvas_category"] = req.get("canvas_category") or CanvasCategory.Agent + req["release"] = bool(req.get("release", "")) + + if req.get("dsl") is None: + return get_json_result( + data=False, + message="No DSL data in request.", + code=RetCode.ARGUMENT_ERROR, + ) + + try: + req["dsl"] = CanvasReplicaService.normalize_dsl(req["dsl"]) + except ValueError as exc: + return get_json_result( + data=False, + message=str(exc), + code=RetCode.ARGUMENT_ERROR, + ) + + if req.get("title") is None: + return get_json_result( + data=False, + message="No title in request.", + code=RetCode.ARGUMENT_ERROR, + ) + + req["title"] = req["title"].strip() + if UserCanvasService.query( + user_id=tenant_id, + title=req["title"], + canvas_category=req["canvas_category"], + ): + return get_data_error_result(message=f"{req['title']} already exists.") + + req["id"] = get_uuid() + if not UserCanvasService.save(**req): + return get_data_error_result(message="Fail to create agent.") + + owner_nickname = _get_user_nickname(tenant_id) + UserCanvasVersionService.save_or_replace_latest( + user_canvas_id=req["id"], + title=UserCanvasVersionService.build_version_title(owner_nickname, req.get("title")), + dsl=req["dsl"], + release=req.get("release"), + ) + replica_ok = CanvasReplicaService.replace_for_set( + canvas_id=req["id"], + tenant_id=str(tenant_id), + runtime_user_id=str(tenant_id), + dsl=req["dsl"], + canvas_category=req["canvas_category"], + title=req.get("title", ""), + ) + if not replica_ok: + return get_data_error_result(message="canvas saved, but replica sync failed.") + + exists, created_agent = UserCanvasService.get_by_canvas_id(req["id"]) + if not exists: + return get_data_error_result(message="Fail to create agent.") + return get_json_result(data=created_agent) + + +@manager.route("/agents//upload", methods=["POST"]) # noqa: F821 +async def upload_agent_file(agent_id): + exists, canvas = UserCanvasService.get_by_canvas_id(agent_id) + if not exists: + return get_data_error_result(message="canvas not found.") + + user_id = canvas["user_id"] + files = await request.files + file_objs = files.getlist("file") if files and files.get("file") else [] + try: + if len(file_objs) == 1: + return get_json_result( + data=FileService.upload_info(user_id, file_objs[0], request.args.get("url")) + ) + results = [FileService.upload_info(user_id, file_obj) for file_obj in file_objs] + return get_json_result(data=results) + except Exception as exc: + return server_error_response(exc) + + +@manager.route("/agents//components//input-form", methods=["GET"]) # noqa: F821 +@login_required +@add_tenant_id_to_kwargs +def get_agent_component_input_form(agent_id, component_id, tenant_id): + try: + exists, user_canvas = UserCanvasService.get_by_id(agent_id) + if not exists: + return get_data_error_result(message="canvas not found.") + if not UserCanvasService.query(user_id=tenant_id, id=agent_id): + return get_json_result( + data=False, + message="Only owner of canvas authorized for this operation.", + code=RetCode.OPERATING_ERROR, + ) + + canvas = Canvas(json.dumps(user_canvas.dsl), tenant_id, canvas_id=user_canvas.id) + return get_json_result(data=canvas.get_component_input_form(component_id)) + except Exception as exc: + return server_error_response(exc) + + +@manager.route("/agents//components//debug", methods=["POST"]) # noqa: F821 +@validate_request("params") +@login_required +@add_tenant_id_to_kwargs +async def debug_agent_component(agent_id, component_id, tenant_id): + req = await get_request_json() + if not UserCanvasService.accessible(agent_id, tenant_id): + return get_json_result( + data=False, + message="Only owner of canvas authorized for this operation.", + code=RetCode.OPERATING_ERROR, + ) + try: + _, user_canvas = UserCanvasService.get_by_id(agent_id) + canvas = Canvas(json.dumps(user_canvas.dsl), tenant_id, canvas_id=user_canvas.id) + canvas.reset() + canvas.message_id = get_uuid() + component = canvas.get_component(component_id)["obj"] + component.reset() + + if isinstance(component, LLM): + component.set_debug_inputs(req["params"]) + component.invoke(**{k: o["value"] for k, o in req["params"].items()}) + outputs = component.output() + for k in outputs.keys(): + if isinstance(outputs[k], partial): + txt = "" + iter_obj = outputs[k]() + if inspect.isasyncgen(iter_obj): + async for c in iter_obj: + txt += c + else: + for c in iter_obj: + txt += c + outputs[k] = txt + return get_json_result(data=outputs) + except Exception as exc: + return server_error_response(exc) + + +@manager.route("/agents/", methods=["GET"]) # noqa: F821 +@login_required +@add_tenant_id_to_kwargs +def get_agent(agent_id, tenant_id): + if not UserCanvasService.accessible(agent_id, tenant_id): + return get_data_error_result(message="canvas not found.") + + exists, canvas = UserCanvasService.get_by_canvas_id(agent_id) + if not exists: + return get_data_error_result(message="canvas not found.") + + try: + CanvasReplicaService.bootstrap( + canvas_id=agent_id, + tenant_id=str(tenant_id), + runtime_user_id=str(tenant_id), + dsl=canvas.get("dsl"), + canvas_category=canvas.get("canvas_category", CanvasCategory.Agent), + title=canvas.get("title", ""), + ) + except ValueError as exc: + return get_data_error_result(message=str(exc)) + + last_publish_time = None + versions = UserCanvasVersionService.list_by_canvas_id(agent_id) + if versions: + released_versions = [version for version in versions if version.release] + if released_versions: + released_versions.sort(key=lambda version: version.update_time, reverse=True) + last_publish_time = released_versions[0].update_time + + canvas["dsl"] = normalize_chunker_dsl(canvas.get("dsl", {})) + canvas["last_publish_time"] = last_publish_time + + if canvas.get("canvas_category") == CanvasCategory.DataFlow: + datasets = list(KnowledgebaseService.query(pipeline_id=agent_id)) + canvas["datasets"] = [{"id": item.id, "name": item.name, "avatar": item.avatar} for item in datasets] + + return get_json_result(data=canvas) + + +@manager.route("/agents//versions", methods=["GET"]) # noqa: F821 +@login_required +@add_tenant_id_to_kwargs +def list_agent_versions(agent_id, tenant_id): + if not UserCanvasService.accessible(agent_id, tenant_id): + return get_json_result( + data=False, + message="Only owner of canvas authorized for this operation.", + code=RetCode.OPERATING_ERROR, + ) + + try: + versions = sorted( + [item.to_dict() for item in UserCanvasVersionService.list_by_canvas_id(agent_id)], + key=lambda item: item["update_time"] * -1, + ) + return get_json_result(data=versions) + except Exception as exc: + return get_data_error_result(message=f"Error getting history files: {exc}") + + +@manager.route("/agents//versions/", methods=["GET"]) # noqa: F821 +@login_required +@add_tenant_id_to_kwargs +def get_agent_version(agent_id, version_id, tenant_id): + if not UserCanvasService.accessible(agent_id, tenant_id): + return get_json_result( + data=False, + message="Only owner of canvas authorized for this operation.", + code=RetCode.OPERATING_ERROR, + ) + + try: + exists, version = UserCanvasVersionService.get_by_id(version_id) + if not exists or not version or str(version.user_canvas_id) != str(agent_id): + return get_data_error_result(message="Version not found.") + return get_json_result(data=version.to_dict()) + except Exception as exc: + return get_data_error_result(message=f"Error getting history file: {exc}") + + +@manager.route("/agents//logs/", methods=["GET"]) # noqa: F821 +@login_required +@add_tenant_id_to_kwargs +def get_agent_logs(agent_id, message_id, tenant_id): + if not UserCanvasService.accessible(agent_id, tenant_id): + return get_json_result( + data=False, + message="Only owner of canvas authorized for this operation.", + code=RetCode.OPERATING_ERROR, + ) + + try: + binary = REDIS_CONN.get(f"{agent_id}-{message_id}-logs") + if not binary: + return get_json_result(data={}) + + return get_json_result(data=json.loads(binary.encode("utf-8"))) + except Exception as exc: + logging.exception(exc) + return server_error_response(exc) + + +@manager.route("/agents/", methods=["DELETE"]) # noqa: F821 +@login_required +@add_tenant_id_to_kwargs +def delete_agent(agent_id, tenant_id): + if not UserCanvasService.query(user_id=tenant_id, id=agent_id): + return get_json_result( + data=False, + message="Only owner of canvas authorized for this operation.", + code=RetCode.OPERATING_ERROR, + ) + + UserCanvasService.delete_by_id(agent_id) + return get_json_result(data=True) + + +@manager.route("/agents/", methods=["PUT"]) # noqa: F821 +@login_required +@add_tenant_id_to_kwargs +async def update_agent(agent_id, tenant_id): + req = {k: v for k, v in (await get_request_json()).items() if v is not None} + req["user_id"] = tenant_id + + if req.get("dsl") is not None: + try: + req["dsl"] = CanvasReplicaService.normalize_dsl(req["dsl"]) + except ValueError as exc: + return get_json_result( + data=False, + message=str(exc), + code=RetCode.ARGUMENT_ERROR, + ) + + if req.get("title") is not None: + req["title"] = req["title"].strip() + + if not UserCanvasService.query(user_id=tenant_id, id=agent_id): + return get_json_result( + data=False, + message="Only owner of canvas authorized for this operation.", + code=RetCode.OPERATING_ERROR, + ) + + _, current_agent = UserCanvasService.get_by_id(agent_id) + agent_title_for_version = req.get("title") or (current_agent.title if current_agent else "") + canvas_category = ( + req.get("canvas_category") + or (current_agent.canvas_category if current_agent else CanvasCategory.Agent) + ) + owner_nickname = _get_user_nickname(tenant_id) + UserCanvasService.update_by_id(agent_id, req) + + if req.get("dsl") is not None: + UserCanvasVersionService.save_or_replace_latest( + user_canvas_id=agent_id, + title=UserCanvasVersionService.build_version_title(owner_nickname, agent_title_for_version), + dsl=req["dsl"], + ) + replica_ok = CanvasReplicaService.replace_for_set( + canvas_id=agent_id, + tenant_id=str(tenant_id), + runtime_user_id=str(tenant_id), + dsl=req["dsl"], + canvas_category=canvas_category, + title=agent_title_for_version, + ) + if not replica_ok: + return get_data_error_result(message="agent saved, but replica sync failed.") + + return get_json_result(data=True) + + +@manager.route("/agents//reset", methods=["POST"]) # noqa: F821 +@login_required +@add_tenant_id_to_kwargs +async def reset_agent(agent_id, tenant_id): + if not UserCanvasService.accessible(agent_id, tenant_id): + return get_json_result( + data=False, + message="Only owner of canvas authorized for this operation.", + code=RetCode.OPERATING_ERROR, + ) + + try: + exists, user_canvas = UserCanvasService.get_by_id(agent_id) + if not exists: + return get_data_error_result(message="canvas not found.") + + canvas = Canvas(json.dumps(user_canvas.dsl), tenant_id, canvas_id=user_canvas.id) + canvas.reset() + dsl = json.loads(str(canvas)) + UserCanvasService.update_by_id(agent_id, {"dsl": dsl}) + replica_ok = CanvasReplicaService.replace_for_set( + canvas_id=agent_id, + tenant_id=str(tenant_id), + runtime_user_id=str(tenant_id), + dsl=dsl, + canvas_category=user_canvas.canvas_category, + title=user_canvas.title, + ) + if not replica_ok: + return get_data_error_result(message="agent reset, but replica sync failed.") + return get_json_result(data=dsl) + except Exception as exc: + return server_error_response(exc) + + +@manager.route("/agents/rerun", methods=["POST"]) # noqa: F821 +@validate_request("id", "dsl", "component_id") +@login_required +@add_tenant_id_to_kwargs +async def rerun_agent(tenant_id): + req = await get_request_json() + doc = PipelineOperationLogService.get_documents_info(req["id"]) + if not doc: + return get_data_error_result(message="Document not found.") + doc = doc[0] + if 0 < doc["progress"] < 1: + return get_data_error_result(message=f"`{doc['name']}` is processing...") + + if settings.docStoreConn.index_exist(search.index_name(tenant_id), doc["kb_id"]): + settings.docStoreConn.delete({"doc_id": doc["id"]}, search.index_name(tenant_id), doc["kb_id"]) + doc["progress_msg"] = "" + doc["chunk_num"] = 0 + doc["token_num"] = 0 + DocumentService.clear_chunk_num_when_rerun(doc["id"]) + DocumentService.update_by_id(doc["id"], doc) + TaskService.filter_delete([Task.doc_id == doc["id"]]) + + dsl = req["dsl"] + dsl["path"] = [req["component_id"]] + PipelineOperationLogService.update_by_id(req["id"], {"dsl": dsl}) + queue_dataflow( + tenant_id=tenant_id, + flow_id=req["id"], + task_id=get_uuid(), + doc_id=doc["id"], + priority=0, + rerun=True, + ) + return get_json_result(data=True) + + +@manager.route("/agents/test_db_connection", methods=["POST"]) # noqa: F821 +@validate_request("db_type", "database", "username", "host", "port", "password") +@login_required +async def test_db_connection(): + req = await get_request_json() + try: + if req["db_type"] in ["mysql", "mariadb"]: + db = MySQLDatabase( + req["database"], + user=req["username"], + host=req["host"], + port=req["port"], + password=req["password"], + ) + elif req["db_type"] == "oceanbase": + db = MySQLDatabase( + req["database"], + user=req["username"], + host=req["host"], + port=req["port"], + password=req["password"], + charset="utf8mb4", + ) + elif req["db_type"] == "postgres": + db = PostgresqlDatabase( + req["database"], + user=req["username"], + host=req["host"], + port=req["port"], + password=req["password"], + ) + elif req["db_type"] == "mssql": + import pyodbc + + connection_string = ( + f"DRIVER={{ODBC Driver 17 for SQL Server}};" + f"SERVER={req['host']},{req['port']};" + f"DATABASE={req['database']};" + f"UID={req['username']};" + f"PWD={req['password']};" + ) + db = pyodbc.connect(connection_string) + cursor = db.cursor() + cursor.execute("SELECT 1") + cursor.close() + elif req["db_type"] == "IBM DB2": + import ibm_db + + conn_str = ( + f"DATABASE={req['database']};" + f"HOSTNAME={req['host']};" + f"PORT={req['port']};" + f"PROTOCOL=TCPIP;" + f"UID={req['username']};" + f"PWD={req['password']};" + ) + logging.info( + "DATABASE=%s;HOSTNAME=%s;PORT=%s;PROTOCOL=TCPIP;UID=%s;PWD=****;", + req["database"], + req["host"], + req["port"], + req["username"], + ) + conn = ibm_db.connect(conn_str, "", "") + stmt = ibm_db.exec_immediate(conn, "SELECT 1 FROM sysibm.sysdummy1") + ibm_db.fetch_assoc(stmt) + ibm_db.close(conn) + return get_json_result(data="Database Connection Successful!") + elif req["db_type"] == "trino": + import os + import trino + + db_name = req["database"] + if "." in db_name: + catalog, schema = db_name.split(".", 1) + elif "/" in db_name: + catalog, schema = db_name.split("/", 1) + else: + catalog, schema = db_name, "default" + + http_scheme = "https" if os.environ.get("TRINO_USE_TLS", "0") == "1" else "http" + auth = None + if http_scheme == "https" and req.get("password"): + auth = trino.BasicAuthentication(req.get("username") or "ragflow", req["password"]) + + conn = trino.dbapi.connect( + host=req["host"], + port=int(req["port"] or 8080), + user=req["username"] or "ragflow", + catalog=catalog, + schema=schema or "default", + http_scheme=http_scheme, + auth=auth, + ) + cur = conn.cursor() + cur.execute("SELECT 1") + cur.fetchall() + cur.close() + conn.close() + return get_json_result(data="Database Connection Successful!") + else: + return server_error_response("Unsupported database type.") + + if req["db_type"] != "mssql": + db.connect() + db.close() + return get_json_result(data="Database Connection Successful!") + except Exception as exc: + return server_error_response(exc) + + +@manager.route("/agents/chat/completion", methods=["POST"]) # noqa: F821 +@login_required +@add_tenant_id_to_kwargs +async def agent_chat_completion(tenant_id): + # This endpoint serves two execution modes: + # 1. Draft/runtime execution without session state. The request runs against the caller's + # runtime replica, which is populated from the editable canvas state. + # 2. Session continuation with an existing session_id. The request resumes from the stored + # API4Conversation state and must stay bound to the same agent and an accessible canvas. + # + # Security constraints: + # - agent_id is always supplied at the route layer and is not forwarded downstream as a free-form kwarg. + # - New runs without session_id must pass UserCanvasService.accessible(...) before the runtime replica is loaded. + # - Existing sessions are validated here at the route layer before handing control to the lower-level + # completion functions, so canvas_service only executes a pre-authorized session payload. + # + # Response modes: + # - Regular mode emits internal agent events. + # - openai-compatible mode reshapes the same execution into an OpenAI-like wire format. + req = await get_request_json() + agent_id = req.get("agent_id") + openai_compatible = bool(req.get("openai-compatible", False)) + if not agent_id: + return get_json_result( + data=False, + message="`agent_id` is required.", + code=RetCode.ARGUMENT_ERROR, + ) + # Route-level selectors should not be forwarded into the lower-level completion functions. + req = dict(req) + req.pop("agent_id", None) + req.pop("openai-compatible", None) + session_id = req.get("session_id") + if session_id: + exists, conv = API4ConversationService.get_by_id(session_id) + if not exists: + return get_data_error_result(message="Session not found!") + if conv.dialog_id != agent_id: + return get_json_result( + data=False, + message="Session does not belong to the requested agent.", + code=RetCode.OPERATING_ERROR, + ) + if not UserCanvasService.accessible(agent_id, tenant_id): + return get_json_result( + data=False, + message="Only authorized users can access this agent session.", + code=RetCode.OPERATING_ERROR, + ) + + if openai_compatible: + # OpenAI-compatible mode uses a different wire format, keep it separate from regular agent events. + messages = req.get("messages", []) + if not messages: + return get_data_error_result(message="You must provide at least one message.") + question = next((m.get("content", "") for m in reversed(messages) if m.get("role") == "user"), "") + stream = req.pop("stream", False) + session_id = req.pop("session_id", req.get("id", "")) or req.get("metadata", {}).get("id", "") + if stream: + return _build_sse_response( + completion_openai( + tenant_id, + agent_id, + question, + session_id=session_id, + stream=True, + **req, + ) + ) + + async for response in completion_openai( + tenant_id, + agent_id, + question, + session_id=session_id, + stream=False, + **req, + ): + return jsonify(response) + return None + + if not session_id: + # Without session state, run against the runtime replica that tracks draft edits. + query = req.get("query", "") + files = req.get("files", []) + inputs = req.get("inputs", {}) + runtime_user_id = req.get("user_id") or tenant_id + user_id = str(runtime_user_id) + if not await thread_pool_exec(UserCanvasService.accessible, agent_id, tenant_id): + return get_json_result( + data=False, + message="Only owner of canvas authorized for this operation.", + code=RetCode.OPERATING_ERROR, + ) + + replica_payload = CanvasReplicaService.load_for_run( + canvas_id=agent_id, + tenant_id=str(tenant_id), + runtime_user_id=user_id, + ) + if not replica_payload: + return get_data_error_result(message="canvas replica not found, please fetch the agent first.") + + replica_dsl = replica_payload.get("dsl", {}) + canvas_title = replica_payload.get("title", "") + canvas_category = replica_payload.get("canvas_category", CanvasCategory.Agent) + dsl_str = json.dumps(replica_dsl, ensure_ascii=False) + + _, cvs = await thread_pool_exec(UserCanvasService.get_by_id, agent_id) + if cvs.canvas_category == CanvasCategory.DataFlow: + task_id = get_uuid() + Pipeline( + dsl_str, + tenant_id=str(tenant_id), + doc_id=CANVAS_DEBUG_DOC_ID, + task_id=task_id, + flow_id=agent_id, + ) + ok, error_message = await thread_pool_exec( + queue_dataflow, + user_id, + agent_id, + task_id, + CANVAS_DEBUG_DOC_ID, + files[0], + 0, + ) + if not ok: + return get_data_error_result(message=error_message) + return get_json_result(data={"message_id": task_id}) + + try: + canvas = Canvas(dsl_str, str(tenant_id), canvas_id=agent_id) + except Exception as exc: + return server_error_response(exc) + + async def sse(): + nonlocal canvas + try: + async for ans in canvas.run(query=query, files=files, user_id=user_id, inputs=inputs): + yield "data:" + json.dumps(ans, ensure_ascii=False) + "\n\n" + + commit_ok = CanvasReplicaService.commit_after_run( + canvas_id=agent_id, + tenant_id=str(tenant_id), + runtime_user_id=user_id, + dsl=json.loads(str(canvas)), + canvas_category=canvas_category, + title=canvas_title, + ) + if not commit_ok: + logging.error( + "Canvas runtime replica commit failed: canvas_id=%s tenant_id=%s runtime_user_id=%s", + agent_id, + tenant_id, + user_id, + ) + except Exception as exc: + logging.exception(exc) + canvas.cancel_task() + yield ( + "data:" + + json.dumps({"code": 500, "message": str(exc), "data": False}, ensure_ascii=False) + + "\n\n" + ) + + return _build_sse_response(sse()) + + return_trace = bool(req.get("return_trace", False)) + if req.get("stream", True): + + async def generate(): + async for ans in _iter_session_completion_events(tenant_id, agent_id, req, return_trace): + yield "data:" + json.dumps(ans, ensure_ascii=False) + "\n\n" + yield "data:[DONE]\n\n" + + return _build_sse_response(generate()) + + full_content = "" + reference = {} + final_ans = "" + trace_items = [] + structured_output = {} + async for ans in _iter_session_completion_events(tenant_id, agent_id, req, return_trace): + try: + if ans["event"] == "message": + full_content += ans["data"]["content"] + if ans.get("data", {}).get("reference", None): + reference.update(ans["data"]["reference"]) + if ans.get("event") == "node_finished": + data = ans.get("data", {}) + node_out = data.get("outputs", {}) + component_id = data.get("component_id") + if component_id is not None and "structured" in node_out: + structured_output[component_id] = copy.deepcopy(node_out["structured"]) + if return_trace: + trace_items = ans.get("data", {}).get("trace", trace_items) + final_ans = ans + except Exception as exc: + return get_result(data=f"**ERROR**: {str(exc)}") + + final_ans["data"]["content"] = full_content + final_ans["data"]["reference"] = reference + if structured_output: + final_ans["data"]["structured"] = structured_output + if return_trace and final_ans: + final_ans["data"]["trace"] = trace_items + return get_result(data=final_ans) diff --git a/api/apps/sdk/agents.py b/api/apps/sdk/agents.py index f7f36fa19f0..993c0b613aa 100644 --- a/api/apps/sdk/agents.py +++ b/api/apps/sdk/agents.py @@ -22,137 +22,18 @@ import json import logging import time -from typing import Any, cast import jwt from agent.canvas import Canvas -from api.apps.services.canvas_replica_service import CanvasReplicaService from api.db import CanvasCategory from api.db.services.canvas_service import UserCanvasService from api.db.services.file_service import FileService -from api.db.services.user_service import UserService -from api.db.services.user_canvas_version import UserCanvasVersionService from common.constants import RetCode -from common.misc_utils import get_uuid -from api.utils.api_utils import get_data_error_result, get_error_data_result, get_json_result, get_request_json, token_required -from api.utils.api_utils import get_result +from api.utils.api_utils import get_data_error_result, get_json_result from quart import request, Response from rag.utils.redis_conn import REDIS_CONN - -def _get_user_nickname(user_id: str) -> str: - exists, user = UserService.get_by_id(user_id) - if not exists: - return user_id - return str(getattr(user, "nickname", "") or user_id) - - -@manager.route('/agents', methods=['GET']) # noqa: F821 -@token_required -def list_agents(tenant_id): - id = request.args.get("id") - title = request.args.get("title") - if id or title: - canvas = UserCanvasService.query(id=id, title=title, user_id=tenant_id) - if not canvas: - return get_error_data_result("The agent doesn't exist.") - page_number = int(request.args.get("page", 1)) - items_per_page = int(request.args.get("page_size", 30)) - order_by = request.args.get("orderby", "update_time") - if str(request.args.get("desc","false")).lower() == "false": - desc = False - else: - desc = True - canvas = UserCanvasService.get_list(tenant_id, page_number, items_per_page, order_by, desc, id, title) - return get_result(data=canvas) - - -@manager.route("/agents", methods=["POST"]) # noqa: F821 -@token_required -async def create_agent(tenant_id: str): - req: dict[str, Any] = cast(dict[str, Any], await get_request_json()) - req["user_id"] = tenant_id - - if req.get("dsl") is not None: - try: - req["dsl"] = CanvasReplicaService.normalize_dsl(req["dsl"]) - except ValueError as e: - return get_json_result(data=False, message=str(e), code=RetCode.ARGUMENT_ERROR) - else: - return get_json_result(data=False, message="No DSL data in request.", code=RetCode.ARGUMENT_ERROR) - - if req.get("title") is not None: - req["title"] = req["title"].strip() - else: - return get_json_result(data=False, message="No title in request.", code=RetCode.ARGUMENT_ERROR) - - if UserCanvasService.query(user_id=tenant_id, title=req["title"]): - return get_data_error_result(message=f"Agent with title {req['title']} already exists.") - - agent_id = get_uuid() - req["id"] = agent_id - - if not UserCanvasService.save(**req): - return get_data_error_result(message="Fail to create agent.") - - owner_nickname = _get_user_nickname(tenant_id) - UserCanvasVersionService.save_or_replace_latest( - user_canvas_id=agent_id, - title=UserCanvasVersionService.build_version_title(owner_nickname, req.get("title")), - dsl=req["dsl"] - ) - - return get_json_result(data=True) - - -@manager.route("/agents/", methods=["PUT"]) # noqa: F821 -@token_required -async def update_agent(tenant_id: str, agent_id: str): - req: dict[str, Any] = {k: v for k, v in cast(dict[str, Any], (await get_request_json())).items() if v is not None} - req["user_id"] = tenant_id - - if req.get("dsl") is not None: - try: - req["dsl"] = CanvasReplicaService.normalize_dsl(req["dsl"]) - except ValueError as e: - return get_json_result(data=False, message=str(e), code=RetCode.ARGUMENT_ERROR) - - if req.get("title") is not None: - req["title"] = req["title"].strip() - - if not UserCanvasService.query(user_id=tenant_id, id=agent_id): - return get_json_result( - data=False, message="Only owner of canvas authorized for this operation.", - code=RetCode.OPERATING_ERROR) - - _, current_agent = UserCanvasService.get_by_id(agent_id) - agent_title_for_version = req.get("title") or (current_agent.title if current_agent else "") - owner_nickname = _get_user_nickname(tenant_id) - - UserCanvasService.update_by_id(agent_id, req) - - if req.get("dsl") is not None: - UserCanvasVersionService.save_or_replace_latest( - user_canvas_id=agent_id, - title=UserCanvasVersionService.build_version_title(owner_nickname, agent_title_for_version), - dsl=req["dsl"] - ) - - return get_json_result(data=True) - - -@manager.route("/agents/", methods=["DELETE"]) # noqa: F821 -@token_required -def delete_agent(tenant_id: str, agent_id: str): - if not UserCanvasService.query(user_id=tenant_id, id=agent_id): - return get_json_result( - data=False, message="Only owner of canvas authorized for this operation.", - code=RetCode.OPERATING_ERROR) - - UserCanvasService.delete_by_id(agent_id) - return get_json_result(data=True) - @manager.route("/webhook/", methods=["POST", "GET", "PUT", "PATCH", "DELETE", "HEAD"]) # noqa: F821 @manager.route("/webhook_test/",methods=["POST", "GET", "PUT", "PATCH", "DELETE", "HEAD"],) # noqa: F821 async def webhook(agent_id: str): diff --git a/api/apps/sdk/session.py b/api/apps/sdk/session.py index 82e048ff17b..92f01233cdf 100644 --- a/api/apps/sdk/session.py +++ b/api/apps/sdk/session.py @@ -14,7 +14,6 @@ # limitations under the License. # import json -import copy import re import time @@ -29,7 +28,7 @@ from agent.canvas import Canvas from api.db.db_models import APIToken from api.db.services.api_service import API4ConversationService -from api.db.services.canvas_service import UserCanvasService, completion_openai +from api.db.services.canvas_service import UserCanvasService from api.db.services.canvas_service import completion as agent_completion from api.db.services.conversation_service import ConversationService from api.db.services.user_canvas_version import UserCanvasVersionService @@ -45,7 +44,7 @@ from api.db.joint_services.tenant_model_service import get_tenant_default_model_by_type, get_model_config_by_id, \ get_model_config_by_type_and_name from common.misc_utils import get_uuid -from api.utils.api_utils import check_duplicate_ids, get_data_openai, get_error_data_result, get_json_result, \ +from api.utils.api_utils import check_duplicate_ids, get_error_data_result, get_json_result, \ get_result, get_request_json, server_error_response, token_required, validate_request from rag.app.tag import label_question from rag.prompts.template import load_prompt @@ -54,7 +53,6 @@ from common import settings -@manager.route("/agents//sessions", methods=["POST"]) # noqa: F821 @token_required async def create_agent_session(tenant_id, agent_id): req = await get_request_json() @@ -435,215 +433,6 @@ async def streamed_response_generator(chat_id, dia, msg): return jsonify(response) -@manager.route("/agents_openai//chat/completions", methods=["POST"]) # noqa: F821 -@validate_request("model", "messages") # noqa: F821 -@token_required -async def agents_completion_openai_compatibility(tenant_id, agent_id): - req = await get_request_json() - messages = req.get("messages", []) - if not messages: - return get_error_data_result("You must provide at least one message.") - if not UserCanvasService.query(user_id=tenant_id, id=agent_id): - return get_error_data_result(f"You don't own the agent {agent_id}") - - filtered_messages = [m for m in messages if m["role"] in ["user", "assistant"]] - prompt_tokens = sum(num_tokens_from_string(m["content"]) for m in filtered_messages) - if not filtered_messages: - return jsonify( - get_data_openai( - id=agent_id, - content="No valid messages found (user or assistant).", - finish_reason="stop", - model=req.get("model", ""), - completion_tokens=num_tokens_from_string("No valid messages found (user or assistant)."), - prompt_tokens=prompt_tokens, - ) - ) - - question = next((m["content"] for m in reversed(messages) if m["role"] == "user"), "") - - stream = req.pop("stream", False) - if stream: - resp = Response( - completion_openai( - tenant_id, - agent_id, - question, - session_id=req.pop("session_id", req.get("id", "")) or req.get("metadata", {}).get("id", ""), - stream=True, - **req, - ), - mimetype="text/event-stream", - ) - resp.headers.add_header("Cache-control", "no-cache") - resp.headers.add_header("Connection", "keep-alive") - resp.headers.add_header("X-Accel-Buffering", "no") - resp.headers.add_header("Content-Type", "text/event-stream; charset=utf-8") - return resp - else: - # For non-streaming, just return the response directly - async for response in completion_openai( - tenant_id, - agent_id, - question, - session_id=req.pop("session_id", req.get("id", "")) or req.get("metadata", {}).get("id", ""), - stream=False, - **req, - ): - return jsonify(response) - - return None - - -@manager.route("/agents//completions", methods=["POST"]) # noqa: F821 -@token_required -async def agent_completions(tenant_id, agent_id): - req = await get_request_json() - return_trace = bool(req.get("return_trace", False)) - - if req.get("stream", True): - - async def generate(): - trace_items = [] - async for answer in agent_completion(tenant_id=tenant_id, agent_id=agent_id, **req): - if isinstance(answer, str): - try: - ans = json.loads(answer[5:]) # remove "data:" - except Exception: - continue - - event = ans.get("event") - if event == "node_finished": - if return_trace: - data = ans.get("data", {}) - trace_items.append( - { - "component_id": data.get("component_id"), - "trace": [copy.deepcopy(data)], - } - ) - ans.setdefault("data", {})["trace"] = trace_items - answer = "data:" + json.dumps(ans, ensure_ascii=False) + "\n\n" - yield answer - - if event not in ["message", "message_end"]: - continue - - yield answer - - yield "data:[DONE]\n\n" - - resp = Response(generate(), mimetype="text/event-stream") - resp.headers.add_header("Cache-control", "no-cache") - resp.headers.add_header("Connection", "keep-alive") - resp.headers.add_header("X-Accel-Buffering", "no") - resp.headers.add_header("Content-Type", "text/event-stream; charset=utf-8") - return resp - - full_content = "" - reference = {} - final_ans = "" - trace_items = [] - structured_output = {} - async for answer in agent_completion(tenant_id=tenant_id, agent_id=agent_id, **req): - try: - ans = json.loads(answer[5:]) - - if ans["event"] == "message": - full_content += ans["data"]["content"] - - if ans.get("data", {}).get("reference", None): - reference.update(ans["data"]["reference"]) - - if ans.get("event") == "node_finished": - data = ans.get("data", {}) - node_out = data.get("outputs", {}) - component_id = data.get("component_id") - if component_id is not None and "structured" in node_out: - structured_output[component_id] = copy.deepcopy(node_out["structured"]) - if return_trace: - trace_items.append( - { - "component_id": data.get("component_id"), - "trace": [copy.deepcopy(data)], - } - ) - - final_ans = ans - except Exception as e: - return get_result(data=f"**ERROR**: {str(e)}") - final_ans["data"]["content"] = full_content - final_ans["data"]["reference"] = reference - if structured_output: - final_ans["data"]["structured"] = structured_output - if return_trace and final_ans: - final_ans["data"]["trace"] = trace_items - return get_result(data=final_ans) - - -@manager.route("/agents//sessions", methods=["GET"]) # noqa: F821 -@token_required -async def list_agent_session(tenant_id, agent_id): - if not UserCanvasService.query(user_id=tenant_id, id=agent_id): - return get_error_data_result(message=f"You don't own the agent {agent_id}.") - id = request.args.get("id") - user_id = request.args.get("user_id") - page_number = int(request.args.get("page", 1)) - items_per_page = int(request.args.get("page_size", 30)) - orderby = request.args.get("orderby", "update_time") - if request.args.get("desc") == "False" or request.args.get("desc") == "false": - desc = False - else: - desc = True - # dsl defaults to True in all cases except for False and false - include_dsl = request.args.get("dsl") != "False" and request.args.get("dsl") != "false" - total, convs = API4ConversationService.get_list(agent_id, tenant_id, page_number, items_per_page, orderby, desc, id, - user_id, include_dsl) - if not convs: - return get_result(data=[]) - for conv in convs: - conv["messages"] = conv.pop("message") - infos = conv["messages"] - for info in infos: - if "prompt" in info: - info.pop("prompt") - conv["agent_id"] = conv.pop("dialog_id") - # Fix for session listing endpoint - if conv["reference"]: - messages = conv["messages"] - message_num = 0 - chunk_num = 0 - # Ensure reference is a list type to prevent KeyError - if not isinstance(conv["reference"], list): - conv["reference"] = [] - while message_num < len(messages): - if message_num != 0 and messages[message_num]["role"] != "user": - chunk_list = [] - # Add boundary and type checks to prevent KeyError - if chunk_num < len(conv["reference"]) and conv["reference"][chunk_num] is not None and isinstance( - conv["reference"][chunk_num], dict) and "chunks" in conv["reference"][chunk_num]: - chunks = conv["reference"][chunk_num]["chunks"] - for chunk in chunks: - # Ensure chunk is a dictionary before calling get method - if not isinstance(chunk, dict): - continue - new_chunk = { - "id": chunk.get("chunk_id", chunk.get("id")), - "content": chunk.get("content_with_weight", chunk.get("content")), - "document_id": chunk.get("doc_id", chunk.get("document_id")), - "document_name": chunk.get("docnm_kwd", chunk.get("document_name")), - "dataset_id": chunk.get("kb_id", chunk.get("dataset_id")), - "image_id": chunk.get("image_id", chunk.get("img_id")), - "positions": chunk.get("positions", chunk.get("position_int")), - } - chunk_list.append(new_chunk) - chunk_num += 1 - messages[message_num]["reference"] = chunk_list - message_num += 1 - del conv["reference"] - return get_result(data=convs) - - @manager.route("/agents//sessions", methods=["DELETE"]) # noqa: F821 @token_required async def delete_agent_session(tenant_id, agent_id): diff --git a/api/db/services/api_service.py b/api/db/services/api_service.py index be41dc1b642..8f60a1c5ab5 100644 --- a/api/db/services/api_service.py +++ b/api/db/services/api_service.py @@ -44,6 +44,14 @@ def delete_by_tenant_id(cls, tenant_id): class API4ConversationService(CommonService): model = API4Conversation + @staticmethod + def _normalize_query_date(value, is_end=False): + if "T" in value: + value = datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone().replace(tzinfo=None).strftime("%Y-%m-%d %H:%M:%S") + elif len(value) == 10: + value = f"{value} 23:59:59" if is_end else f"{value} 00:00:00" + return value + @classmethod @DB.connection_context() def get_list(cls, dialog_id, tenant_id, @@ -62,10 +70,11 @@ def get_list(cls, dialog_id, tenant_id, sessions = sessions.where(cls.model.user_id == user_id) if keywords: sessions = sessions.where(peewee.fn.LOWER(cls.model.message).contains(keywords.lower())) + date_field = cls.model.update_date if orderby.startswith("update_") else cls.model.create_date if from_date: - sessions = sessions.where(cls.model.create_date >= from_date) + sessions = sessions.where(date_field >= cls._normalize_query_date(from_date)) if to_date: - sessions = sessions.where(cls.model.create_date <= to_date) + sessions = sessions.where(date_field <= cls._normalize_query_date(to_date, is_end=True)) if exp_user_id: sessions = sessions.where(cls.model.exp_user_id == exp_user_id) if desc: diff --git a/api/db/services/canvas_service.py b/api/db/services/canvas_service.py index 98925fa246a..ec79bf81881 100644 --- a/api/db/services/canvas_service.py +++ b/api/db/services/canvas_service.py @@ -139,10 +139,17 @@ def get_basic_info_by_canvas_ids(cls, canvas_id): @classmethod @DB.connection_context() - def get_by_tenant_ids(cls, joined_tenant_ids, user_id, - page_number, items_per_page, - orderby, desc, keywords, canvas_category=None - ): + def get_by_tenant_ids( + cls, + joined_tenant_ids, + user_id, + page_number, + items_per_page, + orderby, + desc, + keywords, + canvas_category=None, + ): fields = [ cls.model.id, cls.model.avatar, @@ -201,7 +208,11 @@ def accessible(cls, canvas_id, tenant_id): return False tids = [t.tenant_id for t in UserTenantService.query(user_id=tenant_id)] - if c["user_id"] != canvas_id and c["user_id"] not in tids: + if c["user_id"] == tenant_id: + return True + if c["user_id"] not in tids: + return False + if c["permission"] != TenantPermission.TEAM.value: return False return True diff --git a/docs/references/http_api_reference.md b/docs/references/http_api_reference.md index 7c9fe84effe..06e1a3a47be 100644 --- a/docs/references/http_api_reference.md +++ b/docs/references/http_api_reference.md @@ -4424,62 +4424,71 @@ Failure: Asks a specified agent a question to start an AI-powered conversation. -:::tip NOTE +Uses a single completion endpoint for all agent conversations. -- In streaming mode, not all responses include a reference, as this depends on the system's judgement. -- In streaming mode, the last message is an empty message: +- Standard mode: send `agent_id` with `query`. +- OpenAI-compatible mode: send the same endpoint with `"openai-compatible": true`. - ``` - [DONE] - ``` +:::tip NOTE -- You can optionally return step-by-step trace logs (see `return_trace` below). +- Older agent completion routes have been removed. Use only `/api/v1/agents/chat/completion`. +- In standard streaming mode, not all responses include a reference, as this depends on the workflow result. +- In streaming mode, the server terminates the stream with `[DONE]`. ::: #### Request - Method: POST -- URL: `/api/v1/agents/{agent_id}/completions` +- URL: `/api/v1/agents/chat/completion` - Headers: - `'content-Type: application/json'` - `'Authorization: Bearer '` -- Body: - - `"question"`: `string` - - `"stream"`: `boolean` - - `"session_id"`: `string` (optional) - - `"inputs"`: `object` (optional) - - `"user_id"`: `string` (optional) - - `"return_trace"`: `boolean` (optional, default `false`) — whether to include execution trace logs. See the `node_finished` event. - - `"release"`: `boolean` (optional, default `false`) - whether to visit the latest published canvas. + +#### Standard mode + +Use this mode for the native agent API. + +##### Body + +- `"agent_id"`: `string` +- `"query"`: `string` +- `"stream"`: `boolean` +- `"session_id"`: `string` (optional) +- `"inputs"`: `object` (optional) +- `"files"`: `list[object]` (optional) +- `"user_id"`: `string` (optional) +- `"return_trace"`: `boolean` (optional, default `false`) +- `"release"`: `boolean` (optional, default `false`) #### Streaming events to handle When `stream=true`, the server sends Server-Sent Events (SSE). A client should handle these events: - `message`: Streaming content from the **Message** components. -- `message_end`: End of a **Message** component, which may include `reference`/`attachment`. -- `node_finished`: A component finishes; `data.inputs/outputs/error/elapsed_time` describes the node result. If a component produces structured output, read it from that component's `data.outputs.structured`. If `return_trace=true`, the trace is attached inside the same `node_finished` event (`data.trace`). +- `message_end`: End of a **Message** component, which may include `reference` or `attachment`. +- `node_finished`: A component finishes. `data.inputs`, `data.outputs`, `data.error`, and `data.elapsed_time` describe the node result. If `return_trace=true`, the same event also contains `data.trace`. The stream terminates with `[DONE]`. :::info IMPORTANT -You can include custom parameters in the request body, but first ensure they are defined in the [Begin](../guides/agent/agent_component_reference/begin.mdx) component. +You can include custom parameters in the request body, but they must be defined in the [Begin](../guides/agent/agent_component_reference/begin.mdx) component first. ::: -##### Request example +##### Request examples -- If the **Begin** component does not take parameters: +If the **Begin** component does not take parameters: ```bash curl --request POST \ - --url http://{address}/api/v1/agents/{agent_id}/completions \ + --url http://{address}/api/v1/agents/chat/completion \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ' \ --data-binary ' { - "question": "Hello", - "stream": false, + "agent_id": "AGENT_ID", + "query": "Hello", + "stream": false }' ``` @@ -4487,12 +4496,13 @@ curl --request POST \ ```bash curl --request POST \ - --url http://{address}/api/v1/agents/{agent_id}/completions \ + --url http://{address}/api/v1/agents/chat/completion \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ' \ --data-binary ' - { - "question": "Hello", + { + "agent_id": "AGENT_ID", + "query": "", "stream": false, "inputs": { "line_var": { @@ -4516,25 +4526,26 @@ curl --request POST \ "value": true } } - }' + }' ``` -The following code will execute the completion process +To continue an existing session: ```bash curl --request POST \ - --url http://{address}/api/v1/agents/{agent_id}/completions \ + --url http://{address}/api/v1/agents/chat/completion \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ' \ --data-binary ' { - "question": "Hello", - "stream": true, - "session_id": "cb2f385cb86211efa36e0242ac120005" + "agent_id": "AGENT_ID", + "query": "Hello again", + "stream": true, + "session_id": "cb2f385cb86211efa36e0242ac120005" }' ``` -##### Request Parameters +##### Request parameters - `agent_id`: (*Path parameter*), `string` The ID of the associated agent. @@ -4557,33 +4568,18 @@ For now, this method does *not* support a file type input/variable. As a workaro *You will get a corresponding file ID from its response body.* ::: -#### Response - -success without `session_id` provided and with no variables specified in the **Begin** component: +##### Response -Stream: +Standard mode stream: ```json -... - -data: { - "event": "message", - "message_id": "cecdcb0e83dc11f0858253708ecb6573", - "created_at": 1756364483, - "task_id": "d1f79142831f11f09cc51795b9eb07c0", - "data": { - "content": " themes" - }, - "session_id": "cd097ca083dc11f0858253708ecb6573" -} - data: { "event": "message", "message_id": "cecdcb0e83dc11f0858253708ecb6573", "created_at": 1756364483, "task_id": "d1f79142831f11f09cc51795b9eb07c0", "data": { - "content": "." + "content": "Hello" }, "session_id": "cd097ca083dc11f0858253708ecb6573" } @@ -4594,140 +4590,7 @@ data: { "created_at": 1756364483, "task_id": "d1f79142831f11f09cc51795b9eb07c0", "data": { - "reference": { - "chunks": { - "20": { - "id": "4b8935ac0a22deb1", - "content": "```cd /usr/ports/editors/neovim/ && make install```## Android[Termux](https://github.com/termux/termux-app) offers a Neovim package.", - "document_id": "4bdd2ff65e1511f0907f09f583941b45", - "document_name": "INSTALL22.md", - "dataset_id": "456ce60c5e1511f0907f09f583941b45", - "image_id": "", - "positions": [ - [ - 12, - 11, - 11, - 11, - 11 - ] - ], - "url": null, - "similarity": 0.5705525104787287, - "vector_similarity": 0.7351750337624289, - "term_similarity": 0.5000000005, - "doc_type": "" - } - }, - "doc_aggs": { - "INSTALL22.md": { - "doc_name": "INSTALL22.md", - "doc_id": "4bdd2ff65e1511f0907f09f583941b45", - "count": 3 - }, - "INSTALL.md": { - "doc_name": "INSTALL.md", - "doc_id": "4bd7fdd85e1511f0907f09f583941b45", - "count": 2 - }, - "INSTALL(1).md": { - "doc_name": "INSTALL(1).md", - "doc_id": "4bdfb42e5e1511f0907f09f583941b45", - "count": 2 - }, - "INSTALL3.md": { - "doc_name": "INSTALL3.md", - "doc_id": "4bdab5825e1511f0907f09f583941b45", - "count": 1 - } - } - } - }, - "session_id": "cd097ca083dc11f0858253708ecb6573" -} - -data: { - "event": "node_finished", - "message_id": "cecdcb0e83dc11f0858253708ecb6573", - "created_at": 1756364483, - "task_id": "d1f79142831f11f09cc51795b9eb07c0", - "data": { - "inputs": { - "sys.query": "how to install neovim?" - }, - "outputs": { - "content": "xxxxxxx", - "_created_time": 15294.0382, - "_elapsed_time": 0.00017 - }, - "component_id": "Agent:EveryHairsChew", - "component_name": "Agent_1", - "component_type": "Agent", - "error": null, - "elapsed_time": 11.2091, - "created_at": 15294.0382, - "trace": [ - { - "component_id": "begin", - "trace": [ - { - "inputs": {}, - "outputs": { - "_created_time": 15257.7949, - "_elapsed_time": 0.00070 - }, - "component_id": "begin", - "component_name": "begin", - "component_type": "Begin", - "error": null, - "elapsed_time": 0.00085, - "created_at": 15257.7949 - } - ] - }, - { - "component_id": "Agent:WeakDragonsRead", - "trace": [ - { - "inputs": { - "sys.query": "how to install neovim?" - }, - "outputs": { - "content": "xxxxxxx", - "_created_time": 15257.7982, - "_elapsed_time": 36.2382 - }, - "component_id": "Agent:WeakDragonsRead", - "component_name": "Agent_0", - "component_type": "Agent", - "error": null, - "elapsed_time": 36.2385, - "created_at": 15257.7982 - } - ] - }, - { - "component_id": "Agent:EveryHairsChew", - "trace": [ - { - "inputs": { - "sys.query": "how to install neovim?" - }, - "outputs": { - "content": "xxxxxxxxxxxxxxxxx", - "_created_time": 15294.0382, - "_elapsed_time": 0.00017 - }, - "component_id": "Agent:EveryHairsChew", - "component_name": "Agent_1", - "component_type": "Agent", - "error": null, - "elapsed_time": 11.2091, - "created_at": 15294.0382 - } - ] - } - ] + "reference": {} }, "session_id": "cd097ca083dc11f0858253708ecb6573" } @@ -4737,175 +4600,17 @@ data:[DONE] When `extra_body.reference_metadata.include` is `true`, each reference chunk may include a `document_metadata` object. -Non-stream: - -If one or more components produce structured output, ensure you set `return_trace=true` and check each component's structured output via `trace`. The top-level `data.structured` field is a shortcut aggregated by `component_id`. +Standard mode non-stream: ```json { "code": 0, "data": { - "created_at": 1756363177, "data": { - "content": "\nTo install Neovim, the process varies depending on your operating system:\n\n### For macOS:\nUsing Homebrew:\n```bash\nbrew install neovim\n```\n\n### For Linux (Debian/Ubuntu):\n```bash\nsudo apt update\nsudo apt install neovim\n```\n\nFor other Linux distributions, you can use their respective package managers or build from source.\n\n### For Windows:\n1. Download the latest Windows installer from the official Neovim GitHub releases page\n2. Run the installer and follow the prompts\n3. Add Neovim to your PATH if not done automatically\n\n### From source (Unix-like systems):\n```bash\ngit clone https://github.com/neovim/neovim.git\ncd neovim\nmake CMAKE_BUILD_TYPE=Release\nsudo make install\n```\n\nAfter installation, you can verify it by running `nvim --version` in your terminal.", - "created_at": 18129.044975627, - "elapsed_time": 10.0157331670016, - "inputs": { - "var1": { - "value": "I am var1" - }, - "var2": { - "value": "I am var2" - } - }, - "outputs": { - "_created_time": 18129.502422278, - "_elapsed_time": 0.00013378599760471843, - "content": "\nTo install Neovim, the process varies depending on your operating system:\n\n### For macOS:\nUsing Homebrew:\n```bash\nbrew install neovim\n```\n\n### For Linux (Debian/Ubuntu):\n```bash\nsudo apt update\nsudo apt install neovim\n```\n\nFor other Linux distributions, you can use their respective package managers or build from source.\n\n### For Windows:\n1. Download the latest Windows installer from the official Neovim GitHub releases page\n2. Run the installer and follow the prompts\n3. Add Neovim to your PATH if not done automatically\n\n### From source (Unix-like systems):\n```bash\ngit clone https://github.com/neovim/neovim.git\ncd neovim\nmake CMAKE_BUILD_TYPE=Release\nsudo make install\n```\n\nAfter installation, you can verify it by running `nvim --version` in your terminal." - }, - "reference": { - "chunks": { - "20": { - "content": "```cd /usr/ports/editors/neovim/ && make install```## Android[Termux](https://github.com/termux/termux-app) offers a Neovim package.", - "dataset_id": "456ce60c5e1511f0907f09f583941b45", - "doc_type": "", - "document_id": "4bdd2ff65e1511f0907f09f583941b45", - "document_name": "INSTALL22.md", - "id": "4b8935ac0a22deb1", - "image_id": "", - "positions": [ - [ - 12, - 11, - 11, - 11, - 11 - ] - ], - "similarity": 0.5705525104787287, - "term_similarity": 0.5000000005, - "url": null, - "vector_similarity": 0.7351750337624289 - } - }, - "doc_aggs": { - "INSTALL(1).md": { - "count": 2, - "doc_id": "4bdfb42e5e1511f0907f09f583941b45", - "doc_name": "INSTALL(1).md" - }, - "INSTALL.md": { - "count": 2, - "doc_id": "4bd7fdd85e1511f0907f09f583941b45", - "doc_name": "INSTALL.md" - }, - "INSTALL22.md": { - "count": 3, - "doc_id": "4bdd2ff65e1511f0907f09f583941b45", - "doc_name": "INSTALL22.md" - }, - "INSTALL3.md": { - "count": 1, - "doc_id": "4bdab5825e1511f0907f09f583941b45", - "doc_name": "INSTALL3.md" - } - } - }, - "trace": [ - { - "component_id": "begin", - "trace": [ - { - "component_id": "begin", - "component_name": "begin", - "component_type": "Begin", - "created_at": 15926.567517862, - "elapsed_time": 0.0008189299987861887, - "error": null, - "inputs": {}, - "outputs": { - "_created_time": 15926.567517862, - "_elapsed_time": 0.0006958619997021742 - } - } - ] - }, - { - "component_id": "Agent:WeakDragonsRead", - "trace": [ - { - "component_id": "Agent:WeakDragonsRead", - "component_name": "Agent_0", - "component_type": "Agent", - "created_at": 15926.569121755, - "elapsed_time": 53.49016142000073, - "error": null, - "inputs": { - "sys.query": "how to install neovim?" - }, - "outputs": { - "_created_time": 15926.569121755, - "_elapsed_time": 53.489981256001556, - "content": "xxxxxxxxxxxxxx", - "use_tools": [ - { - "arguments": { - "query": "xxxx" - }, - "name": "search_my_dateset", - "results": "xxxxxxxxxxx" - } - ] - } - } - ] - }, - { - "component_id": "Agent:EveryHairsChew", - "trace": [ - { - "component_id": "Agent:EveryHairsChew", - "component_name": "Agent_1", - "component_type": "Agent", - "created_at": 15980.060569101, - "elapsed_time": 23.61718057500002, - "error": null, - "inputs": { - "sys.query": "how to install neovim?" - }, - "outputs": { - "_created_time": 15980.060569101, - "_elapsed_time": 0.0003451630000199657, - "content": "xxxxxxxxxxxx" - } - } - ] - }, - { - "component_id": "Message:SlickDingosHappen", - "trace": [ - { - "component_id": "Message:SlickDingosHappen", - "component_name": "Message_0", - "component_type": "Message", - "created_at": 15980.061302513, - "elapsed_time": 23.61655923699982, - "error": null, - "inputs": { - "Agent:EveryHairsChew@content": "xxxxxxxxx", - "Agent:WeakDragonsRead@content": "xxxxxxxxxxx" - }, - "outputs": { - "_created_time": 15980.061302513, - "_elapsed_time": 0.0006695749998471001, - "content": "xxxxxxxxxxx" - } - } - ] - } - ] + "content": "Hello", + "reference": {}, + "trace": [] }, - "event": "workflow_finished", "message_id": "c4692a2683d911f0858253708ecb6573", "session_id": "c39f6f9c83d911f0858253708ecb6573", "task_id": "d1f79142831f11f09cc51795b9eb07c0" @@ -4913,159 +4618,126 @@ If one or more components produce structured output, ensure you set `return_trac } ``` -Success without `session_id` provided and with variables specified in the **Begin** component: +If one or more components produce structured output, set `return_trace=true` and inspect that component output from `trace`. -Stream: +#### OpenAI-compatible mode -```json -data:{ - "event": "message", - "message_id": "0e273472783711f0806e1a6272e682d8", - "created_at": 1755083830, - "task_id": "99ee29d6783511f09c921a6272e682d8", - "data": { - "content": "Hello" - }, - "session_id": "0e0d1542783711f0806e1a6272e682d8" -} +Use the same endpoint and add `"openai-compatible": true`. -data:{ - "event": "message", - "message_id": "0e273472783711f0806e1a6272e682d8", - "created_at": 1755083830, - "task_id": "99ee29d6783511f09c921a6272e682d8", - "data": { - "content": "!" - }, - "session_id": "0e0d1542783711f0806e1a6272e682d8" -} +##### Body -data:{ - "event": "message", - "message_id": "0e273472783711f0806e1a6272e682d8", - "created_at": 1755083830, - "task_id": "99ee29d6783511f09c921a6272e682d8", - "data": { - "content": " How" - }, - "session_id": "0e0d1542783711f0806e1a6272e682d8" -} +- `"agent_id"`: `string` +- `"messages"`: `list[object]` +- `"openai-compatible"`: `boolean`, must be `true` +- `"stream"`: `boolean` +- `"session_id"`: `string` (optional) +- `"model"`: `string` (optional, accepted for compatibility) -... +##### Request examples -data:[DONE] +Streaming request: + +```bash +curl --request POST \ + --url http://{address}/api/v1/agents/chat/completion \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer ' \ + --data-binary ' + { + "agent_id": "AGENT_ID", + "openai-compatible": true, + "stream": true, + "messages": [ + { + "role": "user", + "content": "Hello" + } + ] + }' ``` -Non-stream: +Non-stream request with existing session: -```json -{ - "code": 0, - "data": { - "created_at": 1755083779, - "data": { - "created_at": 547400.868004651, - "elapsed_time": 3.5037803899031132, - "inputs": { - "boolean_var": { - "type": "boolean", - "value": true - }, - "int_var": { - "type": "integer", - "value": 1 - }, - "line_var": { - "type": "line", - "value": "I am line_var" - }, - "option_var": { - "type": "options", - "value": "option 2" - }, - "paragraph_var": { - "type": "paragraph", - "value": "a\nb\nc" - } - }, - "outputs": { - "_created_time": 547400.869271305, - "_elapsed_time": 0.0001251999055966735, - "content": "Hello there! How can I assist you today?" +```bash +curl --request POST \ + --url http://{address}/api/v1/agents/chat/completion \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer ' \ + --data-binary ' + { + "agent_id": "AGENT_ID", + "openai-compatible": true, + "stream": false, + "session_id": "cb2f385cb86211efa36e0242ac120005", + "messages": [ + { + "role": "user", + "content": "Hello" } - }, - "event": "workflow_finished", - "message_id": "effdad8c783611f089261a6272e682d8", - "session_id": "efe523b6783611f089261a6272e682d8", - "task_id": "99ee29d6783511f09c921a6272e682d8" - } -} + ] + }' ``` -Success with variables specified in the **Begin** component: +##### Request parameters -Stream: +- `"agent_id"`: (*Body parameter*), `string`, *Required* + The ID of the associated agent. +- `"messages"`: (*Body parameter*), `list[object]`, *Required* + OpenAI-style chat messages. +- `"openai-compatible"`: (*Body parameter*), `boolean`, *Required* + Must be `true` to enable OpenAI-compatible responses. +- `"stream"`: (*Body parameter*), `boolean` + Whether to return streaming chunks. +- `"session_id"`: (*Body parameter*), `string` + Optional existing session ID. +- `"model"`: (*Body parameter*), `string` + Optional compatibility field. The server still routes by `agent_id`. -```json -data:{ - "event": "message", - "message_id": "5b62e790783711f0bc531a6272e682d8", - "created_at": 1755083960, - "task_id": "99ee29d6783511f09c921a6272e682d8", - "data": { - "content": "Hello" - }, - "session_id": "979e450c781d11f095cb729e3aa55728" -} +##### Response -data:{ - "event": "message", - "message_id": "5b62e790783711f0bc531a6272e682d8", - "created_at": 1755083960, - "task_id": "99ee29d6783511f09c921a6272e682d8", - "data": { - "content": "!" - }, - "session_id": "979e450c781d11f095cb729e3aa55728" -} +OpenAI-compatible stream: -data:{ - "event": "message", - "message_id": "5b62e790783711f0bc531a6272e682d8", - "created_at": 1755083960, - "task_id": "99ee29d6783511f09c921a6272e682d8", - "data": { - "content": " You" - }, - "session_id": "979e450c781d11f095cb729e3aa55728" +```json +data: { + "id": "chatcmpl-xxx", + "object": "chat.completion.chunk", + "model": "AGENT_ID", + "choices": [ + { + "delta": { + "content": "Hello" + }, + "finish_reason": null, + "index": 0 + } + ] } -... - -data:[DONE] +data: [DONE] ``` -Non-stream: +OpenAI-compatible non-stream: ```json { - "code": 0, - "data": { - "created_at": 1755084029, - "data": { - "created_at": 547650.750818867, - "elapsed_time": 1.6227330720284954, - "inputs": {}, - "outputs": { - "_created_time": 547650.752800839, - "_elapsed_time": 9.628792759031057e-05, - "content": "Hello! It appears you've sent another \"Hello\" without additional context. I'm here and ready to respond to any requests or questions you may have. Is there something specific you'd like to discuss or learn about?" + "id": "chatcmpl-xxx", + "object": "chat.completion", + "model": "AGENT_ID", + "choices": [ + { + "finish_reason": "stop", + "index": 0, + "message": { + "role": "assistant", + "content": "Hello", + "reference": {} } - }, - "event": "workflow_finished", - "message_id": "84eec534783711f08db41a6272e682d8", - "session_id": "979e450c781d11f095cb729e3aa55728", - "task_id": "99ee29d6783511f09c921a6272e682d8" + } + ], + "usage": { + "prompt_tokens": 6, + "completion_tokens": 1, + "total_tokens": 7 } } ``` @@ -5075,7 +4747,7 @@ Failure: ```json { "code": 102, - "message": "`question` is required." + "message": "Agent not found." } ``` diff --git a/docs/references/python_api_reference.md b/docs/references/python_api_reference.md index 0604c2c96f8..d7a78100059 100644 --- a/docs/references/python_api_reference.md +++ b/docs/references/python_api_reference.md @@ -1710,7 +1710,7 @@ from ragflow_sdk import RAGFlow, Agent rag_object = RAGFlow(api_key="", base_url="http://:9380") agent_id = "AGENT_ID" -agent = rag_object.list_agents(id = agent_id)[0] +agent = rag_object.get_agent(agent_id) session = agent.create_session() # Or create in release mode: # session = agent.create_session(release=True) @@ -1721,10 +1721,10 @@ session = agent.create_session() ### Converse with agent ```python -Session.ask(question: str="", stream: bool = False) -> Optional[Message, iter[Message]] +Session.ask(question: str = "", stream: bool = False, **kwargs) -> Optional[Message | iter[Message]] ``` -Asks a specified agent a question to start an AI-powered conversation. +Asks a specified agent through the unified completion endpoint. :::tip NOTE In streaming mode, not all responses include a reference, as this depends on the system's judgement. @@ -1734,15 +1734,25 @@ In streaming mode, not all responses include a reference, as this depends on the ##### question: `string` -The question to start an AI-powered conversation. If the **Begin** component takes parameters, a question is not required. +The user message sent to the agent. If the **Begin** component takes parameters, `question` can be an empty string. ##### stream: `bool` Indicates whether to output responses in a streaming way: -- `True`: Enable streaming (default). +- `True`: Enable streaming. - `False`: Disable streaming. +##### kwargs: `dict` + +Additional request parameters forwarded to the completion API. Common options: + +- `inputs`: Variables defined in the **Begin** component. +- `session_id`: Continue an existing session instead of creating a new one. +- `release`: Use the latest published version of the agent. +- `return_trace`: Include execution trace information in the response. +- Other custom Begin component parameters supported by the current workflow. + #### Returns - A `Message` object containing the response to the question if `stream` is set to `False` @@ -1792,8 +1802,8 @@ from ragflow_sdk import RAGFlow, Agent rag_object = RAGFlow(api_key="", base_url="http://:9380") AGENT_id = "AGENT_ID" -agent = rag_object.list_agents(id = AGENT_id)[0] -session = agent.create_session() +agent = rag_object.get_agent(AGENT_id) +session = agent.create_session() print("\n===== Miss R ====\n") print("Hello. What can I do for you?") @@ -1808,6 +1818,31 @@ while True: cont = ans.content ``` +Use Begin inputs and request trace output: + +```python +from ragflow_sdk import RAGFlow, Agent + +rag_object = RAGFlow(api_key="", base_url="http://:9380") +agent = rag_object.get_agent("AGENT_ID") +session = agent.create_session() + +message = session.ask( + "", + stream=False, + inputs={ + "line_var": { + "type": "line", + "value": "I am line_var", + } + }, + return_trace=True, +) + +print(message.content) +print(message.reference) +``` + --- ### List agent sessions @@ -1861,7 +1896,7 @@ from ragflow_sdk import RAGFlow rag_object = RAGFlow(api_key="", base_url="http://:9380") AGENT_id = "AGENT_ID" -agent = rag_object.list_agents(id = AGENT_id)[0] +agent = rag_object.get_agent(AGENT_id) sessons = agent.list_sessions() for session in sessions: print(session) @@ -1900,7 +1935,7 @@ from ragflow_sdk import RAGFlow rag_object = RAGFlow(api_key="", base_url="http://:9380") AGENT_id = "AGENT_ID" -agent = rag_object.list_agents(id = AGENT_id)[0] +agent = rag_object.get_agent(AGENT_id) agent.delete_sessions(ids=["id_1","id_2"]) agent.delete_sessions(delete_all=True) ``` @@ -1917,14 +1952,12 @@ agent.delete_sessions(delete_all=True) RAGFlow.list_agents( page: int = 1, page_size: int = 30, - orderby: str = "create_time", - desc: bool = True, - id: str = None, - title: str = None + orderby: str = "update_time", + desc: bool = True ) -> List[Agent] ``` -Lists agents. +Lists agents. This is a collection API and always returns a list. #### Parameters @@ -1940,33 +1973,56 @@ The number of agents on each page. Defaults to `30`. The attribute by which the results are sorted. Available options: -- `"create_time"` (default) -- `"update_time"` +- `"create_time"` +- `"update_time"` (default) ##### desc: `bool` Indicates whether the retrieved agents should be sorted in descending order. Defaults to `True`. -##### id: `string` +#### Returns -The ID of the agent to retrieve. Defaults to `None`. +- Success: A list of `Agent` objects. +- Failure: `Exception`. -##### name: `string` +#### Examples -The name of the agent to retrieve. Defaults to `None`. +```python +from ragflow_sdk import RAGFlow +rag_object = RAGFlow(api_key="", base_url="http://:9380") +for agent in rag_object.list_agents(): + print(agent) +``` + +--- + +### Get agent + +```python +RAGFlow.get_agent(agent_id: str) -> Agent +``` + +Gets a single agent by ID and returns the detailed agent payload. + +#### Parameters + +##### agent_id: `string` + +The ID of the agent to retrieve. #### Returns -- Success: A list of `Agent` objects. +- Success: An `Agent` object. - Failure: `Exception`. #### Examples ```python from ragflow_sdk import RAGFlow + rag_object = RAGFlow(api_key="", base_url="http://:9380") -for agent in rag_object.list_agents(): - print(agent) +agent = rag_object.get_agent("AGENT_ID") +print(agent) ``` --- diff --git a/sdk/python/ragflow_sdk/modules/session.py b/sdk/python/ragflow_sdk/modules/session.py index bc62f22833c..8f7e95dd7e8 100644 --- a/sdk/python/ragflow_sdk/modules/session.py +++ b/sdk/python/ragflow_sdk/modules/session.py @@ -108,10 +108,15 @@ def _ask_chat(self, question: str, stream: bool, **kwargs): return res def _ask_agent(self, question: str, stream: bool, **kwargs): - json_data = {"question": question, "stream": stream, "session_id": self.id} + json_data = { + "agent_id": self.agent_id, + "query": question, + "stream": stream, + "session_id": self.id, + "openai-compatible": False, + } json_data.update(kwargs) - res = self.post(f"/agents/{self.agent_id}/completions", - json_data, stream=stream) + res = self.post("/agents/chat/completion", json_data, stream=stream) return res def update(self, update_message): diff --git a/sdk/python/ragflow_sdk/ragflow.py b/sdk/python/ragflow_sdk/ragflow.py index 163fe0eeec3..fe0a683719c 100644 --- a/sdk/python/ragflow_sdk/ragflow.py +++ b/sdk/python/ragflow_sdk/ragflow.py @@ -230,7 +230,7 @@ def retrieve( return chunks raise Exception(res.get("message")) - def list_agents(self, page: int = 1, page_size: int = 30, orderby: str = "update_time", desc: bool = True, id: str | None = None, title: str | None = None) -> list[Agent]: + def list_agents(self, page: int = 1, page_size: int = 30, orderby: str = "update_time", desc: bool = True) -> list[Agent]: res = self.get( "/agents", { @@ -238,18 +238,25 @@ def list_agents(self, page: int = 1, page_size: int = 30, orderby: str = "update "page_size": page_size, "orderby": orderby, "desc": desc, - "id": id, - "title": title, }, ) res = res.json() result_list = [] if res.get("code") == 0: - for data in res["data"]: + data = res.get("data") or {} + data_list = data.get("canvas", []) + for data in data_list: result_list.append(Agent(self, data)) return result_list raise Exception(res["message"]) + def get_agent(self, agent_id: str) -> Agent: + res = self.get(f"/agents/{agent_id}") + res = res.json() + if res.get("code") == 0: + return Agent(self, res["data"]) + raise Exception(res["message"]) + def create_agent(self, title: str, dsl: dict, description: str | None = None) -> None: req = {"title": title, "dsl": dsl} diff --git a/test.py b/test.py new file mode 100644 index 00000000000..21f395a4675 --- /dev/null +++ b/test.py @@ -0,0 +1,9 @@ +from fastapi import FastAPI, Request +app = FastAPI() +@app.post("/") +async def echo(request: Request): + body = await request.body() + return body +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/test/testcases/test_http_api/common.py b/test/testcases/test_http_api/common.py index 9a84e95277c..0fbdcb7c329 100644 --- a/test/testcases/test_http_api/common.py +++ b/test/testcases/test_http_api/common.py @@ -406,8 +406,11 @@ def delete_all_agent_sessions(auth, agent_id, *, page_size=1000): def agent_completions(auth, agent_id, payload=None): - url = f"{HOST_ADDRESS}{AGENT_API_URL}/{agent_id}/completions" - res = requests.post(url=url, headers=HEADERS, auth=auth, json=payload) + url = f"{HOST_ADDRESS}{AGENT_API_URL}/chat/completion" + body = {"agent_id": agent_id} + if payload: + body.update(payload) + res = requests.post(url=url, headers=HEADERS, auth=auth, json=body) return res.json() diff --git a/test/testcases/test_http_api/test_session_management/test_agent_completions.py b/test/testcases/test_http_api/test_session_management/test_agent_completions.py index bb65fd9f255..6e332436ad1 100644 --- a/test/testcases/test_http_api/test_session_management/test_agent_completions.py +++ b/test/testcases/test_http_api/test_session_management/test_agent_completions.py @@ -49,11 +49,18 @@ "variables": {}, } + +def _agent_items(res): + data = res.get("data", []) + if isinstance(data, dict): + return data.get("canvas", []) + return data + @pytest.fixture(scope="function") def agent_id(HttpApiAuth, request): res = list_agents(HttpApiAuth, {"page_size": 1000}) assert res["code"] == 0, res - for agent in res.get("data", []): + for agent in _agent_items(res): if agent.get("title") == AGENT_TITLE: delete_agent(HttpApiAuth, agent["id"]) @@ -61,8 +68,9 @@ def agent_id(HttpApiAuth, request): assert res["code"] == 0, res res = list_agents(HttpApiAuth, {"title": AGENT_TITLE}) assert res["code"] == 0, res - assert res.get("data"), res - agent_id = res["data"][0]["id"] + agents = _agent_items(res) + assert agents, res + agent_id = agents[0]["id"] def cleanup(): delete_all_agent_sessions(HttpApiAuth, agent_id) @@ -82,7 +90,7 @@ def test_agent_completion_stream_false(self, HttpApiAuth, agent_id): res = agent_completions( HttpApiAuth, agent_id, - {"question": "hello", "stream": False, "session_id": session_id}, + {"query": "hello", "stream": False, "session_id": session_id}, ) assert res["code"] == 0, res if isinstance(res["data"], dict): diff --git a/test/testcases/test_http_api/test_session_management/test_agent_sessions.py b/test/testcases/test_http_api/test_session_management/test_agent_sessions.py index 883ae2af07b..6672a04bd73 100644 --- a/test/testcases/test_http_api/test_session_management/test_agent_sessions.py +++ b/test/testcases/test_http_api/test_session_management/test_agent_sessions.py @@ -17,11 +17,8 @@ import requests from common import ( create_agent, - create_agent_session, delete_agent, delete_all_agent_sessions, - delete_agent_sessions, - list_agent_sessions, list_agents, ) from configs import HOST_ADDRESS, VERSION @@ -52,11 +49,18 @@ "variables": {}, } + +def _agent_items(res): + data = res.get("data", []) + if isinstance(data, dict): + return data.get("canvas", []) + return data + @pytest.fixture(scope="function") def agent_id(HttpApiAuth, request): res = list_agents(HttpApiAuth, {"page_size": 1000}) assert res["code"] == 0, res - for agent in res.get("data", []): + for agent in _agent_items(res): if agent.get("title") == AGENT_TITLE: delete_agent(HttpApiAuth, agent["id"]) @@ -64,8 +68,9 @@ def agent_id(HttpApiAuth, request): assert res["code"] == 0, res res = list_agents(HttpApiAuth, {"title": AGENT_TITLE}) assert res["code"] == 0, res - assert res.get("data"), res - agent_id = res["data"][0]["id"] + agents = _agent_items(res) + assert agents, res + agent_id = agents[0]["id"] def cleanup(): delete_all_agent_sessions(HttpApiAuth, agent_id) @@ -76,39 +81,14 @@ def cleanup(): class TestAgentSessions: - @pytest.mark.p2 - def test_delete_agent_sessions_empty_ids_noop(self, HttpApiAuth, agent_id): - res = create_agent_session(HttpApiAuth, agent_id, payload={}) - assert res["code"] == 0, res - session_id = res["data"]["id"] - - res = delete_agent_sessions(HttpApiAuth, agent_id, {"ids": []}) - assert res["code"] == 0, res - - res = list_agent_sessions(HttpApiAuth, agent_id, params={"id": session_id}) - assert res["code"] == 0, res - assert len(res["data"]) == 1, res - - @pytest.mark.p2 - def test_create_list_delete_agent_sessions(self, HttpApiAuth, agent_id): - res = create_agent_session(HttpApiAuth, agent_id, payload={}) - assert res["code"] == 0, res - session_id = res["data"]["id"] - assert res["data"]["agent_id"] == agent_id, res - - res = list_agent_sessions(HttpApiAuth, agent_id, params={"id": session_id}) - assert res["code"] == 0, res - assert len(res["data"]) == 1, res - assert res["data"][0]["id"] == session_id, res - - res = delete_agent_sessions(HttpApiAuth, agent_id, {"ids": [session_id]}) - assert res["code"] == 0, res @pytest.mark.p2 def test_agent_crud_validation_contract(self, HttpApiAuth, agent_id): res = list_agents(HttpApiAuth, {"id": "missing-agent-id", "title": "missing-agent-title"}) - assert res["code"] == 102, res - assert "doesn't exist" in res["message"], res + assert res["code"] == 0, res + assert isinstance(res.get("data"), dict), res + assert "canvas" in res["data"], res + assert "total" in res["data"], res res = list_agents(HttpApiAuth, {"title": AGENT_TITLE, "desc": "true", "page_size": 1}) assert res["code"] == 0, res diff --git a/test/testcases/test_http_api/test_session_management/test_session_sdk_routes_unit.py b/test/testcases/test_http_api/test_session_management/test_session_sdk_routes_unit.py index dcbe105e37f..b94a6f80c5b 100644 --- a/test/testcases/test_http_api/test_session_management/test_session_sdk_routes_unit.py +++ b/test/testcases/test_http_api/test_session_management/test_session_sdk_routes_unit.py @@ -498,6 +498,14 @@ def __str__(self): monkeypatch.setitem(sys.modules, "agent.canvas", agent_canvas_mod) monkeypatch.setitem(sys.modules, "agent.dsl_migration", agent_dsl_migration_mod) + quart_mod = ModuleType("quart") + quart_mod.request = SimpleNamespace(args=_Args(), headers={}, files=_AwaitableValue({}), method="POST") + quart_mod.Response = _StubResponse + quart_mod.jsonify = lambda payload: payload + quart_mod.current_app = SimpleNamespace() + quart_mod.has_app_context = lambda: False + monkeypatch.setitem(sys.modules, "quart", quart_mod) + module_path = repo_root / "api" / "apps" / "sdk" / "session.py" spec = importlib.util.spec_from_file_location("test_session_sdk_routes_unit_module", module_path) module = importlib.util.module_from_spec(spec) @@ -530,6 +538,134 @@ def get_by_id(tenant_id): return module +def _load_agent_api_module(monkeypatch): + _load_session_module(monkeypatch) + repo_root = Path(__file__).resolve().parents[4] + + agent_component_mod = ModuleType("agent.component") + + class _StubAgentLLM: + pass + + agent_component_mod.LLM = _StubAgentLLM + monkeypatch.setitem(sys.modules, "agent.component", agent_component_mod) + + api_apps_mod = ModuleType("api.apps") + api_apps_mod.__path__ = [str(repo_root / "api" / "apps")] + api_apps_mod.login_required = lambda func: func + monkeypatch.setitem(sys.modules, "api.apps", api_apps_mod) + + api_apps_services_mod = ModuleType("api.apps.services") + api_apps_services_mod.__path__ = [str(repo_root / "api" / "apps" / "services")] + monkeypatch.setitem(sys.modules, "api.apps.services", api_apps_services_mod) + + canvas_replica_mod = ModuleType("api.apps.services.canvas_replica_service") + + class _StubCanvasReplicaService: + @staticmethod + def normalize_dsl(dsl): + return dsl + + @staticmethod + def replace_for_set(**_kwargs): + return True + + @staticmethod + def bootstrap(**_kwargs): + return True + + @staticmethod + def load_for_run(**_kwargs): + return {"dsl": {}, "title": "agent", "canvas_category": "agent"} + + @staticmethod + def commit_after_run(**_kwargs): + return True + + canvas_replica_mod.CanvasReplicaService = _StubCanvasReplicaService + monkeypatch.setitem(sys.modules, "api.apps.services.canvas_replica_service", canvas_replica_mod) + + file_service_mod = ModuleType("api.db.services.file_service") + file_service_mod.FileService = SimpleNamespace(upload_info=lambda *_args, **_kwargs: {}) + monkeypatch.setitem(sys.modules, "api.db.services.file_service", file_service_mod) + + api_service_mod = ModuleType("api.db.services.api_service") + api_service_mod.API4ConversationService = SimpleNamespace( + get_names=lambda *_args, **_kwargs: [], + get_list=lambda *_args, **_kwargs: (0, []), + save=lambda **_kwargs: True, + get_by_id=lambda _session_id: (True, SimpleNamespace(to_dict=lambda: {"id": _session_id})), + delete_by_id=lambda *_args, **_kwargs: True, + ) + monkeypatch.setitem(sys.modules, "api.db.services.api_service", api_service_mod) + + document_service_mod = ModuleType("api.db.services.document_service") + document_service_mod.DocumentService = SimpleNamespace( + clear_chunk_num_when_rerun=lambda *_args, **_kwargs: True, + update_by_id=lambda *_args, **_kwargs: True, + ) + monkeypatch.setitem(sys.modules, "api.db.services.document_service", document_service_mod) + + knowledgebase_service_mod = ModuleType("api.db.services.knowledgebase_service") + knowledgebase_service_mod.KnowledgebaseService = SimpleNamespace(query=lambda **_kwargs: []) + monkeypatch.setitem(sys.modules, "api.db.services.knowledgebase_service", knowledgebase_service_mod) + + task_service_mod = ModuleType("api.db.services.task_service") + task_service_mod.CANVAS_DEBUG_DOC_ID = "debug-doc" + task_service_mod.GRAPH_RAPTOR_FAKE_DOC_ID = "graph-raptor-fake-doc" + task_service_mod.TaskService = SimpleNamespace(filter_delete=lambda *_args, **_kwargs: True) + task_service_mod.queue_dataflow = lambda *_args, **_kwargs: (True, "") + monkeypatch.setitem(sys.modules, "api.db.services.task_service", task_service_mod) + + pipeline_operation_log_service_mod = ModuleType("api.db.services.pipeline_operation_log_service") + pipeline_operation_log_service_mod.PipelineOperationLogService = SimpleNamespace( + get_documents_info=lambda *_args, **_kwargs: [], + update_by_id=lambda *_args, **_kwargs: True, + ) + monkeypatch.setitem( + sys.modules, + "api.db.services.pipeline_operation_log_service", + pipeline_operation_log_service_mod, + ) + + user_service_mod = ModuleType("api.db.services.user_service") + user_service_mod.TenantService = SimpleNamespace(get_joined_tenants_by_user_id=lambda *_args, **_kwargs: []) + user_service_mod.UserService = SimpleNamespace(get_by_id=lambda *_args, **_kwargs: (False, None)) + user_service_mod.UserTenantService = SimpleNamespace(query=lambda **_kwargs: []) + monkeypatch.setitem(sys.modules, "api.db.services.user_service", user_service_mod) + + user_canvas_version_mod = ModuleType("api.db.services.user_canvas_version") + user_canvas_version_mod.UserCanvasVersionService = SimpleNamespace( + list_by_canvas_id=lambda *_args, **_kwargs: [], + get_by_id=lambda *_args, **_kwargs: (False, None), + get_latest_version_title=lambda *_args, **_kwargs: "", + save_or_replace_latest=lambda **_kwargs: True, + build_version_title=lambda *_args, **_kwargs: "v1", + ) + monkeypatch.setitem(sys.modules, "api.db.services.user_canvas_version", user_canvas_version_mod) + + rag_flow_pipeline_mod = ModuleType("rag.flow.pipeline") + + class _StubPipeline: + def __init__(self, *_args, **_kwargs): + pass + + rag_flow_pipeline_mod.Pipeline = _StubPipeline + monkeypatch.setitem(sys.modules, "rag.flow.pipeline", rag_flow_pipeline_mod) + + rag_redis_mod = ModuleType("rag.utils.redis_conn") + rag_redis_mod.REDIS_CONN = SimpleNamespace(get=lambda *_args, **_kwargs: None) + monkeypatch.setitem(sys.modules, "rag.utils.redis_conn", rag_redis_mod) + + module_path = repo_root / "api" / "apps" / "restful_apis" / "agent_api.py" + spec = importlib.util.spec_from_file_location("test_agent_api_unit_module", module_path) + module = importlib.util.module_from_spec(spec) + module.manager = _DummyManager() + monkeypatch.setitem(sys.modules, "test_agent_api_unit_module", module) + spec.loader.exec_module(module) + return module + + @pytest.mark.p2 def test_create_and_update_guard_matrix(monkeypatch): module = _load_session_module(monkeypatch) @@ -734,33 +870,21 @@ async def fake_async_chat(_dia, _msg, _stream, **_kwargs): @pytest.mark.p2 def test_agents_openai_compatibility_unit(monkeypatch): - module = _load_session_module(monkeypatch) + module = _load_agent_api_module(monkeypatch) monkeypatch.setattr(module, "Response", _StubResponse) monkeypatch.setattr(module, "jsonify", lambda payload: payload) - monkeypatch.setattr(module, "num_tokens_from_string", lambda text: len(text or "")) - - monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"model": "model", "messages": []})) - res = _run(inspect.unwrap(module.agents_completion_openai_compatibility)("tenant-1", "agent-1")) - assert "at least one message" in res["message"] - - monkeypatch.setattr( - module, - "get_request_json", - lambda: _AwaitableValue({"model": "model", "messages": [{"role": "user", "content": "hello"}]}), - ) - monkeypatch.setattr(module.UserCanvasService, "query", lambda **_kwargs: []) - res = _run(inspect.unwrap(module.agents_completion_openai_compatibility)("tenant-1", "agent-1")) - assert "don't own the agent" in res["message"] + monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"openai-compatible": True})) + res = _run(inspect.unwrap(module.agent_chat_completion)("tenant-1")) + assert "`agent_id` is required." in res["message"] - monkeypatch.setattr(module.UserCanvasService, "query", lambda **_kwargs: [SimpleNamespace(id="agent-1")]) monkeypatch.setattr( module, "get_request_json", - lambda: _AwaitableValue({"model": "model", "messages": [{"role": "system", "content": "system only"}]}), + lambda: _AwaitableValue({"agent_id": "agent-1", "openai-compatible": True, "model": "model", "messages": []}), ) - res = _run(inspect.unwrap(module.agents_completion_openai_compatibility)("tenant-1", "agent-1")) - assert "No valid messages found" in json.dumps(res) + res = _run(inspect.unwrap(module.agent_chat_completion)("tenant-1")) + assert "at least one message" in res["message"] captured_calls = [] @@ -774,6 +898,8 @@ async def _completion_openai_stream(*args, **kwargs): "get_request_json", lambda: _AwaitableValue( { + "agent_id": "agent-1", + "openai-compatible": True, "model": "model", "messages": [ {"role": "assistant", "content": "preface"}, @@ -784,7 +910,7 @@ async def _completion_openai_stream(*args, **kwargs): } ), ) - resp = _run(inspect.unwrap(module.agents_completion_openai_compatibility)("tenant-1", "agent-1")) + resp = _run(inspect.unwrap(module.agent_chat_completion)("tenant-1")) assert isinstance(resp, _StubResponse) assert resp.headers.get("Content-Type") == "text/event-stream; charset=utf-8" _run(_collect_stream(resp.body)) @@ -795,11 +921,15 @@ async def _completion_openai_nonstream(*args, **kwargs): yield {"id": "non-stream"} monkeypatch.setattr(module, "completion_openai", _completion_openai_nonstream) + monkeypatch.setattr(module.API4ConversationService, "get_by_id", lambda _session_id: (True, SimpleNamespace(dialog_id="agent-1"))) + monkeypatch.setattr(module.UserCanvasService, "accessible", lambda *_args, **_kwargs: True) monkeypatch.setattr( module, "get_request_json", lambda: _AwaitableValue( { + "agent_id": "agent-1", + "openai-compatible": True, "model": "model", "messages": [ {"role": "user", "content": "first"}, @@ -812,7 +942,7 @@ async def _completion_openai_nonstream(*args, **kwargs): } ), ) - res = _run(inspect.unwrap(module.agents_completion_openai_compatibility)("tenant-1", "agent-1")) + res = _run(inspect.unwrap(module.agent_chat_completion)("tenant-1")) assert res["id"] == "non-stream" assert captured_calls[-1][0][2] == "final user" assert captured_calls[-1][1]["stream"] is False @@ -821,9 +951,11 @@ async def _completion_openai_nonstream(*args, **kwargs): @pytest.mark.p2 def test_agent_completions_stream_and_nonstream_unit(monkeypatch): - module = _load_session_module(monkeypatch) + module = _load_agent_api_module(monkeypatch) monkeypatch.setattr(module, "Response", _StubResponse) + monkeypatch.setattr(module.API4ConversationService, "get_by_id", lambda _session_id: (True, SimpleNamespace(dialog_id="agent-1"))) + monkeypatch.setattr(module.UserCanvasService, "accessible", lambda *_args, **_kwargs: True) async def _agent_stream(*_args, **_kwargs): yield "data:not-json" @@ -843,9 +975,20 @@ async def _agent_stream(*_args, **_kwargs): yield "data:" + json.dumps({"event": "message", "data": {"content": "hello"}}) monkeypatch.setattr(module, "agent_completion", _agent_stream) - monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"stream": True, "return_trace": True})) + monkeypatch.setattr( + module, + "get_request_json", + lambda: _AwaitableValue( + { + "agent_id": "agent-1", + "session_id": "session-1", + "stream": True, + "return_trace": True, + } + ), + ) - resp = _run(inspect.unwrap(module.agent_completions)("tenant-1", "agent-1")) + resp = _run(inspect.unwrap(module.agent_chat_completion)("tenant-1")) chunks = _run(_collect_stream(resp.body)) assert resp.headers.get("Content-Type") == "text/event-stream; charset=utf-8" assert any('"trace"' in chunk for chunk in chunks) @@ -874,8 +1017,19 @@ async def _agent_nonstream(*_args, **_kwargs): ) monkeypatch.setattr(module, "agent_completion", _agent_nonstream) - monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"stream": False, "return_trace": True})) - res = _run(inspect.unwrap(module.agent_completions)("tenant-1", "agent-1")) + monkeypatch.setattr( + module, + "get_request_json", + lambda: _AwaitableValue( + { + "agent_id": "agent-1", + "session_id": "session-1", + "stream": False, + "return_trace": True, + } + ), + ) + res = _run(inspect.unwrap(module.agent_chat_completion)("tenant-1")) assert res["data"]["data"]["content"] == "A" assert res["data"]["data"]["reference"] == {"doc": "r"} assert res["data"]["data"]["structured"] == { @@ -884,64 +1038,7 @@ async def _agent_nonstream(*_args, **_kwargs): "c4": {}, } assert [item["component_id"] for item in res["data"]["data"]["trace"]] == ["c2", "c3", "c4"] - - async def _agent_nonstream_broken(*_args, **_kwargs): - yield "data:{" - - monkeypatch.setattr(module, "agent_completion", _agent_nonstream_broken) - monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"stream": False, "return_trace": False})) - res = _run(inspect.unwrap(module.agent_completions)("tenant-1", "agent-1")) - assert res["data"].startswith("**ERROR**") - - -@pytest.mark.p2 -def test_list_agent_session_projection_unit(monkeypatch): - module = _load_session_module(monkeypatch) - - monkeypatch.setattr(module, "request", SimpleNamespace(args=_Args({}))) - monkeypatch.setattr(module.UserCanvasService, "query", lambda **_kwargs: [SimpleNamespace(id="agent-1")]) - - conv_non_list_reference = { - "id": "session-1", - "dialog_id": "agent-1", - "message": [{"role": "assistant", "content": "hello", "prompt": "internal"}], - "reference": {"unexpected": "shape"}, - } - monkeypatch.setattr(module.API4ConversationService, "get_list", lambda *_args, **_kwargs: (1, [conv_non_list_reference])) - res = _run(inspect.unwrap(module.list_agent_session)("tenant-1", "agent-1")) - assert res["data"][0]["agent_id"] == "agent-1" - assert "prompt" not in res["data"][0]["messages"][0] - - conv_with_chunks = { - "id": "session-2", - "dialog_id": "agent-1", - "message": [ - {"role": "user", "content": "question"}, - {"role": "assistant", "content": "answer", "prompt": "internal"}, - ], - "reference": [ - { - "chunks": [ - "not-a-dict", - { - "chunk_id": "chunk-2", - "content_with_weight": "weighted", - "doc_id": "doc-2", - "docnm_kwd": "doc-name-2", - "kb_id": "kb-2", - "image_id": "img-2", - "positions": [9], - }, - ] - } - ], - } - monkeypatch.setattr(module.API4ConversationService, "get_list", lambda *_args, **_kwargs: (1, [conv_with_chunks])) - res = _run(inspect.unwrap(module.list_agent_session)("tenant-1", "agent-1")) - projected_chunk = res["data"][0]["messages"][1]["reference"][0] - assert projected_chunk["image_id"] == "img-2" - assert projected_chunk["positions"] == [9] - + @pytest.mark.p2 def test_delete_routes_partial_duplicate_unit(monkeypatch): diff --git a/test/testcases/test_sdk_api/test_agent_management/test_agent_crud_unit.py b/test/testcases/test_sdk_api/test_agent_management/test_agent_crud_unit.py index a92b3670468..1642c14dde5 100644 --- a/test/testcases/test_sdk_api/test_agent_management/test_agent_crud_unit.py +++ b/test/testcases/test_sdk_api/test_agent_management/test_agent_crud_unit.py @@ -47,12 +47,12 @@ def _ok_get(path, params=None, json=None): captured["path"] = path captured["params"] = params captured["json"] = json - return _DummyResponse({"code": 0, "data": [{"id": "agent-1", "title": "Agent One"}]}) + return _DummyResponse({"code": 0, "data": {"canvas": [{"id": "agent-1", "title": "Agent One"}], "total": 1}}) monkeypatch.setattr(client, "get", _ok_get) - agents = client.list_agents(title="Agent One") + agents = client.list_agents() assert captured["path"] == "/agents" - assert captured["params"]["title"] == "Agent One" + assert captured["params"] == {"page": 1, "page_size": 30, "orderby": "update_time", "desc": True} assert isinstance(agents[0], Agent), str(agents) assert agents[0].id == "agent-1", str(agents[0]) assert agents[0].title == "Agent One", str(agents[0]) diff --git a/test/testcases/test_sdk_api/test_session_management/test_create_session_with_chat_assistant.py b/test/testcases/test_sdk_api/test_session_management/test_create_session_with_chat_assistant.py index eeb8add5908..7ab43ffd1c9 100644 --- a/test/testcases/test_sdk_api/test_session_management/test_create_session_with_chat_assistant.py +++ b/test/testcases/test_sdk_api/test_session_management/test_create_session_with_chat_assistant.py @@ -160,8 +160,10 @@ def _agent_post(path, json=None, stream=False, files=None): assert calls[0][2]["session_id"] == "session-chat" assert calls[0][2]["temperature"] == 0.2 assert calls[0][3] is True - assert calls[1][1] == "/agents/agent-1/completions" - assert calls[1][2]["question"] == "hello agent" + assert calls[1][1] == "/agents/chat/completion" + assert calls[1][2]["agent_id"] == "agent-1" + assert calls[1][2]["query"] == "hello agent" assert calls[1][2]["session_id"] == "session-agent" + assert calls[1][2]["openai-compatible"] is False assert calls[1][2]["top_p"] == 0.8 assert calls[1][3] is True diff --git a/test/testcases/test_web_api/test_agent_app/test_agents_webhook_unit.py b/test/testcases/test_web_api/test_agent_app/test_agents_webhook_unit.py deleted file mode 100644 index 6f3a0a20554..00000000000 --- a/test/testcases/test_web_api/test_agent_app/test_agents_webhook_unit.py +++ /dev/null @@ -1,1272 +0,0 @@ -# -# 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 base64 -import hashlib -import hmac -import importlib.util -import json -import sys -from pathlib import Path -from types import ModuleType, SimpleNamespace - -import pytest - - -class _DummyManager: - def route(self, *_args, **_kwargs): - def decorator(func): - return func - - return decorator - - -class _AwaitableValue: - def __init__(self, value): - self._value = value - - def __await__(self): - async def _co(): - return self._value - - return _co().__await__() - - -class _Args(dict): - def get(self, key, default=None, type=None): - value = super().get(key, default) - if value is None or type is None: - return value - try: - return type(value) - except (TypeError, ValueError): - return default - - -class _DummyRequest: - def __init__( - self, - *, - path="/api/v1/webhook/agent-1", - method="POST", - headers=None, - content_length=0, - remote_addr="127.0.0.1", - args=None, - json_body=None, - raw_body=b"", - form=None, - files=None, - authorization=None, - ): - self.path = path - self.method = method - self.headers = headers or {} - self.content_length = content_length - self.remote_addr = remote_addr - self.args = args or {} - self.authorization = authorization - self.form = _AwaitableValue(form or {}) - self.files = _AwaitableValue(files or {}) - self._json_body = json_body - self._raw_body = raw_body - - async def get_json(self): - return self._json_body - - async def get_data(self): - return self._raw_body - - -class _CanvasRecord: - def __init__(self, *, canvas_category, dsl, user_id="tenant-1"): - self.canvas_category = canvas_category - self.dsl = dsl - self.user_id = user_id - - def to_dict(self): - return {"user_id": self.user_id, "dsl": self.dsl} - - -class _StubCanvas: - def __init__(self, dsl, user_id, agent_id, canvas_id=None): - self.dsl = dsl - self.user_id = user_id - self.agent_id = agent_id - self.canvas_id = canvas_id - - async def run(self, **_kwargs): - if False: - yield {} - - async def get_files_async(self, desc): - return {"files": desc} - - def __str__(self): - return "{}" - - -class _StubRedisConn: - def __init__(self): - self.bucket_result = [1] - self.bucket_exc = None - self.REDIS = object() - - def lua_token_bucket(self, **_kwargs): - if self.bucket_exc is not None: - raise self.bucket_exc - return self.bucket_result - - def get(self, _key): - return None - - def set_obj(self, _key, _obj, _ttl): - return None - - -def _run(coro): - return asyncio.run(coro) - - -def _default_webhook_params( - *, - security=None, - methods=None, - content_types="application/json", - schema=None, - execution_mode="Immediately", - response=None, -): - return { - "mode": "Webhook", - "methods": methods if methods is not None else ["POST"], - "security": security if security is not None else {}, - "content_types": content_types, - "schema": schema - if schema is not None - else { - "query": {"properties": {}, "required": []}, - "headers": {"properties": {}, "required": []}, - "body": {"properties": {}, "required": []}, - }, - "execution_mode": execution_mode, - "response": response if response is not None else {}, - } - - -def _make_webhook_cvs(module, *, params=None, dsl=None, canvas_category=None): - if dsl is None: - if params is None: - params = _default_webhook_params() - dsl = { - "components": { - "begin": { - "obj": {"component_name": "Begin", "params": params}, - "downstream": [], - "upstream": [], - } - } - } - if canvas_category is None: - canvas_category = module.CanvasCategory.Agent - return _CanvasRecord(canvas_category=canvas_category, dsl=dsl) - - -def _patch_background_task(monkeypatch, module): - def _fake_create_task(coro): - coro.close() - return None - - monkeypatch.setattr(module.asyncio, "create_task", _fake_create_task) - - -def _load_agents_app(monkeypatch): - repo_root = Path(__file__).resolve().parents[4] - - common_pkg = ModuleType("common") - common_pkg.__path__ = [str(repo_root / "common")] - monkeypatch.setitem(sys.modules, "common", common_pkg) - - agent_pkg = ModuleType("agent") - agent_pkg.__path__ = [] - canvas_mod = ModuleType("agent.canvas") - canvas_mod.Canvas = _StubCanvas - agent_pkg.canvas = canvas_mod - monkeypatch.setitem(sys.modules, "agent", agent_pkg) - monkeypatch.setitem(sys.modules, "agent.canvas", canvas_mod) - - services_pkg = ModuleType("api.db.services") - services_pkg.__path__ = [] - monkeypatch.setitem(sys.modules, "api.db.services", services_pkg) - - canvas_service_mod = ModuleType("api.db.services.canvas_service") - - class _StubUserCanvasService: - @staticmethod - def query(**_kwargs): - return [] - - @staticmethod - def get_list(*_args, **_kwargs): - return [] - - @staticmethod - def save(**_kwargs): - return True - - @staticmethod - def update_by_id(*_args, **_kwargs): - return True - - @staticmethod - def delete_by_id(*_args, **_kwargs): - return True - - @staticmethod - def get_by_id(_id): - return False, None - - canvas_service_mod.UserCanvasService = _StubUserCanvasService - monkeypatch.setitem(sys.modules, "api.db.services.canvas_service", canvas_service_mod) - services_pkg.canvas_service = canvas_service_mod - - file_service_mod = ModuleType("api.db.services.file_service") - - class _StubFileService: - @staticmethod - def upload_info(*_args, **_kwargs): - return {"id": "uploaded"} - - file_service_mod.FileService = _StubFileService - monkeypatch.setitem(sys.modules, "api.db.services.file_service", file_service_mod) - services_pkg.file_service = file_service_mod - - canvas_version_mod = ModuleType("api.db.services.user_canvas_version") - - class _StubUserCanvasVersionService: - @staticmethod - def insert(**_kwargs): - return True - - @staticmethod - def delete_all_versions(*_args, **_kwargs): - return True - - @staticmethod - def save_or_replace_latest(*_args, **_kwargs): - return True - - @staticmethod - def build_version_title(*_args, **_kwargs): - return "stub_version_title" - - canvas_version_mod.UserCanvasVersionService = _StubUserCanvasVersionService - monkeypatch.setitem(sys.modules, "api.db.services.user_canvas_version", canvas_version_mod) - services_pkg.user_canvas_version = canvas_version_mod - - tenant_llm_service_mod = ModuleType("api.db.services.tenant_llm_service") - - class _StubLLMFactoriesService: - @staticmethod - def get_api_key(*_args, **_kwargs): - return None - - tenant_llm_service_mod.LLMFactoriesService = _StubLLMFactoriesService - monkeypatch.setitem(sys.modules, "api.db.services.tenant_llm_service", tenant_llm_service_mod) - services_pkg.tenant_llm_service = tenant_llm_service_mod - - user_service_mod = ModuleType("api.db.services.user_service") - - class _StubUserService: - @staticmethod - def query(**_kwargs): - return [] - - @staticmethod - def get_by_id(_id): - return False, None - - user_service_mod.UserService = _StubUserService - monkeypatch.setitem(sys.modules, "api.db.services.user_service", user_service_mod) - services_pkg.user_service = user_service_mod - services_pkg.UserService = _StubUserService - - # Stub api.apps package to prevent api/apps/__init__.py from executing - # (it triggers heavy imports like quart, settings, DB connections). - api_apps_pkg = ModuleType("api.apps") - api_apps_pkg.__path__ = [] - monkeypatch.setitem(sys.modules, "api.apps", api_apps_pkg) - - api_apps_services_pkg = ModuleType("api.apps.services") - api_apps_services_pkg.__path__ = [] - monkeypatch.setitem(sys.modules, "api.apps.services", api_apps_services_pkg) - api_apps_pkg.services = api_apps_services_pkg - - canvas_replica_mod = ModuleType("api.apps.services.canvas_replica_service") - - class _StubCanvasReplicaService: - @classmethod - def normalize_dsl(cls, dsl): - import json - if isinstance(dsl, str): - return json.loads(dsl) - return dsl - - @classmethod - def bootstrap(cls, *_args, **_kwargs): - return {} - - @classmethod - def load_for_run(cls, *_args, **_kwargs): - return None - - @classmethod - def commit_after_run(cls, *_args, **_kwargs): - return True - - @classmethod - def replace_for_set(cls, *_args, **_kwargs): - return True - - @classmethod - def create_if_absent(cls, *_args, **_kwargs): - return {} - - canvas_replica_mod.CanvasReplicaService = _StubCanvasReplicaService - monkeypatch.setitem(sys.modules, "api.apps.services.canvas_replica_service", canvas_replica_mod) - api_apps_services_pkg.canvas_replica_service = canvas_replica_mod - - redis_obj = _StubRedisConn() - redis_mod = ModuleType("rag.utils.redis_conn") - redis_mod.REDIS_CONN = redis_obj - monkeypatch.setitem(sys.modules, "rag.utils.redis_conn", redis_mod) - - module_path = repo_root / "api" / "apps" / "sdk" / "agents.py" - spec = importlib.util.spec_from_file_location("test_agents_webhook_unit", module_path) - module = importlib.util.module_from_spec(spec) - module.manager = _DummyManager() - spec.loader.exec_module(module) - return module - - -def _assert_bad_request(res, expected_substring): - assert isinstance(res, tuple), res - payload, code = res - assert code == 400, res - assert payload["code"] == 400, payload - assert expected_substring in payload["message"], payload - - -@pytest.mark.p2 -def test_agents_crud_unit_branches(monkeypatch): - module = _load_agents_app(monkeypatch) - - monkeypatch.setattr( - module, - "request", - SimpleNamespace(args={"id": "missing", "title": "missing", "desc": "false", "page": "1", "page_size": "10"}), - ) - monkeypatch.setattr(module.UserCanvasService, "query", lambda **_kwargs: []) - res = module.list_agents.__wrapped__("tenant-1") - assert res["code"] == module.RetCode.DATA_ERROR - assert "doesn't exist" in res["message"] - - captured = {} - - def fake_get_list(_tenant_id, _page, _page_size, _orderby, desc, *_rest): - captured["desc"] = desc - return [{"id": "agent-1"}] - - monkeypatch.setattr(module.UserCanvasService, "query", lambda **_kwargs: [{"id": "agent-1"}]) - monkeypatch.setattr(module.UserCanvasService, "get_list", fake_get_list) - monkeypatch.setattr(module, "request", SimpleNamespace(args={"desc": "true"})) - res = module.list_agents.__wrapped__("tenant-1") - assert res["code"] == module.RetCode.SUCCESS - assert captured["desc"] is True - - async def req_no_dsl(): - return {"title": "agent-a"} - - monkeypatch.setattr(module, "get_request_json", req_no_dsl) - res = _run(module.create_agent.__wrapped__("tenant-1")) - assert res["code"] == module.RetCode.ARGUMENT_ERROR - assert "No DSL data in request" in res["message"] - - async def req_no_title(): - return {"dsl": {"components": {}}} - - monkeypatch.setattr(module, "get_request_json", req_no_title) - res = _run(module.create_agent.__wrapped__("tenant-1")) - assert res["code"] == module.RetCode.ARGUMENT_ERROR - assert "No title in request" in res["message"] - - async def req_dup(): - return {"dsl": {"components": {}}, "title": "agent-dup"} - - monkeypatch.setattr(module, "get_request_json", req_dup) - monkeypatch.setattr(module.UserCanvasService, "query", lambda **_kwargs: [object()]) - res = _run(module.create_agent.__wrapped__("tenant-1")) - assert res["code"] == module.RetCode.DATA_ERROR - assert "already exists" in res["message"] - - monkeypatch.setattr(module.UserCanvasService, "query", lambda **_kwargs: []) - monkeypatch.setattr(module, "get_uuid", lambda: "agent-created") - monkeypatch.setattr(module.UserCanvasService, "save", lambda **_kwargs: False) - res = _run(module.create_agent.__wrapped__("tenant-1")) - assert res["code"] == module.RetCode.DATA_ERROR - assert "Fail to create agent" in res["message"] - - async def req_update(): - return {"dsl": {"nodes": []}, "title": " webhook-agent ", "unused": None} - - monkeypatch.setattr(module, "get_request_json", req_update) - monkeypatch.setattr(module.UserCanvasService, "query", lambda **_kwargs: False) - res = _run(module.update_agent.__wrapped__("tenant-1", "agent-1")) - assert res["code"] == module.RetCode.OPERATING_ERROR - - calls = {"update": 0, "save_or_replace_latest": 0} - monkeypatch.setattr(module.UserCanvasService, "query", lambda **_kwargs: True) - monkeypatch.setattr( - module.UserCanvasService, - "update_by_id", - lambda *_args, **_kwargs: calls.__setitem__("update", calls["update"] + 1), - ) - monkeypatch.setattr( - module.UserCanvasVersionService, - "save_or_replace_latest", - lambda *_args, **_kwargs: calls.__setitem__("save_or_replace_latest", calls["save_or_replace_latest"] + 1), - ) - res = _run(module.update_agent.__wrapped__("tenant-1", "agent-1")) - assert res["code"] == module.RetCode.SUCCESS - assert calls == {"update": 1, "save_or_replace_latest": 1} - - monkeypatch.setattr(module.UserCanvasService, "query", lambda **_kwargs: False) - res = module.delete_agent.__wrapped__("tenant-1", "agent-1") - assert res["code"] == module.RetCode.OPERATING_ERROR - - -@pytest.mark.p2 -def test_webhook_prechecks(monkeypatch): - module = _load_agents_app(monkeypatch) - monkeypatch.setattr(module, "request", _DummyRequest(headers={"Content-Type": "application/json"}, json_body={})) - - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (False, None)) - _assert_bad_request(_run(module.webhook("agent-1")), "Canvas not found") - - cvs = _make_webhook_cvs(module, canvas_category=module.CanvasCategory.DataFlow) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - _assert_bad_request(_run(module.webhook("agent-1")), "Dataflow can not be triggered") - - cvs = _make_webhook_cvs(module, dsl="invalid-dsl") - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - _assert_bad_request(_run(module.webhook("agent-1")), "Invalid DSL format") - - cvs = _make_webhook_cvs( - module, - dsl={"components": {"begin": {"obj": {"component_name": "Begin", "params": {"mode": "Chat"}}}}}, - ) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - _assert_bad_request(_run(module.webhook("agent-1")), "Webhook not configured") - - params = _default_webhook_params(methods=["GET"]) - cvs = _make_webhook_cvs(module, params=params) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - _assert_bad_request(_run(module.webhook("agent-1")), "not allowed") - - -@pytest.mark.p2 -def test_webhook_security_dispatch(monkeypatch): - module = _load_agents_app(monkeypatch) - _patch_background_task(monkeypatch, module) - - monkeypatch.setattr( - module, - "request", - _DummyRequest(headers={"Content-Type": "application/json"}, json_body={}, args={"a": "b"}), - ) - - for security in ({}, {"auth_type": "none"}): - cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=security)) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id, _cvs=cvs: (True, _cvs)) - res = _run(module.webhook("agent-1")) - assert hasattr(res, "status_code"), res - assert res.status_code == 200 - - cvs = _make_webhook_cvs(module, params=_default_webhook_params(security={"auth_type": "unsupported"})) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - _assert_bad_request(_run(module.webhook("agent-1")), "Unsupported auth_type") - - -@pytest.mark.p2 -def test_webhook_max_body_size(monkeypatch): - module = _load_agents_app(monkeypatch) - _patch_background_task(monkeypatch, module) - - base_request = _DummyRequest(headers={"Content-Type": "application/json"}, json_body={}) - monkeypatch.setattr(module, "request", base_request) - - cvs = _make_webhook_cvs(module, params=_default_webhook_params(security={"auth_type": "none"})) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - res = _run(module.webhook("agent-1")) - assert hasattr(res, "status_code") - assert res.status_code == 200 - - security = {"auth_type": "none", "max_body_size": "123"} - cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=security)) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - _assert_bad_request(_run(module.webhook("agent-1")), "Invalid max_body_size format") - - security = {"auth_type": "none", "max_body_size": "11mb"} - cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=security)) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - _assert_bad_request(_run(module.webhook("agent-1")), "exceeds maximum allowed size") - - monkeypatch.setattr( - module, - "request", - _DummyRequest(headers={"Content-Type": "application/json"}, json_body={}, content_length=2048), - ) - security = {"auth_type": "none", "max_body_size": "1kb"} - cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=security)) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - _assert_bad_request(_run(module.webhook("agent-1")), "Request body too large") - - -@pytest.mark.p2 -def test_webhook_ip_whitelist(monkeypatch): - module = _load_agents_app(monkeypatch) - _patch_background_task(monkeypatch, module) - - monkeypatch.setattr( - module, - "request", - _DummyRequest(headers={"Content-Type": "application/json"}, json_body={}, remote_addr="127.0.0.1"), - ) - - for whitelist in ([], ["127.0.0.0/24"], ["127.0.0.1"]): - security = {"auth_type": "none", "ip_whitelist": whitelist} - cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=security)) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id, _cvs=cvs: (True, _cvs)) - res = _run(module.webhook("agent-1")) - assert hasattr(res, "status_code"), res - assert res.status_code == 200 - - security = {"auth_type": "none", "ip_whitelist": ["10.0.0.1"]} - cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=security)) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - _assert_bad_request(_run(module.webhook("agent-1")), "is not allowed") - - -@pytest.mark.p2 -def test_webhook_rate_limit(monkeypatch): - module = _load_agents_app(monkeypatch) - _patch_background_task(monkeypatch, module) - - monkeypatch.setattr(module, "request", _DummyRequest(headers={"Content-Type": "application/json"}, json_body={})) - - cvs = _make_webhook_cvs(module, params=_default_webhook_params(security={"auth_type": "none"})) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - res = _run(module.webhook("agent-1")) - assert hasattr(res, "status_code") - assert res.status_code == 200 - - bad_limit = {"auth_type": "none", "rate_limit": {"limit": 0, "per": "minute"}} - cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=bad_limit)) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - _assert_bad_request(_run(module.webhook("agent-1")), "rate_limit.limit must be > 0") - - bad_per = {"auth_type": "none", "rate_limit": {"limit": 1, "per": "week"}} - cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=bad_per)) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - _assert_bad_request(_run(module.webhook("agent-1")), "Invalid rate_limit.per") - - module.REDIS_CONN.bucket_result = [0] - module.REDIS_CONN.bucket_exc = None - denied = {"auth_type": "none", "rate_limit": {"limit": 1, "per": "minute"}} - cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=denied)) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - _assert_bad_request(_run(module.webhook("agent-1")), "Too many requests") - - module.REDIS_CONN.bucket_result = [1] - module.REDIS_CONN.bucket_exc = RuntimeError("redis failure") - cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=denied)) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - _assert_bad_request(_run(module.webhook("agent-1")), "Rate limit error") - - -@pytest.mark.p2 -def test_webhook_token_basic_jwt_auth(monkeypatch): - module = _load_agents_app(monkeypatch) - _patch_background_task(monkeypatch, module) - - monkeypatch.setattr(module, "request", _DummyRequest(headers={"Content-Type": "application/json"}, json_body={})) - - token_security = {"auth_type": "token", "token": {"token_header": "X-TOKEN", "token_value": "ok"}} - cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=token_security)) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - _assert_bad_request(_run(module.webhook("agent-1")), "Invalid token authentication") - - monkeypatch.setattr( - module, - "request", - _DummyRequest( - headers={"Content-Type": "application/json"}, - json_body={}, - authorization=SimpleNamespace(username="u", password="bad"), - ), - ) - basic_security = {"auth_type": "basic", "basic_auth": {"username": "u", "password": "p"}} - cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=basic_security)) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - _assert_bad_request(_run(module.webhook("agent-1")), "Invalid Basic Auth credentials") - - monkeypatch.setattr(module, "request", _DummyRequest(headers={"Content-Type": "application/json"}, json_body={})) - jwt_missing_secret = {"auth_type": "jwt", "jwt": {}} - cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=jwt_missing_secret)) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - _assert_bad_request(_run(module.webhook("agent-1")), "JWT secret not configured") - - jwt_base = {"auth_type": "jwt", "jwt": {"secret": "secret"}} - cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=jwt_base)) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - _assert_bad_request(_run(module.webhook("agent-1")), "Missing Bearer token") - - monkeypatch.setattr( - module, - "request", - _DummyRequest(headers={"Content-Type": "application/json", "Authorization": "Bearer "}, json_body={}), - ) - _assert_bad_request(_run(module.webhook("agent-1")), "Empty Bearer token") - - monkeypatch.setattr( - module, - "request", - _DummyRequest(headers={"Content-Type": "application/json", "Authorization": "Bearer token"}, json_body={}), - ) - monkeypatch.setattr(module.jwt, "decode", lambda *_args, **_kwargs: (_ for _ in ()).throw(Exception("decode boom"))) - _assert_bad_request(_run(module.webhook("agent-1")), "Invalid JWT") - - monkeypatch.setattr(module.jwt, "decode", lambda *_args, **_kwargs: {"exp": 1}) - jwt_reserved = {"auth_type": "jwt", "jwt": {"secret": "secret", "required_claims": ["exp"]}} - cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=jwt_reserved)) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - _assert_bad_request(_run(module.webhook("agent-1")), "Reserved JWT claim cannot be required") - - monkeypatch.setattr(module.jwt, "decode", lambda *_args, **_kwargs: {}) - jwt_missing_claim = {"auth_type": "jwt", "jwt": {"secret": "secret", "required_claims": ["role"]}} - cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=jwt_missing_claim)) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - _assert_bad_request(_run(module.webhook("agent-1")), "Missing JWT claim") - - captured = {} - - def fake_decode(token, options, **kwargs): - captured["token"] = token - captured["options"] = options - captured["kwargs"] = kwargs - return {"role": "admin"} - - monkeypatch.setattr(module.jwt, "decode", fake_decode) - jwt_success = { - "auth_type": "jwt", - "jwt": { - "secret": "secret", - "audience": "aud", - "issuer": "iss", - "required_claims": "role", - }, - } - cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=jwt_success)) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - res = _run(module.webhook("agent-1")) - assert hasattr(res, "status_code") - assert res.status_code == 200 - assert captured["kwargs"]["audience"] == "aud" - assert captured["kwargs"]["issuer"] == "iss" - assert captured["options"]["verify_aud"] is True - assert captured["options"]["verify_iss"] is True - - monkeypatch.setattr(module.jwt, "decode", lambda *_args, **_kwargs: {}) - jwt_success_invalid_type = {"auth_type": "jwt", "jwt": {"secret": "secret", "required_claims": 123}} - cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=jwt_success_invalid_type)) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - res = _run(module.webhook("agent-1")) - assert hasattr(res, "status_code") - assert res.status_code == 200 - - -@pytest.mark.p2 -def test_webhook_parse_request_branches(monkeypatch): - module = _load_agents_app(monkeypatch) - _patch_background_task(monkeypatch, module) - - security = {"auth_type": "none"} - params = _default_webhook_params(security=security, content_types="application/json") - cvs = _make_webhook_cvs(module, params=params) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - - monkeypatch.setattr( - module, - "request", - _DummyRequest(headers={"Content-Type": "text/plain"}, raw_body=b'{"x":1}', json_body={}), - ) - with pytest.raises(ValueError, match="Invalid Content-Type"): - _run(module.webhook("agent-1")) - - monkeypatch.setattr( - module, - "request", - _DummyRequest(headers={"Content-Type": "application/json"}, json_body={"x": 1}, args={"q": "1"}), - ) - res = _run(module.webhook("agent-1")) - assert hasattr(res, "status_code") - assert res.status_code == 200 - - params = _default_webhook_params(security=security, content_types="multipart/form-data") - cvs = _make_webhook_cvs(module, params=params) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - files = {f"file{i}": object() for i in range(11)} - monkeypatch.setattr( - module, - "request", - _DummyRequest( - headers={"Content-Type": "multipart/form-data"}, - form={"key": "value"}, - files=files, - json_body={}, - ), - ) - res = _run(module.webhook("agent-1")) - assert hasattr(res, "status_code") - assert res.status_code == 200 - - uploaded = {"count": 0} - monkeypatch.setattr( - module.FileService, - "upload_info", - lambda *_args, **_kwargs: uploaded.__setitem__("count", uploaded["count"] + 1) or {"id": "uploaded"}, - ) - monkeypatch.setattr( - module, - "request", - _DummyRequest( - headers={"Content-Type": "multipart/form-data"}, - form={"k": "v"}, - files={"file1": object()}, - json_body={}, - ), - ) - res = _run(module.webhook("agent-1")) - assert hasattr(res, "status_code") - assert res.status_code == 200 - assert uploaded["count"] == 1 - - -@pytest.mark.p2 -def test_webhook_canvas_constructor_exception(monkeypatch): - module = _load_agents_app(monkeypatch) - - params = _default_webhook_params(security={"auth_type": "none"}) - cvs = _make_webhook_cvs(module, params=params) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - monkeypatch.setattr( - module, - "request", - _DummyRequest(headers={"Content-Type": "application/json"}, json_body={}), - ) - monkeypatch.setattr(module, "Canvas", lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("canvas init failed"))) - - def fake_error_result(*, code, message): - return SimpleNamespace(code=code, message=message) - - monkeypatch.setattr(module, "get_data_error_result", fake_error_result) - res = _run(module.webhook("agent-1")) - assert isinstance(res, SimpleNamespace) - assert res.code == module.RetCode.BAD_REQUEST - assert "canvas init failed" in res.message - assert res.status_code == module.RetCode.BAD_REQUEST - - -@pytest.mark.p2 -def test_webhook_trace_polling_branches(monkeypatch): - module = _load_agents_app(monkeypatch) - - # Missing since_ts. - monkeypatch.setattr(module, "request", SimpleNamespace(args=_Args())) - res = _run(module.webhook_trace("agent-1")) - assert res["code"] == module.RetCode.SUCCESS - assert res["data"]["webhook_id"] is None - assert res["data"]["events"] == [] - assert res["data"]["finished"] is False - - # since_ts provided but no Redis data. - monkeypatch.setattr(module, "request", SimpleNamespace(args=_Args({"since_ts": "100.0"}))) - monkeypatch.setattr(module.REDIS_CONN, "get", lambda _k: None) - res = _run(module.webhook_trace("agent-1")) - assert res["code"] == module.RetCode.SUCCESS - assert res["data"]["webhook_id"] is None - assert res["data"]["next_since_ts"] == 100.0 - assert res["data"]["events"] == [] - assert res["data"]["finished"] is False - - webhooks_obj = { - "webhooks": { - "101.0": { - "events": [ - {"event": "message", "ts": 101.2, "data": {"content": "a"}}, - {"event": "finished", "ts": 102.5}, - ] - }, - "99.0": {"events": [{"event": "message", "ts": 99.1}]}, - } - } - raw = json.dumps(webhooks_obj) - monkeypatch.setattr(module.REDIS_CONN, "get", lambda _k: raw) - - # No candidates newer than since_ts. - monkeypatch.setattr(module, "request", SimpleNamespace(args=_Args({"since_ts": "200.0"}))) - res = _run(module.webhook_trace("agent-1")) - assert res["code"] == module.RetCode.SUCCESS - assert res["data"]["webhook_id"] is None - assert res["data"]["next_since_ts"] == 200.0 - assert res["data"]["events"] == [] - assert res["data"]["finished"] is False - - # Candidate exists and webhook id is assigned. - monkeypatch.setattr(module, "request", SimpleNamespace(args=_Args({"since_ts": "100.0"}))) - res = _run(module.webhook_trace("agent-1")) - assert res["code"] == module.RetCode.SUCCESS - webhook_id = res["data"]["webhook_id"] - assert webhook_id - assert res["data"]["events"] == [] - assert res["data"]["next_since_ts"] == 101.0 - assert res["data"]["finished"] is False - - # Invalid webhook id. - monkeypatch.setattr( - module, - "request", - SimpleNamespace(args=_Args({"since_ts": "100.0", "webhook_id": "bad-id"})), - ) - res = _run(module.webhook_trace("agent-1")) - assert res["code"] == module.RetCode.SUCCESS - assert res["data"]["webhook_id"] == "bad-id" - assert res["data"]["events"] == [] - assert res["data"]["next_since_ts"] == 100.0 - assert res["data"]["finished"] is True - - # Valid webhook id with event filtering and finished flag. - monkeypatch.setattr( - module, - "request", - SimpleNamespace(args=_Args({"since_ts": "101.0", "webhook_id": webhook_id})), - ) - res = _run(module.webhook_trace("agent-1")) - assert res["code"] == module.RetCode.SUCCESS - assert res["data"]["webhook_id"] == webhook_id - assert [event["ts"] for event in res["data"]["events"]] == [101.2, 102.5] - assert res["data"]["next_since_ts"] == 102.5 - assert res["data"]["finished"] is True - - -@pytest.mark.p2 -def test_webhook_parse_request_form_and_raw_body_paths(monkeypatch): - module = _load_agents_app(monkeypatch) - _patch_background_task(monkeypatch, module) - - security = {"auth_type": "none"} - - def _run_with(params, req): - cvs = _make_webhook_cvs(module, params=params) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id, _cvs=cvs: (True, _cvs)) - monkeypatch.setattr(module, "request", req) - res = _run(module.webhook("agent-1")) - assert hasattr(res, "status_code"), res - assert res.status_code == 200 - - _run_with( - _default_webhook_params(security=security, content_types="application/x-www-form-urlencoded"), - _DummyRequest( - headers={"Content-Type": "application/x-www-form-urlencoded"}, - form={"a": "1", "b": "2"}, - json_body={}, - ), - ) - - _run_with( - _default_webhook_params(security=security, content_types="text/plain"), - _DummyRequest(headers={"Content-Type": "text/plain"}, raw_body=b'{"k": 1}', json_body={}), - ) - - _run_with( - _default_webhook_params(security=security, content_types="text/plain"), - _DummyRequest(headers={"Content-Type": "text/plain"}, raw_body=b"{bad-json}", json_body={}), - ) - - _run_with( - _default_webhook_params(security=security, content_types="text/plain"), - _DummyRequest(headers={"Content-Type": "text/plain"}, raw_body=b"", json_body={}), - ) - - class _BrokenRawRequest(_DummyRequest): - async def get_data(self): - raise RuntimeError("raw read failed") - - _run_with( - _default_webhook_params(security=security, content_types="text/plain"), - _BrokenRawRequest(headers={"Content-Type": "text/plain"}, json_body={}), - ) - - -@pytest.mark.p2 -def test_webhook_schema_extract_cast_defaults_and_validation_errors(monkeypatch): - module = _load_agents_app(monkeypatch) - _patch_background_task(monkeypatch, module) - - base_schema = { - "query": { - "properties": { - "q_file": {"type": "file"}, - "q_object": {"type": "object"}, - "q_boolean": {"type": "boolean"}, - "q_number": {"type": "number"}, - "q_string": {"type": "string"}, - "q_array": {"type": "array"}, - "q_null": {"type": "null"}, - "q_default_none": {}, - }, - "required": [], - }, - "headers": {"properties": {"Content-Type": {"type": "string"}}, "required": []}, - "body": { - "properties": { - "bool_true": {"type": "boolean"}, - "bool_false": {"type": "boolean"}, - "number_int": {"type": "number"}, - "number_float": {"type": "number"}, - "obj": {"type": "object"}, - "arr": {"type": "array"}, - "text": {"type": "string"}, - "file_list": {"type": "file"}, - "unknown": {"type": "mystery"}, - }, - "required": [ - "bool_true", - "number_int", - "obj", - "arr", - "text", - "file_list", - "unknown", - ], - }, - } - - params = _default_webhook_params( - security={"auth_type": "none"}, - content_types="application/json", - schema=base_schema, - ) - cvs = _make_webhook_cvs(module, params=params) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - monkeypatch.setattr( - module, - "request", - _DummyRequest( - headers={"Content-Type": "application/json"}, - args={}, - json_body={ - "bool_true": "true", - "bool_false": "0", - "number_int": "-3", - "number_float": "2.5", - "obj": '{"a": 1}', - "arr": "[1, 2]", - "text": "hello", - "file_list": ["f1"], - "unknown": "mystery", - }, - ), - ) - res = _run(module.webhook("agent-1")) - assert hasattr(res, "status_code"), res - assert res.status_code == 200 - - failure_cases = [ - ( - {"query": {"properties": {}, "required": []}, "headers": {"properties": {}, "required": []}, "body": {"properties": {"must": {"type": "string"}}, "required": ["must"]}}, - {}, - "missing required field", - ), - ( - {"query": {"properties": {}, "required": []}, "headers": {"properties": {}, "required": []}, "body": {"properties": {"flag": {"type": "boolean"}}, "required": ["flag"]}}, - {"flag": "maybe"}, - "auto-cast failed", - ), - ( - {"query": {"properties": {}, "required": []}, "headers": {"properties": {}, "required": []}, "body": {"properties": {"num": {"type": "number"}}, "required": ["num"]}}, - {"num": "abc"}, - "auto-cast failed", - ), - ( - {"query": {"properties": {}, "required": []}, "headers": {"properties": {}, "required": []}, "body": {"properties": {"obj": {"type": "object"}}, "required": ["obj"]}}, - {"obj": "[]"}, - "auto-cast failed", - ), - ( - {"query": {"properties": {}, "required": []}, "headers": {"properties": {}, "required": []}, "body": {"properties": {"arr": {"type": "array"}}, "required": ["arr"]}}, - {"arr": "{}"}, - "auto-cast failed", - ), - ( - {"query": {"properties": {}, "required": []}, "headers": {"properties": {}, "required": []}, "body": {"properties": {"num": {"type": "number"}}, "required": ["num"]}}, - {"num": []}, - "type mismatch", - ), - ( - {"query": {"properties": {}, "required": []}, "headers": {"properties": {}, "required": []}, "body": {"properties": {"arr": {"type": "array"}}, "required": ["arr"]}}, - {"arr": 3}, - "type mismatch", - ), - ( - {"query": {"properties": {}, "required": []}, "headers": {"properties": {}, "required": []}, "body": {"properties": {"arr": {"type": "array"}}, "required": ["arr"]}}, - {"arr": [1, "x"]}, - "type mismatch", - ), - ( - {"query": {"properties": {}, "required": []}, "headers": {"properties": {}, "required": []}, "body": {"properties": {"file": {"type": "file"}}, "required": ["file"]}}, - {"file": "inline-file"}, - "type mismatch", - ), - ] - - for schema, body_payload, expected_substring in failure_cases: - params = _default_webhook_params( - security={"auth_type": "none"}, - content_types="application/json", - schema=schema, - ) - cvs = _make_webhook_cvs(module, params=params) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id, _cvs=cvs: (True, _cvs)) - monkeypatch.setattr( - module, - "request", - _DummyRequest(headers={"Content-Type": "application/json"}, json_body=body_payload), - ) - res = _run(module.webhook("agent-1")) - _assert_bad_request(res, expected_substring) - - -@pytest.mark.p2 -def test_webhook_immediate_response_status_and_template_validation(monkeypatch): - module = _load_agents_app(monkeypatch) - _patch_background_task(monkeypatch, module) - - def _run_case(response_cfg): - params = _default_webhook_params( - security={"auth_type": "none"}, - content_types="application/json", - response=response_cfg, - ) - cvs = _make_webhook_cvs(module, params=params) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id, _cvs=cvs: (True, _cvs)) - monkeypatch.setattr(module, "request", _DummyRequest(headers={"Content-Type": "application/json"}, json_body={})) - return _run(module.webhook("agent-1")) - - _assert_bad_request(_run_case({"status": "abc"}), "Invalid response status code") - _assert_bad_request(_run_case({"status": 500}), "must be between 200 and 399") - - empty_res = _run_case({"status": 204, "body_template": ""}) - assert empty_res.status_code == 204 - assert empty_res.content_type == "application/json" - assert _run(empty_res.get_data(as_text=True)) == "null" - - json_res = _run_case({"status": 201, "body_template": '{"ok": true}'}) - assert json_res.status_code == 201 - assert json_res.content_type == "application/json" - assert json.loads(_run(json_res.get_data(as_text=True))) == {"ok": True} - - plain_res = _run_case({"status": 202, "body_template": "plain-text"}) - assert plain_res.status_code == 202 - assert plain_res.content_type == "text/plain" - assert _run(plain_res.get_data(as_text=True)) == "plain-text" - - -@pytest.mark.p2 -def test_webhook_background_run_success_and_error_trace_paths(monkeypatch): - module = _load_agents_app(monkeypatch) - - redis_store = {} - - def redis_get(key): - return redis_store.get(key) - - def redis_set_obj(key, obj, _ttl): - redis_store[key] = json.dumps(obj) - - monkeypatch.setattr(module.REDIS_CONN, "get", redis_get) - monkeypatch.setattr(module.REDIS_CONN, "set_obj", redis_set_obj) - - update_calls = [] - monkeypatch.setattr(module.UserCanvasService, "update_by_id", lambda *_args, **_kwargs: update_calls.append(True)) - - tasks = [] - - def _capture_task(coro): - tasks.append(coro) - return SimpleNamespace() - - monkeypatch.setattr(module.asyncio, "create_task", _capture_task) - - class _CanvasSuccess(_StubCanvas): - async def run(self, **_kwargs): - yield {"event": "message", "data": {"content": "ok"}} - - def __str__(self): - return "{}" - - monkeypatch.setattr(module, "Canvas", _CanvasSuccess) - - params = _default_webhook_params(security={"auth_type": "none"}, content_types="application/json") - cvs = _make_webhook_cvs(module, params=params) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - monkeypatch.setattr( - module, - "request", - _DummyRequest(path="/api/v1/webhook_test/agent-1", headers={"Content-Type": "application/json"}, json_body={}), - ) - - res = _run(module.webhook("agent-1")) - assert res.status_code == 200 - assert len(tasks) == 1 - _run(tasks.pop(0)) - assert update_calls == [True] - - key = "webhook-trace-agent-1-logs" - trace_obj = json.loads(redis_store[key]) - ws = next(iter(trace_obj["webhooks"].values())) - events = ws["events"] - assert any(event.get("event") == "message" for event in events) - assert any(event.get("event") == "finished" and event.get("success") is True for event in events) - - class _CanvasError(_StubCanvas): - async def run(self, **_kwargs): - raise RuntimeError("run failed") - yield {} - - monkeypatch.setattr(module, "Canvas", _CanvasError) - tasks.clear() - redis_store.clear() - cvs = _make_webhook_cvs(module, params=params) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id, _cvs=cvs: (True, _cvs)) - res = _run(module.webhook("agent-1")) - assert res.status_code == 200 - _run(tasks.pop(0)) - trace_obj = json.loads(redis_store[key]) - ws = next(iter(trace_obj["webhooks"].values())) - events = ws["events"] - assert any(event.get("event") == "error" for event in events) - assert any(event.get("event") == "finished" and event.get("success") is False for event in events) - - log_messages = [] - monkeypatch.setattr(module.logging, "exception", lambda msg, *_args, **_kwargs: log_messages.append(str(msg))) - monkeypatch.setattr(module.REDIS_CONN, "get", lambda _key: "{") - monkeypatch.setattr(module.REDIS_CONN, "set_obj", lambda *_args, **_kwargs: None) - tasks.clear() - cvs = _make_webhook_cvs(module, params=params) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id, _cvs=cvs: (True, _cvs)) - _run(module.webhook("agent-1")) - _run(tasks.pop(0)) - assert any("Failed to append webhook trace" in msg for msg in log_messages) - - -@pytest.mark.p2 -def test_webhook_sse_success_and_exception_paths(monkeypatch): - module = _load_agents_app(monkeypatch) - - redis_store = {} - monkeypatch.setattr(module.REDIS_CONN, "get", lambda key: redis_store.get(key)) - monkeypatch.setattr(module.REDIS_CONN, "set_obj", lambda key, obj, _ttl: redis_store.__setitem__(key, json.dumps(obj))) - - params = _default_webhook_params( - security={"auth_type": "none"}, - content_types="application/json", - execution_mode="Deferred", - ) - cvs = _make_webhook_cvs(module, params=params) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs)) - - class _CanvasSSESuccess(_StubCanvas): - async def run(self, **_kwargs): - yield {"event": "message", "data": {"content": "x", "start_to_think": True}} - yield {"event": "message", "data": {"content": "y", "end_to_think": True}} - yield {"event": "message", "data": {"content": "Hello"}} - yield {"event": "message_end", "data": {"status": "201"}} - - monkeypatch.setattr(module, "Canvas", _CanvasSSESuccess) - monkeypatch.setattr( - module, - "request", - _DummyRequest(path="/api/v1/webhook_test/agent-1", headers={"Content-Type": "application/json"}, json_body={}), - ) - res = _run(module.webhook("agent-1")) - assert res.status_code == 201 - payload = json.loads(_run(res.get_data(as_text=True))) - assert payload == {"message": "Hello", "success": True, "code": 201} - - class _CanvasSSEError(_StubCanvas): - async def run(self, **_kwargs): - raise RuntimeError("sse failed") - yield {} - - monkeypatch.setattr(module, "Canvas", _CanvasSSEError) - monkeypatch.setattr( - module, - "request", - _DummyRequest(path="/api/v1/webhook_test/agent-1", headers={"Content-Type": "application/json"}, json_body={}), - ) - res = _run(module.webhook("agent-1")) - assert res.status_code == 400 - payload = json.loads(_run(res.get_data(as_text=True))) - assert payload["code"] == 400 - assert payload["success"] is False - assert "sse failed" in payload["message"] - - -@pytest.mark.p2 -def test_webhook_trace_encoded_id_generation(monkeypatch): - module = _load_agents_app(monkeypatch) - - webhooks_obj = { - "webhooks": { - "101.0": { - "events": [{"event": "message", "ts": 101.2}], - } - } - } - monkeypatch.setattr(module.REDIS_CONN, "get", lambda _key: json.dumps(webhooks_obj)) - monkeypatch.setattr(module, "request", SimpleNamespace(args=_Args({"since_ts": "100.0"}))) - res = _run(module.webhook_trace("agent-1")) - assert res["code"] == module.RetCode.SUCCESS - - expected = base64.urlsafe_b64encode( - hmac.new( - b"webhook_id_secret", - b"101.0", - hashlib.sha256, - ).digest() - ).decode("utf-8").rstrip("=") - assert res["data"]["webhook_id"] == expected diff --git a/test/testcases/test_web_api/test_canvas_app/test_canvas_routes_unit.py b/test/testcases/test_web_api/test_canvas_app/test_canvas_routes_unit.py deleted file mode 100644 index 811d6aded8f..00000000000 --- a/test/testcases/test_web_api/test_canvas_app/test_canvas_routes_unit.py +++ /dev/null @@ -1,1442 +0,0 @@ -# -# 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 importlib.util -import inspect -import sys -from copy import deepcopy -from functools import partial -from pathlib import Path -from types import ModuleType, SimpleNamespace - -import pytest - - -class _DummyManager: - def route(self, *_args, **_kwargs): - def decorator(func): - return func - - return decorator - - -class _AwaitableValue: - def __init__(self, value): - self._value = value - - def __await__(self): - async def _co(): - return self._value - - return _co().__await__() - - -class _Args(dict): - def get(self, key, default=None, type=None): - value = super().get(key, default) - if value is None or type is None: - return value - try: - return type(value) - except (TypeError, ValueError): - return default - - -class _StubHeaders: - def __init__(self): - self._items = [] - - def add_header(self, key, value): - self._items.append((key, value)) - - def get(self, key, default=None): - for existing_key, value in reversed(self._items): - if existing_key == key: - return value - return default - - -class _StubResponse: - def __init__(self, body, mimetype=None, content_type=None): - self.response = body - self.body = body - self.mimetype = mimetype - self.content_type = content_type - self.headers = _StubHeaders() - - -class _DummyRequest: - def __init__(self, *, headers=None, args=None, files=None, method="POST", content_length=0): - self.headers = headers or {} - self.args = args or _Args() - self.files = _AwaitableValue(files if files is not None else {}) - self.method = method - self.content_length = content_length - - -class _DummyRetCode: - SUCCESS = 0 - EXCEPTION_ERROR = 100 - ARGUMENT_ERROR = 101 - DATA_ERROR = 102 - OPERATING_ERROR = 103 - - -class _DummyCanvasCategory: - Agent = "agent" - DataFlow = "dataflow" - - -class _TaskField: - def __eq__(self, other): - return ("eq", other) - - -class _DummyTask: - doc_id = _TaskField() - - -class _FileMap(dict): - def getlist(self, key): - return list(self.get(key, [])) - - -def _run(coro): - return asyncio.run(coro) - - -async def _collect_stream(body): - items = [] - if hasattr(body, "__aiter__"): - async for item in body: - if isinstance(item, bytes): - item = item.decode("utf-8") - items.append(item) - else: - for item in body: - if isinstance(item, bytes): - item = item.decode("utf-8") - items.append(item) - return items - - -def _set_request_json(monkeypatch, module, payload): - async def _req(): - return deepcopy(payload) - - monkeypatch.setattr(module, "get_request_json", _req) - - -@pytest.fixture(scope="session") -def auth(): - return "unit-auth" - - -@pytest.fixture(scope="session", autouse=True) -def set_tenant_info(): - return None - - -def _load_canvas_module(monkeypatch): - repo_root = Path(__file__).resolve().parents[4] - - common_pkg = ModuleType("common") - common_pkg.__path__ = [str(repo_root / "common")] - monkeypatch.setitem(sys.modules, "common", common_pkg) - - settings_mod = ModuleType("common.settings") - settings_mod.docStoreConn = SimpleNamespace( - index_exist=lambda *_args, **_kwargs: False, - delete=lambda *_args, **_kwargs: True, - ) - common_pkg.settings = settings_mod - monkeypatch.setitem(sys.modules, "common.settings", settings_mod) - - constants_mod = ModuleType("common.constants") - constants_mod.RetCode = _DummyRetCode - monkeypatch.setitem(sys.modules, "common.constants", constants_mod) - - misc_utils_mod = ModuleType("common.misc_utils") - misc_utils_mod.get_uuid = lambda: "uuid-1" - - async def _thread_pool_exec(func, *args, **kwargs): - return func(*args, **kwargs) - - misc_utils_mod.thread_pool_exec = _thread_pool_exec - monkeypatch.setitem(sys.modules, "common.misc_utils", misc_utils_mod) - - api_pkg = ModuleType("api") - api_pkg.__path__ = [str(repo_root / "api")] - monkeypatch.setitem(sys.modules, "api", api_pkg) - - db_pkg = ModuleType("api.db") - db_pkg.__path__ = [str(repo_root / "api" / "db")] - monkeypatch.setitem(sys.modules, "api.db", db_pkg) - - db_services_pkg = ModuleType("api.db.services") - db_services_pkg.__path__ = [str(repo_root / "api" / "db" / "services")] - monkeypatch.setitem(sys.modules, "api.db.services", db_services_pkg) - - apps_mod = ModuleType("api.apps") - apps_mod.__path__ = [] - apps_mod.current_user = SimpleNamespace(id="user-1") - apps_mod.login_required = lambda func: func - monkeypatch.setitem(sys.modules, "api.apps", apps_mod) - - apps_services_pkg = ModuleType("api.apps.services") - apps_services_pkg.__path__ = [] - monkeypatch.setitem(sys.modules, "api.apps.services", apps_services_pkg) - apps_mod.services = apps_services_pkg - - canvas_replica_mod = ModuleType("api.apps.services.canvas_replica_service") - - class _StubCanvasReplicaService: - @classmethod - def normalize_dsl(cls, dsl): - import json - if isinstance(dsl, str): - return json.loads(dsl) - return dsl - - @classmethod - def bootstrap(cls, *_args, **_kwargs): - return {} - - @classmethod - def load_for_run(cls, *_args, **_kwargs): - return None - - @classmethod - def commit_after_run(cls, *_args, **_kwargs): - return True - - @classmethod - def replace_for_set(cls, *_args, **_kwargs): - return True - - @classmethod - def create_if_absent(cls, *_args, **_kwargs): - return {} - - canvas_replica_mod.CanvasReplicaService = _StubCanvasReplicaService - monkeypatch.setitem(sys.modules, "api.apps.services.canvas_replica_service", canvas_replica_mod) - apps_services_pkg.canvas_replica_service = canvas_replica_mod - - db_pkg = ModuleType("api.db") - db_pkg.CanvasCategory = _DummyCanvasCategory - monkeypatch.setitem(sys.modules, "api.db", db_pkg) - - services_pkg = ModuleType("api.db.services") - services_pkg.__path__ = [] - monkeypatch.setitem(sys.modules, "api.db.services", services_pkg) - - canvas_service_mod = ModuleType("api.db.services.canvas_service") - - class _StubCanvasTemplateService: - @staticmethod - def get_all(): - return [] - - class _StubUserCanvasService: - @staticmethod - def accessible(*_args, **_kwargs): - return True - - @staticmethod - def delete_by_id(*_args, **_kwargs): - return True - - @staticmethod - def query(*_args, **_kwargs): - return [] - - @staticmethod - def save(**_kwargs): - return True - - @staticmethod - def update_by_id(*_args, **_kwargs): - return True - - @staticmethod - def get_by_canvas_id(_canvas_id): - return True, {"id": _canvas_id} - - @staticmethod - def get_by_id(_canvas_id): - return True, SimpleNamespace( - id=_canvas_id, - user_id="user-1", - dsl="{}", - canvas_category=_DummyCanvasCategory.Agent, - to_dict=lambda: {"id": _canvas_id}, - ) - - @staticmethod - def get_by_tenant_ids(*_args, **_kwargs): - return [], 0 - - class _StubAPI4ConversationService: - @staticmethod - def get_names(*_args, **_kwargs): - return [] - - @staticmethod - def get_list(*_args, **_kwargs): - return 0, [] - - @staticmethod - def save(**_kwargs): - return True - - @staticmethod - def get_by_id(_session_id): - return True, SimpleNamespace(to_dict=lambda: {"id": _session_id}) - - @staticmethod - def delete_by_id(*_args, **_kwargs): - return True - - async def _completion(*_args, **_kwargs): - if False: - yield {} - - canvas_service_mod.CanvasTemplateService = _StubCanvasTemplateService - canvas_service_mod.UserCanvasService = _StubUserCanvasService - canvas_service_mod.API4ConversationService = _StubAPI4ConversationService - canvas_service_mod.completion = _completion - monkeypatch.setitem(sys.modules, "api.db.services.canvas_service", canvas_service_mod) - - document_service_mod = ModuleType("api.db.services.document_service") - document_service_mod.DocumentService = SimpleNamespace( - clear_chunk_num_when_rerun=lambda *_args, **_kwargs: True, - update_by_id=lambda *_args, **_kwargs: True, - ) - monkeypatch.setitem(sys.modules, "api.db.services.document_service", document_service_mod) - - file_service_mod = ModuleType("api.db.services.file_service") - file_service_mod.FileService = SimpleNamespace( - upload_info=lambda *_args, **_kwargs: {"ok": True}, - get_blob=lambda *_args, **_kwargs: b"", - ) - monkeypatch.setitem(sys.modules, "api.db.services.file_service", file_service_mod) - - knowledgebase_service_mod = ModuleType("api.db.services.knowledgebase_service") - knowledgebase_service_mod.KnowledgebaseService = SimpleNamespace( - query=lambda **_kwargs: [], - ) - monkeypatch.setitem(sys.modules, "api.db.services.knowledgebase_service", knowledgebase_service_mod) - - pipeline_log_service_mod = ModuleType("api.db.services.pipeline_operation_log_service") - pipeline_log_service_mod.PipelineOperationLogService = SimpleNamespace( - get_documents_info=lambda *_args, **_kwargs: [], - update_by_id=lambda *_args, **_kwargs: True, - ) - monkeypatch.setitem(sys.modules, "api.db.services.pipeline_operation_log_service", pipeline_log_service_mod) - - task_service_mod = ModuleType("api.db.services.task_service") - task_service_mod.queue_dataflow = lambda *_args, **_kwargs: (True, "") - task_service_mod.CANVAS_DEBUG_DOC_ID = "debug-doc" - task_service_mod.TaskService = SimpleNamespace(filter_delete=lambda *_args, **_kwargs: True) - monkeypatch.setitem(sys.modules, "api.db.services.task_service", task_service_mod) - - user_service_mod = ModuleType("api.db.services.user_service") - user_service_mod.TenantService = SimpleNamespace(get_joined_tenants_by_user_id=lambda *_args, **_kwargs: []) - monkeypatch.setitem(sys.modules, "api.db.services.user_service", user_service_mod) - - canvas_version_mod = ModuleType("api.db.services.user_canvas_version") - canvas_version_mod.UserCanvasVersionService = SimpleNamespace( - insert=lambda **_kwargs: True, - delete_all_versions=lambda *_args, **_kwargs: True, - list_by_canvas_id=lambda *_args, **_kwargs: [], - get_by_id=lambda *_args, **_kwargs: (True, None), - save_or_replace_latest=lambda *_args, **_kwargs: True, - build_version_title=lambda *_args, **_kwargs: "stub_version_title", - get_latest_version_title=lambda *_args, **_kwargs: "stub_version_title", - ) - monkeypatch.setitem(sys.modules, "api.db.services.user_canvas_version", canvas_version_mod) - - db_models_mod = ModuleType("api.db.db_models") - - class _StubAPIToken: - @staticmethod - def query(**_kwargs): - return [] - - db_models_mod.APIToken = _StubAPIToken - db_models_mod.Task = _DummyTask - monkeypatch.setitem(sys.modules, "api.db.db_models", db_models_mod) - - api_utils_mod = ModuleType("api.utils.api_utils") - - def _get_json_result(code=_DummyRetCode.SUCCESS, message="success", data=None): - return {"code": code, "message": message, "data": data} - - def _get_data_error_result(code=_DummyRetCode.DATA_ERROR, message="Sorry! Data missing!"): - return {"code": code, "message": message} - - def _server_error_response(exc): - return {"code": _DummyRetCode.EXCEPTION_ERROR, "message": repr(exc), "data": None} - - async def _get_request_json(): - return {} - - def _validate_request(*_args, **_kwargs): - def _decorator(func): - return func - - return _decorator - - api_utils_mod.get_json_result = _get_json_result - api_utils_mod.server_error_response = _server_error_response - api_utils_mod.validate_request = _validate_request - api_utils_mod.get_data_error_result = _get_data_error_result - api_utils_mod.get_request_json = _get_request_json - monkeypatch.setitem(sys.modules, "api.utils.api_utils", api_utils_mod) - - rag_pkg = ModuleType("rag") - rag_pkg.__path__ = [] - monkeypatch.setitem(sys.modules, "rag", rag_pkg) - - rag_flow_pkg = ModuleType("rag.flow") - rag_flow_pkg.__path__ = [] - monkeypatch.setitem(sys.modules, "rag.flow", rag_flow_pkg) - - pipeline_mod = ModuleType("rag.flow.pipeline") - - class _StubPipeline: - def __init__(self, *_args, **_kwargs): - pass - - pipeline_mod.Pipeline = _StubPipeline - monkeypatch.setitem(sys.modules, "rag.flow.pipeline", pipeline_mod) - - rag_nlp_mod = ModuleType("rag.nlp") - rag_nlp_mod.search = SimpleNamespace(index_name=lambda tenant_id: f"idx-{tenant_id}") - monkeypatch.setitem(sys.modules, "rag.nlp", rag_nlp_mod) - - rag_utils_pkg = ModuleType("rag.utils") - rag_utils_pkg.__path__ = [] - monkeypatch.setitem(sys.modules, "rag.utils", rag_utils_pkg) - - redis_mod = ModuleType("rag.utils.redis_conn") - redis_mod.REDIS_CONN = SimpleNamespace( - set=lambda *_args, **_kwargs: True, - get=lambda *_args, **_kwargs: None, - ) - monkeypatch.setitem(sys.modules, "rag.utils.redis_conn", redis_mod) - - agent_pkg = ModuleType("agent") - agent_pkg.__path__ = [] - agent_dsl_migration_mod = ModuleType("agent.dsl_migration") - agent_dsl_migration_mod.normalize_chunker_dsl = lambda dsl: dsl - monkeypatch.setitem(sys.modules, "agent", agent_pkg) - monkeypatch.setitem(sys.modules, "agent.dsl_migration", agent_dsl_migration_mod) - - agent_component_mod = ModuleType("agent.component") - - class _StubLLM: - pass - - agent_component_mod.LLM = _StubLLM - agent_pkg.component = agent_component_mod - monkeypatch.setitem(sys.modules, "agent.component", agent_component_mod) - - agent_canvas_mod = ModuleType("agent.canvas") - - class _StubCanvas: - def __init__(self, dsl, _user_id, _agent_id=None, canvas_id=None): - self.dsl = dsl - self.id = canvas_id - - async def run(self, **_kwargs): - if False: - yield {} - - def cancel_task(self): - return None - - def reset(self): - return None - - def get_component_input_form(self, _component_id): - return {} - - def get_component(self, _component_id): - return {"obj": SimpleNamespace(reset=lambda: None, invoke=lambda **_kwargs: None, output=lambda: {})} - - def __str__(self): - return "{}" - - agent_canvas_mod.Canvas = _StubCanvas - agent_pkg.canvas = agent_canvas_mod - agent_pkg.dsl_migration = agent_dsl_migration_mod - monkeypatch.setitem(sys.modules, "agent.canvas", agent_canvas_mod) - - quart_mod = ModuleType("quart") - quart_mod.request = _DummyRequest() - quart_mod.Response = _StubResponse - - async def _make_response(blob): - return {"blob": blob} - - quart_mod.make_response = _make_response - monkeypatch.setitem(sys.modules, "quart", quart_mod) - - module_path = repo_root / "api" / "apps" / "canvas_app.py" - spec = importlib.util.spec_from_file_location("test_canvas_routes_unit_module", module_path) - module = importlib.util.module_from_spec(spec) - module.manager = _DummyManager() - monkeypatch.setitem(sys.modules, "test_canvas_routes_unit_module", module) - spec.loader.exec_module(module) - return module - - -@pytest.mark.p2 -def test_templates_rm_save_get_matrix_unit(monkeypatch): - module = _load_canvas_module(monkeypatch) - - class _Template: - def __init__(self, template_id): - self.template_id = template_id - - def to_dict(self): - return {"id": self.template_id, "canvas_type": "Recommended", "canvas_types": ["Recommended", "Agent"]} - - monkeypatch.setattr(module.CanvasTemplateService, "get_all", lambda: [_Template("tpl-1")]) - res = module.templates() - assert res["code"] == module.RetCode.SUCCESS - assert res["data"] == [{"id": "tpl-1", "canvas_type": "Recommended", "canvas_types": ["Recommended", "Agent"]}] - - _set_request_json(monkeypatch, module, {"canvas_ids": ["c1", "c2"]}) - monkeypatch.setattr(module.UserCanvasService, "accessible", lambda *_args, **_kwargs: False) - res = _run(inspect.unwrap(module.rm)()) - assert res["code"] == module.RetCode.OPERATING_ERROR - assert "Only owner of canvas authorized" in res["message"] - - deleted = [] - _set_request_json(monkeypatch, module, {"canvas_ids": ["c1", "c2"]}) - monkeypatch.setattr(module.UserCanvasService, "accessible", lambda *_args, **_kwargs: True) - monkeypatch.setattr(module.UserCanvasService, "delete_by_id", lambda canvas_id: deleted.append(canvas_id)) - res = _run(inspect.unwrap(module.rm)()) - assert res["data"] is True - assert deleted == ["c1", "c2"] - - _set_request_json(monkeypatch, module, {"title": " Demo ", "dsl": {"n": 1}}) - monkeypatch.setattr(module.UserCanvasService, "query", lambda **_kwargs: [object()]) - res = _run(inspect.unwrap(module.save)()) - assert res["code"] == module.RetCode.DATA_ERROR - assert "already exists" in res["message"] - - _set_request_json(monkeypatch, module, {"title": "Demo", "dsl": {"n": 1}}) - monkeypatch.setattr(module, "get_uuid", lambda: "canvas-new") - monkeypatch.setattr(module.UserCanvasService, "query", lambda **_kwargs: []) - monkeypatch.setattr(module.UserCanvasService, "save", lambda **_kwargs: False) - res = _run(inspect.unwrap(module.save)()) - assert res["code"] == module.RetCode.DATA_ERROR - assert "Fail to save canvas." in res["message"] - - created = {"save": [], "versions": []} - _set_request_json(monkeypatch, module, {"title": "Demo", "dsl": {"n": 1}}) - monkeypatch.setattr(module, "get_uuid", lambda: "canvas-new") - monkeypatch.setattr(module.UserCanvasService, "query", lambda **_kwargs: []) - monkeypatch.setattr(module.UserCanvasService, "save", lambda **kwargs: created["save"].append(kwargs) or True) - monkeypatch.setattr(module.UserCanvasVersionService, "save_or_replace_latest", lambda *_args, **kwargs: created["versions"].append(("save_or_replace_latest", kwargs))) - res = _run(inspect.unwrap(module.save)()) - assert res["code"] == module.RetCode.SUCCESS - assert res["data"]["id"] == "canvas-new" - assert created["save"] - assert any(item[0] == "save_or_replace_latest" for item in created["versions"]) - - _set_request_json(monkeypatch, module, {"id": "canvas-1", "title": "Renamed", "dsl": "{\"m\": 1}"}) - monkeypatch.setattr(module.UserCanvasService, "accessible", lambda *_args, **_kwargs: False) - res = _run(inspect.unwrap(module.save)()) - assert res["code"] == module.RetCode.OPERATING_ERROR - - updates = [] - versions = [] - _set_request_json(monkeypatch, module, {"id": "canvas-1", "title": "Renamed", "dsl": "{\"m\": 1}"}) - monkeypatch.setattr(module.UserCanvasService, "accessible", lambda *_args, **_kwargs: True) - monkeypatch.setattr(module.UserCanvasService, "update_by_id", lambda canvas_id, payload: updates.append((canvas_id, payload))) - monkeypatch.setattr(module.UserCanvasVersionService, "save_or_replace_latest", lambda *_args, **kwargs: versions.append(("save_or_replace_latest", kwargs))) - res = _run(inspect.unwrap(module.save)()) - assert res["code"] == module.RetCode.SUCCESS - assert updates and updates[0][0] == "canvas-1" - assert any(item[0] == "save_or_replace_latest" for item in versions) - - monkeypatch.setattr(module.UserCanvasService, "accessible", lambda *_args, **_kwargs: False) - res = module.get("canvas-1") - assert res["code"] == module.RetCode.DATA_ERROR - assert res["message"] == "canvas not found." - - monkeypatch.setattr(module.UserCanvasService, "accessible", lambda *_args, **_kwargs: True) - monkeypatch.setattr(module.UserCanvasService, "get_by_canvas_id", lambda _canvas_id: (True, {"id": "canvas-1"})) - res = module.get("canvas-1") - assert res["code"] == module.RetCode.SUCCESS - assert res["data"]["id"] == "canvas-1" - - -@pytest.mark.p2 -def test_getsse_auth_token_and_ownership_matrix_unit(monkeypatch): - module = _load_canvas_module(monkeypatch) - - monkeypatch.setattr(module, "request", _DummyRequest(headers={"Authorization": "Bearer"})) - res = module.getsse("canvas-1") - assert res["message"] == "Authorization is not valid!" - - monkeypatch.setattr(module, "request", _DummyRequest(headers={"Authorization": "Bearer invalid"})) - monkeypatch.setattr(module.APIToken, "query", lambda **_kwargs: []) - res = module.getsse("canvas-1") - assert "API key is invalid" in res["message"] - - monkeypatch.setattr(module, "request", _DummyRequest(headers={"Authorization": "Bearer ok"})) - monkeypatch.setattr(module.APIToken, "query", lambda **_kwargs: [SimpleNamespace(tenant_id="tenant-1")]) - monkeypatch.setattr(module.UserCanvasService, "query", lambda **_kwargs: []) - res = module.getsse("canvas-1") - assert res["code"] == module.RetCode.OPERATING_ERROR - - monkeypatch.setattr(module.UserCanvasService, "query", lambda **_kwargs: [object()]) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _canvas_id: (False, None)) - res = module.getsse("canvas-1") - assert res["message"] == "canvas not found." - - bad_owner = SimpleNamespace(user_id="tenant-2", to_dict=lambda: {"id": "canvas-1"}) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _canvas_id: (True, bad_owner)) - res = module.getsse("canvas-1") - assert res["message"] == "canvas not found." - - good_owner = SimpleNamespace(user_id="tenant-1", to_dict=lambda: {"id": "canvas-1"}) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _canvas_id: (True, good_owner)) - res = module.getsse("canvas-1") - assert res["code"] == module.RetCode.SUCCESS - assert res["data"]["id"] == "canvas-1" - - -@pytest.mark.p2 -def test_run_dataflow_and_canvas_sse_matrix_unit(monkeypatch): - module = _load_canvas_module(monkeypatch) - - async def _thread_pool_exec(func, *args, **kwargs): - return func(*args, **kwargs) - - monkeypatch.setattr(module, "thread_pool_exec", _thread_pool_exec) - - _set_request_json(monkeypatch, module, {"id": "c1"}) - monkeypatch.setattr(module.UserCanvasService, "accessible", lambda *_args, **_kwargs: False) - res = _run(inspect.unwrap(module.run)()) - assert res["code"] == module.RetCode.OPERATING_ERROR - - _set_request_json(monkeypatch, module, {"id": "c1"}) - monkeypatch.setattr(module.UserCanvasService, "accessible", lambda *_args, **_kwargs: True) - monkeypatch.setattr(module.CanvasReplicaService, "load_for_run", lambda *_args, **_kwargs: None) - res = _run(inspect.unwrap(module.run)()) - assert res["message"] == "canvas replica not found, please call /get/ first." - - _set_request_json(monkeypatch, module, {"id": "ag-1", "query": "q", "files": [], "inputs": {}}) - monkeypatch.setattr(module.CanvasReplicaService, "load_for_run", lambda *_args, **_kwargs: {"dsl": {"x": 1}, "title": "ag", "canvas_category": module.CanvasCategory.Agent}) - monkeypatch.setattr(module, "Canvas", lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("canvas init failed"))) - res = _run(inspect.unwrap(module.run)()) - assert res["code"] == module.RetCode.EXCEPTION_ERROR - assert "canvas init failed" in res["message"] - - updates = [] - - class _CanvasSSESuccess: - def __init__(self, *_args, **_kwargs): - self.cancelled = False - - async def run(self, **_kwargs): - yield {"answer": "stream-ok"} - - def cancel_task(self): - self.cancelled = True - - def __str__(self): - return '{"updated": true}' - - _set_request_json(monkeypatch, module, {"id": "ag-2", "query": "q", "files": [], "inputs": {}, "user_id": "exp-2"}) - monkeypatch.setattr(module, "Canvas", _CanvasSSESuccess) - monkeypatch.setattr(module.CanvasReplicaService, "load_for_run", lambda *_args, **_kwargs: {"dsl": {}, "title": "ag2", "canvas_category": module.CanvasCategory.Agent}) - monkeypatch.setattr(module.UserCanvasService, "update_by_id", lambda canvas_id, payload: updates.append((canvas_id, payload))) - resp = _run(inspect.unwrap(module.run)()) - assert isinstance(resp, _StubResponse) - assert resp.headers.get("Content-Type") == "text/event-stream; charset=utf-8" - chunks = _run(_collect_stream(resp.response)) - assert any('"answer": "stream-ok"' in chunk for chunk in chunks) - - class _CanvasSSEError: - last_instance = None - - def __init__(self, *_args, **_kwargs): - self.cancelled = False - _CanvasSSEError.last_instance = self - - async def run(self, **_kwargs): - yield {"answer": "start"} - raise RuntimeError("stream boom") - - def cancel_task(self): - self.cancelled = True - - def __str__(self): - return "{}" - - _set_request_json(monkeypatch, module, {"id": "ag-3", "query": "q", "files": [], "inputs": {}, "user_id": "exp-3"}) - monkeypatch.setattr(module, "Canvas", _CanvasSSEError) - monkeypatch.setattr(module.CanvasReplicaService, "load_for_run", lambda *_args, **_kwargs: {"dsl": {}, "title": "ag3", "canvas_category": module.CanvasCategory.Agent}) - resp = _run(inspect.unwrap(module.run)()) - chunks = _run(_collect_stream(resp.response)) - assert any('"code": 500' in chunk and "stream boom" in chunk for chunk in chunks) - assert _CanvasSSEError.last_instance.cancelled is True - - -@pytest.mark.p2 -def test_exp_agent_completion_trace_and_filtering_unit(monkeypatch): - module = _load_canvas_module(monkeypatch) - _set_request_json(monkeypatch, module, {"return_trace": True}) - - async def _agent_completion(*_args, **_kwargs): - yield "data:not-json" - yield 'data:{"event":"node_finished","data":{"component_id":"cmp-1","step":"done"}}' - yield 'data:{"event":"heartbeat","data":{"t":1}}' - yield 'data:{"event":"message","data":{"content":"hello"}}' - yield 'data:{"event":"message_end","data":{"content":"bye"}}' - - monkeypatch.setattr(module, "agent_completion", _agent_completion) - resp = _run(inspect.unwrap(module.exp_agent_completion)("canvas-1")) - assert isinstance(resp, _StubResponse) - assert resp.headers.get("Content-Type") == "text/event-stream; charset=utf-8" - - chunks = _run(_collect_stream(resp.response)) - assert any('"event": "node_finished"' in chunk and '"trace"' in chunk for chunk in chunks) - assert not any('"event":"heartbeat"' in chunk or '"event": "heartbeat"' in chunk for chunk in chunks) - assert any('"event":"message"' in chunk or '"event": "message"' in chunk for chunk in chunks) - assert chunks[-1] == "data:[DONE]\n\n" - - -@pytest.mark.p2 -def test_rerun_and_cancel_matrix_unit(monkeypatch): - module = _load_canvas_module(monkeypatch) - _set_request_json(monkeypatch, module, {"id": "flow-1", "dsl": {"n": 1}, "component_id": "cmp-1"}) - - monkeypatch.setattr(module.PipelineOperationLogService, "get_documents_info", lambda _id: []) - res = _run(inspect.unwrap(module.rerun)()) - assert res["message"] == "Document not found." - - processing_doc = {"id": "doc-1", "name": "Doc-1", "kb_id": "kb-1", "progress": 0.5} - monkeypatch.setattr(module.PipelineOperationLogService, "get_documents_info", lambda _id: [dict(processing_doc)]) - res = _run(inspect.unwrap(module.rerun)()) - assert "is processing" in res["message"] - - class _DocStore: - def __init__(self): - self.deleted = [] - - def index_exist(self, *_args, **_kwargs): - return True - - def delete(self, *args, **_kwargs): - self.deleted.append(args) - return True - - doc_store = _DocStore() - monkeypatch.setattr(module.settings, "docStoreConn", doc_store) - - doc = { - "id": "doc-1", - "name": "Doc-1", - "kb_id": "kb-1", - "progress": 1.0, - "progress_msg": "old", - "chunk_num": 8, - "token_num": 12, - } - updates = {"doc": [], "pipeline": [], "tasks": [], "queue": []} - monkeypatch.setattr(module.PipelineOperationLogService, "get_documents_info", lambda _id: [dict(doc)]) - monkeypatch.setattr(module.DocumentService, "clear_chunk_num_when_rerun", lambda doc_id: updates["doc"].append(("clear", doc_id))) - monkeypatch.setattr(module.DocumentService, "update_by_id", lambda doc_id, payload: updates["doc"].append(("update", doc_id, payload))) - monkeypatch.setattr(module.TaskService, "filter_delete", lambda expr: updates["tasks"].append(expr)) - monkeypatch.setattr(module.PipelineOperationLogService, "update_by_id", lambda flow_id, payload: updates["pipeline"].append((flow_id, payload))) - monkeypatch.setattr( - module, - "queue_dataflow", - lambda **kwargs: updates["queue"].append(kwargs) or (True, ""), - ) - monkeypatch.setattr(module, "get_uuid", lambda: "task-rerun") - _set_request_json(monkeypatch, module, {"id": "flow-1", "dsl": {"n": 1}, "component_id": "cmp-1"}) - res = _run(inspect.unwrap(module.rerun)()) - assert res["code"] == module.RetCode.SUCCESS - assert doc_store.deleted - assert any(item[0] == "clear" and item[1] == "doc-1" for item in updates["doc"]) - assert updates["pipeline"] and updates["pipeline"][0][1]["dsl"]["path"] == ["cmp-1"] - assert updates["queue"] and updates["queue"][0]["rerun"] is True - - redis_calls = [] - monkeypatch.setattr(module.REDIS_CONN, "set", lambda key, value: redis_calls.append((key, value))) - res = module.cancel("task-9") - assert res["code"] == module.RetCode.SUCCESS - assert redis_calls == [("task-9-cancel", "x")] - - monkeypatch.setattr(module.REDIS_CONN, "set", lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("redis fail"))) - res = module.cancel("task-9") - assert res["code"] == module.RetCode.SUCCESS - - -@pytest.mark.p2 -def test_reset_upload_input_form_debug_matrix_unit(monkeypatch): - module = _load_canvas_module(monkeypatch) - - _set_request_json(monkeypatch, module, {"id": "canvas-1"}) - monkeypatch.setattr(module.UserCanvasService, "accessible", lambda *_args, **_kwargs: False) - res = _run(inspect.unwrap(module.reset)()) - assert res["code"] == module.RetCode.OPERATING_ERROR - - _set_request_json(monkeypatch, module, {"id": "canvas-1"}) - monkeypatch.setattr(module.UserCanvasService, "accessible", lambda *_args, **_kwargs: True) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _canvas_id: (False, None)) - res = _run(inspect.unwrap(module.reset)()) - assert res["message"] == "canvas not found." - - class _ResetCanvas: - def __init__(self, *_args, **_kwargs): - self.reset_called = False - - def reset(self): - self.reset_called = True - - def __str__(self): - return '{"v": 2}' - - updates = [] - _set_request_json(monkeypatch, module, {"id": "canvas-1"}) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _canvas_id: (True, SimpleNamespace(id="canvas-1", dsl={"v": 1}))) - monkeypatch.setattr(module.UserCanvasService, "update_by_id", lambda canvas_id, payload: updates.append((canvas_id, payload))) - monkeypatch.setattr(module, "Canvas", _ResetCanvas) - res = _run(inspect.unwrap(module.reset)()) - assert res["code"] == module.RetCode.SUCCESS - assert res["data"] == {"v": 2} - assert updates == [("canvas-1", {"dsl": {"v": 2}})] - - _set_request_json(monkeypatch, module, {"id": "canvas-1"}) - monkeypatch.setattr(module, "Canvas", lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("reset boom"))) - res = _run(inspect.unwrap(module.reset)()) - assert res["code"] == module.RetCode.EXCEPTION_ERROR - assert "reset boom" in res["message"] - - monkeypatch.setattr(module.UserCanvasService, "get_by_canvas_id", lambda _canvas_id: (False, None)) - monkeypatch.setattr(module, "request", _DummyRequest(args=_Args({"url": "http://example.com"}), files=_FileMap())) - res = _run(module.upload("canvas-1")) - assert res["message"] == "canvas not found." - - monkeypatch.setattr(module.UserCanvasService, "get_by_canvas_id", lambda _canvas_id: (True, {"user_id": "tenant-1"})) - monkeypatch.setattr( - module, - "request", - _DummyRequest( - args=_Args({"url": "http://example.com"}), - files=_FileMap({"file": ["file-1"]}), - ), - ) - monkeypatch.setattr(module.FileService, "upload_info", lambda user_id, file_obj, url=None: {"uid": user_id, "file": file_obj, "url": url}) - res = _run(module.upload("canvas-1")) - assert res["data"]["url"] == "http://example.com" - - monkeypatch.setattr( - module, - "request", - _DummyRequest( - args=_Args({"url": "http://example.com"}), - files=_FileMap({"file": ["f1", "f2"]}), - ), - ) - monkeypatch.setattr(module.FileService, "upload_info", lambda user_id, file_obj, url=None: {"uid": user_id, "file": file_obj, "url": url}) - res = _run(module.upload("canvas-1")) - assert len(res["data"]) == 2 - - monkeypatch.setattr(module.FileService, "upload_info", lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("upload boom"))) - res = _run(module.upload("canvas-1")) - assert res["code"] == module.RetCode.EXCEPTION_ERROR - assert "upload boom" in res["message"] - - monkeypatch.setattr(module, "request", _DummyRequest(args=_Args({"id": "canvas-1", "component_id": "begin"}))) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _canvas_id: (False, None)) - res = module.input_form() - assert res["message"] == "canvas not found." - - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _canvas_id: (True, SimpleNamespace(id="canvas-1", dsl={"n": 1}))) - monkeypatch.setattr(module.UserCanvasService, "query", lambda **_kwargs: []) - res = module.input_form() - assert res["code"] == module.RetCode.OPERATING_ERROR - - class _InputCanvas: - def __init__(self, *_args, **_kwargs): - pass - - def get_component_input_form(self, component_id): - return {"component_id": component_id} - - monkeypatch.setattr(module.UserCanvasService, "query", lambda **_kwargs: [object()]) - monkeypatch.setattr(module, "Canvas", _InputCanvas) - res = module.input_form() - assert res["code"] == module.RetCode.SUCCESS - assert res["data"]["component_id"] == "begin" - - monkeypatch.setattr(module, "Canvas", lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("input boom"))) - res = module.input_form() - assert res["code"] == module.RetCode.EXCEPTION_ERROR - assert "input boom" in res["message"] - - _set_request_json( - monkeypatch, - module, - {"id": "canvas-1", "component_id": "llm-node", "params": {"p": {"value": "v"}}}, - ) - monkeypatch.setattr(module.UserCanvasService, "accessible", lambda *_args, **_kwargs: False) - res = _run(inspect.unwrap(module.debug)()) - assert res["code"] == module.RetCode.OPERATING_ERROR - - class _DebugComponent(module.LLM): - def __init__(self): - self.reset_called = False - self.debug_inputs = None - self.invoked = None - - def reset(self): - self.reset_called = True - - def set_debug_inputs(self, params): - self.debug_inputs = params - - def invoke(self, **kwargs): - self.invoked = kwargs - - def output(self): - async def _gen(): - yield "A" - yield "B" - - return {"stream": partial(_gen)} - - class _DebugCanvas: - last_component = None - - def __init__(self, *_args, **_kwargs): - self.message_id = "" - self._component = _DebugComponent() - _DebugCanvas.last_component = self._component - - def reset(self): - return None - - def get_component(self, _component_id): - return {"obj": self._component} - - _set_request_json( - monkeypatch, - module, - {"id": "canvas-1", "component_id": "llm-node", "params": {"p": {"value": "v"}}}, - ) - monkeypatch.setattr(module.UserCanvasService, "accessible", lambda *_args, **_kwargs: True) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _canvas_id: (True, SimpleNamespace(id="canvas-1", dsl={"n": 1}))) - monkeypatch.setattr(module, "get_uuid", lambda: "msg-1") - monkeypatch.setattr(module, "Canvas", _DebugCanvas) - res = _run(inspect.unwrap(module.debug)()) - assert res["code"] == module.RetCode.SUCCESS - assert res["data"]["stream"] == "AB" - assert _DebugCanvas.last_component.reset_called is True - assert _DebugCanvas.last_component.debug_inputs == {"p": {"value": "v"}} - assert _DebugCanvas.last_component.invoked == {"p": "v"} - - -@pytest.mark.p2 -def test_debug_sync_iter_and_exception_matrix_unit(monkeypatch): - module = _load_canvas_module(monkeypatch) - - class _SyncDebugComponent(module.LLM): - def __init__(self): - self.invoked = {} - - def reset(self): - return None - - def set_debug_inputs(self, _params): - return None - - def invoke(self, **kwargs): - self.invoked = kwargs - - def output(self): - def _gen(): - yield "S" - yield "Y" - yield "N" - yield "C" - - return {"stream": partial(_gen)} - - class _SyncDebugCanvas: - def __init__(self, *_args, **_kwargs): - self.message_id = "" - self.component = _SyncDebugComponent() - - def reset(self): - return None - - def get_component(self, _component_id): - return {"obj": self.component} - - _set_request_json( - monkeypatch, - module, - {"id": "canvas-1", "component_id": "sync-node", "params": {"p": {"value": "v"}}}, - ) - monkeypatch.setattr(module.UserCanvasService, "accessible", lambda *_args, **_kwargs: True) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _canvas_id: (True, SimpleNamespace(id="canvas-1", dsl={"n": 1}))) - monkeypatch.setattr(module, "Canvas", _SyncDebugCanvas) - res = _run(inspect.unwrap(module.debug)()) - assert res["code"] == module.RetCode.SUCCESS - assert res["data"]["stream"] == "SYNC" - - monkeypatch.setattr(module, "Canvas", lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("debug boom"))) - res = _run(inspect.unwrap(module.debug)()) - assert res["code"] == module.RetCode.EXCEPTION_ERROR - assert "debug boom" in res["message"] - - -@pytest.mark.p2 -def test_test_db_connect_dialect_matrix_unit(monkeypatch): - module = _load_canvas_module(monkeypatch) - - class _FakeDB: - def __init__(self, *args, **kwargs): - self.args = args - self.kwargs = kwargs - self.connected = 0 - self.closed = 0 - - def connect(self): - self.connected += 1 - - def close(self): - self.closed += 1 - - mysql_objs = [] - postgres_objs = [] - - def _mysql_ctor(*args, **kwargs): - obj = _FakeDB(*args, **kwargs) - mysql_objs.append(obj) - return obj - - def _postgres_ctor(*args, **kwargs): - obj = _FakeDB(*args, **kwargs) - postgres_objs.append(obj) - return obj - - monkeypatch.setattr(module, "MySQLDatabase", _mysql_ctor) - monkeypatch.setattr(module, "PostgresqlDatabase", _postgres_ctor) - - def _run_case(payload): - _set_request_json(monkeypatch, module, payload) - return _run(inspect.unwrap(module.test_db_connect)()) - - req_base = { - "database": "db", - "username": "user", - "host": "host", - "port": 3306, - "password": "pwd", - } - - res = _run_case({**req_base, "db_type": "mysql"}) - assert res["code"] == module.RetCode.SUCCESS - assert mysql_objs[-1].connected == 1 - assert mysql_objs[-1].closed == 1 - - res = _run_case({**req_base, "db_type": "mariadb"}) - assert res["code"] == module.RetCode.SUCCESS - assert mysql_objs[-1].connected == 1 - - res = _run_case({**req_base, "db_type": "oceanbase"}) - assert res["code"] == module.RetCode.SUCCESS - assert mysql_objs[-1].kwargs["charset"] == "utf8mb4" - - res = _run_case({**req_base, "db_type": "postgres"}) - assert res["code"] == module.RetCode.SUCCESS - assert postgres_objs[-1].closed == 1 - - mssql_calls = {} - - class _MssqlCursor: - def execute(self, sql): - mssql_calls["sql"] = sql - - def close(self): - mssql_calls["cursor_closed"] = True - - class _MssqlConn: - def cursor(self): - mssql_calls["cursor_opened"] = True - return _MssqlCursor() - - def close(self): - mssql_calls["conn_closed"] = True - - pyodbc_mod = ModuleType("pyodbc") - - def _pyodbc_connect(conn_str): - mssql_calls["conn_str"] = conn_str - return _MssqlConn() - - pyodbc_mod.connect = _pyodbc_connect - monkeypatch.setitem(sys.modules, "pyodbc", pyodbc_mod) - res = _run_case({**req_base, "db_type": "mssql"}) - assert res["code"] == module.RetCode.SUCCESS - assert "DRIVER={ODBC Driver 17 for SQL Server}" in mssql_calls["conn_str"] - assert mssql_calls["sql"] == "SELECT 1" - - ibm_calls = {} - ibm_db_mod = ModuleType("ibm_db") - - def _ibm_connect(conn_str, *_args): - ibm_calls["conn_str"] = conn_str - return "ibm-conn" - - def _ibm_exec_immediate(conn, sql): - ibm_calls["exec"] = (conn, sql) - return "ibm-stmt" - - ibm_db_mod.connect = _ibm_connect - ibm_db_mod.exec_immediate = _ibm_exec_immediate - ibm_db_mod.fetch_assoc = lambda stmt: ibm_calls.update({"fetch": stmt}) or {"one": 1} - ibm_db_mod.close = lambda conn: ibm_calls.update({"close": conn}) - monkeypatch.setitem(sys.modules, "ibm_db", ibm_db_mod) - res = _run_case({**req_base, "db_type": "IBM DB2"}) - assert res["code"] == module.RetCode.SUCCESS - assert ibm_calls["exec"] == ("ibm-conn", "SELECT 1 FROM sysibm.sysdummy1") - - monkeypatch.setitem(sys.modules, "trino", None) - res = _run_case({**req_base, "db_type": "trino", "database": "catalog.schema"}) - assert res["code"] == module.RetCode.EXCEPTION_ERROR - assert "Missing dependency 'trino'" in res["message"] - - trino_calls = {"connect": [], "auth": []} - - class _TrinoCursor: - def execute(self, sql): - trino_calls["sql"] = sql - - def fetchall(self): - trino_calls["fetched"] = True - return [(1,)] - - def close(self): - trino_calls["cursor_closed"] = True - - class _TrinoConn: - def cursor(self): - return _TrinoCursor() - - def close(self): - trino_calls["conn_closed"] = True - - trino_mod = ModuleType("trino") - trino_mod.BasicAuthentication = lambda user, password: trino_calls["auth"].append((user, password)) or ("auth", user) - trino_mod.dbapi = SimpleNamespace(connect=lambda **kwargs: trino_calls["connect"].append(kwargs) or _TrinoConn()) - monkeypatch.setitem(sys.modules, "trino", trino_mod) - - res = _run_case({**req_base, "db_type": "trino", "database": ""}) - assert res["code"] == module.RetCode.EXCEPTION_ERROR - assert "catalog.schema" in res["message"] - - monkeypatch.setenv("TRINO_USE_TLS", "1") - res = _run_case({**req_base, "db_type": "trino", "database": "cat.schema"}) - assert res["code"] == module.RetCode.SUCCESS - assert trino_calls["connect"][-1]["catalog"] == "cat" - assert trino_calls["connect"][-1]["schema"] == "schema" - assert trino_calls["auth"][-1] == ("user", "pwd") - - res = _run_case({**req_base, "db_type": "trino", "database": "cat/schema"}) - assert res["code"] == module.RetCode.SUCCESS - assert trino_calls["connect"][-1]["catalog"] == "cat" - assert trino_calls["connect"][-1]["schema"] == "schema" - - res = _run_case({**req_base, "db_type": "trino", "database": "catalog"}) - assert res["code"] == module.RetCode.SUCCESS - assert trino_calls["connect"][-1]["catalog"] == "catalog" - assert trino_calls["connect"][-1]["schema"] == "default" - - res = _run_case({**req_base, "db_type": "unknown"}) - assert res["code"] == module.RetCode.EXCEPTION_ERROR - assert "Unsupported database type." in res["message"] - - class _BoomDB(_FakeDB): - def connect(self): - raise RuntimeError("connect boom") - - monkeypatch.setattr(module, "MySQLDatabase", lambda *_args, **_kwargs: _BoomDB()) - res = _run_case({**req_base, "db_type": "mysql"}) - assert res["code"] == module.RetCode.EXCEPTION_ERROR - assert "connect boom" in res["message"] - - -@pytest.mark.p2 -def test_canvas_history_list_and_setting_matrix_unit(monkeypatch): - module = _load_canvas_module(monkeypatch) - - class _Version: - def __init__(self, version_id, update_time): - self.version_id = version_id - self.update_time = update_time - - def to_dict(self): - return {"id": self.version_id, "update_time": self.update_time} - - monkeypatch.setattr( - module.UserCanvasVersionService, - "list_by_canvas_id", - lambda _canvas_id: [_Version("v1", 1), _Version("v2", 5)], - ) - res = module.getlistversion("canvas-1") - assert [item["id"] for item in res["data"]] == ["v2", "v1"] - - monkeypatch.setattr( - module.UserCanvasVersionService, - "list_by_canvas_id", - lambda _canvas_id: (_ for _ in ()).throw(RuntimeError("history boom")), - ) - res = module.getlistversion("canvas-1") - assert "Error getting history files: history boom" in res["message"] - - monkeypatch.setattr( - module.UserCanvasVersionService, - "get_by_id", - lambda _version_id: (True, _Version("v3", 3)), - ) - res = module.getversion("v3") - assert res["code"] == module.RetCode.SUCCESS - assert res["data"]["id"] == "v3" - - monkeypatch.setattr( - module.UserCanvasVersionService, - "get_by_id", - lambda _version_id: (_ for _ in ()).throw(RuntimeError("version boom")), - ) - res = module.getversion("v3") - assert "Error getting history file: version boom" in res["data"] - - list_calls = [] - - def _get_by_tenant_ids(tenants, user_id, page_number, page_size, orderby, desc, keywords, canvas_category): - list_calls.append((tenants, user_id, page_number, page_size, orderby, desc, keywords, canvas_category)) - return [{"id": "canvas-1"}], 1 - - monkeypatch.setattr(module.UserCanvasService, "get_by_tenant_ids", _get_by_tenant_ids) - monkeypatch.setattr( - module.TenantService, - "get_joined_tenants_by_user_id", - lambda _user_id: [{"tenant_id": "t1"}, {"tenant_id": "t2"}], - ) - - monkeypatch.setattr( - module, - "request", - _DummyRequest( - args=_Args( - { - "keywords": "kw", - "page": "2", - "page_size": "3", - "orderby": "update_time", - "canvas_category": "agent", - "desc": "false", - } - ) - ), - ) - res = module.list_canvas() - assert res["code"] == module.RetCode.SUCCESS - assert list_calls[-1][0] == ["t1", "t2", "user-1"] - assert list_calls[-1][2:6] == (2, 3, "update_time", False) - - monkeypatch.setattr(module, "request", _DummyRequest(args=_Args({"owner_ids": "u1,u2", "desc": "true"}))) - res = module.list_canvas() - assert res["code"] == module.RetCode.SUCCESS - assert list_calls[-1][0] == ["u1", "u2"] - assert list_calls[-1][2:4] == (0, 0) - assert list_calls[-1][5] is True - - _set_request_json(monkeypatch, module, {"id": "canvas-1", "title": "T", "permission": "private"}) - monkeypatch.setattr(module.UserCanvasService, "accessible", lambda *_args, **_kwargs: False) - res = _run(inspect.unwrap(module.setting)()) - assert res["code"] == module.RetCode.OPERATING_ERROR - - _set_request_json(monkeypatch, module, {"id": "canvas-1", "title": "T", "permission": "private"}) - monkeypatch.setattr(module.UserCanvasService, "accessible", lambda *_args, **_kwargs: True) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _canvas_id: (False, None)) - res = _run(inspect.unwrap(module.setting)()) - assert res["message"] == "canvas not found." - - updates = [] - _set_request_json( - monkeypatch, - module, - { - "id": "canvas-1", - "title": "New title", - "permission": "private", - "description": "new desc", - "avatar": "avatar.png", - }, - ) - monkeypatch.setattr( - module.UserCanvasService, - "get_by_id", - lambda _canvas_id: (True, SimpleNamespace(to_dict=lambda: {"id": "canvas-1", "title": "Old"})), - ) - monkeypatch.setattr(module.UserCanvasService, "update_by_id", lambda canvas_id, payload: updates.append((canvas_id, payload)) or 2) - res = _run(inspect.unwrap(module.setting)()) - assert res["code"] == module.RetCode.SUCCESS - assert res["data"] == 2 - assert updates[-1][0] == "canvas-1" - assert updates[-1][1]["title"] == "New title" - assert updates[-1][1]["description"] == "new desc" - assert updates[-1][1]["permission"] == "private" - assert updates[-1][1]["avatar"] == "avatar.png" - - -@pytest.mark.p2 -def test_trace_and_sessions_matrix_unit(monkeypatch): - module = _load_canvas_module(monkeypatch) - - monkeypatch.setattr(module, "request", _DummyRequest(args=_Args({"canvas_id": "c1", "message_id": "m1"}))) - monkeypatch.setattr(module.REDIS_CONN, "get", lambda _key: None) - res = module.trace() - assert res["code"] == module.RetCode.SUCCESS - assert res["data"] == {} - - monkeypatch.setattr(module.REDIS_CONN, "get", lambda _key: '{"event":"ok"}') - res = module.trace() - assert res["code"] == module.RetCode.SUCCESS - assert res["data"] == {"event": "ok"} - - monkeypatch.setattr(module.REDIS_CONN, "get", lambda _key: (_ for _ in ()).throw(RuntimeError("trace boom"))) - res = module.trace() - assert res is None - - monkeypatch.setattr(module.UserCanvasService, "accessible", lambda *_args, **_kwargs: False) - monkeypatch.setattr(module, "request", _DummyRequest(args=_Args({}))) - res = module.sessions("canvas-1") - assert res["code"] == module.RetCode.OPERATING_ERROR - - monkeypatch.setattr(module.UserCanvasService, "accessible", lambda *_args, **_kwargs: True) - monkeypatch.setattr(module, "request", _DummyRequest(args=_Args({"desc": "false", "exp_user_id": "exp-1"}))) - monkeypatch.setattr(module.API4ConversationService, "get_names", lambda _canvas_id, _exp_user_id: [{"id": "s1"}, {"id": "s2"}]) - res = module.sessions("canvas-1") - assert res["code"] == module.RetCode.SUCCESS - assert res["data"]["total"] == 2 - - list_calls = [] - - def _get_list(*args, **kwargs): - list_calls.append((args, kwargs)) - return 7, [{"id": "s3"}] - - monkeypatch.setattr(module.API4ConversationService, "get_list", _get_list) - monkeypatch.setattr( - module, - "request", - _DummyRequest(args=_Args({"page": "3", "page_size": "9", "orderby": "update_time", "dsl": "false"})), - ) - res = module.sessions("canvas-1") - assert res["code"] == module.RetCode.SUCCESS - assert res["data"]["total"] == 7 - assert list_calls[-1][0][4] == "update_time" - assert list_calls[-1][0][5] is True - assert list_calls[-1][0][8] is False - - monkeypatch.setattr(module, "get_json_result", lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("result boom"))) - res = module.sessions("canvas-1") - assert res["code"] == module.RetCode.EXCEPTION_ERROR - assert "result boom" in res["message"] - - -@pytest.mark.p2 -def test_session_crud_prompts_and_download_matrix_unit(monkeypatch): - module = _load_canvas_module(monkeypatch) - - class _SessionCanvas: - def __init__(self, *_args, **_kwargs): - self.reset_called = False - - def reset(self): - self.reset_called = True - - _set_request_json(monkeypatch, module, {"name": "Sess1"}) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _canvas_id: (True, SimpleNamespace(id="canvas-1", dsl={"n": 1}))) - monkeypatch.setattr(module, "Canvas", _SessionCanvas) - monkeypatch.setattr(module, "get_uuid", lambda: "sess-1") - saved = [] - monkeypatch.setattr(module.API4ConversationService, "save", lambda **kwargs: saved.append(kwargs)) - res = _run(inspect.unwrap(module.set_session)("canvas-1")) - assert res["code"] == module.RetCode.SUCCESS - assert res["data"]["id"] == "sess-1" - assert isinstance(res["data"]["dsl"], str) - assert saved and saved[-1]["id"] == "sess-1" - - monkeypatch.setattr(module.UserCanvasService, "accessible", lambda *_args, **_kwargs: False) - res = module.get_session("canvas-1", "sess-1") - assert res["code"] == module.RetCode.OPERATING_ERROR - - monkeypatch.setattr(module.UserCanvasService, "accessible", lambda *_args, **_kwargs: True) - monkeypatch.setattr(module.API4ConversationService, "get_by_id", lambda _session_id: (True, SimpleNamespace(to_dict=lambda: {"id": _session_id}))) - res = module.get_session("canvas-1", "sess-1") - assert res["code"] == module.RetCode.SUCCESS - assert res["data"]["id"] == "sess-1" - - monkeypatch.setattr(module.UserCanvasService, "accessible", lambda *_args, **_kwargs: False) - res = module.del_session("canvas-1", "sess-1") - assert res["code"] == module.RetCode.OPERATING_ERROR - - monkeypatch.setattr(module.UserCanvasService, "accessible", lambda *_args, **_kwargs: True) - monkeypatch.setattr(module.API4ConversationService, "delete_by_id", lambda _session_id: _session_id == "sess-1") - res = module.del_session("canvas-1", "sess-1") - assert res["code"] == module.RetCode.SUCCESS - assert res["data"] is True - - rag_prompts_pkg = ModuleType("rag.prompts") - rag_prompts_pkg.__path__ = [] - monkeypatch.setitem(sys.modules, "rag.prompts", rag_prompts_pkg) - rag_generator_mod = ModuleType("rag.prompts.generator") - rag_generator_mod.ANALYZE_TASK_SYSTEM = "SYS" - rag_generator_mod.ANALYZE_TASK_USER = "USER" - rag_generator_mod.NEXT_STEP = "NEXT" - rag_generator_mod.REFLECT = "REFLECT" - rag_generator_mod.CITATION_PROMPT_TEMPLATE = "CITE" - monkeypatch.setitem(sys.modules, "rag.prompts.generator", rag_generator_mod) - - res = module.prompts() - assert res["code"] == module.RetCode.SUCCESS - assert res["data"]["task_analysis"] == "SYS\n\nUSER" - assert res["data"]["plan_generation"] == "NEXT" - assert res["data"]["reflection"] == "REFLECT" - assert res["data"]["citation_guidelines"] == "CITE" - - monkeypatch.setattr(module, "request", _DummyRequest(args=_Args({"id": "f1", "created_by": "u1"}))) - monkeypatch.setattr(module.FileService, "get_blob", lambda _created_by, _id: b"blob-data") - res = _run(module.download()) - assert res == {"blob": b"blob-data"} diff --git a/web/src/hooks/use-agent-request.ts b/web/src/hooks/use-agent-request.ts index 4e14c0f2124..bb7ed7cbc47 100644 --- a/web/src/hooks/use-agent-request.ts +++ b/web/src/hooks/use-agent-request.ts @@ -28,8 +28,9 @@ import agentService, { fetchPipeLineList, fetchTrace, fetchWebhookTrace, + updateAgent, + uploadAgentFile, } from '@/services/agent-service'; -import api from '@/utils/api'; import { buildMessageListWithUuid } from '@/utils/chat'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useDebounce } from 'ahooks'; @@ -51,15 +52,14 @@ export const enum AgentApiAction { ResetAgent = 'resetAgent', SetAgent = 'setAgent', FetchAgentTemplates = 'fetchAgentTemplates', - UploadCanvasFile = 'uploadCanvasFile', - UploadCanvasFileWithProgress = 'uploadCanvasFileWithProgress', + UploadAgentFile = 'uploadAgentFile', + UploadAgentFileWithProgress = 'uploadAgentFileWithProgress', Trace = 'trace', TestDbConnect = 'testDbConnect', DebugSingle = 'debugSingle', FetchInputForm = 'fetchInputForm', FetchVersionList = 'fetchVersionList', FetchVersion = 'fetchVersion', - FetchAgentAvatar = 'fetchAgentAvatar', FetchExternalAgentInputs = 'fetchExternalAgentInputs', SetAgentSetting = 'setAgentSetting', FetchPrompt = 'fetchPrompt', @@ -72,7 +72,7 @@ export const enum AgentApiAction { DeleteAgentSession = 'deleteAgentSession', FetchSessionByIdManually = 'fetchSessionByIdManually', FetchAgentLog = 'fetchAgentLog', - FetchFlowDetailSSE = 'flowDetailSSE', + FetchSharedAgent = 'fetchSharedAgent', } export const useFetchAgentTemplates = () => { @@ -80,7 +80,7 @@ export const useFetchAgentTemplates = () => { queryKey: [AgentApiAction.FetchAgentTemplates], initialData: [], queryFn: async () => { - const { data } = await agentService.listTemplates(); + const { data } = await agentService.listAgentTemplate(); return data.data; }, @@ -89,6 +89,37 @@ export const useFetchAgentTemplates = () => { return data; }; +const buildAgentListParams = ({ + page, + pageSize, + keywords, + canvasCategory, + ownerIds, +}: { + page: number; + pageSize: number; + keywords?: string; + canvasCategory?: string; + ownerIds?: string[]; +}) => { + const params: Record = { + page, + page_size: pageSize, + }; + + if (keywords) { + params.keywords = keywords; + } + if (canvasCategory) { + params.canvas_category = canvasCategory; + } + if (Array.isArray(ownerIds) && ownerIds.length > 0) { + params.owner_ids = ownerIds.join(','); + } + + return params; +}; + export const useFetchAgentListByPage = () => { const { searchString, handleInputChange } = useHandleSearchChange(); const { pagination, setPagination } = useGetPaginationWithRouter(); @@ -99,17 +130,13 @@ export const useFetchAgentListByPage = () => { : []; const owner = filterValue.owner; - const requestParams: Record = { - keywords: debouncedSearchString, - page_size: pagination.pageSize, + const requestParams = buildAgentListParams({ page: pagination.current, - canvas_category: - canvasCategory.length === 1 ? canvasCategory[0] : undefined, - }; - - if (Array.isArray(owner) && owner.length > 0) { - requestParams.owner_ids = owner.join(','); - } + pageSize: pagination.pageSize, + keywords: debouncedSearchString, + canvasCategory: canvasCategory.length === 1 ? canvasCategory[0] : undefined, + ownerIds: Array.isArray(owner) ? owner : undefined, + }); const { data, isFetching: loading } = useQuery<{ canvas: IFlow[]; @@ -131,7 +158,7 @@ export const useFetchAgentListByPage = () => { }, gcTime: 0, queryFn: async () => { - const { data } = await agentService.listCanvas( + const { data } = await agentService.listAgents( { params: requestParams, }, @@ -166,13 +193,13 @@ export function useFetchAllAgentList() { const { data, isFetching: loading } = useQuery({ queryKey: [AgentApiAction.FetchAllAgentList], queryFn: async () => { - const { data } = await agentService.listCanvas( + const { data } = await agentService.listAgents( { - params: { + params: buildAgentListParams({ page: 1, - page_size: 100000, - canvas_category: AgentCategory.AgentCanvas, - }, + pageSize: 100000, + canvasCategory: AgentCategory.AgentCanvas, + }), }, true, ); @@ -194,7 +221,12 @@ export const useUpdateAgentSetting = () => { } = useMutation({ mutationKey: [AgentApiAction.UpdateAgentSetting], mutationFn: async (params: any) => { - const ret = await agentService.settingCanvas(params); + const ret = await updateAgent(params.id, { + title: params.title, + description: params.description, + permission: params.permission, + avatar: params.avatar, + }); if (ret?.data?.code === 0) { message.success('success'); queryClient.invalidateQueries({ @@ -218,14 +250,14 @@ export const useDeleteAgent = () => { mutateAsync, } = useMutation({ mutationKey: [AgentApiAction.DeleteAgent], - mutationFn: async (canvasIds: string[]) => { - const { data } = await agentService.removeCanvas({ canvasIds }); + mutationFn: async (agentId: string) => { + const { data } = await agentService.deleteAgent(agentId); if (data.code === 0) { queryClient.invalidateQueries({ queryKey: [AgentApiAction.FetchAgentListByPage], }); } - return data?.data ?? []; + return data?.data ?? false; }, }); @@ -252,7 +284,7 @@ export const useFetchAgent = (): { refetchOnWindowFocus: false, gcTime: 0, queryFn: async () => { - const { data } = await agentService.fetchCanvas(sharedId || id); + const { data } = await agentService.getAgent(sharedId || id); const messageList = buildMessageListWithUuid( get(data, 'data.dsl.messages', []), @@ -286,7 +318,7 @@ export const useResetAgent = () => { } = useMutation({ mutationKey: [AgentApiAction.ResetAgent], mutationFn: async () => { - const { data } = await agentService.resetCanvas({ id }); + const { data } = await agentService.resetAgent(id); return data; }, }); @@ -295,6 +327,7 @@ export const useResetAgent = () => { }; export const useSetAgent = (showMessage: boolean = true) => { + const { id } = useParams(); const queryClient = useQueryClient(); const { data, @@ -309,17 +342,34 @@ export const useSetAgent = (showMessage: boolean = true) => { avatar?: string; canvas_category?: string; release?: string; + description?: string | null; + permission?: string; }) => { - const { data = {} } = await agentService.setCanvas(params); + const agentId = params.id ?? id; + const { data = {} } = agentId + ? await updateAgent(agentId, { + title: params.title, + dsl: params.dsl, + avatar: params.avatar, + description: params.description, + permission: params.permission, + release: params.release, + }) + : await agentService.createAgent(params); if (data.code === 0) { if (showMessage) { message.success( - i18n.t(`message.${params?.id ? 'modified' : 'created'}`), + i18n.t(`message.${agentId ? 'modified' : 'created'}`), ); } queryClient.invalidateQueries({ queryKey: [AgentApiAction.FetchAgentListByPage], }); + if (agentId) { + queryClient.invalidateQueries({ + queryKey: [AgentApiAction.FetchAgentDetail], + }); + } } return data; }, @@ -329,17 +379,17 @@ export const useSetAgent = (showMessage: boolean = true) => { }; // Only one file can be uploaded at a time -export const useUploadCanvasFile = () => { +export const useUploadAgentFile = () => { const { id } = useParams(); const [searchParams] = useSearchParams(); const shared_id = searchParams.get('shared_id'); - const canvasId = id || shared_id; + const agentId = id || shared_id; const { data, isPending: loading, mutateAsync, } = useMutation({ - mutationKey: [AgentApiAction.UploadCanvasFile], + mutationKey: [AgentApiAction.UploadAgentFile], mutationFn: async (body: any) => { let nextBody = body; try { @@ -350,10 +400,7 @@ export const useUploadCanvasFile = () => { }); } - const { data } = await agentService.uploadCanvasFile( - { url: api.uploadAgentFile(canvasId as string), data: nextBody }, - true, - ); + const { data } = await uploadAgentFile(agentId as string, nextBody); if (data?.code === 0) { message.success(i18n.t('message.uploaded')); } @@ -364,10 +411,10 @@ export const useUploadCanvasFile = () => { }, }); - return { data, loading, uploadCanvasFile: mutateAsync }; + return { data, loading, uploadAgentFile: mutateAsync }; }; -export const useUploadCanvasFileWithProgress = (identifier?: string | null) => { +export const useUploadAgentFileWithProgress = (identifier?: string | null) => { const { id } = useParams(); type UploadParameters = Parameters>; @@ -379,7 +426,7 @@ export const useUploadCanvasFileWithProgress = (identifier?: string | null) => { isPending: loading, mutateAsync, } = useMutation({ - mutationKey: [AgentApiAction.UploadCanvasFileWithProgress], + mutationKey: [AgentApiAction.UploadAgentFileWithProgress], mutationFn: async ({ files, options: { onError, onSuccess, onProgress }, @@ -392,9 +439,9 @@ export const useUploadCanvasFileWithProgress = (identifier?: string | null) => { }); } - const { data } = await agentService.uploadCanvasFile( + const { data } = await agentService.uploadAgentFile( { - url: api.uploadAgentFile(identifier || id), + agentId: identifier || id, data: formData, onUploadProgress: ({ progress }) => { files.forEach((file) => { @@ -420,7 +467,7 @@ export const useUploadCanvasFileWithProgress = (identifier?: string | null) => { }, }); - return { data, loading, uploadCanvasFile: mutateAsync }; + return { data, loading, uploadAgentFile: mutateAsync }; }; export const useFetchMessageTrace = (canvasId?: string) => { @@ -490,9 +537,18 @@ export const useDebugSingle = () => { isPending: loading, mutateAsync, } = useMutation({ - mutationKey: [AgentApiAction.FetchInputForm], + mutationKey: [AgentApiAction.DebugSingle], mutationFn: async (params: IDebugSingleRequestBody) => { - const ret = await agentService.debugSingle({ id, ...params }); + const ret = await agentService.debugSingle( + { + agentId: id as string, + componentId: params.component_id, + data: { + params: params.params, + }, + }, + true, + ); if (ret?.data?.code !== 0) { message.error(ret?.data?.message); } @@ -512,12 +568,7 @@ export const useFetchInputForm = (componentId?: string) => { enabled: !!id && !!componentId, queryFn: async () => { const { data } = await agentService.inputForm( - { - params: { - id, - component_id: componentId, - }, - }, + { agentId: id as string, componentId: componentId as string }, true, ); @@ -552,15 +603,19 @@ export const useFetchVersion = ( data?: IFlow; loading: boolean; } => { + const { id } = useParams(); const { data, isFetching: loading } = useQuery({ - queryKey: [AgentApiAction.FetchVersion, version_id], + queryKey: [AgentApiAction.FetchVersion, id, version_id], initialData: undefined, gcTime: 0, - enabled: !!version_id, // Only call API when both values are provided + enabled: !!id && !!version_id, queryFn: async () => { - if (!version_id) return undefined; + if (!id || !version_id) return undefined; - const { data } = await agentService.fetchVersion(version_id); + const { data } = await agentService.fetchVersion({ + agentId: id, + versionId: version_id, + }); return data?.data ?? undefined; }, @@ -569,35 +624,6 @@ export const useFetchVersion = ( return { data, loading }; }; -export const useFetchAgentAvatar = (): { - data: IFlow; - loading: boolean; - refetch: () => void; -} => { - const { sharedId } = useGetSharedChatSearchParams(); - - const { - data, - isFetching: loading, - refetch, - } = useQuery({ - queryKey: [AgentApiAction.FetchAgentAvatar], - initialData: {} as IFlow, - refetchOnReconnect: false, - refetchOnMount: false, - refetchOnWindowFocus: false, - gcTime: 0, - queryFn: async () => { - if (!sharedId) return {}; - const { data } = await agentService.fetchAgentAvatar(sharedId); - - return data?.data ?? {}; - }, - }); - - return { data, loading, refetch }; -}; - export const useFetchAgentLog = (searchParams: IAgentLogsRequest) => { const { id } = useParams(); const { data, isFetching: loading } = useQuery({ @@ -609,7 +635,7 @@ export const useFetchAgentLog = (searchParams: IAgentLogsRequest) => { ...searchParams, }); - return data?.data ?? []; + return { total: data?.total ?? 0, sessions: data?.data ?? [] }; }, }); @@ -636,7 +662,7 @@ export const useFetchSessionsByCanvasId = () => { exp_user_id: tenantInfo.tenant_id, }); - return data?.data ?? { total: 0, sessions: [] }; + return { total: data?.total ?? 0, sessions: data?.data ?? [] }; }, }); @@ -672,33 +698,6 @@ export const useFetchExternalAgentInputs = () => { return { data, loading, refetch }; }; -export const useSetAgentSetting = () => { - const { id } = useParams(); - const queryClient = useQueryClient(); - - const { - data, - isPending: loading, - mutateAsync, - } = useMutation({ - mutationKey: [AgentApiAction.SetAgentSetting], - mutationFn: async (params: any) => { - const ret = await agentService.settingCanvas({ id, ...params }); - if (ret?.data?.code === 0) { - message.success('success'); - queryClient.invalidateQueries({ - queryKey: [AgentApiAction.FetchAgentDetail], - }); - } else { - message.error(ret?.data?.data); - } - return ret?.data?.code; - }, - }); - - return { data, loading, setAgentSetting: mutateAsync }; -}; - export const useFetchPrompt = () => { const { data, @@ -731,7 +730,9 @@ export const useFetchAgentList = ({ initialData: { canvas: [], total: 0 }, gcTime: 0, queryFn: async () => { - const { data } = await fetchPipeLineList({ canvas_category }); + const { data } = await fetchPipeLineList({ + canvas_category, + }); return data?.data ?? []; }, @@ -767,7 +768,7 @@ export const useCancelDataflow = () => { // initialData: [], // gcTime: 0, // https://tanstack.com/query/latest/docs/framework/react/guides/caching?from=reactQueryV3 // queryFn: async () => { -// const { data } = await agentService.listCanvas(); +// const { data } = await agentService.listAgents(); // return data?.data ?? []; // }, @@ -793,7 +794,7 @@ export function useCancelConversation() { return { data, loading, cancelConversation: mutateAsync }; } -export const useFetchFlowSSE = (): { +export const useFetchSharedAgent = (): { data: IFlow; loading: boolean; refetch: () => void; @@ -805,7 +806,7 @@ export const useFetchFlowSSE = (): { isFetching: loading, refetch, } = useQuery({ - queryKey: [AgentApiAction.FetchFlowDetailSSE], + queryKey: [AgentApiAction.FetchSharedAgent, sharedId], initialData: {} as IFlow, refetchOnReconnect: false, refetchOnMount: false, @@ -813,7 +814,7 @@ export const useFetchFlowSSE = (): { gcTime: 0, queryFn: async () => { if (!sharedId) return {}; - const { data } = await agentService.getCanvasSSE(sharedId); + const { data } = await agentService.getAgent(sharedId); const messageList = buildMessageListWithUuid( get(data, 'data.dsl.messages', []), diff --git a/web/src/interfaces/database/agent.ts b/web/src/interfaces/database/agent.ts index 86576d759af..97e8324b33e 100644 --- a/web/src/interfaces/database/agent.ts +++ b/web/src/interfaces/database/agent.ts @@ -297,6 +297,7 @@ export interface IPipeLineListRequest { orderby?: string; desc?: boolean; canvas_category?: AgentCategory; + ext?: string; } export interface GlobalVariableType { diff --git a/web/src/pages/agent/chat/box.tsx b/web/src/pages/agent/chat/box.tsx index d210b21c21d..b22891cb92e 100644 --- a/web/src/pages/agent/chat/box.tsx +++ b/web/src/pages/agent/chat/box.tsx @@ -10,7 +10,7 @@ import PdfSheet from '@/components/pdf-drawer'; import { useClickDrawer } from '@/components/pdf-drawer/hooks'; import { useFetchAgent, - useUploadCanvasFileWithProgress, + useUploadAgentFileWithProgress, } from '@/hooks/use-agent-request'; import { useFetchUserInfo } from '@/hooks/use-user-setting-request'; import { buildMessageUuidWithRole } from '@/utils/chat'; @@ -44,7 +44,7 @@ function AgentChatBox() { useGetFileIcon(); const { data: userInfo } = useFetchUserInfo(); const { id: canvasId } = useParams(); - const { uploadCanvasFile, loading } = useUploadCanvasFileWithProgress(); + const { uploadAgentFile, loading } = useUploadAgentFileWithProgress(); const { buildInputList, handleOk, isWaitting } = useAwaitCompentData({ derivedMessages, @@ -60,10 +60,10 @@ function AgentChatBox() { const handleUploadFile: NonNullable = useCallback( async (files, options) => { - const ret = await uploadCanvasFile({ files, options }); + const ret = await uploadAgentFile({ files, options }); appendUploadResponseList(ret.data, files); }, - [appendUploadResponseList, uploadCanvasFile], + [appendUploadResponseList, uploadAgentFile], ); return ( diff --git a/web/src/pages/agent/chat/use-send-agent-message.ts b/web/src/pages/agent/chat/use-send-agent-message.ts index 8208ffb7543..c037f236b4f 100644 --- a/web/src/pages/agent/chat/use-send-agent-message.ts +++ b/web/src/pages/agent/chat/use-send-agent-message.ts @@ -240,7 +240,7 @@ export const useSendAgentMessage = ({ const inputs = useSelectBeginNodeDataInputs(); const [sessionId, setSessionId] = useState(null); const { send, answerList, done, stopOutputMessage, resetAnswerList } = - useSendMessageBySSE(url || api.runCanvas); + useSendMessageBySSE(url || api.agentChatCompletion); const firstAnswer = answerList[0]; const messageId = useMemo(() => { return firstAnswer?.message_id; @@ -298,13 +298,12 @@ export const useSendAgentMessage = ({ beginInputs?: BeginQuery[]; exploreSessionId?: string; }) => { - const params: Record = { - id: agentId, - }; + const params: Record = { agent_id: agentId }; params.running_hint_text = i18n.t('flow.runningHintText', { defaultValue: 'is running...🕞', }); + params['openai-compatible'] = false; if (typeof message.content === 'string') { const query = inputs; @@ -361,7 +360,7 @@ export const useSendAgentMessage = ({ ); const sendFormMessage = useCallback( - async (body: { id?: string; inputs: Record }) => { + async (body: { agent_id?: string; inputs: Record }) => { addNewestOneQuestion({ content: Object.entries(body.inputs) .map(([, val]) => `${val.name}: ${val.value}`) diff --git a/web/src/pages/agent/debug-content/uploader.tsx b/web/src/pages/agent/debug-content/uploader.tsx index 9dddb90defd..ed147b23aa0 100644 --- a/web/src/pages/agent/debug-content/uploader.tsx +++ b/web/src/pages/agent/debug-content/uploader.tsx @@ -13,7 +13,7 @@ import { type FileUploadProps, } from '@/components/file-upload'; import { Button } from '@/components/ui/button'; -import { useUploadCanvasFile } from '@/hooks/use-agent-request'; +import { useUploadAgentFile } from '@/hooks/use-agent-request'; import { Upload, X } from 'lucide-react'; import * as React from 'react'; import { toast } from 'sonner'; @@ -34,7 +34,7 @@ export function FileUploadDirectUpload({ Array.isArray(value) ? value : value ? [value] : [], ); - const { uploadCanvasFile } = useUploadCanvasFile(); + const { uploadAgentFile } = useUploadAgentFile(); const onUpload: NonNullable = React.useCallback( async (files, { onSuccess, onError }) => { @@ -47,7 +47,7 @@ export function FileUploadDirectUpload({ ); }; try { - const ret = await uploadCanvasFile([file]); + const ret = await uploadAgentFile([file]); if (ret.code === 0) { onSuccess(file); uploadedFilesRef.current = [ @@ -70,7 +70,7 @@ export function FileUploadDirectUpload({ console.error('Unexpected error during upload:', error); } }, - [onChange, uploadCanvasFile], + [onChange, uploadAgentFile], ); const onFileReject = React.useCallback((file: File, message: string) => { diff --git a/web/src/pages/agent/explore/components/session-chat.tsx b/web/src/pages/agent/explore/components/session-chat.tsx index 954670dc6be..43533251355 100644 --- a/web/src/pages/agent/explore/components/session-chat.tsx +++ b/web/src/pages/agent/explore/components/session-chat.tsx @@ -4,7 +4,7 @@ import MessageItem from '@/components/next-message-item'; import PdfSheet from '@/components/pdf-drawer'; import { useClickDrawer } from '@/components/pdf-drawer/hooks'; import { MessageType } from '@/constants/chat'; -import { useUploadCanvasFileWithProgress } from '@/hooks/use-agent-request'; +import { useUploadAgentFileWithProgress } from '@/hooks/use-agent-request'; import { useFetchUserInfo } from '@/hooks/use-user-setting-request'; import { IAgentLogResponse } from '@/interfaces/database/agent'; import { IMessage } from '@/interfaces/database/chat'; @@ -55,16 +55,16 @@ export function SessionChat({ session }: SessionChatProps) { useClickDrawer(); // File upload - const { uploadCanvasFile, loading: isUploading } = - useUploadCanvasFileWithProgress(); + const { uploadAgentFile, loading: isUploading } = + useUploadAgentFileWithProgress(); const handleUploadFile: NonNullable = useCallback( async (files, options) => { - const ret = await uploadCanvasFile({ files, options }); + const ret = await uploadAgentFile({ files, options }); appendUploadResponseList(ret.data, files); }, - [appendUploadResponseList, uploadCanvasFile], + [appendUploadResponseList, uploadAgentFile], ); useEffect(() => { diff --git a/web/src/pages/agent/explore/hooks/use-send-session-message.ts b/web/src/pages/agent/explore/hooks/use-send-session-message.ts index 34baaf98a62..0aa7cfaa2d4 100644 --- a/web/src/pages/agent/explore/hooks/use-send-session-message.ts +++ b/web/src/pages/agent/explore/hooks/use-send-session-message.ts @@ -6,7 +6,6 @@ import { } from '@/hooks/use-agent-request'; import { useSendAgentMessage } from '@/pages/agent/chat/use-send-agent-message'; import { buildBeginInputListFromObject } from '@/pages/agent/form/begin-form/utils'; -import api from '@/utils/api'; import { get, isEmpty } from 'lodash'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useParams } from 'react-router'; @@ -63,7 +62,6 @@ export const useSendSessionMessage = () => { value, ...chatLogic } = useSendAgentMessage({ - url: api.runCanvasExplore(canvasId!), beginParams, }); diff --git a/web/src/pages/agent/hooks/use-chat-logic.ts b/web/src/pages/agent/hooks/use-chat-logic.ts index 3c62ae4d1d1..2fa1b00166f 100644 --- a/web/src/pages/agent/hooks/use-chat-logic.ts +++ b/web/src/pages/agent/hooks/use-chat-logic.ts @@ -8,7 +8,7 @@ type IAwaitCompentData = { derivedMessages: IMessage[]; sendFormMessage: (params: { inputs: Record; - id: string; + agent_id: string; }) => void; canvasId: string; }; @@ -37,7 +37,7 @@ const useAwaitCompentData = (props: IAwaitCompentData) => { const nextInputs = buildBeginQueryWithObject(inputs, values); sendFormMessage({ inputs: nextInputs, - id: canvasId, + agent_id: canvasId, }); }, [getInputs, sendFormMessage, canvasId], diff --git a/web/src/pages/agent/hooks/use-run-dataflow.ts b/web/src/pages/agent/hooks/use-run-dataflow.ts index 0d290a7959a..6dac58acb99 100644 --- a/web/src/pages/agent/hooks/use-run-dataflow.ts +++ b/web/src/pages/agent/hooks/use-run-dataflow.ts @@ -13,7 +13,7 @@ export function useRunDataflow({ }: { showLogSheet: () => void; } & Pick) { - const { send } = useSendMessageBySSE(api.runCanvas); + const { send } = useSendMessageBySSE(api.agentChatCompletion); const { id } = useParams(); const { saveGraph, loading } = useSaveGraph(); const [uploadedFileData, setUploadedFileData] = @@ -27,8 +27,9 @@ export function useRunDataflow({ showLogSheet(); const res = await send({ - id, + agent_id: id, query: '', + 'openai-compatible': false, session_id: null, files: [fileResponseData.file], }); diff --git a/web/src/pages/agent/setting-dialog/index.tsx b/web/src/pages/agent/setting-dialog/index.tsx index 37d11ec1cd8..c09255868fd 100644 --- a/web/src/pages/agent/setting-dialog/index.tsx +++ b/web/src/pages/agent/setting-dialog/index.tsx @@ -6,7 +6,7 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog'; -import { useSetAgentSetting } from '@/hooks/use-agent-request'; +import { useSetAgent } from '@/hooks/use-agent-request'; import { IModalProps } from '@/interfaces/common'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -18,16 +18,16 @@ import { export function SettingDialog({ hideModal }: IModalProps) { const { t } = useTranslation(); - const { setAgentSetting } = useSetAgentSetting(); + const { setAgent } = useSetAgent(); const submit = useCallback( async (values: SettingFormSchemaType) => { - const code = await setAgentSetting(values); - if (code === 0) { + const ret = await setAgent(values); + if (ret?.code === 0) { hideModal?.(); } }, - [hideModal, setAgentSetting], + [hideModal, setAgent], ); return ( diff --git a/web/src/pages/agent/share/index.tsx b/web/src/pages/agent/share/index.tsx index 7222dcd858b..6fb1d2964fd 100644 --- a/web/src/pages/agent/share/index.tsx +++ b/web/src/pages/agent/share/index.tsx @@ -6,7 +6,7 @@ import PdfSheet from '@/components/pdf-drawer'; import { useClickDrawer } from '@/components/pdf-drawer/hooks'; import { useSyncThemeFromParams } from '@/components/theme-provider'; import { MessageType } from '@/constants/chat'; -import { useUploadCanvasFileWithProgress } from '@/hooks/use-agent-request'; +import { useUploadAgentFileWithProgress } from '@/hooks/use-agent-request'; import { cn } from '@/lib/utils'; import i18n, { changeLanguageAsync } from '@/locales/config'; import DebugContent from '@/pages/agent/debug-content'; @@ -33,8 +33,8 @@ const ChatContainer = () => { const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = useClickDrawer(); - const { uploadCanvasFile, loading } = - useUploadCanvasFileWithProgress(conversationId); + const { uploadAgentFile, loading } = + useUploadAgentFileWithProgress(conversationId); const { addEventList, setCurrentMessageId, @@ -80,10 +80,10 @@ const ChatContainer = () => { const handleUploadFile: NonNullable = useCallback( async (files, options) => { - const ret = await uploadCanvasFile({ files, options }); + const ret = await uploadAgentFile({ files, options }); appendUploadResponseList(ret.data, files); }, - [appendUploadResponseList, uploadCanvasFile], + [appendUploadResponseList, uploadAgentFile], ); React.useEffect(() => { diff --git a/web/src/pages/agents/agent-dropdown.tsx b/web/src/pages/agents/agent-dropdown.tsx index e6f54ccaac1..5370f2a39df 100644 --- a/web/src/pages/agents/agent-dropdown.tsx +++ b/web/src/pages/agents/agent-dropdown.tsx @@ -37,7 +37,7 @@ export function AgentDropdown({ ); const handleDelete: MouseEventHandler = useCallback(() => { - deleteAgent([agent.id]); + deleteAgent(agent.id); }, [agent.id, deleteAgent]); return ( diff --git a/web/src/pages/next-chats/share/index.tsx b/web/src/pages/next-chats/share/index.tsx index dd109dccc8a..8a25e07b721 100644 --- a/web/src/pages/next-chats/share/index.tsx +++ b/web/src/pages/next-chats/share/index.tsx @@ -5,7 +5,7 @@ import PdfSheet from '@/components/pdf-drawer'; import { useClickDrawer } from '@/components/pdf-drawer/hooks'; import { useSyncThemeFromParams } from '@/components/theme-provider'; import { MessageType, SharedFrom } from '@/constants/chat'; -import { useFetchFlowSSE } from '@/hooks/use-agent-request'; +import { useFetchSharedAgent } from '@/hooks/use-agent-request'; import { useFetchExternalChatInfo } from '@/hooks/use-chat-request'; import i18n, { changeLanguageAsync } from '@/locales/config'; import { buildMessageUuidWithRole } from '@/utils/chat'; @@ -44,7 +44,7 @@ const ChatContainer = () => { const sendDisabled = useSendButtonDisabled(value); const { data: chatInfo } = useFetchExternalChatInfo(); - const { data: flowData } = useFetchFlowSSE(); + const { data: flowData } = useFetchSharedAgent(); React.useEffect(() => { if (locale && i18n.language !== locale) { changeLanguageAsync(locale); diff --git a/web/src/services/agent-service.ts b/web/src/services/agent-service.ts index 77652b088cc..0c43b939835 100644 --- a/web/src/services/agent-service.ts +++ b/web/src/services/agent-service.ts @@ -8,25 +8,20 @@ import { registerNextServer } from '@/utils/register-server'; import request from '@/utils/request'; const { - getCanvasSSE, - setCanvas, - listCanvas, - resetCanvas, - removeCanvas, - runCanvas, - listTemplates, + createAgent, + updateAgent: updateAgentApi, + listAgents, + deleteAgent, + agentChatCompletion, + resetAgent, + listAgentTemplate, testDbConnect, getInputElements, - debug, - settingCanvas, - uploadCanvasFile, trace, - inputForm, fetchVersionList, fetchVersion, - fetchCanvas, - fetchAgentAvatar, - fetchAgentLogs, + getAgent, + fetchAgentSessions, fetchExternalAgentInputs, prompt, cancelDataflow, @@ -34,16 +29,12 @@ const { } = api; const methods = { - fetchCanvas: { - url: fetchCanvas, + getAgent: { + url: getAgent, method: 'get', }, - getCanvasSSE: { - url: getCanvasSSE, - method: 'get', - }, - setCanvas: { - url: setCanvas, + createAgent: { + url: createAgent, method: 'post', }, fetchVersionList: { @@ -51,27 +42,28 @@ const methods = { method: 'get', }, fetchVersion: { - url: fetchVersion, + url: (config: { agentId: string; versionId: string }) => + fetchVersion(config.agentId, config.versionId), method: 'get', }, - listCanvas: { - url: listCanvas, + listAgents: { + url: listAgents, method: 'get', }, - resetCanvas: { - url: resetCanvas, + resetAgent: { + url: resetAgent, method: 'post', }, - removeCanvas: { - url: removeCanvas, - method: 'post', + deleteAgent: { + url: deleteAgent, + method: 'delete', }, - runCanvas: { - url: runCanvas, + agentChatCompletion: { + url: agentChatCompletion, method: 'post', }, - listTemplates: { - url: listTemplates, + listAgentTemplate: { + url: listAgentTemplate, method: 'get', }, testDbConnect: { @@ -83,31 +75,26 @@ const methods = { method: 'get', }, debugSingle: { - url: debug, - method: 'post', - }, - settingCanvas: { - url: settingCanvas, + url: (config: { agentId: string; componentId: string }) => + api.debug(config.agentId, config.componentId), method: 'post', }, - uploadCanvasFile: { - url: uploadCanvasFile, + uploadAgentFile: { + url: (config: { agentId: string }) => api.uploadAgentFile(config.agentId), method: 'post', }, trace: { - url: trace, + url: (config: { agentId: string; messageId: string }) => + trace(config.agentId, config.messageId), method: 'get', }, inputForm: { - url: inputForm, - method: 'get', - }, - fetchAgentAvatar: { - url: fetchAgentAvatar, + url: (config: { agentId: string; componentId: string }) => + api.inputForm(config.agentId, config.componentId), method: 'get', }, fetchAgentLogs: { - url: fetchAgentLogs, + url: fetchAgentSessions, method: 'get', }, fetchExternalAgentInputs: { @@ -127,15 +114,34 @@ const methods = { method: 'put', }, createAgentSession: { - url: fetchAgentLogs, - method: 'put', + url: api.createAgentSession, + method: 'post', }, } as const; const agentService = registerNextServer(methods); +export const updateAgent = ( + agentId: string, + params: { + title?: string; + dsl?: Record; + avatar?: string; + description?: string | null; + permission?: string; + release?: string; + }, +) => { + return request(updateAgentApi(agentId), { method: 'put', data: params }); +}; + export const fetchTrace = (data: { canvas_id: string; message_id: string }) => { - return request.get(methods.trace.url, { params: data }); + return request.get( + methods.trace.url({ + agentId: data.canvas_id, + messageId: data.message_id, + }), + ); }; export const fetchAgentLogsByCanvasId = ( canvasId: string, @@ -145,11 +151,11 @@ export const fetchAgentLogsByCanvasId = ( }; export const fetchAgentLogsById = (canvasId: string, sessionId: string) => { - return request.get(api.fetchAgentLogsById(canvasId, sessionId)); + return request.get(api.fetchAgentSessionById(canvasId, sessionId)); }; export const fetchPipeLineList = (params: IPipeLineListRequest) => { - return request.get(api.listCanvas, { params: params }); + return request.get(api.listAgents, { params: params }); }; export const fetchWebhookTrace = ( @@ -160,11 +166,18 @@ export const fetchWebhookTrace = ( }; export function createAgentSession({ id, name }: { id: string; name: string }) { - return request.put(api.fetchAgentLogs(id), { data: { name } }); + return request.post(api.createAgentSession(id), { data: { name } }); } export const deleteAgentSession = (canvasId: string, sessionId: string) => { - return request.delete(api.fetchAgentLogsById(canvasId, sessionId)); + return request.delete(api.fetchAgentSessionById(canvasId, sessionId)); +}; + +export const uploadAgentFile = (agentId: string, data: FormData) => { + return request(api.uploadAgentFile(agentId), { + method: 'post', + data, + }); }; export default agentService; diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts index 462384f2f25..67a2076ee64 100644 --- a/web/src/utils/api.ts +++ b/web/src/utils/api.ts @@ -83,7 +83,7 @@ export default { `${restAPIv1}/datasets/${datasetId}/trace_raptor`, unbindPipelineTask: ({ kb_id, type }: { kb_id: string; type: string }) => `${webAPI}/kb/unbind_task?kb_id=${kb_id}&pipeline_task_type=${type}`, - pipelineRerun: `${webAPI}/canvas/rerun`, + pipelineRerun: `${restAPIv1}/agents/rerun`, getMetaData: (datasetId: string) => `${restAPIv1}/datasets/${datasetId}/metadata/summary`, updateDocumentsMetadata: (datasetId: string) => @@ -177,46 +177,45 @@ export default { setLangfuseConfig: `${restAPIv1}/langfuse/api_key`, // flow - listTemplates: `${webAPI}/canvas/templates`, - listCanvas: `${webAPI}/canvas/list`, - getCanvas: `${webAPI}/canvas/get`, - getCanvasSSE: (canvasId: string) => `${webAPI}/canvas/getsse/${canvasId}`, - removeCanvas: `${webAPI}/canvas/rm`, - setCanvas: `${webAPI}/canvas/set`, - settingCanvas: `${webAPI}/canvas/setting`, - getListVersion: `${webAPI}/canvas/getlistversion`, - getVersion: `${webAPI}/canvas/getversion`, - resetCanvas: `${webAPI}/canvas/reset`, - runCanvas: `${webAPI}/canvas/completion`, - testDbConnect: `${webAPI}/canvas/test_db_connect`, + listAgentTemplate: `${restAPIv1}/agents/templates`, + listAgents: `${restAPIv1}/agents`, + createAgent: `${restAPIv1}/agents`, + updateAgent: (agentId: string) => `${restAPIv1}/agents/${agentId}`, + deleteAgent: (agentId: string) => `${restAPIv1}/agents/${agentId}`, + agentChatCompletion: `${restAPIv1}/agents/chat/completion`, + resetAgent: (agentId: string) => `${restAPIv1}/agents/${agentId}/reset`, + testDbConnect: `${restAPIv1}/agents/test_db_connection`, getInputElements: `${webAPI}/canvas/input_elements`, - debug: `${webAPI}/canvas/debug`, - uploadCanvasFile: `${webAPI}/canvas/upload`, - trace: `${webAPI}/canvas/trace`, + debug: (agentId: string, componentId: string) => + `${restAPIv1}/agents/${agentId}/components/${componentId}/debug`, + trace: (agentId: string, messageId: string) => + `${restAPIv1}/agents/${agentId}/logs/${messageId}`, cancelCanvas: (taskId: string) => `${webAPI}/canvas/cancel/${taskId}`, // cancel conversation // agent - inputForm: `${webAPI}/canvas/input_form`, - fetchVersionList: (id: string) => `${webAPI}/canvas/getlistversion/${id}`, - fetchVersion: (id: string) => `${webAPI}/canvas/getversion/${id}`, - fetchCanvas: (id: string) => `${webAPI}/canvas/get/${id}`, - fetchAgentAvatar: (id: string) => `${webAPI}/canvas/getsse/${id}`, - uploadAgentFile: (id?: string) => `${webAPI}/canvas/upload/${id}`, + inputForm: (agentId: string, componentId: string) => + `${restAPIv1}/agents/${agentId}/components/${componentId}/input-form`, + fetchVersionList: (id: string) => `${restAPIv1}/agents/${id}/versions`, + fetchVersion: (agentId: string, versionId: string) => + `${restAPIv1}/agents/${agentId}/versions/${versionId}`, + getAgent: (id: string) => `${restAPIv1}/agents/${id}`, + uploadAgentFile: (id?: string) => `${restAPIv1}/agents/${id}/upload`, + createAgentSession: (agentId: string) => + `${restAPIv1}/agents/${agentId}/sessions`, fetchAgentLogs: (canvasId: string) => `${webAPI}/canvas/${canvasId}/sessions`, - fetchAgentLogsById: (canvasId: string, sessionId: string) => - `${webAPI}/canvas/${canvasId}/sessions/${sessionId}`, + fetchAgentSessions: (agentId: string) => + `${restAPIv1}/agents/${agentId}/sessions`, + fetchAgentSessionById: (agentId: string, sessionId: string) => + `${restAPIv1}/agents/${agentId}/sessions/${sessionId}`, fetchExternalAgentInputs: (canvasId: string) => `${restAPIv1}/agentbots/${canvasId}/inputs`, - prompt: `${webAPI}/canvas/prompts`, + prompt: `${restAPIv1}/agents/prompts`, cancelDataflow: (id: string) => `${webAPI}/canvas/cancel/${id}`, - downloadFile: `${webAPI}/canvas/download`, + downloadFile: `${restAPIv1}/agents/download`, testWebhook: (id: string) => `${restAPIv1}/webhook_test/${id}`, fetchWebhookTrace: (id: string) => `${restAPIv1}/webhook_trace/${id}`, // explore - runCanvasExplore: (canvasId: string) => - `${webAPI}/canvas/${canvasId}/completion`, - // mcp server listMcpServer: `${restAPIv1}/mcp/servers`, getMcpServer: (id: string) => `${restAPIv1}/mcp/servers/${id}`,