@@ -29,8 +29,14 @@ const {
2929 INTEGRATION ,
3030 DECORATOR ,
3131 PROPAGATED_ML_APP_KEY ,
32+ DEFAULT_PROMPT_NAME ,
33+ INTERNAL_CONTEXT_VARIABLE_KEYS ,
34+ INTERNAL_QUERY_VARIABLE_KEYS ,
35+ INPUT_PROMPT ,
3236 ROUTING_API_KEY ,
33- ROUTING_SITE
37+ ROUTING_SITE ,
38+ PROMPT_TRACKING_INSTRUMENTATION_METHOD ,
39+ INSTRUMENTATION_METHOD_ANNOTATED
3440} = require ( './constants/tags' )
3541const { storage } = require ( './storage' )
3642
@@ -113,6 +119,10 @@ class LLMObsTagger {
113119 const annotationContextName = annotationContext ?. name
114120 if ( annotationContextName ) this . _setTag ( span , NAME , annotationContextName )
115121
122+ // apply annotation context prompt
123+ const annotationContextPrompt = annotationContext ?. prompt
124+ if ( annotationContextPrompt ) this . tagPrompt ( span , annotationContextPrompt )
125+
116126 const routing = storage . getStore ( ) ?. routingContext
117127 if ( routing ) {
118128 this . _setTag ( span , ROUTING_API_KEY , routing . apiKey )
@@ -204,6 +214,160 @@ class LLMObsTagger {
204214 }
205215 }
206216
217+ /**
218+ * Tags a prompt on an LLMObs span.
219+ * @param {import('../opentracing/span') } span
220+ * @param {string | Record<string, unknown> } prompt
221+ * @param {boolean? } strictValidation
222+ * whether to validate the prompt against the strict schema, used for auto-instrumentation
223+ */
224+ tagPrompt ( span , prompt , strictValidation = false ) {
225+ const spanKind = registry . get ( span ) ?. [ SPAN_KIND ]
226+ if ( spanKind !== 'llm' ) {
227+ log . warn ( 'Dropping prompt on non-LLM span kind, annotating prompts is only supported for LLM span kinds.' )
228+ return
229+ }
230+
231+ if ( ! prompt || typeof prompt !== 'object' ) {
232+ this . #handleFailure( 'Prompt must be an object.' , 'invalid_prompt' )
233+ return
234+ }
235+
236+ const mlApp = registry . get ( span ) ?. [ ML_APP ] // this should be defined at this point
237+ const {
238+ id,
239+ version,
240+ tags,
241+ variables,
242+ template,
243+ contextVariables,
244+ queryVariables,
245+ } = prompt
246+
247+ if ( strictValidation ) {
248+ if ( id == null ) {
249+ this . #handleFailure( 'Prompt ID is required.' , 'invalid_prompt' )
250+ return
251+ }
252+
253+ if ( template == null ) {
254+ this . #handleFailure( 'Prompt template is required.' , 'invalid_prompt' )
255+ return
256+ }
257+ }
258+
259+ const finalPromptId = id ?? `${ mlApp } _${ DEFAULT_PROMPT_NAME } `
260+ const finalCtxVariablesKeys = contextVariables ?? [ 'context' ]
261+ const finalQueryVariablesKeys = queryVariables ?? [ 'question' ]
262+
263+ // validate prompt id
264+ if ( typeof finalPromptId !== 'string' ) {
265+ this . #handleFailure( 'Prompt ID must be a string.' , 'invalid_prompt' )
266+ return
267+ }
268+
269+ // validate prompt context variables keys
270+ if ( Array . isArray ( finalCtxVariablesKeys ) ) {
271+ for ( const key of finalCtxVariablesKeys ) {
272+ if ( typeof key !== 'string' ) {
273+ this . #handleFailure( 'Prompt context variables keys must be an array of strings.' , 'invalid_prompt' )
274+ return
275+ }
276+ }
277+ } else if ( finalCtxVariablesKeys ) {
278+ this . #handleFailure( 'Prompt context variables keys must be an array.' , 'invalid_prompt' )
279+ return
280+ }
281+
282+ // validate prompt query variables keys
283+ if ( Array . isArray ( finalQueryVariablesKeys ) ) {
284+ for ( const key of finalQueryVariablesKeys ) {
285+ if ( typeof key !== 'string' ) {
286+ this . #handleFailure( 'Prompt query variables keys must be an array of strings.' , 'invalid_prompt' )
287+ return
288+ }
289+ }
290+ } else if ( finalQueryVariablesKeys ) {
291+ this . #handleFailure( 'Prompt query variables keys must be an array.' , 'invalid_prompt' )
292+ return
293+ }
294+
295+ // validate prompt version
296+ if ( version && typeof version !== 'string' ) {
297+ this . #handleFailure( 'Prompt version must be a string.' , 'invalid_prompt' )
298+ return
299+ }
300+
301+ // validate prompt tags
302+ if ( tags && ( typeof tags !== 'object' || tags instanceof Map ) ) {
303+ this . #handleFailure( 'Prompt tags must be an non-Map object.' , 'invalid_prompt' )
304+ return
305+ } else if ( tags ) {
306+ for ( const [ key , value ] of Object . entries ( tags ) ) {
307+ if ( typeof key !== 'string' || typeof value !== 'string' ) {
308+ this . #handleFailure( 'Prompt tags must be an object of string key-value pairs.' , 'invalid_prompt' )
309+ return
310+ }
311+ }
312+ }
313+
314+ // validate prompt template is either string or list of messages
315+ if ( template && ! ( typeof template === 'string' || Array . isArray ( template ) ) ) {
316+ this . #handleFailure( 'Prompt template must be a string or an array of messages.' , 'invalid_prompt' )
317+ return
318+ }
319+
320+ if ( Array . isArray ( template ) ) {
321+ for ( const message of template ) {
322+ if ( typeof message !== 'object' || ! message . role || ! message . content ) {
323+ this . #handleFailure(
324+ 'Prompt chat template must be an array of objects with role and content properties.' , 'invalid_prompt'
325+ )
326+ return
327+ }
328+ }
329+ }
330+
331+ // validate variables are a string-string mapping
332+ if ( variables && ( typeof variables !== 'object' || variables instanceof Map ) ) {
333+ this . #handleFailure( 'Prompt variables must be an non-Map object.' , 'invalid_prompt' )
334+ return
335+ } else if ( variables ) {
336+ for ( const [ key , value ] of Object . entries ( variables ) ) {
337+ if ( typeof key !== 'string' || typeof value !== 'string' ) {
338+ this . #handleFailure( 'Prompt variables must be an object of string key-value pairs.' , 'invalid_prompt' )
339+ return
340+ }
341+ }
342+ }
343+
344+ let finalTemplate , finalChatTemplate
345+ if ( typeof template === 'string' ) {
346+ finalTemplate = template
347+ } else if ( Array . isArray ( template ) ) {
348+ finalChatTemplate = template . map ( message => ( { role : message . role , content : message . content } ) )
349+ }
350+
351+ const validatedPrompt = { }
352+ if ( finalPromptId ) validatedPrompt . id = finalPromptId
353+ if ( version ) validatedPrompt . version = version
354+ if ( variables ) validatedPrompt . variables = variables
355+ if ( finalTemplate ) validatedPrompt . template = finalTemplate
356+ if ( finalChatTemplate ?. length ) validatedPrompt . chat_template = finalChatTemplate
357+ if ( tags ) validatedPrompt . tags = tags
358+ if ( finalCtxVariablesKeys ) validatedPrompt [ INTERNAL_CONTEXT_VARIABLE_KEYS ] = finalCtxVariablesKeys
359+ if ( finalQueryVariablesKeys ) validatedPrompt [ INTERNAL_QUERY_VARIABLE_KEYS ] = finalQueryVariablesKeys
360+
361+ const currentPrompt = registry . get ( span ) ?. [ INPUT_PROMPT ]
362+ if ( currentPrompt ) {
363+ Object . assign ( currentPrompt , validatedPrompt )
364+ } else {
365+ this . _setTag ( span , INPUT_PROMPT , validatedPrompt )
366+ }
367+
368+ this . tagSpanTags ( span , { [ PROMPT_TRACKING_INSTRUMENTATION_METHOD ] : INSTRUMENTATION_METHOD_ANNOTATED } )
369+ }
370+
207371 changeKind ( span , newKind ) {
208372 this . _setTag ( span , SPAN_KIND , newKind )
209373 }
0 commit comments