2323from galileo_core .schemas .logging .llm import Message
2424from galileo_core .schemas .logging .session import Session
2525from galileo_core .schemas .logging .span import LlmSpan , RetrieverSpan , ToolSpan
26+ from galileo_core .schemas .logging .step import BaseStep
2627from galileo_core .schemas .logging .trace import Trace
2728from galileo_core .schemas .shared .content_parts import FileContentPart , TextContentPart
2829from galileo_core .schemas .shared .document import Document
@@ -48,6 +49,64 @@ def test_required_galileo_core_public_exports_are_importable() -> None:
4849 assert all ((LlmSpan , ToolSpan , RetrieverSpan , Trace , Session , Document , Message ))
4950
5051
52+ def test_factory_records_are_serializable_concrete_base_steps () -> None :
53+ # Given: one record of each type accepted by the Galileo factory
54+ records = [
55+ record_from_step (Step (type = "llm" , name = "answer" , input = "question" , output = "answer" )),
56+ record_from_step (Step (type = "tool" , name = "search" , input = {"query" : "q" }, output = "result" )),
57+ record_from_step (
58+ Step (type = "retriever" , name = "retrieve" , input = "question" , output = ["document" ])
59+ ),
60+ record_from_step (
61+ Step (
62+ type = "trace" ,
63+ name = "request" ,
64+ input = "question" ,
65+ output = "answer" ,
66+ context = {"spans" : [{"type" : "llm" , "name" : "answer" , "input" : "question" }]},
67+ )
68+ ),
69+ record_from_step (
70+ Step (
71+ type = "session" ,
72+ name = "conversation" ,
73+ input = "question" ,
74+ context = {
75+ "traces" : [
76+ {
77+ "type" : "trace" ,
78+ "name" : "request" ,
79+ "input" : "question" ,
80+ "spans" : [{"type" : "llm" , "name" : "answer" , "input" : "question" }],
81+ }
82+ ]
83+ },
84+ )
85+ ),
86+ ]
87+
88+ # When: each concrete Galileo step is serialized and parsed back through its own schema
89+ rebuilt = [
90+ type (record ).model_validate (record .model_dump (mode = "json" , exclude_none = True ))
91+ for record in records
92+ ]
93+
94+ # Then: every concrete class remains a BaseStep and keeps its discriminator and children
95+ assert all (isinstance (record , BaseStep ) for record in records )
96+ assert all (isinstance (record , BaseStep ) for record in rebuilt )
97+ assert [record .type .value for record in rebuilt ] == [
98+ "llm" ,
99+ "tool" ,
100+ "retriever" ,
101+ "trace" ,
102+ "session" ,
103+ ]
104+ assert isinstance (rebuilt [3 ], Trace )
105+ assert isinstance (rebuilt [4 ], Session )
106+ assert isinstance (rebuilt [3 ].spans [0 ], LlmSpan )
107+ assert isinstance (rebuilt [4 ].traces [0 ].spans [0 ], LlmSpan )
108+
109+
51110def test_llm_messages_use_public_canonical_validation () -> None :
52111 record = record_from_step (
53112 Step (
0 commit comments