Skip to content

Commit cf07cd8

Browse files
author
kappa90
committed
feat(ph-ai): agent artifacts schema
1 parent fc07aea commit cf07cd8

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": {
@@ -17493,6 +17529,20 @@
1749317529
"required": ["results"],
1749417530
"type": "object"
1749517531
},
17532+
"MarkdownBlock": {
17533+
"additionalProperties": false,
17534+
"properties": {
17535+
"content": {
17536+
"type": "string"
17537+
},
17538+
"type": {
17539+
"const": "markdown",
17540+
"type": "string"
17541+
}
17542+
},
17543+
"required": ["type", "content"],
17544+
"type": "object"
17545+
},
1749617546
"MarketingAnalyticsAggregatedQuery": {
1749717547
"additionalProperties": false,
1749817548
"properties": {
@@ -26214,6 +26264,26 @@
2621426264
"required": ["id", "viewed", "viewers", "recording_duration", "start_time", "end_time", "snapshot_source"],
2621526265
"type": "object"
2621626266
},
26267+
"SessionReplayBlock": {
26268+
"additionalProperties": false,
26269+
"properties": {
26270+
"session_id": {
26271+
"type": "string"
26272+
},
26273+
"timestamp_ms": {
26274+
"type": "number"
26275+
},
26276+
"title": {
26277+
"type": ["string", "null"]
26278+
},
26279+
"type": {
26280+
"const": "session_replay",
26281+
"type": "string"
26282+
}
26283+
},
26284+
"required": ["type", "session_id", "timestamp_ms"],
26285+
"type": "object"
26286+
},
2621726287
"SessionsQuery": {
2621826288
"additionalProperties": false,
2621926289
"properties": {
@@ -28747,6 +28817,49 @@
2874728817
"required": ["id", "distance"],
2874828818
"type": "object"
2874928819
},
28820+
"VisualizationArtifactContent": {
28821+
"additionalProperties": false,
28822+
"properties": {
28823+
"description": {
28824+
"type": ["string", "null"]
28825+
},
28826+
"name": {
28827+
"type": ["string", "null"]
28828+
},
28829+
"query": {
28830+
"anyOf": [
28831+
{
28832+
"$ref": "#/definitions/AssistantTrendsQuery"
28833+
},
28834+
{
28835+
"$ref": "#/definitions/AssistantFunnelsQuery"
28836+
},
28837+
{
28838+
"$ref": "#/definitions/AssistantRetentionQuery"
28839+
},
28840+
{
28841+
"$ref": "#/definitions/AssistantHogQLQuery"
28842+
}
28843+
]
28844+
}
28845+
},
28846+
"required": ["query"],
28847+
"type": "object"
28848+
},
28849+
"VisualizationBlock": {
28850+
"additionalProperties": false,
28851+
"properties": {
28852+
"artifact_id": {
28853+
"type": "string"
28854+
},
28855+
"type": {
28856+
"const": "visualization",
28857+
"type": "string"
28858+
}
28859+
},
28860+
"required": ["type", "artifact_id"],
28861+
"type": "object"
28862+
},
2875028863
"VisualizationItem": {
2875128864
"additionalProperties": false,
2875228865
"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)