Log failed chat turns as chat entries

A turn whose agent raised wrote no user_logs row, so it only surfaced as
the agent's system error row. Every finished turn now writes its chat row,
at level error with the error when it failed, and linked to its trace; the
system row for the same traced activity is no longer listed twice.
This commit is contained in:
arc53-machine committed 2026-09-24 00:08:53 +01:00
1 parent 3156ee37a1
commit f2d92bf8df
4 files changed
+203 -39

No files matched your search

+46
View File
@@ -185,6 +185,52 @@ class TestTraceWithPersistence:
assert row[0]["request_id"] == "req-p"
assert row[0]["message_id"] == trace.message_id
def test_failed_turn_is_logged_as_a_chat_row(self, pg_conn, flask_app):
"""A raised failure still writes the turn's chat row, at level error."""
from docsgpt.api.answer.routes.base import BaseAnswerResource
from tests.api.answer.test_base_routes import _patch_db_session
agent = MagicMock()
agent.gen.side_effect = RuntimeError("upstream down")
agent.tool_calls = []
with flask_app.app_context(), _patch_db_session(pg_conn), _captured_flushes():
_run(
BaseAnswerResource(),
agent,
should_persist=True,
model_id="gpt-4",
request_id="req-failed",
)
from sqlalchemy import text as sql_text
rows = pg_conn.execute(
sql_text("SELECT data FROM user_logs WHERE user_id = 'u-trace'")
).fetchall()
assert len(rows) == 1
data = rows[0][0]
assert data["level"] == "error"
assert data["request_id"] == "req-failed"
assert data["error"] == "RuntimeError: upstream down"
def test_yielded_error_logs_the_chat_row_at_error_level(self, pg_conn, flask_app):
from docsgpt.api.answer.routes.base import BaseAnswerResource
from tests.api.answer.test_base_routes import _patch_db_session
with flask_app.app_context(), _patch_db_session(pg_conn), _captured_flushes():
_run(
BaseAnswerResource(),
_agent([{"type": "error", "error": "node failed"}]),
should_persist=True,
model_id="gpt-4",
)
from sqlalchemy import text as sql_text
data = pg_conn.execute(
sql_text("SELECT data FROM user_logs WHERE user_id = 'u-trace'")
).fetchone()[0]
assert data["level"] == "error"
assert data["error"] == "node failed"
def test_paused_turn_is_flushed_paused(self, pg_conn, flask_app):
from docsgpt.api.answer.routes.base import BaseAnswerResource
from tests.api.answer.test_base_routes import _patch_db_session
+36
View File
@@ -212,3 +212,39 @@ class TestSummaryFailure:
assert response.status_code == 200
(row,) = response.json["logs"]
assert "trace" not in row
class TestFailedChatTurns:
def _failed_turn(self, pg_conn, *, with_trace):
from docsgpt.storage.db.repositories.stack_logs import StackLogsRepository
from docsgpt.storage.db.repositories.user_logs import UserLogsRepository
StackLogsRepository(pg_conn).insert(
activity_id="act-chat", endpoint="stream", level="error", user_id="owner", query="q"
)
UserLogsRepository(pg_conn).insert(
user_id="owner",
endpoint="stream_answer",
data={
"question": "q",
"level": "error",
"error": "RuntimeError: upstream down",
"request_id": "req-f",
},
)
if with_trace:
_trace(pg_conn, request_id="req-f", activity_id="act-chat", status="error")
def test_failed_turn_is_one_chat_row_not_also_a_system_row(self, app, pg_conn):
self._failed_turn(pg_conn, with_trace=True)
rows = _logs(app, pg_conn, "owner", {}).json["logs"]
assert [r["event_type"] for r in rows] == ["chat"]
assert rows[0]["level"] == "error"
assert rows[0]["error"] == "RuntimeError: upstream down"
assert rows[0]["trace"]["status"] == "error"
def test_untraced_failures_keep_their_system_row(self, app, pg_conn):
"""Rows from before traces (or with tracing off) have nothing to dedupe against."""
self._failed_turn(pg_conn, with_trace=False)
rows = _logs(app, pg_conn, "owner", {}).json["logs"]
assert sorted(r["event_type"] for r in rows) == ["chat", "system"]