Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate OpenAI #103

Merged
merged 26 commits into from
Oct 22, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6a4f2f5
openapi generated code
maxdeichmann Aug 24, 2023
f25219c
Integrate OpenAI
Dev-Khant Sep 23, 2023
2732e32
change func name
Dev-Khant Sep 26, 2023
3a23319
Add custom generation name
Dev-Khant Sep 26, 2023
6ee05eb
Merge branch 'main' into openai-integration
Dev-Khant Sep 26, 2023
7ba1926
Merge branch 'main' into openai-integration
Dev-Khant Sep 27, 2023
926138e
check for trace_id
Dev-Khant Sep 27, 2023
ca38a38
Merge branch 'main' into openai-integration
Dev-Khant Sep 28, 2023
d83a8b0
Merge branch 'main' into openai-integration
maxdeichmann Sep 28, 2023
a985c00
Singleton init of custom class
Dev-Khant Sep 29, 2023
c6dc1e6
Handle concurrency in class init
Dev-Khant Oct 4, 2023
0c4277c
Merge branch 'main' into openai-integration
Dev-Khant Oct 4, 2023
c3a0199
Merge branch 'main' into openai-integration
Dev-Khant Oct 6, 2023
000e08f
Merge branch 'main' into openai-integration
maxdeichmann Oct 10, 2023
ab79850
Merge branch 'main' into openai-integration
maxdeichmann Oct 17, 2023
043c00f
handle openai tests
Dev-Khant Oct 20, 2023
201bd13
Merge branch 'main' into openai-integration
Dev-Khant Oct 20, 2023
fc3eb43
Merge branch 'main' into openai-integration
maxdeichmann Oct 22, 2023
108e9dd
push
maxdeichmann Oct 22, 2023
a1607b8
additions
maxdeichmann Oct 22, 2023
f3d2877
additional test
maxdeichmann Oct 22, 2023
5060d7a
push
maxdeichmann Oct 22, 2023
c447e72
tests
maxdeichmann Oct 22, 2023
72c0601
fix
maxdeichmann Oct 22, 2023
b944f17
Merge branch 'main' into openai-integration
maxdeichmann Oct 22, 2023
501957c
go
maxdeichmann Oct 22, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions langfuse/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .client import Langfuse
from .integrations import openai
99 changes: 99 additions & 0 deletions langfuse/integrations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import os
import functools
from datetime import datetime
from dotenv import load_dotenv

import openai
from openai.api_resources import ChatCompletion, Completion
maxdeichmann marked this conversation as resolved.
Show resolved Hide resolved

from langfuse import Langfuse
from langfuse.client import InitialGeneration
from langfuse.api.resources.commons.types.llm_usage import LlmUsage


load_dotenv()
Dev-Khant marked this conversation as resolved.
Show resolved Hide resolved


class CreateArgsExtractor:
def __init__(self, name=None, **kwargs):
self.args = {}
self.args["name"] = name
self.kwargs = kwargs

def get_langfuse_args(self):
return {**self.args, **self.kwargs}

def get_openai_args(self):
return self.kwargs


class OpenAILangfuse:
Dev-Khant marked this conversation as resolved.
Show resolved Hide resolved
def __init__(self):
self.langfuse = Langfuse(os.environ["LF_PK"], os.environ["LF_SK"], os.environ["HOST"])
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will throw an error if key is not present afaik. Is this intended? Rather use getenv() and provide fallback logic in case key is not present? It's a personal preference so feel free to ignore.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, If keys are not present it is handled in Langfuse class
image


def _get_call_details(self, result, **kwargs):
name = kwargs.get("name", "OpenAI-generation")
if result.object == "chat.completion":
prompt = kwargs.get("messages", [{}])[-1].get("content", "")
completion = result.choices[-1].message.content
elif result.object == "text_completion":
prompt = kwargs.get("prompt", "")
completion = result.choices[-1].text
else:
completion = ""
model = result.model
usage = None if result.usage is None else LlmUsage(**result.usage)
endTime = datetime.now()
modelParameters = {
"temperature": kwargs.get("temperature", 1),
"maxTokens": kwargs.get("max_tokens", float("inf")),
"top_p": kwargs.get("top_p", 1),
"frequency_penalty": kwargs.get("frequency_penalty", 0),
"presence_penalty": kwargs.get("presence_penalty", 0),
}
all_details = {
"name": name,
"prompt": prompt,
"completion": completion,
"endTime": endTime,
"model": model,
"modelParameters": modelParameters,
"usage": usage,
}
return all_details

def _log_result(self, result, call_details):
generation = InitialGeneration(**call_details)
self.langfuse.generation(generation)
self.langfuse.flush()
return result

def langfuse_modified(self, func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
startTime = datetime.now()
arg_extractor = CreateArgsExtractor(*args, **kwargs)
result = func(**arg_extractor.get_openai_args())
call_details = self._get_call_details(result, **arg_extractor.get_langfuse_args())
call_details["startTime"] = startTime
except Exception as ex:
raise ex

return self._log_result(result, call_details)

return wrapper

def replace_openai_funcs(self):
api_resources_classes = [
(ChatCompletion, "create"),
(Completion, "create"),
]

for api_resource_class, method in api_resources_classes:
create_method = getattr(api_resource_class, method)
setattr(api_resource_class, method, self.langfuse_modified(create_method))


modifier = OpenAILangfuse()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we can instantiate this as a Singleton? I fear our users will import this multiple times and then create multiple Langfuse objects under the hood.
I think one instantiation of the Langfuse object would be ideal. Could we also expose the flush function from there? If yes, we can remove it above in the _log_result method and thereby save latencies for our users

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other than this, i think we are good to go. I just tested it on my machine also - seems to work well :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for the change on the singleton! What do you think of concurrency? I just read this here: https://stackabuse.com/creating-a-singleton-in-python/.
Also, can you add a flush functionality to the module? Otherwise we hide the langfuse object and our users are not able to flush before the program exits.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't find a way to directly calling flush using openai. In order to use it we need to create a object of OpenAILangfuse. Do you need to do that??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @maxdeichmann didn't saw the above comment. It's a useful approach but the languse inside openai wrapper will flush the inner processes and while the outer langfuse will handle processes defined out the class. For eg. processes like as you showed here span = langfuse.span(...). So there won't be any conflict with flush() being used by two different instances of languse, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries :)

Yes, you can create two different Langfuse objects in theory, they wont interact with each other. However, each of these will create their own worker thread to take care of network requests. I dont think this would be great for the performance of our users applications.
Also, the above approach gives us way more flexibility using the integration. Users could create generations in sub-spans of their trace which would not be possible without.

Copy link
Contributor Author

@Dev-Khant Dev-Khant Oct 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@maxdeichmann Sure then we could go with this approach!
There won't be any change in the OpenAILangfuse.

Copy link
Member

@maxdeichmann maxdeichmann Oct 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think before merging we should have that in here. I can take that over on the weekend, if you want to.

    langfuse = Langfuse()

    span = langfuse.span(...)

    completion = openai.ChatCompletion.create(
        langfuse=span, model="gpt-3.5-turbo", messages=[{"role": "user", "content": "1 + 1 = "}], temperature=0
    )
    langfuse.flush()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, let me know if you need my help.

modifier.replace_openai_funcs()
40 changes: 40 additions & 0 deletions tests/test_integrations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import os
from dotenv import load_dotenv
from langfuse.integrations import openai
from langfuse.api.client import FintoLangfuse
from langfuse.version import __version__ as version

from tests.utils import create_uuid


load_dotenv()

api = FintoLangfuse(
environment=os.environ["HOST"],
username=os.environ["LF_PK"],
password=os.environ["LF_SK"],
)


def test_openai_chat_completion():
trace_id = create_uuid()
completion = openai.ChatCompletion.create(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tested this out locally. openai is shown on my end as type any in the IDE, whereas when using the original openai SDK, i get autocompletions. Is there a way to solve that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't understand exactly, what's the issue @maxdeichmann

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

create is of type any here. If you import import openai into integrations.py, this should be typed. Could you add that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here @maxdeichmann for me its same, so how do I fix it
image
image

name=trace_id, model="gpt-3.5-turbo", messages=[{"role": "user", "content": "1 + 1 = "}], temperature=0
)

generation = api.generations.get(name=trace_id)

assert len(generation.data) != 0
assert len(completion.choices) != 0


def test_openai_completion():
trace_id = create_uuid()
completion = openai.Completion.create(
name=trace_id, model="gpt-3.5-turbo-instruct", prompt="1 + 1 = ", temperature=0
)

generation = api.generations.get(name=trace_id)

assert len(generation.data) != 0
assert len(completion.choices) != 0
Dev-Khant marked this conversation as resolved.
Show resolved Hide resolved
Loading