Skip to content

Commit 9703c15

Browse files
committed
Make feedback resilient without LangFuse
1 parent e5bc7b8 commit 9703c15

5 files changed

Lines changed: 107 additions & 50 deletions

File tree

cloudrun/service.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,5 @@ spec:
115115
# langsmith-api-key / langfuse-* secrets when tracing matters.
116116
- name: LANGSMITH_TRACING
117117
value: "false"
118+
- name: LANGFUSE_ENABLED
119+
value: "false"

docs/env-vars-production.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ All secrets should be stored in **Google Secret Manager** and referenced via `va
5050
| `COHERE_API_KEY` | yes | Enables cross-encoder reranking |
5151
| `LANGFUSE_PUBLIC_KEY` | yes | |
5252
| `LANGFUSE_SECRET_KEY` | yes | |
53+
| `LANGFUSE_ENABLED` | no | Set to `true` only when both LangFuse keys are configured; otherwise feedback remains stored locally |
5354
| `LANGFUSE_ENV` | no | `prod` |
5455
| `LANGFUSE_RELEASE` | no | Git SHA or semver, e.g. `v1.2.3` |
5556
| `SENTRY_DSN` | yes | Error tracking; unset disables it entirely |

scripts/deploy.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ gcloud container images add-tag "$TAG" "$LATEST" --quiet
5353
# ── Deploy ────────────────────────────────────────────────────────────────────
5454

5555
# Inject the SHA-tagged image (and project id) so Cloud Run sees a spec change
56-
TMP_YAML=$(mktemp /tmp/service-XXXXXX.yaml)
56+
TMP_YAML=$(mktemp /tmp/service-XXXXXX)
5757
sed "s|gcr.io/PROJECT_ID/cortex:latest|$TAG|g" "$SERVICE_YAML" > "$TMP_YAML"
5858

5959
if ! grep -q "$TAG" "$TMP_YAML"; then

src/api/routers/sessions.py

Lines changed: 38 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -772,7 +772,7 @@ async def submit_run_feedback(
772772
body: RunFeedbackRequest,
773773
current_user: AuthenticatedUser = Depends(get_authenticated_user),
774774
):
775-
"""Record simple user feedback for a completed run in LangFuse."""
775+
"""Record user feedback locally and mirror it to LangFuse when available."""
776776
session = await get_session(session_id, current_user.user_id)
777777
if session is None:
778778
raise HTTPException(
@@ -795,6 +795,15 @@ async def submit_run_feedback(
795795
status_code=409,
796796
detail="Feedback can only be submitted for completed runs.",
797797
)
798+
comment: str | None = None
799+
if body.comment is not None:
800+
trimmed = " ".join(body.comment.strip().split())
801+
if not trimmed:
802+
raise HTTPException(status_code=400, detail="Feedback comment cannot be empty.")
803+
if len(trimmed) > 500:
804+
raise HTTPException(status_code=400, detail="Feedback comment is too long.")
805+
comment = trimmed
806+
798807
trace_id = run.langfuse_trace_id
799808
observation_id = run.langfuse_observation_id
800809
if not trace_id:
@@ -806,68 +815,48 @@ async def submit_run_feedback(
806815
query=run.query,
807816
report=run.report,
808817
)
809-
except Exception as exc:
810-
raise HTTPException(
811-
status_code=502, detail=f"Could not link run to LangFuse: {exc}"
812-
)
813-
if not trace_id:
814-
raise HTTPException(
815-
status_code=502, detail="Could not link run to LangFuse."
816-
)
817-
linkage_updated = await update_session_run(
818-
run_id=run_id,
819-
user_id=current_user.user_id,
820-
session_id=session_id,
821-
patch={
818+
except Exception:
819+
# LangFuse is optional. A missing or unavailable observability backend
820+
# must not prevent the product from accepting user feedback.
821+
logger.warning("Could not create LangFuse feedback anchor", exc_info=True)
822+
trace_id = None
823+
observation_id = None
824+
825+
submitted_at = datetime.now(UTC).isoformat()
826+
patch: dict[str, object] = {
827+
"feedback_submitted_at": submitted_at,
828+
"feedback_helpful": body.helpful,
829+
}
830+
if trace_id:
831+
patch.update(
832+
{
822833
"langfuse_trace_id": trace_id,
823834
"langfuse_observation_id": observation_id,
824-
},
825-
)
826-
if not linkage_updated:
827-
raise HTTPException(
828-
status_code=404,
829-
detail=f"Run '{run_id}' not found in session '{session_id}'.",
830-
)
831-
832-
comment: str | None = None
833-
if body.comment is not None:
834-
trimmed = " ".join(body.comment.strip().split())
835-
if not trimmed:
836-
raise HTTPException(
837-
status_code=400, detail="Feedback comment cannot be empty."
838-
)
839-
if len(trimmed) > 500:
840-
raise HTTPException(status_code=400, detail="Feedback comment is too long.")
841-
comment = trimmed
842-
843-
try:
844-
submit_user_feedback_score(
845-
trace_id=trace_id,
846-
observation_id=observation_id,
847-
helpful=body.helpful,
848-
comment=comment,
849-
)
850-
except Exception as exc:
851-
raise HTTPException(
852-
status_code=502, detail=f"Could not submit LangFuse feedback: {exc}"
835+
}
853836
)
854-
855-
submitted_at = datetime.now(UTC).isoformat()
856837
updated = await update_session_run(
857838
run_id=run_id,
858839
user_id=current_user.user_id,
859840
session_id=session_id,
860-
patch={
861-
"feedback_submitted_at": submitted_at,
862-
"feedback_helpful": body.helpful,
863-
},
841+
patch=patch,
864842
)
865843
if not updated:
866844
raise HTTPException(
867845
status_code=404,
868846
detail=f"Run '{run_id}' not found in session '{session_id}'.",
869847
)
870848

849+
if trace_id:
850+
try:
851+
submit_user_feedback_score(
852+
trace_id=trace_id,
853+
observation_id=observation_id,
854+
helpful=body.helpful,
855+
comment=comment,
856+
)
857+
except Exception:
858+
logger.warning("Could not mirror user feedback to LangFuse", exc_info=True)
859+
871860
return {
872861
"session_id": session_id,
873862
"run_id": run_id,

tests/test_api.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,71 @@ def test_submit_run_feedback_backfills_langfuse_linkage_when_missing():
481481
)
482482

483483

484+
def test_submit_run_feedback_succeeds_when_langfuse_is_not_configured():
485+
mock_session = Session(
486+
session_id="session-1",
487+
runs=[SessionRun(run_id="run-1", query="q", source_urls=[], report="", created_at="2026")],
488+
conversation=[],
489+
created_at="2026",
490+
)
491+
492+
with (
493+
patch("src.api.routers.sessions.get_session", new=AsyncMock(return_value=mock_session)),
494+
patch(
495+
"src.api.routers.sessions.create_feedback_anchor_for_run",
496+
side_effect=RuntimeError("LangFuse is not configured"),
497+
),
498+
patch("src.api.routers.sessions.submit_user_feedback_score") as mock_score,
499+
patch(
500+
"src.api.routers.sessions.update_session_run", new=AsyncMock(return_value=True)
501+
) as mock_update,
502+
):
503+
response = client.post(
504+
"/sessions/session-1/runs/run-1/feedback",
505+
json={"helpful": False},
506+
)
507+
508+
assert response.status_code == 200
509+
mock_score.assert_not_called()
510+
assert mock_update.await_args.kwargs["patch"]["feedback_helpful"] is False
511+
512+
513+
def test_submit_run_feedback_succeeds_when_langfuse_score_fails():
514+
mock_session = Session(
515+
session_id="session-1",
516+
runs=[
517+
SessionRun(
518+
run_id="run-1",
519+
query="q",
520+
source_urls=[],
521+
report="",
522+
created_at="2026",
523+
langfuse_trace_id="trace-1",
524+
)
525+
],
526+
conversation=[],
527+
created_at="2026",
528+
)
529+
530+
with (
531+
patch("src.api.routers.sessions.get_session", new=AsyncMock(return_value=mock_session)),
532+
patch(
533+
"src.api.routers.sessions.submit_user_feedback_score",
534+
side_effect=RuntimeError("LangFuse unavailable"),
535+
),
536+
patch(
537+
"src.api.routers.sessions.update_session_run", new=AsyncMock(return_value=True)
538+
) as mock_update,
539+
):
540+
response = client.post(
541+
"/sessions/session-1/runs/run-1/feedback",
542+
json={"helpful": True},
543+
)
544+
545+
assert response.status_code == 200
546+
assert mock_update.await_args.kwargs["patch"]["feedback_helpful"] is True
547+
548+
484549
def test_submit_run_feedback_rejects_duplicate_submission():
485550
mock_session = Session(
486551
session_id="session-1",

0 commit comments

Comments
 (0)