Appearance
AI Observability
All AI calls in FieldFlo are automatically traced to Langfuse. No extra setup is needed — observability is on by default for every request.
What's captured by default
Every agent call is traced automatically:
- Agent name (derived from class name)
- Module tag (derived from namespace — e.g.
onboarding,crm) - Model name, token usage, start/end time
- Input and output text
Adding metadata to an agent
Implement CollectsMetadataInterface on your agent and use the ProvidesMetadataTrait trait. This gives you setMetadata() and addMetadata() methods from NeuronAI's HasMetadata.
php
use App\Modules\AI\CollectsMetadataInterface;
use App\Modules\AI\ProvidesMetadataTrait;
use NeuronAI\Agent\Agent;
class MyAgent extends Agent implements CollectsMetadataInterface
{
use ProvidesMetadataTrait;
// ...
}Set context before running the agent:
php
$container = Container::get();
$agent = $container->make(MyAgent::class)
->setMetadata([
'company_uuid' => $companyUUID,
'work_types' => 'Demolition, Abatement',
]);These fields appear on the Langfuse trace under metadata.
Reserved metadata keys
Three keys on the agent have special meaning and are extracted before reaching Langfuse:
| Key | Type | Effect |
|---|---|---|
langfuse_user_id | string | Sets the Langfuse userId field |
langfuse_tags | array | Adds extra tags to the trace |
langfuse_input | mixed | Overrides the trace input — useful when the real message is binary (e.g. a base64 PDF) |
php
$agent->setMetadata([
'proposalId' => $id,
'langfuse_user_id' => $companyUUID,
'langfuse_tags' => ['tenant:acme'],
'langfuse_input' => ['filename' => 'estimate.pdf', 'sizeBytes' => 204800],
]);LLM-as-a-judge scoring
Use JudgeScorer to evaluate an agent's output with a second judge agent. The score is linked to the main agent's Langfuse trace automatically. Judge agents do not produce their own trace.
Judge agents must extend App\Modules\AI\JudgeAgent:
php
use App\Modules\AI\JudgeAgent;
use NeuronAI\Providers\AIProviderInterface;
use NeuronAI\Providers\OpenAI\OpenAI;
class MyJudgeAgent extends JudgeAgent
{
protected function provider(): AIProviderInterface
{
return new OpenAI(key: $this->openaiApiKey, model: 'gpt-4o-mini');
}
protected function instructions(): string
{
return 'Evaluate the output and respond with ONLY valid JSON: {"score": 0.0-1.0, "comment": "..."}';
}
}Then call JudgeScorer after the main agent finishes:
php
use App\Modules\AI\Observability\JudgeScorer;
JudgeScorer::create()->score(
judgeAgent: $container->make(MyJudgeAgent::class),
context: $prompt,
output: $result,
scoreName: 'recommendation-quality',
);Scoring failures are caught and logged — they never interrupt the main flow.
If the real input is binary (e.g. an image), use judgeInput to override what the judge sees:
php
JudgeScorer::create()->score(
judgeAgent: $container->make(MyJudgeAgent::class),
context: $prompt,
output: $result,
scoreName: 'output-quality',
judgeInput: 'Describe what you see in this logo image.',
);Implementation details
LangfuseObserver is registered globally via NeuronAI's EventBus when the DI container is first built (Container::get()). It listens to inference-start, inference-stop, and agent-error events emitted by every agent call in the request.
On inference-stop the observer:
- Calls
$agent->collectMetadata()if the agent implementsCollectsMetadataInterface - Extracts the reserved keys (
langfuse_user_id,langfuse_tags,langfuse_input) - POSTs a trace + generation to Langfuse via
/api/public/ingestion
Because registration happens at bootstrap, observability works even if no metadata is set — the trace still captures agent name, module, model, tokens, and timing.