Skip to content

fix(llma): extract pydantic-ai tool calls from output parts#59755

Merged
carlos-marchal-ph merged 3 commits into
PostHog:masterfrom
shauryapednekar:shauryapednekar/fix-llma-pydantic-ai-tool-calls
May 27, 2026
Merged

fix(llma): extract pydantic-ai tool calls from output parts#59755
carlos-marchal-ph merged 3 commits into
PostHog:masterfrom
shauryapednekar:shauryapednekar/fix-llma-pydantic-ai-tool-calls

Conversation

@shauryapednekar
Copy link
Copy Markdown
Contributor

Problem

Pydantic AI's current OpenTelemetry instrumentation stores generation output in gen_ai.output.messages, which PostHog maps to $ai_output_choices. Tool calls in that output are represented as message parts with type: "tool_call" and a top-level name.

PostHog already extracts tool calls from several provider shapes, but not this Pydantic AI message-parts shape, so affected $ai_generation events do not get $ai_tools_called or $ai_tool_call_count populated.

Pydantic AI instrumentation docs: https://pydantic.dev/docs/ai/api/models/instrumented/

Changes

  • Teach AI tool-call extraction to read parts[] arrays on output choices and message wrappers.
  • Extract part.name when part.type === "tool_call".
  • Reuse existing sanitization, ordering, duplicate preservation, and per-event cap behavior.
  • Add tests for direct extraction, malformed parts, stringified JSON, and the full OTel ingestion path.

How did you test this code?

I tested this locally in Docker.

pnpm --filter=@posthog/nodejs format:check
pnpm --filter=@posthog/nodejs lint
pnpm --filter=@posthog/nodejs build

All passed.

pnpm exec jest --runInBand --forceExit \
  src/ingestion/ai/tools/extract-tool-calls.test.ts \
  src/ingestion/ai/otel/attribute-mapping.test.ts \
  src/ingestion/ai/process-ai-event.test.ts

Result: 3 suites passed, 288 tests passed.

git diff --check

Passed.

Publish to changelog?

Do not publish to changelog.

Docs update

No docs update. This is a backend ingestion compatibility fix for an already documented tools normalization field.

🤖 Agent context

I used Codex to help me with this PR.

Codex helped inspect the existing tool-call extractor patterns, implement the narrow Pydantic AI parts[].type === "tool_call" extraction path, and add focused tests.

The implementation stays local to the existing extractor and reuses the current sanitizer, ordering, duplicate preservation, and count behavior. The added tests cover wrapped and unwrapped output shapes, malformed parts, mixed text/tool parts, stringified JSON, and the full OTel ingestion path.

Teach tool-call extraction to read Pydantic AI OTel output message
parts. Pydantic AI emits assistant tool calls as parts with
type="tool_call" and a top-level name; normalize those into the
existing $ai_tools_called and $ai_tool_call_count fields.

Add extractor coverage for single, multiple, mixed, wrapped, malformed,
and stringified JSON shapes, plus a processAiEvent OTel ingestion test.
@assign-reviewers-posthog assign-reviewers-posthog Bot requested review from a team May 23, 2026 00:11
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 23, 2026

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
nodejs/src/ingestion/ai/tools/extract-tool-calls.test.ts:681-709
This is the only new test in `processAiToolCallExtraction` that isn't parameterised. Given that the team always prefers parameterised tests, this test (along with the existing non-parameterised neighbours) could be rolled into an `it.each` table in the same style used for `extractToolCallNames` above, making it easy to add further provider shapes later.

```suggestion
    it.each([
        [
            'Pydantic AI OTel tool_call parts from stringified JSON',
            JSON.stringify([
                {
                    role: 'assistant',
                    parts: [
                        { type: 'text', content: 'Let me check.' },
                        { type: 'tool_call', id: 'call_abc', name: 'get_weather', arguments: '{"city":"NYC"}' },
                        { type: 'tool_call', id: 'call_def', name: 'search_docs', arguments: '{"q":"weather"}' },
                    ],
                },
            ]),
            'get_weather,search_docs',
            2,
        ],
    ])('%s', (_description, outputChoices, expectedToolsCalled, expectedCount) => {
        const event = createEvent('$ai_generation', { $ai_output_choices: outputChoices })
        const result = processAiToolCallExtraction(event)
        expect(result.properties!['$ai_tools_called']).toBe(expectedToolsCalled)
        expect(result.properties!['$ai_tool_call_count']).toBe(expectedCount)
    })
```

Reviews (1): Last reviewed commit: "fix(llma): extract pydantic-ai tool call..." | Re-trigger Greptile

Comment thread nodejs/src/ingestion/ai/tools/extract-tool-calls.test.ts
Address review feedback by converting the Pydantic AI
processAiToolCallExtraction case to an it.each table.
Copy link
Copy Markdown
Contributor

@carlos-marchal-ph carlos-marchal-ph left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, thanks for the contribution! I'm gonna go ahead with the test change I suggested and then merge it.

Comment on lines +681 to +706
it.each([
[
'Pydantic AI OTel tool_call parts from stringified JSON',
JSON.stringify([
{
role: 'assistant',
parts: [
{ type: 'text', content: 'Let me check.' },
{
type: 'tool_call',
id: 'call_abc',
name: 'get_weather',
arguments: '{"city":"NYC"}',
},
{
type: 'tool_call',
id: 'call_def',
name: 'search_docs',
arguments: '{"q":"weather"}',
},
],
},
]),
'get_weather,search_docs',
2,
],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to make this a tabular test since it has a single input

@carlos-marchal-ph carlos-marchal-ph merged commit 42a7cf3 into PostHog:master May 27, 2026
143 checks passed
@deployment-status-posthog
Copy link
Copy Markdown

deployment-status-posthog Bot commented May 27, 2026

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-05-27 10:09 UTC Run
prod-us ✅ Deployed 2026-05-27 10:20 UTC Run
prod-eu ✅ Deployed 2026-05-27 10:24 UTC Run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants