mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-15 14:16:05 +00:00
Add document I/O to workflows and surface run artifacts
Let workflow runs consume and produce documents end to end: bridge uploaded attachments into run-scoped artifacts so nodes receive the input documents (with a per-run cap and server-computed size/sha256, and the run row pre-created so produced artifacts are authorized during the run); emit the run id to the client and add a builder panel that lists, previews, and downloads a run's artifacts; and allow attaching documents to a Preview run via the existing upload flow. Also fixes issues a compliance workflow surfaced: attachment ownership now keys on the raw identity instead of a sanitized one (the sanitized form could not be read back and could collide across users); workflow code nodes read prior state from a state.json data file instead of templating it into the program, so untrusted document content can never be interpolated into executed code; structured node output wrapped in code fences is recovered; and the live speech-to-text ownership check compares the raw identity.
This commit is contained in:
1 parent
396eb92ab0
commit
5a87fa6258
20 files changed
+1186
-47
No files matched your search
@@ -108,6 +108,41 @@ class TestStreamProcessorAgentConfiguration:
|
||||
assert isinstance(processor.agent_config, dict)
|
||||
assert processor.agent_id is None
|
||||
|
||||
def test_embedded_workflow_without_saved_id(self):
|
||||
"""A preview run with no saved workflow id carries no ``workflow_id``."""
|
||||
from application.api.answer.services.stream_processor import StreamProcessor
|
||||
|
||||
request_data = {
|
||||
"question": "Test",
|
||||
"workflow": {"nodes": [], "edges": []},
|
||||
}
|
||||
processor = StreamProcessor(request_data, {"sub": "user_123"})
|
||||
processor._configure_agent()
|
||||
|
||||
assert processor.agent_config["agent_type"] == "workflow"
|
||||
assert processor.agent_config["workflow"] == {"nodes": [], "edges": []}
|
||||
assert "workflow_id" not in processor.agent_config
|
||||
|
||||
def test_embedded_workflow_with_saved_id_persists_run(self):
|
||||
"""A saved workflow id alongside the embedded graph is captured so the
|
||||
run can persist a ``workflow_runs`` row for artifact listing."""
|
||||
from application.api.answer.services.stream_processor import StreamProcessor
|
||||
|
||||
request_data = {
|
||||
"question": "Test",
|
||||
"workflow": {"nodes": [], "edges": []},
|
||||
"workflow_id": "11111111-1111-1111-1111-111111111111",
|
||||
}
|
||||
processor = StreamProcessor(request_data, {"sub": "user_123"})
|
||||
processor._configure_agent()
|
||||
|
||||
assert processor.agent_config["agent_type"] == "workflow"
|
||||
assert (
|
||||
processor.agent_config["workflow_id"]
|
||||
== "11111111-1111-1111-1111-111111111111"
|
||||
)
|
||||
assert processor.agent_config["workflow_owner"] == "user_123"
|
||||
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
|
||||
@@ -486,6 +486,18 @@ class TestResolveAuthenticatedUser:
|
||||
assert result is not None
|
||||
assert "jwt_user" in result
|
||||
|
||||
def test_returns_raw_email_sub_unsanitized(self, flask_app):
|
||||
# Regression: an email-style sub must be returned verbatim so the
|
||||
# attachment row's user_id matches the raw sub /stream queries with.
|
||||
# Previously safe_filename stripped "@"/"." (a@b.com -> abcom), making
|
||||
# an uploaded attachment unreadable by its own owner on /stream.
|
||||
from application.api.user.attachments.routes import _resolve_authenticated_user
|
||||
|
||||
app = Flask(__name__)
|
||||
with app.test_request_context("/api/store_attachment", method="POST"):
|
||||
request.decoded_token = {"sub": "alex@arc53.com"}
|
||||
assert _resolve_authenticated_user() == "alex@arc53.com"
|
||||
|
||||
def test_returns_user_from_valid_api_key_form(self, flask_app):
|
||||
from application.api.user.attachments.routes import _resolve_authenticated_user
|
||||
|
||||
@@ -1439,6 +1451,70 @@ class TestLiveSpeechToTextAdditional:
|
||||
response = finish_resource.post()
|
||||
assert _get_response_status(response) == 403
|
||||
|
||||
@patch("application.api.user.attachments.routes.STTCreator.create_stt")
|
||||
@patch("application.api.user.attachments.routes.get_redis_instance")
|
||||
def test_live_stt_email_sub_owner_round_trip(
|
||||
self, mock_get_redis, mock_create_stt, flask_app
|
||||
):
|
||||
# Regression: an email-style sub (a@b.com) is stored RAW on the session, so
|
||||
# the owner must pass the raw-vs-raw ownership gate on chunk + finish. Before
|
||||
# the fix, safe_filename(stored_user) != raw auth_user 403'd the owner.
|
||||
from application.api.user.attachments.routes import (
|
||||
LiveSpeechToTextChunk,
|
||||
LiveSpeechToTextFinish,
|
||||
LiveSpeechToTextStart,
|
||||
)
|
||||
|
||||
app = Flask(__name__)
|
||||
fake_redis = FakeRedis()
|
||||
mock_get_redis.return_value = fake_redis
|
||||
owner_sub = "alex@arc53.com"
|
||||
|
||||
start_resource = LiveSpeechToTextStart()
|
||||
with app.test_request_context(
|
||||
"/api/stt/live/start",
|
||||
method="POST",
|
||||
json={"language": "en"},
|
||||
):
|
||||
request.decoded_token = {"sub": owner_sub}
|
||||
start_response = start_resource.post()
|
||||
session_id = _get_response_json(start_response)["session_id"]
|
||||
|
||||
mock_stt = MagicMock()
|
||||
mock_stt.transcribe.return_value = {
|
||||
"text": "hello this is a longer test phrase for transcript stabilization today now",
|
||||
"language": "en",
|
||||
"duration_s": 1.0,
|
||||
"segments": [],
|
||||
"provider": "openai",
|
||||
}
|
||||
mock_create_stt.return_value = mock_stt
|
||||
|
||||
chunk_resource = LiveSpeechToTextChunk()
|
||||
with app.test_request_context(
|
||||
"/api/stt/live/chunk",
|
||||
method="POST",
|
||||
data={
|
||||
"session_id": session_id,
|
||||
"chunk_index": "0",
|
||||
"file": (io.BytesIO(b"chunk-0"), "chunk-0.wav"),
|
||||
},
|
||||
content_type="multipart/form-data",
|
||||
):
|
||||
request.decoded_token = {"sub": owner_sub}
|
||||
chunk_response = chunk_resource.post()
|
||||
assert _get_response_status(chunk_response) == 200
|
||||
|
||||
finish_resource = LiveSpeechToTextFinish()
|
||||
with app.test_request_context(
|
||||
"/api/stt/live/finish",
|
||||
method="POST",
|
||||
json={"session_id": session_id},
|
||||
):
|
||||
request.decoded_token = {"sub": owner_sub}
|
||||
finish_response = finish_resource.post()
|
||||
assert _get_response_status(finish_response) == 200
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestServeImage:
|
||||
|
||||
Reference in new issue
Block a user