From 9c967e3c5faaccdc07fb56f4910c456f5c1abc18 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Mon, 1 Apr 2024 22:30:21 +0200 Subject: [PATCH] fix(sdk): allow passing a function to the decorator --- packages/sample-app/src/sample_decorators.ts | 4 +- .../recording.har | 474 ++++++++++++++++++ .../src/lib/tracing/decorators.ts | 39 +- .../traceloop-sdk/test/decorators.test.ts | 93 +++- 4 files changed, 598 insertions(+), 12 deletions(-) create mode 100644 packages/traceloop-sdk/recordings/Test-SDK-Decorators_847855269/should-create-spans-for-workflows-using-decoration-syntax-method-variant_2462514347/recording.har diff --git a/packages/sample-app/src/sample_decorators.ts b/packages/sample-app/src/sample_decorators.ts index b252b032..235048df 100644 --- a/packages/sample-app/src/sample_decorators.ts +++ b/packages/sample-app/src/sample_decorators.ts @@ -23,7 +23,9 @@ class SampleOpenAI { return chatCompletion.choices[0].message.content; } - @traceloop.workflow({ name: "sample_completion" }) + @traceloop.workflow((thisArg) => ({ + name: `sample_${(thisArg as SampleOpenAI).model}`, + })) async completion(jokeSubject: string) { const completion = await openai.completions.create({ prompt: `Tell me a joke about ${jokeSubject}`, diff --git a/packages/traceloop-sdk/recordings/Test-SDK-Decorators_847855269/should-create-spans-for-workflows-using-decoration-syntax-method-variant_2462514347/recording.har b/packages/traceloop-sdk/recordings/Test-SDK-Decorators_847855269/should-create-spans-for-workflows-using-decoration-syntax-method-variant_2462514347/recording.har new file mode 100644 index 00000000..b6eef379 --- /dev/null +++ b/packages/traceloop-sdk/recordings/Test-SDK-Decorators_847855269/should-create-spans-for-workflows-using-decoration-syntax-method-variant_2462514347/recording.har @@ -0,0 +1,474 @@ +{ + "log": { + "_recordingName": "Test SDK Decorators/should create spans for workflows using decoration syntax, method variant", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "80b48c70cae76a3c38b9af59d5273e33", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 139, + "cookies": [], + "headers": [ + { + "_fromType": "array", + "name": "content-length", + "value": "139" + }, + { + "_fromType": "array", + "name": "accept", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "content-type", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "user-agent", + "value": "OpenAI/JS 4.31.0" + }, + { + "_fromType": "array", + "name": "x-stainless-lang", + "value": "js" + }, + { + "_fromType": "array", + "name": "x-stainless-package-version", + "value": "4.31.0" + }, + { + "_fromType": "array", + "name": "x-stainless-os", + "value": "MacOS" + }, + { + "_fromType": "array", + "name": "x-stainless-arch", + "value": "arm64" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime", + "value": "node" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime-version", + "value": "v18.17.1" + }, + { + "_fromType": "array", + "name": "accept-encoding", + "value": "gzip,deflate" + }, + { + "name": "host", + "value": "api.openai.com" + } + ], + "headersSize": 470, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"Tell me a joke about OpenTelemetry\"\n }\n ],\n \"model\": \"gpt-3.5-turbo\"\n}" + }, + "queryString": [], + "url": "https://api.openai.com/v1/chat/completions" + }, + "response": { + "bodySize": 503, + "content": { + "encoding": "base64", + "mimeType": "application/json", + "size": 503, + "text": "[\"H4sIAAAAAAAAA1SRQU/DMAyF7/0VJucNrZQJugsSTNoBISSYhARCU5q6bVgah8SbKGj/HaXrNrjk8D4/59n+SQCELsUMhGokq9aZcZ4vlq/zy/nD4vnzrto+Le633y+TT50t+TEVo+ig4gMVH1znilpnkDXZPVYeJWPsml6laZ5PszTrQUslmmirHQ==\",\"j7Pz6Zg3vqDxJL2YDs6GtMIgZvCWAAD89G/MaEv8EjOYjA5KiyHIGsXsWAQgPJmoCBmCDiwti9EJKrKMto/90nRQ6hK4QXh0aJdosEX2HZS4RUMOPdQEhac13sAtKrkJGKs7WKNjYN9pWwMTsJeqJ9oDfjm0AcOZGD7dHdMaqp2nIk5mN8Yc9UpbHZqVRxnIxmSBye3tuwTgvd/K5t+gwnlqHa+Y1mhjw3S6bydOd/gD8wEysTQnPbtMhnwidIGxXVXa1uid1/sVVW5VXFwXWV5V8loku+QXAAD//wMAlSw4UiwCAAA=\"]" + }, + "cookies": [ + { + "domain": ".api.openai.com", + "expires": "2024-04-01T18:45:14.000Z", + "httpOnly": true, + "name": "__cf_bm", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "el0TjNiEA29RkNHrQiz9TQ3Z13j5aMF8VBFh3vn_Vh0-1711995314-1.0.1.1-Ja38pkbP2hCTznBXMDgsUoQApO0OyH_GXB_z_EOdJ79j5RhIAbN2JEizy7l3K9YdKoZKnARfTSwWZltzuOqeUg" + }, + { + "domain": ".api.openai.com", + "httpOnly": true, + "name": "_cfuvid", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "yuY.1SC0F6Gj8Z2YJV0aVAO2uc9czwx4VbCeAK8nm94-1711995314155-0.0.1.1-604800000" + } + ], + "headers": [ + { + "name": "date", + "value": "Mon, 01 Apr 2024 18:15:14 GMT" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "transfer-encoding", + "value": "chunked" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "access-control-allow-origin", + "value": "*" + }, + { + "name": "cache-control", + "value": "no-cache, must-revalidate" + }, + { + "name": "openai-model", + "value": "gpt-3.5-turbo-0125" + }, + { + "name": "openai-organization", + "value": "traceloop" + }, + { + "name": "openai-processing-ms", + "value": "374" + }, + { + "name": "openai-version", + "value": "2020-10-01" + }, + { + "name": "strict-transport-security", + "value": "max-age=15724800; includeSubDomains" + }, + { + "name": "x-ratelimit-limit-requests", + "value": "5000" + }, + { + "name": "x-ratelimit-limit-tokens", + "value": "160000" + }, + { + "name": "x-ratelimit-remaining-requests", + "value": "4999" + }, + { + "name": "x-ratelimit-remaining-tokens", + "value": "159974" + }, + { + "name": "x-ratelimit-reset-requests", + "value": "12ms" + }, + { + "name": "x-ratelimit-reset-tokens", + "value": "9ms" + }, + { + "name": "x-request-id", + "value": "req_fd02054dc83b27236e9f9b19ab52205e" + }, + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "__cf_bm=el0TjNiEA29RkNHrQiz9TQ3Z13j5aMF8VBFh3vn_Vh0-1711995314-1.0.1.1-Ja38pkbP2hCTznBXMDgsUoQApO0OyH_GXB_z_EOdJ79j5RhIAbN2JEizy7l3K9YdKoZKnARfTSwWZltzuOqeUg; path=/; expires=Mon, 01-Apr-24 18:45:14 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "_cfuvid=yuY.1SC0F6Gj8Z2YJV0aVAO2uc9czwx4VbCeAK8nm94-1711995314155-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "cf-ray", + "value": "86da9035ebdd526c-MXP" + }, + { + "name": "content-encoding", + "value": "gzip" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=86400" + } + ], + "headersSize": 1208, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2024-04-01T18:15:13.454Z", + "time": 643, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 643 + } + }, + { + "_id": "d4be3eb45f9e267d7e2a1694e080fc01", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 136, + "cookies": [], + "headers": [ + { + "_fromType": "array", + "name": "content-length", + "value": "136" + }, + { + "_fromType": "array", + "name": "accept", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "content-type", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "user-agent", + "value": "OpenAI/JS 4.31.0" + }, + { + "_fromType": "array", + "name": "x-stainless-lang", + "value": "js" + }, + { + "_fromType": "array", + "name": "x-stainless-package-version", + "value": "4.31.0" + }, + { + "_fromType": "array", + "name": "x-stainless-os", + "value": "MacOS" + }, + { + "_fromType": "array", + "name": "x-stainless-arch", + "value": "arm64" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime", + "value": "node" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime-version", + "value": "v18.17.1" + }, + { + "_fromType": "array", + "name": "accept-encoding", + "value": "gzip,deflate" + }, + { + "name": "host", + "value": "api.openai.com" + } + ], + "headersSize": 470, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"Tell me a fact about JavaScript\"\n }\n ],\n \"model\": \"gpt-3.5-turbo\"\n}" + }, + "queryString": [], + "url": "https://api.openai.com/v1/chat/completions" + }, + "response": { + "bodySize": 531, + "content": { + "encoding": "base64", + "mimeType": "application/json", + "size": 531, + "text": "[\"H4sIAAAAAAAAA1RRwW7bMAy9+ysInZOgjuMFzrWHFgN2ag/FhiGgZcbWKomaRDsLivz7ICdN0AsP7/E9PD5+FADKdGoHSg8o2gW7bJqnV9zIcd08vz5O2x9B3tbPXG+69+HvT7XICm7/kJZP1UqzC5bEsL/QOhIKZddyW5ZNU1flZiYcd2SzrA+yrA==\",\"VvVSxtjy8qFc11flwEZTUjv4VQAAfMwzZ/Qd/VM7eFh8Io5Swp7U7rYEoCLbjChMySRBL2pxJzV7IT/H/o4TvuhogoBJgNCdPDqjAX0HE8WEYixBiNxHdM74Hiz6fsSeQAacRZqdY29PMCbq4MAROprIcsjbxgtF1GImgiO1yQil2fxILWAI1mjMfaWVuuY73w6z3IfIbS7Bj9be8IPxJg37SJjY5yOScLjIzwXA77nA8UsnKkR2QfbC7+SzYVld7NT9ZXdyfS1XCQvaO15VxTWfSqck5PYH43uKIZpLm4ewr1pdtvV2802r4lz8BwAA//8DACcoU1VXAgAA\"]" + }, + "cookies": [ + { + "domain": ".api.openai.com", + "expires": "2024-04-01T18:45:15.000Z", + "httpOnly": true, + "name": "__cf_bm", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "kkiqo0V181NAAuzedVm.6yB17bDBWXgUYPlNhUkNwCw-1711995315-1.0.1.1-oRrPe2q6gsBUoe4vYhX9Cto2xghC5VQxLvYNUQsF4YfdZS9Qed8HI1m1S.CgEdazfLcsPwNXlHeGmYLFDrn2zA" + }, + { + "domain": ".api.openai.com", + "httpOnly": true, + "name": "_cfuvid", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "5SFZD5OCyvcmUW858m9frjLg0enwmRHxE93Xx0WmkFw-1711995315142-0.0.1.1-604800000" + } + ], + "headers": [ + { + "name": "date", + "value": "Mon, 01 Apr 2024 18:15:15 GMT" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "transfer-encoding", + "value": "chunked" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "access-control-allow-origin", + "value": "*" + }, + { + "name": "cache-control", + "value": "no-cache, must-revalidate" + }, + { + "name": "openai-model", + "value": "gpt-3.5-turbo-0125" + }, + { + "name": "openai-organization", + "value": "traceloop" + }, + { + "name": "openai-processing-ms", + "value": "634" + }, + { + "name": "openai-version", + "value": "2020-10-01" + }, + { + "name": "strict-transport-security", + "value": "max-age=15724800; includeSubDomains" + }, + { + "name": "x-ratelimit-limit-requests", + "value": "5000" + }, + { + "name": "x-ratelimit-limit-tokens", + "value": "160000" + }, + { + "name": "x-ratelimit-remaining-requests", + "value": "4999" + }, + { + "name": "x-ratelimit-remaining-tokens", + "value": "159975" + }, + { + "name": "x-ratelimit-reset-requests", + "value": "12ms" + }, + { + "name": "x-ratelimit-reset-tokens", + "value": "9ms" + }, + { + "name": "x-request-id", + "value": "req_326334affa7dbb3f76fd09b1f21ca343" + }, + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "__cf_bm=kkiqo0V181NAAuzedVm.6yB17bDBWXgUYPlNhUkNwCw-1711995315-1.0.1.1-oRrPe2q6gsBUoe4vYhX9Cto2xghC5VQxLvYNUQsF4YfdZS9Qed8HI1m1S.CgEdazfLcsPwNXlHeGmYLFDrn2zA; path=/; expires=Mon, 01-Apr-24 18:45:15 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "_cfuvid=5SFZD5OCyvcmUW858m9frjLg0enwmRHxE93Xx0WmkFw-1711995315142-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "cf-ray", + "value": "86da9039aa5e526c-MXP" + }, + { + "name": "content-encoding", + "value": "gzip" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=86400" + } + ], + "headersSize": 1208, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2024-04-01T18:15:14.101Z", + "time": 983, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 983 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/packages/traceloop-sdk/src/lib/tracing/decorators.ts b/packages/traceloop-sdk/src/lib/tracing/decorators.ts index 14334968..239bd1aa 100644 --- a/packages/traceloop-sdk/src/lib/tracing/decorators.ts +++ b/packages/traceloop-sdk/src/lib/tracing/decorators.ts @@ -180,17 +180,24 @@ export function withTool< function entity( type: TraceloopSpanKindValues, - config: Partial, + config: + | Partial + | ((thisArg: unknown, ...funcArgs: unknown[]) => Partial), ) { return function ( - target: any, + target: unknown, propertyKey: string, descriptor: PropertyDescriptor, ) { const originalMethod = descriptor.value; - const entityName = config.name ?? originalMethod.name; - descriptor.value = function (...args: any[]) { + descriptor.value = function (...args: unknown[]) { + if (typeof config === "function") { + config = config(this, ...args); + } + + const entityName = config.name ?? originalMethod.name; + return withEntity( type, { ...config, name: entityName }, @@ -202,18 +209,34 @@ function entity( }; } -export function workflow(config?: Partial) { +export function workflow( + config: + | Partial + | ((thisArg: unknown, ...funcArgs: unknown[]) => Partial), +) { return entity(TraceloopSpanKindValues.WORKFLOW, config ?? {}); } -export function task(config?: Partial) { +export function task( + config: + | Partial + | ((thisArg: unknown, ...funcArgs: unknown[]) => Partial), +) { return entity(TraceloopSpanKindValues.TASK, config ?? {}); } -export function agent(config?: Partial) { +export function agent( + config: + | Partial + | ((thisArg: unknown, ...funcArgs: unknown[]) => Partial), +) { return entity(TraceloopSpanKindValues.AGENT, config ?? {}); } -export function tool(config?: Partial) { +export function tool( + config: + | Partial + | ((thisArg: unknown, ...funcArgs: unknown[]) => Partial), +) { return entity(TraceloopSpanKindValues.TOOL, config ?? {}); } diff --git a/packages/traceloop-sdk/test/decorators.test.ts b/packages/traceloop-sdk/test/decorators.test.ts index be6a19e7..8245c4a5 100644 --- a/packages/traceloop-sdk/test/decorators.test.ts +++ b/packages/traceloop-sdk/test/decorators.test.ts @@ -134,8 +134,6 @@ describe("Test SDK Decorators", () => { it("should create spans for workflows using decoration syntax", async () => { class TestOpenAI { - constructor(private model = "gpt-3.5-turbo") {} - @traceloop.workflow({ name: "sample_chat" }) async chat(things: Map) { const generations: Map = new Map(); @@ -144,7 +142,7 @@ describe("Test SDK Decorators", () => { messages: [ { role: "user", content: `Tell me a ${key} about ${value}` }, ], - model: this.model, + model: "gpt-3.5-turbo", }); if (chatCompletion.choices[0].message.content) { @@ -218,6 +216,95 @@ describe("Test SDK Decorators", () => { ); }); + it("should create spans for workflows using decoration syntax, method variant", async () => { + class TestOpenAI { + constructor(private model = "gpt-3.5-turbo") {} + + @traceloop.workflow((thisArg, things) => ({ + name: `${(thisArg as TestOpenAI).model}_${(things as Map).get("joke")}`, + })) + async chat(things: Map) { + const generations: Map = new Map(); + for await (const [key, value] of things) { + const chatCompletion = await openai.chat.completions.create({ + messages: [ + { role: "user", content: `Tell me a ${key} about ${value}` }, + ], + model: this.model, + }); + + if (chatCompletion.choices[0].message.content) { + generations.set(key, chatCompletion.choices[0].message.content); + } + } + + return generations; + } + } + + const testOpenAI = new TestOpenAI(); + const result = await testOpenAI.chat( + new Map([ + ["joke", "OpenTelemetry"], + ["fact", "JavaScript"], + ]), + ); + + const spans = memoryExporter.getFinishedSpans(); + const workflowName = "gpt-3.5-turbo_OpenTelemetry"; + const workflowSpan = spans.find( + (span) => span.name === `${workflowName}.workflow`, + ); + const chatSpan = spans.find((span) => span.name === "openai.chat"); + + assert.ok(result); + assert.ok(workflowSpan); + assert.strictEqual( + workflowSpan.attributes[`${SpanAttributes.TRACELOOP_WORKFLOW_NAME}`], + workflowName, + ); + assert.strictEqual( + workflowSpan.attributes[`${SpanAttributes.TRACELOOP_SPAN_KIND}`], + "workflow", + ); + assert.strictEqual( + workflowSpan.attributes[`${SpanAttributes.TRACELOOP_ENTITY_NAME}`], + workflowName, + ); + assert.strictEqual( + workflowSpan.attributes[`${SpanAttributes.TRACELOOP_ENTITY_INPUT}`], + JSON.stringify({ + args: [ + [ + ["joke", "OpenTelemetry"], + ["fact", "JavaScript"], + ], + ], + kwargs: {}, + }), + ); + assert.strictEqual( + workflowSpan.attributes[`${SpanAttributes.TRACELOOP_ENTITY_OUTPUT}`], + JSON.stringify([ + ["joke", result.get("joke")], + ["fact", result.get("fact")], + ]), + ); + assert.ok(chatSpan); + assert.strictEqual( + chatSpan.attributes[`${SpanAttributes.TRACELOOP_WORKFLOW_NAME}`], + workflowName, + ); + assert.strictEqual( + chatSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + "user", + ); + assert.strictEqual( + chatSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + "Tell me a joke about OpenTelemetry", + ); + }); + it("should not log prompts if traceContent is disabled", async () => { const jokeSubject = "OpenTelemetry"; const result = await traceloop.withWorkflow(