Skip to content

Commit 46bd64f

Browse files
kappa90kappa90
authored andcommitted
feat(ph-ai): agent artifacts schema
1 parent 892a55c commit 46bd64f

File tree

6 files changed

+346
-0
lines changed

6 files changed

+346
-0
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import pytest
2+
from posthog.test.base import BaseTest
3+
4+
from parameterized import parameterized
5+
from pydantic import ValidationError
6+
7+
from posthog.schema import (
8+
AssistantTrendsQuery,
9+
DocumentArtifactContent,
10+
MarkdownBlock,
11+
SessionReplayBlock,
12+
VisualizationArtifactContent,
13+
VisualizationBlock,
14+
)
15+
16+
17+
class TestDocumentBlocks(BaseTest):
18+
@parameterized.expand(
19+
[
20+
("simple_markdown", "# Hello World", "# Hello World"),
21+
("empty_content", "", ""),
22+
("multiline", "Line 1\nLine 2\n**Bold**", "Line 1\nLine 2\n**Bold**"),
23+
]
24+
)
25+
def test_markdown_block_valid(self, _name: str, content: str, expected: str):
26+
block = MarkdownBlock(content=content)
27+
self.assertEqual(block.type, "markdown")
28+
self.assertEqual(block.content, expected)
29+
30+
def test_visualization_block_valid(self):
31+
block = VisualizationBlock(artifact_id="abc123")
32+
self.assertEqual(block.type, "visualization")
33+
self.assertEqual(block.artifact_id, "abc123")
34+
35+
@parameterized.expand(
36+
[
37+
("with_title", "session_123", 5000, "Event at 00:05"),
38+
("without_title", "session_456", 0, None),
39+
("large_timestamp", "session_789", 3600000, None),
40+
]
41+
)
42+
def test_session_replay_block_valid(self, _name: str, session_id: str, timestamp_ms: int, title: str | None):
43+
block = SessionReplayBlock(session_id=session_id, timestamp_ms=timestamp_ms, title=title)
44+
self.assertEqual(block.type, "session_replay")
45+
self.assertEqual(block.session_id, session_id)
46+
self.assertEqual(block.timestamp_ms, timestamp_ms)
47+
self.assertEqual(block.title, title)
48+
49+
def test_session_replay_block_zero_timestamp_valid(self):
50+
# Timestamp validation happens at the application level, not schema level
51+
# since TypeScript schemas don't support numeric constraints
52+
block = SessionReplayBlock(session_id="session_123", timestamp_ms=0)
53+
self.assertEqual(block.timestamp_ms, 0)
54+
55+
56+
class TestDocumentArtifactContent(BaseTest):
57+
def test_empty_blocks(self):
58+
content = DocumentArtifactContent(blocks=[])
59+
self.assertEqual(content.blocks, [])
60+
61+
def test_mixed_blocks(self):
62+
blocks = [
63+
{"type": "markdown", "content": "# Introduction"},
64+
{"type": "visualization", "artifact_id": "vis123"},
65+
{"type": "session_replay", "session_id": "sess456", "timestamp_ms": 1000, "title": "Example"},
66+
{"type": "markdown", "content": "## Summary"},
67+
]
68+
content = DocumentArtifactContent(blocks=blocks)
69+
70+
self.assertEqual(len(content.blocks), 4)
71+
self.assertIsInstance(content.blocks[0], MarkdownBlock)
72+
self.assertIsInstance(content.blocks[1], VisualizationBlock)
73+
self.assertIsInstance(content.blocks[2], SessionReplayBlock)
74+
self.assertIsInstance(content.blocks[3], MarkdownBlock)
75+
76+
def test_invalid_block_type(self):
77+
with pytest.raises(ValidationError):
78+
DocumentArtifactContent(blocks=[{"type": "invalid", "content": "test"}])
79+
80+
def test_serialization_round_trip(self):
81+
original = DocumentArtifactContent(
82+
blocks=[
83+
MarkdownBlock(content="# Title"),
84+
VisualizationBlock(artifact_id="abc123"),
85+
SessionReplayBlock(session_id="sess", timestamp_ms=5000, title="Test"),
86+
]
87+
)
88+
serialized = original.model_dump()
89+
deserialized = DocumentArtifactContent.model_validate(serialized)
90+
91+
self.assertEqual(len(deserialized.blocks), 3)
92+
block0 = deserialized.blocks[0]
93+
block1 = deserialized.blocks[1]
94+
block2 = deserialized.blocks[2]
95+
assert isinstance(block0, MarkdownBlock)
96+
assert isinstance(block1, VisualizationBlock)
97+
assert isinstance(block2, SessionReplayBlock)
98+
self.assertEqual(block0.content, "# Title")
99+
self.assertEqual(block1.artifact_id, "abc123")
100+
self.assertEqual(block2.session_id, "sess")
101+
102+
103+
class TestVisualizationArtifactContent(BaseTest):
104+
def test_trends_query(self):
105+
trends = AssistantTrendsQuery(series=[])
106+
content = VisualizationArtifactContent(query=trends, name="Test Trends", description="Shows trend data")
107+
108+
self.assertEqual(content.query, trends)
109+
self.assertEqual(content.name, "Test Trends")
110+
self.assertEqual(content.description, "Shows trend data")
111+
112+
def test_minimal_content(self):
113+
trends = AssistantTrendsQuery(series=[])
114+
content = VisualizationArtifactContent(query=trends)
115+
116+
self.assertEqual(content.query, trends)
117+
self.assertIsNone(content.name)
118+
self.assertIsNone(content.description)
119+
120+
def test_serialization_round_trip(self):
121+
original = VisualizationArtifactContent(
122+
query=AssistantTrendsQuery(series=[]),
123+
name="My Chart",
124+
description="Chart description",
125+
)
126+
serialized = original.model_dump()
127+
deserialized = VisualizationArtifactContent.model_validate(serialized)
128+
129+
self.assertEqual(deserialized.name, "My Chart")
130+
self.assertEqual(deserialized.description, "Chart description")
131+
self.assertIsNotNone(deserialized.query)

frontend/src/queries/schema.json

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,16 @@
343343
"required": ["columns", "hogql", "limit", "offset", "results"],
344344
"type": "object"
345345
},
346+
"AgentArtifactContent": {
347+
"anyOf": [
348+
{
349+
"$ref": "#/definitions/DocumentArtifactContent"
350+
},
351+
{
352+
"$ref": "#/definitions/VisualizationArtifactContent"
353+
}
354+
]
355+
},
346356
"AgentMode": {
347357
"enum": ["product_analytics", "sql", "session_replay"],
348358
"type": "string"
@@ -10731,6 +10741,32 @@
1073110741
],
1073210742
"type": "string"
1073310743
},
10744+
"DocumentArtifactContent": {
10745+
"additionalProperties": false,
10746+
"properties": {
10747+
"blocks": {
10748+
"items": {
10749+
"$ref": "#/definitions/DocumentBlock"
10750+
},
10751+
"type": "array"
10752+
}
10753+
},
10754+
"required": ["blocks"],
10755+
"type": "object"
10756+
},
10757+
"DocumentBlock": {
10758+
"anyOf": [
10759+
{
10760+
"$ref": "#/definitions/MarkdownBlock"
10761+
},
10762+
{
10763+
"$ref": "#/definitions/VisualizationBlock"
10764+
},
10765+
{
10766+
"$ref": "#/definitions/SessionReplayBlock"
10767+
}
10768+
]
10769+
},
1073410770
"DocumentSimilarityQuery": {
1073510771
"additionalProperties": false,
1073610772
"properties": {
@@ -17490,6 +17526,20 @@
1749017526
"required": ["results"],
1749117527
"type": "object"
1749217528
},
17529+
"MarkdownBlock": {
17530+
"additionalProperties": false,
17531+
"properties": {
17532+
"content": {
17533+
"type": "string"
17534+
},
17535+
"type": {
17536+
"const": "markdown",
17537+
"type": "string"
17538+
}
17539+
},
17540+
"required": ["type", "content"],
17541+
"type": "object"
17542+
},
1749317543
"MarketingAnalyticsAggregatedQuery": {
1749417544
"additionalProperties": false,
1749517545
"properties": {
@@ -26265,6 +26315,26 @@
2626526315
"required": ["id", "viewed", "viewers", "recording_duration", "start_time", "end_time", "snapshot_source"],
2626626316
"type": "object"
2626726317
},
26318+
"SessionReplayBlock": {
26319+
"additionalProperties": false,
26320+
"properties": {
26321+
"session_id": {
26322+
"type": "string"
26323+
},
26324+
"timestamp_ms": {
26325+
"type": "number"
26326+
},
26327+
"title": {
26328+
"type": ["string", "null"]
26329+
},
26330+
"type": {
26331+
"const": "session_replay",
26332+
"type": "string"
26333+
}
26334+
},
26335+
"required": ["type", "session_id", "timestamp_ms"],
26336+
"type": "object"
26337+
},
2626826338
"SessionsQuery": {
2626926339
"additionalProperties": false,
2627026340
"properties": {
@@ -28813,6 +28883,49 @@
2881328883
"required": ["id", "distance"],
2881428884
"type": "object"
2881528885
},
28886+
"VisualizationArtifactContent": {
28887+
"additionalProperties": false,
28888+
"properties": {
28889+
"description": {
28890+
"type": ["string", "null"]
28891+
},
28892+
"name": {
28893+
"type": ["string", "null"]
28894+
},
28895+
"query": {
28896+
"anyOf": [
28897+
{
28898+
"$ref": "#/definitions/AssistantTrendsQuery"
28899+
},
28900+
{
28901+
"$ref": "#/definitions/AssistantFunnelsQuery"
28902+
},
28903+
{
28904+
"$ref": "#/definitions/AssistantRetentionQuery"
28905+
},
28906+
{
28907+
"$ref": "#/definitions/AssistantHogQLQuery"
28908+
}
28909+
]
28910+
}
28911+
},
28912+
"required": ["query"],
28913+
"type": "object"
28914+
},
28915+
"VisualizationBlock": {
28916+
"additionalProperties": false,
28917+
"properties": {
28918+
"artifact_id": {
28919+
"type": "string"
28920+
},
28921+
"type": {
28922+
"const": "visualization",
28923+
"type": "string"
28924+
}
28925+
},
28926+
"required": ["type", "artifact_id"],
28927+
"type": "object"
28928+
},
2881628929
"VisualizationItem": {
2881728930
"additionalProperties": false,
2881828931
"properties": {

frontend/src/queries/schema/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// (even though our actual app's esbuild setup compiles perfectly well.)
66

77
// sort-imports-ignore
8+
export * from './schema-assistant-artifacts'
89
export * from './schema-assistant-messages'
910
export * from './schema-assistant-queries'
1011
export * from './schema-assistant-replay'
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Agent Artifact Types - these will be auto-generated to Python via pnpm schema:build
2+
import {
3+
AssistantFunnelsQuery,
4+
AssistantHogQLQuery,
5+
AssistantRetentionQuery,
6+
AssistantTrendsQuery,
7+
} from './schema-assistant-queries'
8+
9+
export interface MarkdownBlock {
10+
type: 'markdown'
11+
content: string
12+
}
13+
14+
export interface VisualizationBlock {
15+
type: 'visualization'
16+
artifact_id: string
17+
}
18+
19+
export interface SessionReplayBlock {
20+
type: 'session_replay'
21+
session_id: string
22+
timestamp_ms: number
23+
title?: string | null
24+
}
25+
26+
export type DocumentBlock = MarkdownBlock | VisualizationBlock | SessionReplayBlock
27+
28+
export interface DocumentArtifactContent {
29+
blocks: DocumentBlock[]
30+
}
31+
32+
export interface VisualizationArtifactContent {
33+
query: AssistantTrendsQuery | AssistantFunnelsQuery | AssistantRetentionQuery | AssistantHogQLQuery
34+
name?: string | null
35+
description?: string | null
36+
}
37+
38+
export type AgentArtifactContent = DocumentArtifactContent | VisualizationArtifactContent

frontend/src/scenes/max/maxTypes.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { AgentArtifactContent } from '~/queries/schema/schema-assistant-artifacts'
12
import { AgentMode } from '~/queries/schema/schema-assistant-messages'
23
import { DashboardFilter, HogQLVariable, QuerySchema } from '~/queries/schema/schema-general'
34
import { integer } from '~/queries/schema/type-utils'
@@ -135,3 +136,19 @@ export const createMaxContextHelpers = {
135136
export function isAgentMode(mode: unknown): mode is AgentMode {
136137
return typeof mode === 'string' && Object.values(AgentMode).includes(mode as AgentMode)
137138
}
139+
140+
export enum AgentArtifactType {
141+
VISUALIZATION = 'visualization',
142+
DOCUMENT = 'document',
143+
}
144+
145+
export interface AgentArtifact {
146+
id: string
147+
short_id: string
148+
name: string
149+
type: AgentArtifactType
150+
content: AgentArtifactContent
151+
conversation: string
152+
team: number
153+
created_at: string
154+
}

0 commit comments

Comments
 (0)