mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-15 04:17:56 +00:00
feat(agents): make sources optional and drop the synthetic "Default" source
The sources list used to start with a fake "Default" entry that had no id and, at run time, meant "no source, skip retrieval". The agent form pre-selected it, snapped back to it when the last source was deselected, and refused to publish without it, so a new agent always looked like it had a knowledge base when it had none. Backend - /api/sources returns only ingested sources; no placeholder row. - Publishing an agent no longer requires a source on create or update. The legacy "default" value is still accepted and maps to NULL. Frontend - The agent source picker starts empty, can be cleared, and shows a hint that a source-less agent answers from the model and its tools only. - The picker groups sources into "Your sources" and "Shared with team" when any team-shared source exists, shows "N sources selected" for a multi-selection, and gets the same "Go to Sources" / "Upload new" footer as the chat picker. A source uploaded from the form is selected when it lands. - Source selection serialisation and the picker id live in one helper shared with the chat picker; the four copies in the form are gone. - The client no longer seeds a placeholder source in the store, and the dead auto-select of a "default" document is removed.
This commit is contained in:
1 parent
fb15649a3f
commit
79d418f32a
23 files changed
+532
-365
No files matched your search
@@ -782,12 +782,10 @@ class TestUpdateAgent:
|
||||
response = UpdateAgent().put(str(agent["id"]))
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_publish_with_default_source_succeeds(self, app, pg_conn):
|
||||
"""The frontend's auto-selected "Default" source has no UUID — it
|
||||
sends ``source: ''`` and ``sources: []`` with ``retriever: 'classic'``.
|
||||
Pre-fix, the publish gate rejected this with
|
||||
``Missing or invalid required fields: Source``. The retriever
|
||||
carries the runtime identity, so the gate now accepts it.
|
||||
def test_publish_with_no_source_succeeds(self, app, pg_conn):
|
||||
"""An agent with no source is a valid published state: it answers
|
||||
from the model and its tools. The form sends ``source: ''`` and
|
||||
``sources: []`` with the default ``retriever``.
|
||||
"""
|
||||
from docsgpt.api.user.agents.routes import UpdateAgent
|
||||
from docsgpt.storage.db.repositories.agents import AgentsRepository
|
||||
@@ -827,11 +825,11 @@ class TestUpdateAgent:
|
||||
f"unexpected error: {response.json}"
|
||||
)
|
||||
|
||||
def test_publish_without_source_or_retriever_returns_400(
|
||||
def test_publish_without_source_or_retriever_succeeds(
|
||||
self, app, pg_conn,
|
||||
):
|
||||
"""If neither a source nor a retriever is configured, the gate
|
||||
still trips — the agent has no way to retrieve anything."""
|
||||
"""Neither a source nor a retriever is required to publish; a
|
||||
source-less agent simply skips retrieval at run time."""
|
||||
from docsgpt.api.user.agents.routes import UpdateAgent
|
||||
from docsgpt.storage.db.repositories.agents import AgentsRepository
|
||||
from docsgpt.storage.db.repositories.prompts import PromptsRepository
|
||||
@@ -864,8 +862,12 @@ class TestUpdateAgent:
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": user}
|
||||
response = UpdateAgent().put(str(agent["id"]))
|
||||
assert response.status_code == 400
|
||||
assert "Source or retriever" in response.json.get("message", "")
|
||||
assert response.status_code == 200, (
|
||||
f"unexpected error: {response.json}"
|
||||
)
|
||||
stored = AgentsRepository(pg_conn).get_by_id(str(agent["id"]))
|
||||
assert stored["status"] == "published"
|
||||
assert stored["source_id"] is None
|
||||
|
||||
def test_publishing_generates_api_key(self, app, pg_conn):
|
||||
from docsgpt.api.user.agents.routes import UpdateAgent
|
||||
@@ -1075,25 +1077,71 @@ class TestCreateAgentMore:
|
||||
)
|
||||
assert status == 400
|
||||
|
||||
def test_publish_classic_without_source_returns_400(self, app):
|
||||
def test_publish_classic_without_source_succeeds(self, app, pg_conn):
|
||||
"""Publishing with ``source: ''`` and ``sources: []`` creates a
|
||||
source-less agent (``source_id`` NULL) instead of a 400."""
|
||||
from docsgpt.api.user.agents.routes import CreateAgent
|
||||
from docsgpt.storage.db.repositories.agents import AgentsRepository
|
||||
|
||||
with app.test_request_context(
|
||||
user = "u-create-no-source"
|
||||
with _patch_db(pg_conn), app.test_request_context(
|
||||
"/api/create_agent", method="POST",
|
||||
json={
|
||||
"name": "n",
|
||||
"name": "no-source",
|
||||
"description": "d",
|
||||
"status": "published",
|
||||
"agent_type": "classic",
|
||||
"chunks": "2",
|
||||
"retriever": "classic",
|
||||
"prompt_id": "default",
|
||||
"source": "",
|
||||
"sources": [],
|
||||
},
|
||||
):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": "u"}
|
||||
request.decoded_token = {"sub": user}
|
||||
response = CreateAgent().post()
|
||||
status = (
|
||||
response[1] if isinstance(response, tuple) else response.status_code
|
||||
assert response.status_code == 201, (
|
||||
f"unexpected error: {response.json}"
|
||||
)
|
||||
assert status == 400
|
||||
agents = AgentsRepository(pg_conn).list_for_user(user)
|
||||
created = next(a for a in agents if a["name"] == "no-source")
|
||||
assert created["status"] == "published"
|
||||
assert created["source_id"] is None
|
||||
assert not created.get("extra_source_ids")
|
||||
assert created["key"]
|
||||
|
||||
def test_publish_classic_with_legacy_default_sentinel_succeeds(
|
||||
self, app, pg_conn,
|
||||
):
|
||||
"""Older clients still send ``source: "default"``; it keeps mapping
|
||||
to a NULL source rather than being rejected."""
|
||||
from docsgpt.api.user.agents.routes import CreateAgent
|
||||
from docsgpt.storage.db.repositories.agents import AgentsRepository
|
||||
|
||||
user = "u-create-legacy-default"
|
||||
with _patch_db(pg_conn), app.test_request_context(
|
||||
"/api/create_agent", method="POST",
|
||||
json={
|
||||
"name": "legacy-default",
|
||||
"description": "d",
|
||||
"status": "published",
|
||||
"agent_type": "classic",
|
||||
"chunks": "2",
|
||||
"retriever": "classic",
|
||||
"prompt_id": "default",
|
||||
"source": "default",
|
||||
},
|
||||
):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": user}
|
||||
response = CreateAgent().post()
|
||||
assert response.status_code == 201, (
|
||||
f"unexpected error: {response.json}"
|
||||
)
|
||||
agents = AgentsRepository(pg_conn).list_for_user(user)
|
||||
created = next(a for a in agents if a["name"] == "legacy-default")
|
||||
assert created["source_id"] is None
|
||||
|
||||
def test_unknown_agent_type_falls_back_classic(self, app, pg_conn):
|
||||
from docsgpt.api.user.agents.routes import CreateAgent
|
||||
|
||||
@@ -80,7 +80,9 @@ class TestCombinedJson:
|
||||
response = CombinedJson().get()
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_returns_default_plus_user_sources(self, app, pg_conn):
|
||||
def test_returns_only_real_user_sources(self, app, pg_conn):
|
||||
"""No synthetic "Default" row: every entry is an ingested source
|
||||
with an id, so clients never have to special-case a placeholder."""
|
||||
from docsgpt.api.user.sources.routes import CombinedJson
|
||||
|
||||
user = "u-list-sources"
|
||||
@@ -93,8 +95,20 @@ class TestCombinedJson:
|
||||
|
||||
assert response.status_code == 200
|
||||
names = [d["name"] for d in response.json]
|
||||
assert "Default" in names
|
||||
assert "doc1" in names
|
||||
assert "Default" not in names
|
||||
assert names == ["doc1"]
|
||||
assert all(d.get("id") for d in response.json)
|
||||
|
||||
def test_returns_empty_list_when_user_has_no_sources(self, app, pg_conn):
|
||||
from docsgpt.api.user.sources.routes import CombinedJson
|
||||
|
||||
with _patch_db(pg_conn), app.test_request_context("/api/sources"):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": "u-no-sources"}
|
||||
response = CombinedJson().get()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json == []
|
||||
|
||||
def test_db_error_returns_400(self, app):
|
||||
from docsgpt.api.user.sources.routes import CombinedJson
|
||||
|
||||
Reference in new issue
Block a user