Skip to main content

praxis_observability/
types.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4// Re-export TokenUsage from praxis-llm to avoid duplication
5pub use praxis_llm::TokenUsage;
6
7/// Observation data captured during node execution
8/// 
9/// Contains all input/output information needed for tracing.
10/// The structure varies based on node type (LLM vs Tool).
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct NodeObservation {
13    /// Unique identifier for this observation/span
14    pub span_id: String,
15    
16    /// Run identifier for the overall graph execution
17    pub run_id: String,
18    
19    /// Conversation/thread identifier
20    pub conversation_id: String,
21    
22    /// Node type: "llm" or "tool"
23    pub node_type: String,
24    
25    /// Timestamp when node execution started
26    pub started_at: chrono::DateTime<chrono::Utc>,
27    
28    /// Duration of node execution in milliseconds
29    pub duration_ms: u64,
30    
31    /// Input/output data specific to node type
32    pub data: NodeObservationData,
33    
34    /// Optional metadata
35    pub metadata: HashMap<String, serde_json::Value>,
36}
37
38/// Output from a node execution
39/// 
40/// Represents structured outputs that can be traced separately
41#[derive(Debug, Clone, Serialize, Deserialize)]
42#[serde(tag = "output_type", rename_all = "snake_case")]
43pub enum NodeOutput {
44    /// Reasoning output from models like GPT-5, o1
45    Reasoning {
46        /// OpenAI reasoning ID (rs_xxx)
47        id: String,
48        /// Reasoning content/summary
49        content: String,
50    },
51    /// Regular message output
52    Message {
53        /// OpenAI message ID (msg_xxx)
54        id: String,
55        /// Message content
56        content: String,
57    },
58    /// Tool calls output
59    ToolCalls {
60        /// Tool call information
61        calls: Vec<ToolCallInfo>,
62    },
63}
64
65/// Node-specific observation data
66#[derive(Debug, Clone, Serialize, Deserialize)]
67#[serde(tag = "type", rename_all = "snake_case")]
68pub enum NodeObservationData {
69    /// LLM node execution data
70    Llm {
71        /// Messages sent to the LLM (input)
72        input_messages: Vec<LangfuseMessage>,
73        
74        /// Structured outputs from the LLM (can be multiple: reasoning + message)
75        outputs: Vec<NodeOutput>,
76        
77        /// Model identifier
78        model: String,
79        
80        /// Token usage information
81        #[serde(skip_serializing_if = "Option::is_none")]
82        usage: Option<TokenUsage>,
83    },
84    
85    /// Tool node execution data
86    Tool {
87        /// Tool calls that were executed (input)
88        tool_calls: Vec<ToolCallInfo>,
89        
90        /// Results from tool executions (output)
91        tool_results: Vec<ToolResultInfo>,
92    },
93}
94
95/// Message format compatible with Langfuse
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct LangfuseMessage {
98    /// Message role: "system", "user", "assistant", "tool"
99    pub role: String,
100    
101    /// Message content
102    pub content: String,
103    
104    /// Optional message name
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub name: Option<String>,
107    
108    /// Optional tool call ID (for tool messages)
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub tool_call_id: Option<String>,
111    
112    /// Optional tool calls (for assistant messages)
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub tool_calls: Option<Vec<ToolCallInfo>>,
115}
116
117/// Tool call information
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct ToolCallInfo {
120    /// Tool call identifier
121    pub id: String,
122    
123    /// Tool name
124    pub name: String,
125    
126    /// Tool arguments as JSON
127    pub arguments: serde_json::Value,
128}
129
130/// Tool execution result
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct ToolResultInfo {
133    /// Tool call identifier this result corresponds to
134    pub tool_call_id: String,
135    
136    /// Tool name
137    pub tool_name: String,
138    
139    /// Result content
140    pub result: String,
141    
142    /// Whether the tool execution resulted in an error
143    pub is_error: bool,
144    
145    /// Execution duration in milliseconds
146    pub duration_ms: u64,
147}
148
149/// Context for managing trace and span IDs
150/// 
151/// Maintains state across the lifetime of a graph execution.
152#[derive(Debug, Clone)]
153pub struct TraceContext {
154    /// Unique trace identifier
155    pub trace_id: String,
156    
157    /// Run identifier
158    pub run_id: String,
159    
160    /// Conversation identifier
161    pub conversation_id: String,
162    
163    /// Timestamp when trace started
164    pub started_at: chrono::DateTime<chrono::Utc>,
165    
166    /// Counter for generating span IDs
167    pub span_counter: u32,
168}
169
170impl TraceContext {
171    /// Create a new trace context
172    pub fn new(run_id: String, conversation_id: String) -> Self {
173        Self {
174            trace_id: uuid::Uuid::new_v4().to_string(),
175            run_id,
176            conversation_id,
177            started_at: chrono::Utc::now(),
178            span_counter: 0,
179        }
180    }
181    
182    /// Generate a new unique span ID
183    pub fn next_span_id(&mut self) -> String {
184        self.span_counter += 1;
185        format!("{}-span-{}", self.trace_id, self.span_counter)
186    }
187}
188