It would be nice if we could also use the feedback system that's available in Langfuse

Issue fork langfuse-3548465

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

nielsaers created an issue. See original summary.

abhisekmazumdar’s picture

What's needed to make this work

ai_answers needs this: score a Langfuse trace from feedback submitted after the fact. Wiring it up against a real, self-hosted Langfuse v4 instance surfaced two blockers, both reproduced live, not guessed. Here's what needs to land:

1. Add createScore() to LangFuseClient

The natural call a consumer makes is:

$langfuseClient->createScore($traceId, $name, $value, $comment);

No such method exists: not on Drupal\langfuse\LangFuseClient, not on LangFuseClientInterface, not on the underlying dropsolid/langfuse-php-sdk. Calling it throws:

Error: Call to undefined method Drupal\langfuse\LangFuseClient::createScore()

2. It has to work without a live local Trace object

The obvious implementation, $client->getTrace($traceId)->score(...), doesn't hold up in practice. Feedback normally arrives in a separate, later request — the user reads the answer, then clicks a rating. By then the trace is gone from local state: LangfuseSyncSubscriber::onKernelTerminate() unconditionally clears langfuse_current_trace_id at the end of every request, and (outside CLI) LangFuseClient::syncTraces() also deletes the whole langfuse_active_traces State cache once shouldSyncNow() allows it. I reproduced this directly: wiped the same State keys the sync subscriber wipes, then confirmed getTrace($traceId) returns NULL afterward. So createScore() can't be built on getTrace() at all — it needs its own path.

The fix, verified live

Client::sendEvent() doesn't need a live local Trace object — a score-create event only needs traceId as a string:

{
  "id": "...",
  "timestamp": "...",
  "type": "score-create",
  "body": {
    "id": "...",
    "traceId": "...",
    "name": "...",
    "value": ...,
    "comment": "...",
    "timestamp": "..."
  }
}

I built this event by hand and called $client->sendEvent(['batch' => [$event]]) directly against a trace whose local State cache I had already wiped (simulating the real cross-request case). It landed correctly — confirmed in ClickHouse's scores table (trace_id, name, value, comment all correct) and in the Langfuse UI.

So, concretely, this needs:

createScore(string $traceId, string $name, float $value, ?string $comment = null): void

on LangFuseClient/LangFuseClientInterface, building this event directly and calling sendEvent() — no getTrace()/getCurrentTrace() involved. Once that lands, ai_answers's feedback flow (thumbs up/down on an answer) needs no changes on its side to start working — it already calls createScore() with exactly this signature; it's just calling a method that doesn't exist yet.

Related: self-hosted v4 compatibility (context, not part of this ask)

Getting a self-hosted Langfuse v4 instance to accept the SDK's ingestion events at all (a prerequisite for hitting the bug above) required setting LANGFUSE_MIGRATION_V4_WRITE_MODE=legacy and LANGFUSE_MIGRATION_V4_NATIVE_OTEL_BEHAVIOUR=dual_write. The default events_only mode rejects trace-create outright ("Event type not accepted"), and Langfuse's own v4 source marks the legacy routes as a deprecated migration-window shim, not the target architecture. The real v4-native path is OTLP via /api/public/otel/v1/traces, which dropsolid/langfuse-php-sdk has no support for at all — no OTel dependency anywhere in its composer.json. Not asking for an OTel rewrite here, just flagging how fragile the current self-hosted-v4 story is for anyone else hitting this.

Related: #3561460: Force deepchat question/reply to be 1 trace

Same root cause as the deepchat multi-trace problem in #3561460: Force deepchat question/reply to be 1 trace — the module assumes one Drupal request equals one trace lifecycle. This surfaces the same assumption through a different door (feedback-after-the-fact instead of multi-ajax). The createScore() fix above is deliberately scoped to avoid needing to solve that bigger problem: treating a score as a stateless event keyed by trace ID sidesteps the lifecycle question entirely for this case.

Next I will try to submit an MR with the createScore() method plus a test.

abhisekmazumdar’s picture

Status: Active » Needs review

What this does

Adds createScore() to LangFuseClient/LangFuseClientInterface, plus an optional langfuse_feedback submodule with ready-made thumbs-up/down buttons.

Why

There was no way to submit a feedback score for a trace. getTrace($traceId)->score(...) doesn't work in practice, since feedback usually arrives in a later request after the trace's local state is already gone. createScore() avoids this: it sends a score-create event keyed only by trace ID, no live local trace needed. Verified against a real Langfuse instance.

Credit to Niels for the groundwork on the submodule: the JS, template, routing, and controller started from his commit on this issue's fork. Adapted it to use createScore(), added a block plugin, and fixed the CSRF handling.

How to test

  1. Point langfuse.settings at a real Langfuse instance. ddev-langfuse gives you one locally.
  2. Create a trace, then call createScore($traceId, 'test', 1.0) and confirm the score shows up in Langfuse.
  3. Optionally, enable langfuse_feedback and try the "Langfuse Feedback" block or its /langfuse/feedback route directly.
nikro’s picture

Assigned: Unassigned » nikro
nikro’s picture

Went through the langfuse_feedback submodule properly and pushed a follow-up commit - with practical testing against an instance.

Pulled the Block plugin entirely. It can only ever render when something explicitly maps a trace_id context (Layout Builder, or hand-written code) - plain block placement can never do that, and its own fallback (the request's "current trace") doesn't survive the common case where the trace was created in an earlier, separate request. Tried placing it myself and got nothing, for exactly this reason. The theme hook underneath it already renders standalone with no block involved, so nothing is lost by dropping it.

Normalized the hardcoded score name from user-feedback to user_feedback, added value validation (it took anything before, now it's numeric and 0-1), and switched catch (\Exception) to catch (\Throwable) in the controller so a \TypeError doesn't slip past as an uncaught fatal instead of the 502 it's supposed to return.

Bigger one: the endpoint had zero abuse protection — no rate limit, no ownership check on the trace id, reachable anonymously. Added flood control (core's flood service, no new dependency) at 20 submissions/hour. Then for repeat votes from the same person on the same trace, gave createScore() an optional $id param and derive it deterministically from identity + trace + score name. Confirmed live: sending the same id twice upserts the score rather than duplicating it, so changing your vote updates the existing score instead of piling up a new one each time.

Also wrapped the button labels for translation, and added a real test file for the controller — flood limiting, validation, idempotent score ids, the Throwable fix — since there weren't any tests for this part before.

Everything's on the branch as a follow-up commit on top of the existing one. Let me know if you'd rather I split the block removal out separately.

nikro’s picture

Assigned: nikro » Unassigned

Abhisek - can you run a small test and check the diff and see - if you agree with my changes, I'll merge tmw.

abhisekmazumdar’s picture

Assigned: Unassigned » abhisekmazumdar
abhisekmazumdar’s picture

Assigned: abhisekmazumdar » Unassigned
Status: Needs review » Reviewed & tested by the community

I agree with all of it. Thank you.
Just AI highlighted a cspell error, which added a new commit.

Rest phpstan error are all existing.....

nikro’s picture

Status: Reviewed & tested by the community » Fixed

Merged, thank you! :)

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.