@outputai/core 0.4.1-dev.622e67b.0 → 0.4.1-dev.7aa9a5f.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,80 @@
1
+ import Decimal from 'decimal.js';
2
+
3
+ /**
4
+ * All attributes inherit from this
5
+ */
6
+ export class BaseAttribute {
7
+ activityId;
8
+ activityName;
9
+ date = Date.now();
10
+ type;
11
+
12
+ constructor( type ) {
13
+ this.type = type;
14
+ }
15
+
16
+ setActivity( id, name ) {
17
+ this.activityId = id;
18
+ this.activityName = name;
19
+ }
20
+ }
21
+
22
+ class HTTPRequestCount extends BaseAttribute {
23
+ static TYPE = 'http:request:count';
24
+ url;
25
+ requestId;
26
+
27
+ constructor( url, requestId ) {
28
+ super( HTTPRequestCount.TYPE );
29
+ this.url = url;
30
+ this.requestId = requestId;
31
+ }
32
+ }
33
+
34
+ class HTTPRequestCost extends BaseAttribute {
35
+ static TYPE = 'http:request:cost';
36
+ url;
37
+ requestId;
38
+ total = 0;
39
+
40
+ constructor( url, requestId, total ) {
41
+ super( HTTPRequestCost.TYPE );
42
+ this.url = url;
43
+ this.requestId = requestId;
44
+ this.total = total;
45
+ }
46
+ }
47
+
48
+ class LLMUsage extends BaseAttribute {
49
+ static TYPE = 'llm:usage';
50
+ modelId;
51
+ usage = [];
52
+ total = 0;
53
+ tokensUsed = 0;
54
+
55
+ constructor( modelId ) {
56
+ super( LLMUsage.TYPE );
57
+ this.modelId = modelId;
58
+ }
59
+
60
+ addUsage( { type, ppm, amount } ) {
61
+ const total = Decimal( amount ).div( 1_000_000 ).mul( ppm ).toNumber();
62
+ this.usage.push( {
63
+ type,
64
+ ppm,
65
+ amount,
66
+ total
67
+ } );
68
+ this.total = Decimal( this.total ).add( total ).toNumber();
69
+ this.tokensUsed = Decimal( this.tokensUsed ).add( amount ).toNumber();
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Types of ADD_ATTR attributes
75
+ */
76
+ export const Attribute = {
77
+ LLMUsage,
78
+ HTTPRequestCost,
79
+ HTTPRequestCount
80
+ };
@@ -4,8 +4,10 @@ import { serializeError } from './tools/utils.js';
4
4
  import { isStringboolTrue } from '#utils';
5
5
  import * as localProcessor from './processors/local/index.js';
6
6
  import * as s3Processor from './processors/s3/index.js';
7
- import { ComponentType } from '#consts';
7
+ import { ComponentType, Signal } from '#consts';
8
8
  import { createChildLogger } from '#logger';
9
+ import { EventAction } from './trace_consts.js';
10
+ import { BaseAttribute } from './trace_attribute.js';
9
11
 
10
12
  const log = createChildLogger( 'Tracing' );
11
13
 
@@ -91,7 +93,15 @@ export const addEventAction = ( action, { kind, name, id, parentId, details, exe
91
93
  export function addEventActionWithContext( action, options ) {
92
94
  const storeContent = Storage.load();
93
95
  if ( storeContent ) { // If there is no storageContext this was not called from a Temporal environment
94
- const { parentId, executionContext } = storeContent;
96
+ const { parentId, parentName, executionContext, workflowHandle } = storeContent;
97
+ if ( action === EventAction.ADD_ATTR ) {
98
+ const attribute = options.details;
99
+ if ( !( attribute instanceof BaseAttribute ) ) {
100
+ throw new Error( `${EventAction.ADD_ATTR} called argument that is not a BaseAttribute instance` );
101
+ }
102
+ attribute.setActivity( parentId, parentName );
103
+ workflowHandle.signal( Signal.ADD_ATTRIBUTE, attribute );
104
+ }
95
105
  addEventAction( action, { ...options, parentId, executionContext } );
96
106
  }
97
107
  };
@@ -69,7 +69,7 @@ const callerDir = process.argv[2];
69
69
  workflowsPath,
70
70
  activities,
71
71
  sinks,
72
- interceptors: initInterceptors( { activities, workflows } ),
72
+ interceptors: initInterceptors( { activities, workflows, connection } ),
73
73
  maxConcurrentWorkflowTaskExecutions,
74
74
  maxConcurrentActivityTaskExecutions,
75
75
  maxCachedWorkflows,
@@ -123,7 +123,7 @@ describe( 'worker/index', () => {
123
123
  maxConcurrentActivityTaskPolls: configValues.maxConcurrentActivityTaskPolls,
124
124
  maxConcurrentWorkflowTaskPolls: configValues.maxConcurrentWorkflowTaskPolls
125
125
  } ) );
126
- expect( initInterceptorsMock ).toHaveBeenCalledWith( { activities: {}, workflows: [] } );
126
+ expect( initInterceptorsMock ).toHaveBeenCalledWith( { activities: {}, workflows: [], connection: mockConnection } );
127
127
  expect( registerShutdownMock ).toHaveBeenCalledWith( { worker: mockWorker, log: mockLog } );
128
128
  expect( startCatalogMock ).toHaveBeenCalledWith( {
129
129
  connection: mockConnection,
@@ -5,6 +5,7 @@ import { headersToObject } from '../sandboxed_utils.js';
5
5
  import { BusEventType, METADATA_ACCESS_SYMBOL } from '#consts';
6
6
  import { activityHeartbeatEnabled, activityHeartbeatIntervalMs } from '../configs.js';
7
7
  import { messageBus } from '#bus';
8
+ import { Client } from '@temporalio/client';
8
9
 
9
10
  /*
10
11
  This interceptor wraps every activity execution with cross-cutting concerns:
@@ -23,7 +24,7 @@ import { messageBus } from '#bus';
23
24
  - Headers injected by the workflow interceptor (executionContext)
24
25
  */
25
26
  export class ActivityExecutionInterceptor {
26
- constructor( { activities, workflows } ) {
27
+ constructor( { activities, workflows, connection } ) {
27
28
  this.activities = activities;
28
29
  this.workflowsMap = workflows.reduce( ( map, w ) => {
29
30
  map.set( w.name, w );
@@ -32,14 +33,19 @@ export class ActivityExecutionInterceptor {
32
33
  }
33
34
  return map;
34
35
  }, new Map() );
36
+ this.connection = connection;
35
37
  };
36
38
 
37
39
  async execute( input, next ) {
38
40
  const startDate = Date.now();
41
+ const client = new Client( { connection: this.connection } );
42
+
39
43
  const { workflowExecution: { workflowId }, activityId: id, activityType: name, workflowType: workflowName } = Context.current().info;
40
44
  const { executionContext } = headersToObject( input.headers );
41
45
  const { type: kind } = this.activities?.[name]?.[METADATA_ACCESS_SYMBOL];
42
46
 
47
+ const workflowHandle = client.workflow.getHandle( workflowId );
48
+
43
49
  messageBus.emit( BusEventType.ACTIVITY_START, { id, name, kind, workflowId, workflowName } );
44
50
  Tracing.addEventStart( { id, name, kind, parentId: workflowId, details: input.args[0], executionContext } );
45
51
 
@@ -56,7 +62,8 @@ export class ActivityExecutionInterceptor {
56
62
  intervals.heartbeat = activityHeartbeatEnabled && setInterval( () => Context.current().heartbeat(), activityHeartbeatIntervalMs );
57
63
 
58
64
  // Wraps the execution with accessible metadata for the activity
59
- const output = await Storage.runWithContext( async _ => next( input ), { parentId: id, executionContext, workflowFilename } );
65
+ const ctx = { parentId: id, parentName: name, executionContext, workflowFilename, workflowHandle };
66
+ const output = await Storage.runWithContext( async _ => next( input ), ctx );
60
67
 
61
68
  messageBus.emit( BusEventType.ACTIVITY_END, { id, name, kind, workflowId, workflowName, duration: Date.now() - startDate } );
62
69
  Tracing.addEventEnd( { id, details: output, executionContext } );
@@ -2,6 +2,8 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
2
  import { BusEventType } from '#consts';
3
3
 
4
4
  const METADATA_ACCESS_SYMBOL = vi.hoisted( () => Symbol( '__metadata' ) );
5
+ const workflowHandleMock = vi.hoisted( () => ( { signal: vi.fn() } ) );
6
+ const getHandleMock = vi.hoisted( () => vi.fn( () => workflowHandleMock ) );
5
7
 
6
8
  const heartbeatMock = vi.fn();
7
9
  const runWithContextMock = vi.hoisted( () => vi.fn().mockImplementation( async fn => fn() ) );
@@ -21,6 +23,14 @@ vi.mock( '@temporalio/activity', () => ( {
21
23
  }
22
24
  } ) );
23
25
 
26
+ vi.mock( '@temporalio/client', () => ( {
27
+ Client: class Client {
28
+ workflow = {
29
+ getHandle: getHandleMock
30
+ };
31
+ }
32
+ } ) );
33
+
24
34
  vi.mock( '#async_storage', () => ( {
25
35
  Storage: {
26
36
  runWithContext: runWithContextMock
@@ -108,12 +118,15 @@ describe( 'ActivityExecutionInterceptor', () => {
108
118
  expect( addEventErrorMock ).not.toHaveBeenCalled();
109
119
  expect( runWithContextMock ).toHaveBeenCalledWith(
110
120
  expect.any( Function ),
111
- {
121
+ expect.objectContaining( {
112
122
  parentId: 'act-1',
123
+ parentName: 'myWorkflow#myStep',
113
124
  executionContext: { workflowId: 'wf-1' },
114
- workflowFilename: '/workflows/myWorkflow.js'
115
- }
125
+ workflowFilename: '/workflows/myWorkflow.js',
126
+ workflowHandle: workflowHandleMock
127
+ } )
116
128
  );
129
+ expect( getHandleMock ).toHaveBeenCalledWith( 'wf-1' );
117
130
  } );
118
131
 
119
132
  it( 'records trace error event on failed execution', async () => {
@@ -4,7 +4,7 @@ import { ActivityExecutionInterceptor } from './interceptors/activity.js';
4
4
 
5
5
  const __dirname = dirname( fileURLToPath( import.meta.url ) );
6
6
 
7
- export const initInterceptors = ( { activities, workflows } ) => ( {
7
+ export const initInterceptors = ( { activities, workflows, connection } ) => ( {
8
8
  workflowModules: [ join( __dirname, './interceptors/workflow.js' ) ],
9
- activityInbound: [ () => new ActivityExecutionInterceptor( { activities, workflows } ) ]
9
+ activityInbound: [ () => new ActivityExecutionInterceptor( { activities, workflows, connection } ) ]
10
10
  } );