mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-01 16:25:44 +08:00
## Summary Fixes #15169 — `POST /api/v1/agents/chat/completions` returned `data: {}` with no `session_id` when the agent produced no events (e.g. the reporter's payload sent `"query": ""`). ## Root cause For `{"agent_id": "...", "query": "", "stream": false}`: 1. No `session_id` in the request → new-session branch at `agent_api.py:1278`. 2. `session_id = get_uuid()` at `agent_api.py:1294`. 3. Falls into `_run_workflow_session`. 4. `canvas.run(query="")` produces no events, so `final_ans` stays `{}`. 5. Non-streaming path then hit: ```python if not final_ans: await commit_runtime_replica() return get_result(data={}) ``` `session_id` was allocated but silently dropped on the way out. The streaming path had the same shape (only a bare `[DONE]` was yielded — no SSE event carrying `session_id`). The session-continuation path at `agent_api.py:1463` had the same bug for callers that passed `session_id` and got `{}` back. The successful (non-empty) paths were fine because every canvas event has `ans["session_id"] = session_id` attached before being yielded / captured into `final_ans` (see `agent_api.py:255` and `:303`). ## Fix Three minimal changes, all in `api/apps/restful_apis/agent_api.py`: 1. **`_run_workflow_session` (non-streaming)**: `return get_result(data={"session_id": session_id})` instead of `data={}`. 2. **`_run_workflow_session` (SSE)**: if the canvas loop emits no events, yield one `data:{"session_id": "...", "data": {}}` event before `[DONE]`, so the client receives the id over the wire. 3. **`agent_chat_completion` session-continuation**: echo the caller-supplied `session_id` back in the empty-events case instead of `{}`. No change needed on the happy paths — they already attach `session_id` to every event. ## Test plan - [ ] Repro from the issue: `POST /api/v1/agents/chat/completions` with `{"agent_id": "<id>", "query": "", "stream": false}`. Response `data` should now contain `session_id`. - [ ] Same payload with `"stream": true`. SSE stream should contain one event with `session_id` before `data:[DONE]`. - [ ] Same shape but with a real, non-empty `"query"` (new session). Response should be unchanged from before — every event still carries `session_id`, final response still includes it on `final_ans`. - [ ] Pass an existing `session_id` plus `"query": ""`. Response should echo that `session_id` back instead of `{}`. - [ ] Pass an existing `session_id` plus a normal query. Response should be unchanged from before. - [ ] `openai-compatible: true` path is untouched — sanity-check it still works. - [ ] Run `uv run pytest` to make sure no existing tests regress. ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) - [ ] New Feature (non-breaking change which adds functionality) - [ ] Documentation Update - [ ] Refactoring - [ ] Performance Improvement - [ ] Other (please describe):
This commit is contained in:
@@ -323,6 +323,20 @@ async def _run_workflow_session(
|
||||
final_ans["data"]["structured"] = structured_output
|
||||
if trace_items:
|
||||
final_ans["data"]["trace"] = trace_items
|
||||
else:
|
||||
# Canvas produced no events (e.g. empty query). Still
|
||||
# surface the session_id so the client can resume the
|
||||
# conversation — without it the SSE stream is just a
|
||||
# bare [DONE] (fixes #15169).
|
||||
logging.info(
|
||||
"empty agent output - returning session_id (agent_id=%s session_id=%s stream=%s)",
|
||||
agent_id, session_id, True,
|
||||
)
|
||||
yield (
|
||||
"data:"
|
||||
+ json.dumps({"session_id": session_id, "data": {}}, ensure_ascii=False)
|
||||
+ "\n\n"
|
||||
)
|
||||
await persist_workflow_session()
|
||||
except Exception as exc:
|
||||
logging.exception(exc)
|
||||
@@ -366,8 +380,16 @@ async def _run_workflow_session(
|
||||
return get_result(data=f"**ERROR**: {str(exc)}")
|
||||
|
||||
if not final_ans:
|
||||
# Canvas produced no events (e.g. caller sent an empty query). The
|
||||
# API contract still promises a session_id back so the client can
|
||||
# resume the conversation — return it instead of an empty dict
|
||||
# (fixes #15169).
|
||||
logging.info(
|
||||
"empty agent output - returning session_id (agent_id=%s session_id=%s stream=%s)",
|
||||
agent_id, session_id, False,
|
||||
)
|
||||
await commit_runtime_replica()
|
||||
return get_result(data={})
|
||||
return get_result(data={"session_id": session_id})
|
||||
|
||||
if "data" not in final_ans or not isinstance(final_ans["data"], dict):
|
||||
final_ans["data"] = {}
|
||||
@@ -1549,8 +1571,24 @@ async def agent_chat_completion(tenant_id, agent_id=None):
|
||||
if req.get("stream", True):
|
||||
|
||||
async def generate():
|
||||
emitted = False
|
||||
async for ans in _iter_session_completion_events(tenant_id, agent_id, req, return_trace):
|
||||
emitted = True
|
||||
yield "data:" + json.dumps(ans, ensure_ascii=False) + "\n\n"
|
||||
if not emitted:
|
||||
# Parity with the new-session SSE path: if the canvas yields
|
||||
# no events on an existing session (e.g. empty query), still
|
||||
# echo the session_id so clients can recover it instead of
|
||||
# seeing only a bare [DONE] (fixes #15169).
|
||||
logging.info(
|
||||
"empty agent output - returning session_id (agent_id=%s session_id=%s stream=%s)",
|
||||
agent_id, session_id, True,
|
||||
)
|
||||
yield (
|
||||
"data:"
|
||||
+ json.dumps({"session_id": session_id, "data": {}}, ensure_ascii=False)
|
||||
+ "\n\n"
|
||||
)
|
||||
yield "data:[DONE]\n\n"
|
||||
|
||||
return _build_sse_response(generate())
|
||||
@@ -1587,7 +1625,15 @@ async def agent_chat_completion(tenant_id, agent_id=None):
|
||||
return get_result(data=f"**ERROR**: {str(exc)}")
|
||||
|
||||
if not final_ans:
|
||||
return get_result(data={})
|
||||
# Same contract as the new-session path: even when the canvas
|
||||
# emits nothing (e.g. empty query against an existing session),
|
||||
# echo the session_id back so the client can keep using it
|
||||
# (fixes #15169).
|
||||
logging.info(
|
||||
"empty agent output - returning session_id (agent_id=%s session_id=%s stream=%s)",
|
||||
agent_id, session_id, False,
|
||||
)
|
||||
return get_result(data={"session_id": session_id})
|
||||
|
||||
if "data" not in final_ans or not isinstance(final_ans["data"], dict):
|
||||
final_ans["data"] = {}
|
||||
|
||||
Reference in New Issue
Block a user