pyconverters-openai_vision 0.5.2__py3-none-any.whl

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,2 @@
1
+ """OpenAIVision converter"""
2
+ __version__ = "0.5.2"
@@ -0,0 +1,117 @@
1
+ import os
2
+ from logging import Logger
3
+
4
+ import requests
5
+ from openai import OpenAI
6
+ from openai.lib.azure import AzureOpenAI
7
+ from pymultirole_plugins.util import comma_separated_to_list
8
+ from strenum import StrEnum
9
+
10
+ logger = Logger("pymultirole")
11
+ DEFAULT_CHAT_GPT_MODEL = "gpt-4o-mini"
12
+
13
+
14
+ # Now use default retry with backoff of openai api
15
+ def openai_chat_completion(prefix, **kwargs):
16
+ client = set_openai(prefix)
17
+ response = client.chat.completions.create(**kwargs)
18
+ return response
19
+
20
+
21
+ def openai_list_models(prefix, **kwargs):
22
+ def sort_by_created(x):
23
+ if 'created' in x:
24
+ return x['created']
25
+ elif 'created_at' in x:
26
+ return x['created_at']
27
+ elif 'deprecated' in x:
28
+ return x['deprecated'] or 9999999999
29
+ else:
30
+ return x.id
31
+
32
+ models = []
33
+ client = set_openai(prefix)
34
+ if prefix.startswith("DEEPINFRA"):
35
+ deepinfra_url = client.base_url
36
+ deepinfra_models = {}
37
+ public_models_list_url = f"{deepinfra_url.scheme}://{deepinfra_url.host}/models/list"
38
+ response = requests.get(public_models_list_url,
39
+ headers={'Accept': "application/json", 'Authorization': f"Bearer {client.api_key}"})
40
+ if response.ok:
41
+ resp = response.json()
42
+ mods = sorted(resp, key=sort_by_created, reverse=True)
43
+ mods = list(
44
+ {m['model_name'] for m in mods if m['type'] == 'text-generation'})
45
+ deepinfra_models.update({m: m for m in mods})
46
+
47
+ private_models_list_url = f"{deepinfra_url.scheme}://{deepinfra_url.host}/models/private/list"
48
+ response = requests.get(private_models_list_url,
49
+ headers={'Accept': "application/json", 'Authorization': f"Bearer {client.api_key}"})
50
+ if response.ok:
51
+ resp = response.json()
52
+ mods = sorted(resp, key=sort_by_created, reverse=True)
53
+ mods = list(
54
+ {m['model_name'] for m in mods if m['type'] == 'text-generation'})
55
+ deepinfra_models.update({m: m for m in mods})
56
+
57
+ deployed_models_list_url = f"{deepinfra_url.scheme}://{deepinfra_url.host}/deploy/list/"
58
+ response = requests.get(deployed_models_list_url,
59
+ headers={'Accept': "application/json", 'Authorization': f"Bearer {client.api_key}"})
60
+ if response.ok:
61
+ resp = response.json()
62
+ mods = sorted(resp, key=sort_by_created, reverse=True)
63
+ mods = list(
64
+ {m['model_name'] for m in mods if m['task'] == 'text-generation' and m['status'] == 'running'})
65
+ deepinfra_models.update({m: m for m in mods})
66
+ models = [m for m in deepinfra_models.keys() if 'vision' in m.lower()]
67
+ elif prefix.startswith("AZURE"):
68
+ models = comma_separated_to_list(os.getenv(prefix + "OPENAI_DEPLOYMENT_ID", None))
69
+ else:
70
+ response = client.models.list(**kwargs)
71
+ models = sorted(response.data, key=sort_by_created, reverse=True)
72
+ models = [m.id for m in models]
73
+ return models
74
+
75
+
76
+ def set_openai(prefix):
77
+ if prefix.startswith("AZURE"):
78
+ client = AzureOpenAI(
79
+ # This is the default and can be omitted
80
+ api_key=os.getenv(prefix + "OPENAI_API_KEY"),
81
+ azure_endpoint=os.getenv(prefix + "OPENAI_API_BASE", None),
82
+ api_version=os.getenv(prefix + "OPENAI_API_VERSION", None),
83
+ # azure_deployment=os.getenv(prefix + "OPENAI_DEPLOYMENT_ID", None)
84
+ )
85
+ else:
86
+ client = OpenAI(
87
+ # This is the default and can be omitted
88
+ api_key=os.getenv(prefix + "OPENAI_API_KEY"),
89
+ base_url=os.getenv(prefix + "OPENAI_API_BASE", None)
90
+ )
91
+ return client
92
+
93
+
94
+ def gpt_filter(m: str):
95
+ return m.startswith('gpt') and not m.startswith('gpt-3.5-turbo-instruct') and 'vision' not in m
96
+
97
+
98
+ NO_DEPLOYED_MODELS = 'no deployed models - check API key'
99
+
100
+
101
+ def create_openai_model_enum(name, prefix="", key=lambda m: m):
102
+ chat_gpt_models = []
103
+ default_chat_gpt_model = None
104
+ try:
105
+ chat_gpt_models = [m for m in openai_list_models(prefix) if key(m)]
106
+ if chat_gpt_models:
107
+ default_chat_gpt_model = DEFAULT_CHAT_GPT_MODEL if DEFAULT_CHAT_GPT_MODEL in chat_gpt_models else \
108
+ chat_gpt_models[0]
109
+ except BaseException:
110
+ logger.warning("Can't list models from endpoint", exc_info=True)
111
+
112
+ if len(chat_gpt_models) == 0:
113
+ chat_gpt_models = [NO_DEPLOYED_MODELS]
114
+ models = [("".join([c if c.isalnum() else "_" for c in m]), m) for m in chat_gpt_models]
115
+ model_enum = StrEnum(name, dict(models))
116
+ default_chat_gpt_model = model_enum(default_chat_gpt_model) if default_chat_gpt_model is not None else None
117
+ return model_enum, default_chat_gpt_model
@@ -0,0 +1,230 @@
1
+ import base64
2
+ import os
3
+ from enum import Enum
4
+ from logging import Logger
5
+ from typing import List, cast, Type, Dict, Any
6
+
7
+ import filetype as filetype
8
+ from pydantic import Field, BaseModel
9
+ from pymultirole_plugins.v1.converter import ConverterParameters, ConverterBase
10
+ from pymultirole_plugins.v1.schema import Document
11
+ from starlette.datastructures import UploadFile
12
+
13
+ from .openai_utils import NO_DEPLOYED_MODELS, \
14
+ openai_chat_completion, create_openai_model_enum
15
+
16
+ logger = Logger("pymultirole")
17
+
18
+
19
+ class OpenAIVisionBaseParameters(ConverterParameters):
20
+ model_str: str = Field(
21
+ None, extra="internal"
22
+ )
23
+ model: str = Field(
24
+ None, extra="internal"
25
+ )
26
+ prompt: str = Field(
27
+ "Describe the image with a lot of details",
28
+ description="""Contains the prompt as a string""",
29
+ extra="multiline",
30
+ )
31
+ max_tokens: int = Field(
32
+ 256,
33
+ description="""The maximum number of tokens to generate in the completion.
34
+ The token count of your prompt plus max_tokens cannot exceed the model's context length.
35
+ Most models have a context length of 2048 tokens (except for the newest models, which support 4096).""",
36
+ )
37
+ system_prompt: str = Field(
38
+ None,
39
+ description="""Contains the system prompt""",
40
+ extra="multiline,advanced",
41
+ )
42
+ temperature: float = Field(
43
+ 1.0,
44
+ description="""What sampling temperature to use, between 0 and 2.
45
+ Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.
46
+ We generally recommend altering this or `top_p` but not both.""",
47
+ extra="advanced",
48
+ )
49
+ top_p: int = Field(
50
+ 1,
51
+ description="""An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass.
52
+ So 0.1 means only the tokens comprising the top 10% probability mass are considered.
53
+ We generally recommend altering this or `temperature` but not both.""",
54
+ extra="advanced",
55
+ )
56
+ n: int = Field(
57
+ 1,
58
+ description="""How many completions to generate for each prompt.
59
+ Note: Because this parameter generates many completions, it can quickly consume your token quota.
60
+ Use carefully and ensure that you have reasonable settings for `max_tokens`.""",
61
+ extra="advanced",
62
+ )
63
+ best_of: int = Field(
64
+ 1,
65
+ description="""Generates best_of completions server-side and returns the "best" (the one with the highest log probability per token).
66
+ Results cannot be streamed.
67
+ When used with `n`, `best_of` controls the number of candidate completions and `n` specifies how many to return – `best_of` must be greater than `n`.
68
+ Use carefully and ensure that you have reasonable settings for `max_tokens`.""",
69
+ extra="advanced",
70
+ )
71
+ presence_penalty: float = Field(
72
+ 0.0,
73
+ description="""Number between -2.0 and 2.0.
74
+ Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.""",
75
+ extra="advanced",
76
+ )
77
+ frequency_penalty: float = Field(
78
+ 0.0,
79
+ description="""Number between -2.0 and 2.0.
80
+ Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.""",
81
+ extra="advanced",
82
+ )
83
+
84
+
85
+ class OpenAIVisionModel(str, Enum):
86
+ gpt_4o_mini = "gpt-4o-mini"
87
+ gpt_4o = "gpt-4o"
88
+
89
+
90
+ class OpenAIVisionParameters(OpenAIVisionBaseParameters):
91
+ model: OpenAIVisionModel = Field(
92
+ OpenAIVisionModel.gpt_4o_mini,
93
+ description="""The [OpenAI model](https://platform.openai.com/docs/models) used for speech to text transcription. Options currently available:</br>
94
+ <li>`whisper-1` - state-of-the-art open source large-v2 Whisper model.
95
+ """, extra="pipeline-naming-hint"
96
+ )
97
+
98
+
99
+ DEEPINFRA_PREFIX = "DEEPINFRA_"
100
+ DEEPINFRA_VISION_MODEL_ENUM, DEEPINFRA_DEFAULT_VISION_MODEL = create_openai_model_enum('DeepInfraVisionModel',
101
+ prefix=DEEPINFRA_PREFIX)
102
+
103
+
104
+ class DeepInfraOpenAIVisionParameters(OpenAIVisionBaseParameters):
105
+ model: DEEPINFRA_VISION_MODEL_ENUM = Field(
106
+ None,
107
+ description="""The [DeepInfra 'OpenAI compatible' model](https://deepinfra.com/models?type=automatic-speech-recognition) used for speech to text transcription. It must be deployed on your [DeepInfra dashboard](https://deepinfra.com/dash).
108
+ """, extra="pipeline-naming-hint"
109
+ )
110
+
111
+
112
+ # AZURE_PREFIX = "AZURE_"
113
+ #
114
+ #
115
+ # class AzureOpenAIVisionParameters(OpenAIVisionBaseParameters):
116
+ # model: OpenAIVisionModel = Field(
117
+ # OpenAIVisionModel.whisper_1,
118
+ # description="""The [Azure OpenAI model](https://platform.openai.com/docs/models) used for speech to text transcription. Options currently available:</br>
119
+ # <li>`whisper-1` - state-of-the-art open source large-v2 Whisper model.
120
+ # """, extra="pipeline-naming-hint"
121
+ # )
122
+
123
+
124
+ class OpenAIVisionConverterBase(ConverterBase):
125
+ __doc__ = """Generate text using [OpenAI Text Completion](https://platform.openai.com/docs/guides/completion) API
126
+ You input some text as a prompt, and the model will generate a text completion that attempts to match whatever context or pattern you gave it."""
127
+ PREFIX: str = ""
128
+
129
+ def compute_args(self, params: OpenAIVisionBaseParameters, source: UploadFile
130
+ ) -> Dict[str, Any]:
131
+ data = source.file.read()
132
+ rv = base64.b64encode(data)
133
+ messages = [{"role": "system", "content": params.system_prompt}] if params.system_prompt is not None else []
134
+ messages.append({"role": "user",
135
+ "content": [
136
+ {
137
+ "type": "text",
138
+ "text": params.prompt
139
+ },
140
+ {
141
+ "type": "image_url",
142
+ "image_url": {
143
+ "url": f"data:image/jpeg;base64,{rv.decode('utf-8')}"
144
+ }
145
+ }]})
146
+ kwargs = {
147
+ 'model': params.model_str,
148
+ 'messages': messages,
149
+ 'max_tokens': params.max_tokens,
150
+ 'temperature': params.temperature,
151
+ 'top_p': params.top_p,
152
+ 'n': params.n,
153
+ 'frequency_penalty': params.frequency_penalty,
154
+ 'presence_penalty': params.presence_penalty,
155
+ }
156
+ return kwargs
157
+
158
+ def compute_result(self, **kwargs):
159
+ response = openai_chat_completion(self.PREFIX, **kwargs)
160
+ contents = []
161
+ for choice in response.choices:
162
+ if choice.message.content:
163
+ contents.append(choice.message.content)
164
+ if contents:
165
+ result = "\n".join(contents)
166
+ return result
167
+
168
+ def convert(self, source: UploadFile, parameters: ConverterParameters) \
169
+ -> List[Document]:
170
+
171
+ params: OpenAIVisionBaseParameters = cast(
172
+ OpenAIVisionBaseParameters, parameters
173
+ )
174
+ OPENAI_MODEL = os.getenv(self.PREFIX + "OPENAI_MODEL", None)
175
+ if OPENAI_MODEL:
176
+ params.model_str = OPENAI_MODEL
177
+ doc = None
178
+ try:
179
+ kind = filetype.guess(source.file)
180
+ source.file.seek(0)
181
+ if kind.mime.startswith("image"):
182
+ result = None
183
+ kwargs = self.compute_args(params, source)
184
+ if kwargs['model'] != NO_DEPLOYED_MODELS:
185
+ result = self.compute_result(**kwargs)
186
+ if result:
187
+ doc = Document(identifier=source.filename, text=result)
188
+ doc.properties = {"fileName": source.filename}
189
+ except BaseException as err:
190
+ raise err
191
+ if doc is None:
192
+ raise TypeError(f"Conversion of audio file {source.filename} failed")
193
+ return [doc]
194
+
195
+ @classmethod
196
+ def get_model(cls) -> Type[BaseModel]:
197
+ return OpenAIVisionBaseParameters
198
+
199
+
200
+ class OpenAIVisionConverter(OpenAIVisionConverterBase):
201
+ __doc__ = """Convert audio using [OpenAI Audio](https://platform.openai.com/docs/guides/speech-to-text) API"""
202
+
203
+ def convert(self, source: UploadFile, parameters: ConverterParameters) \
204
+ -> List[Document]:
205
+ params: OpenAIVisionParameters = cast(
206
+ OpenAIVisionParameters, parameters
207
+ )
208
+ params.model_str = params.model.value
209
+ return super().convert(source, params)
210
+
211
+ @classmethod
212
+ def get_model(cls) -> Type[BaseModel]:
213
+ return OpenAIVisionParameters
214
+
215
+
216
+ class DeepInfraOpenAIVisionConverter(OpenAIVisionConverterBase):
217
+ __doc__ = """Convert images using [DeepInfra Vision](https://deepinfra.com/docs/tutorials/whisper) API"""
218
+ PREFIX = DEEPINFRA_PREFIX
219
+
220
+ def convert(self, source: UploadFile, parameters: ConverterParameters) \
221
+ -> List[Document]:
222
+ params: DeepInfraOpenAIVisionParameters = cast(
223
+ DeepInfraOpenAIVisionParameters, parameters
224
+ )
225
+ params.model_str = params.model.value
226
+ return super().convert(source, params)
227
+
228
+ @classmethod
229
+ def get_model(cls) -> Type[BaseModel]:
230
+ return DeepInfraOpenAIVisionParameters
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.1
2
+ Name: pyconverters-openai_vision
3
+ Version: 0.5.2
4
+ Summary: OpenAIVision converter
5
+ Home-page: https://kairntech.com/
6
+ Author: Olivier Terrier
7
+ Author-email: olivier.terrier@kairntech.com
8
+ Requires-Python: >=3.8
9
+ Description-Content-Type: text/markdown
10
+ Classifier: Intended Audience :: Information Technology
11
+ Classifier: Intended Audience :: System Administrators
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Topic :: Internet
16
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Classifier: Topic :: Software Development
20
+ Classifier: Typing :: Typed
21
+ Classifier: Development Status :: 4 - Beta
22
+ Classifier: Environment :: Web Environment
23
+ Classifier: Framework :: AsyncIO
24
+ Classifier: Intended Audience :: Developers
25
+ Classifier: Programming Language :: Python :: 3 :: Only
26
+ Classifier: Programming Language :: Python :: 3.8
27
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
28
+ Classifier: Topic :: Internet :: WWW/HTTP
29
+ Requires-Dist: pymultirole-plugins>=0.5.0,<0.6.0
30
+ Requires-Dist: openai==1.9.0
31
+ Requires-Dist: Jinja2
32
+ Requires-Dist: tenacity
33
+ Requires-Dist: log-with-context
34
+ Requires-Dist: StrEnum
35
+ Requires-Dist: filetype==1.0.13
36
+ Requires-Dist: requests
37
+ Requires-Dist: flit ; extra == "dev"
38
+ Requires-Dist: pre-commit ; extra == "dev"
39
+ Requires-Dist: bump2version ; extra == "dev"
40
+ Requires-Dist: sphinx ; extra == "docs"
41
+ Requires-Dist: sphinx-rtd-theme ; extra == "docs"
42
+ Requires-Dist: m2r2 ; extra == "docs"
43
+ Requires-Dist: sphinxcontrib.apidoc ; extra == "docs"
44
+ Requires-Dist: jupyter_sphinx ; extra == "docs"
45
+ Requires-Dist: pytest>=7.0 ; extra == "test"
46
+ Requires-Dist: pytest-cov ; extra == "test"
47
+ Requires-Dist: pytest-flake8 ; extra == "test"
48
+ Requires-Dist: pytest-black ; extra == "test"
49
+ Requires-Dist: flake8==3.9.2 ; extra == "test"
50
+ Requires-Dist: tox ; extra == "test"
51
+ Requires-Dist: dirty-equals ; extra == "test"
52
+ Requires-Dist: werkzeug==2.0.0 ; extra == "test"
53
+ Requires-Dist: flask==2.1.3 ; extra == "test"
54
+ Provides-Extra: dev
55
+ Provides-Extra: docs
56
+ Provides-Extra: test
57
+
58
+ ## Requirements
59
+
60
+ - Python 3.8+
61
+ - Flit to put Python packages and modules on PyPI
62
+ - Pydantic for the data parts.
63
+
64
+ ## Installation
65
+ ```
66
+ pip install flit
67
+ pip install pymultirole-plugins
68
+ ```
69
+
70
+ ## Publish the Python Package to PyPI
71
+ - Increment the version of your package in the `__init__.py` file:
72
+ ```
73
+ """An amazing package!"""
74
+
75
+ __version__ = 'x.y.z'
76
+ ```
77
+ - Publish
78
+ ```
79
+ flit publish
80
+ ```
81
+
@@ -0,0 +1,7 @@
1
+ pyconverters_openai_vision/__init__.py,sha256=59Qc-bsfVK9kVVX4vnt2PQF_YysgYAQ-T0KEABdUD64,51
2
+ pyconverters_openai_vision/openai_utils.py,sha256=YNOcbeh1sJKeRvkaMh6FnixrUcOfF4rJIGKG5fmnUDo,4806
3
+ pyconverters_openai_vision/openai_vision.py,sha256=MoEyjYCZ75jWiMonaQdR1erpFBpWgYEiqabp9D8102Y,9562
4
+ pyconverters_openai_vision-0.5.2.dist-info/entry_points.txt,sha256=BeU_sWrRTfIdBnkWCBALpYNOrYOYIlRjQdpR2XQhkSU,285
5
+ pyconverters_openai_vision-0.5.2.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
6
+ pyconverters_openai_vision-0.5.2.dist-info/METADATA,sha256=d2SvC6t_svBjtYRBo-JLShoD_9fVg5ODMvlI9pDULRo,2635
7
+ pyconverters_openai_vision-0.5.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.9.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,5 @@
1
+ [pyconverters.plugins]
2
+ azure_openai_vision=pyconverters_openai_vision.openai_vision:AzureOpenAIVisionConverter
3
+ deepinfra_openai_vision=pyconverters_openai_vision.openai_vision:DeepInfraOpenAIVisionConverter
4
+ openai_vision=pyconverters_openai_vision.openai_vision:OpenAIVisionConverter
5
+