Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into tool_calls
Browse files Browse the repository at this point in the history
  • Loading branch information
mikeldking committed Jan 12, 2024
2 parents 2da8a45 + b490ca3 commit 7c120d3
Show file tree
Hide file tree
Showing 32 changed files with 2,663 additions and 56 deletions.
26 changes: 26 additions & 0 deletions .github/workflows/python-CI.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Python CI

on:
push:
branches: [main]
pull_request:
paths:
- "python/**"

defaults:
run:
working-directory: ./python

jobs:
ci:
name: CI Python
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: |
3.8
3.11
- run: pip install tox==4.11.4
- run: tox run-parallel
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"python/openinference-semantic-conventions":"0.1.2"}
{"python/openinference-semantic-conventions":"0.1.2","python/instrumentation/openinference-instrumentation-openai":"0.1.0"}
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ OpenInference provides a set of instrumentations for popular machine learning SD

## Python

| Package | Description |
| --------------------------------------------------------------------------------------------- | --------------------------------------------- |
| [`openinference-semantic-conventions`](./python/openinference-semantic-conventions/README.md) | Semantic conventions for tracing of LLM Apps. |
| Package | Description | Version |
| ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [`openinference-semantic-conventions`](./python/openinference-semantic-conventions/README.md) | Semantic conventions for tracing of LLM Apps. | [![PyPI Version](https://img.shields.io/pypi/v/openinference-semantic-conventions.svg)](https://pypi.python.org/pypi/openinference-semantic-conventions) |
| [`openinference-instrumentation-openai`](./python/instrumentation/openinference-instrumentation-openai/README.rst) | OpenInference Instrumentation for OpenAI SDK. | [![PyPI Version](https://img.shields.io/pypi/v/openinference-instrumentation-openai.svg)](https://pypi.python.org/pypi/openinference-instrumentation-openai) |

## JavaScript

| Package | Description |
| --------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
| [`@arizeai/openinference-semantic-conventions`](./js/packages/openinference-semantic-conventions/README.md) | Semantic conventions for tracing of LLM Apps. |
| [`@arizeai/openinference-instrumentation-openai`](./js/packages/openinference-instrumentation-openai/README.md) | OpenInference Instrumentation for OpenAI SDK. |
| Package | Description | Version |
| --------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`@arizeai/openinference-semantic-conventions`](./js/packages/openinference-semantic-conventions/README.md) | Semantic conventions for tracing of LLM Apps. | [![NPM Version](https://img.shields.io/npm/v/@arizeai/openinference-semantic-conventions.svg)](https://www.npmjs.com/package/@arizeai/openinference-semantic-conventions) |
| [`@arizeai/openinference-instrumentation-openai`](./js/packages/openinference-instrumentation-openai/README.md) | OpenInference Instrumentation for OpenAI SDK. | [![NPM Version](https://img.shields.io/npm/v/@arizeai/openinference-instrumentation-openai)](https://www.npmjs.com/package/@arizeai/openinference-instrumentation-openai) |
5 changes: 5 additions & 0 deletions js/.changeset/purple-cherries-judge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@arizeai/openinference-instrumentation-openai": minor
---

add support for legacy completions api
5 changes: 5 additions & 0 deletions js/.changeset/swift-grapes-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@arizeai/openinference-semantic-conventions": minor
---

add llm.prompts semantic convention
1 change: 1 addition & 0 deletions js/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@ module.exports = {
varsIgnorePattern: "^_",
},
], // ignore unused variables starting with underscore
eqeqeq: ["error", "always"],
},
};
10 changes: 10 additions & 0 deletions js/DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
## Development

This project is built using [pnpm](https://pnpm.io/) in conjunction with [changesets](https://pnpm.io/using-changesets). To install dependencies, run `pnpm install`.

## Publishing

```shell
npx changeset # create a changeset
pnpm -r publish # publish to npm
```
31 changes: 25 additions & 6 deletions js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,32 @@

This is the JavaScript version of OpenInference, a framework for collecting traces from LLM applications.

## Development
> [!NOTE]
> Currently we only support OpenAI but we are working on adding support for other LLM frameworks and SDKs. If you are interested in contributing, please reach out to us by joining our slack community or opening an issue!
This project is built using [pnpm](https://pnpm.io/) in conjunction with [changesets](https://pnpm.io/using-changesets). To install dependencies, run `pnpm install`.

## Publishing
## Installation

```shell
npx changeset # create a changeset
pnpm -r publish # publish to npm
npm install --save @arizeai/openinference-instrumentation-openai
```

## Usage

To load the OpenAI instrumentation, specify it in the registerInstrumentations call along with any additional instrumentation you wish to enable.

```typescript
const { NodeTracerProvider } = require("@opentelemetry/sdk-trace-node");
const {
OpenAIInstrumentation,
} = require("@arizeai/openinference-instrumentation-openai");
const { registerInstrumentations } = require("@opentelemetry/instrumentation");

const provider = new NodeTracerProvider();
provider.register();

registerInstrumentations({
instrumentations: [new OpenAIInstrumentation()],
});
```

For more information on OpenTelemetry Node.js SDK, see the [OpenTelemetry Node.js SDK documentation](https://opentelemetry.io/docs/instrumentation/js/getting-started/nodejs/).
175 changes: 154 additions & 21 deletions js/packages/openinference-instrumentation-openai/src/instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ import {
ChatCompletionCreateParamsBase,
ChatCompletionMessageParam,
} from "openai/resources/chat/completions";
import { CompletionCreateParamsBase } from "openai/resources/completions";
import { Stream } from "openai/streaming";
import {
Completion,
CreateEmbeddingResponse,
EmbeddingCreateParams,
} from "openai/resources";
Expand Down Expand Up @@ -67,20 +69,19 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
}
// eslint-disable-next-line @typescript-eslint/no-this-alias
const instrumentation: OpenAIInstrumentation = this;
type CompletionCreateType =

type ChatCompletionCreateType =
typeof module.OpenAI.Chat.Completions.prototype.create;

// Patch create chat completions
this._wrap(
module.OpenAI.Chat.Completions.prototype,
"create",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(original: CompletionCreateType): any => {
(original: ChatCompletionCreateType): any => {
return function patchedCreate(
this: unknown,
...args: Parameters<
typeof module.OpenAI.Chat.Completions.prototype.create
>
...args: Parameters<ChatCompletionCreateType>
) {
const body = args[0];
const { messages: _messages, ...invocationParameters } = body;
Expand All @@ -102,7 +103,7 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
);
const execContext = trace.setSpan(context.active(), span);
const execPromise = safeExecuteInTheMiddle<
ReturnType<CompletionCreateType>
ReturnType<ChatCompletionCreateType>
>(
() => {
return context.with(execContext, () => {
Expand All @@ -129,7 +130,7 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
[SemanticConventions.OUTPUT_MIME_TYPE]: MimeType.JSON,
// Override the model from the value sent by the server
[SemanticConventions.LLM_MODEL_NAME]: result.model,
...getLLMOutputMessagesAttributes(result),
...getChatCompletionLLMOutputMessagesAttributes(result),
...getUsageAttributes(result),
});
span.setStatus({ code: SpanStatusCode.OK });
Expand All @@ -150,6 +151,74 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
},
);

// Patch create completions
type CompletionsCreateType =
typeof module.OpenAI.Completions.prototype.create;

this._wrap(
module.OpenAI.Completions.prototype,
"create",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(original: CompletionsCreateType): any => {
return function patchedCreate(
this: unknown,
...args: Parameters<CompletionsCreateType>
) {
const body = args[0];
const { prompt: _prompt, ...invocationParameters } = body;
const span = instrumentation.tracer.startSpan(`OpenAI Completions`, {
kind: SpanKind.INTERNAL,
attributes: {
[SemanticConventions.OPENINFERENCE_SPAN_KIND]:
OpenInferenceSpanKind.LLM,
[SemanticConventions.LLM_MODEL_NAME]: body.model,
[SemanticConventions.LLM_INVOCATION_PARAMETERS]:
JSON.stringify(invocationParameters),
...getCompletionInputValueAndMimeType(body),
},
});
const execContext = trace.setSpan(context.active(), span);
const execPromise = safeExecuteInTheMiddle<
ReturnType<CompletionsCreateType>
>(
() => {
return context.with(execContext, () => {
return original.apply(this, args);
});
},
(error) => {
// Push the error to the span
if (error) {
span.recordException(error);
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
});
span.end();
}
},
);
const wrappedPromise = execPromise.then((result) => {
if (isCompletionResponse(result)) {
// Record the results
span.setAttributes({
[SemanticConventions.OUTPUT_VALUE]: JSON.stringify(result),
[SemanticConventions.OUTPUT_MIME_TYPE]: MimeType.JSON,
// Override the model from the value sent by the server
[SemanticConventions.LLM_MODEL_NAME]: result.model,
...getCompletionOutputValueAndMimeType(result),
...getUsageAttributes(result),
});
span.setStatus({ code: SpanStatusCode.OK });
span.end();
}
return result;
});
return context.bind(execContext, wrappedPromise);
};
},
);

// Patch embeddings
type EmbeddingsCreateType =
typeof module.OpenAI.Embeddings.prototype.create;
Expand All @@ -160,11 +229,11 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
(original: EmbeddingsCreateType): any => {
return function patchedEmbeddingCreate(
this: unknown,
...args: Parameters<typeof module.OpenAI.Embeddings.prototype.create>
...args: Parameters<EmbeddingsCreateType>
) {
const body = args[0];
const { input } = body;
const isStringInput = typeof input == "string";
const isStringInput = typeof input === "string";
const span = instrumentation.tracer.startSpan(`OpenAI Embeddings`, {
kind: SpanKind.INTERNAL,
attributes: {
Expand Down Expand Up @@ -241,7 +310,27 @@ function isChatCompletionResponse(
}

/**
* Converts the body of the request to LLM input messages
* type-guard that checks if the response is a completion response
*/
function isCompletionResponse(
response: Stream<Completion> | Completion,
): response is Completion {
return "choices" in response;
}

/**
* type-guard that checks if completion prompt attribute is an array of strings
*/
function isPromptStringArray(
prompt: CompletionCreateParamsBase["prompt"],
): prompt is Array<string> {
return (
Array.isArray(prompt) && prompt.every((item) => typeof item === "string")
);
}

/**
* Converts the body of a chat completions request to LLM input messages
*/
function getLLMInputMessagesAttributes(
body: ChatCompletionCreateParamsBase,
Expand Down Expand Up @@ -306,9 +395,36 @@ function getChatCompletionInputMessageAttributes(
}

/**
* Get Usage attributes
* Converts the body of a completions request to input attributes
*/
function getCompletionInputValueAndMimeType(
body: CompletionCreateParamsBase,
): Attributes {
if (typeof body.prompt === "string") {
return {
[SemanticConventions.INPUT_VALUE]: body.prompt,
[SemanticConventions.INPUT_MIME_TYPE]: MimeType.TEXT,
};
} else if (isPromptStringArray(body.prompt)) {
const prompt = body.prompt[0]; // Only single prompts are currently supported
if (prompt === undefined) {
return {};
}
return {
[SemanticConventions.INPUT_VALUE]: prompt,
[SemanticConventions.INPUT_MIME_TYPE]: MimeType.TEXT,
};
}
// Other cases in which the prompt is a token or array of tokens are currently unsupported
return {};
}

/**
* Get usage attributes
*/
function getUsageAttributes(completion: ChatCompletion): Attributes {
function getUsageAttributes(
completion: ChatCompletion | Completion,
): Attributes {
if (completion.usage) {
return {
[SemanticConventions.LLM_TOKEN_COUNT_COMPLETION]:
Expand All @@ -323,41 +439,58 @@ function getUsageAttributes(completion: ChatCompletion): Attributes {
}

/**
* Converts the result to LLM output attributes
* Converts the chat completion result to LLM output attributes
*/
function getLLMOutputMessagesAttributes(
completion: ChatCompletion,
function getChatCompletionLLMOutputMessagesAttributes(
chatCompletion: ChatCompletion,
): Attributes {
// Right now support just the first choice
const choice = completion.choices[0];
const choice = chatCompletion.choices[0];
if (!choice) {
return {};
}
return [choice.message].reduce((acc, message, index) => {
const index_prefix = `${SemanticConventions.LLM_OUTPUT_MESSAGES}.${index}`;
acc[`${index_prefix}.${SemanticConventions.MESSAGE_CONTENT}`] = String(
const indexPrefix = `${SemanticConventions.LLM_OUTPUT_MESSAGES}.${index}`;
acc[`${indexPrefix}.${SemanticConventions.MESSAGE_CONTENT}`] = String(
message.content,
);
acc[`${index_prefix}.${SemanticConventions.MESSAGE_ROLE}`] = message.role;
acc[`${indexPrefix}.${SemanticConventions.MESSAGE_ROLE}`] = message.role;
return acc;
}, {} as Attributes);
}

/**
* Converts the completion result to output attributes
*/
function getCompletionOutputValueAndMimeType(
completion: Completion,
): Attributes {
// Right now support just the first choice
const choice = completion.choices[0];
if (!choice) {
return {};
}
return {
[SemanticConventions.OUTPUT_VALUE]: String(choice.text),
[SemanticConventions.OUTPUT_MIME_TYPE]: MimeType.TEXT,
};
}

/**
* Converts the embedding result payload to embedding attributes
*/
function getEmbeddingTextAttributes(
request: EmbeddingCreateParams,
): Attributes {
if (typeof request.input == "string") {
if (typeof request.input === "string") {
return {
[`${SemanticConventions.EMBEDDING_EMBEDDINGS}.0.${SemanticConventions.EMBEDDING_TEXT}`]:
request.input,
};
} else if (
Array.isArray(request.input) &&
request.input.length > 0 &&
typeof request.input[0] == "string"
typeof request.input[0] === "string"
) {
return request.input.reduce((acc, input, index) => {
const index_prefix = `${SemanticConventions.EMBEDDING_EMBEDDINGS}.${index}`;
Expand Down
Loading

0 comments on commit 7c120d3

Please sign in to comment.