pyconverters-openai_vision 0.5.2__tar.gz
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.
- pyconverters_openai_vision-0.5.2/.dockerignore +9 -0
- pyconverters_openai_vision-0.5.2/.gitignore +18 -0
- pyconverters_openai_vision-0.5.2/Dockerfile +19 -0
- pyconverters_openai_vision-0.5.2/Jenkinsfile +408 -0
- pyconverters_openai_vision-0.5.2/PKG-INFO +81 -0
- pyconverters_openai_vision-0.5.2/README.md +23 -0
- pyconverters_openai_vision-0.5.2/bumpversion.py +41 -0
- pyconverters_openai_vision-0.5.2/pyconverters_openai_vision/__init__.py +2 -0
- pyconverters_openai_vision-0.5.2/pyconverters_openai_vision/openai_utils.py +117 -0
- pyconverters_openai_vision-0.5.2/pyconverters_openai_vision/openai_vision.py +230 -0
- pyconverters_openai_vision-0.5.2/pyproject.toml +103 -0
- pyconverters_openai_vision-0.5.2/setup.py +59 -0
- pyconverters_openai_vision-0.5.2/tests/__init__.py +0 -0
- pyconverters_openai_vision-0.5.2/tests/data/colducoq.jpg +0 -0
- pyconverters_openai_vision-0.5.2/tests/data/webinar.png +0 -0
- pyconverters_openai_vision-0.5.2/tests/test_openai_vision.py +57 -0
- pyconverters_openai_vision-0.5.2/tox.ini +52 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
FROM python:3.8-slim-buster
|
|
2
|
+
# Install prerequisites
|
|
3
|
+
RUN apt-get update -y && \
|
|
4
|
+
apt-get install -y \
|
|
5
|
+
patch \
|
|
6
|
+
gcc && \
|
|
7
|
+
apt-get install -y --no-install-recommends \
|
|
8
|
+
g++ \
|
|
9
|
+
git && \
|
|
10
|
+
# Final upgrade + clean
|
|
11
|
+
apt-get update -y && \
|
|
12
|
+
apt-get clean all -y
|
|
13
|
+
|
|
14
|
+
# Enable Installing packages as root
|
|
15
|
+
ENV FLIT_ROOT_INSTALL=1
|
|
16
|
+
|
|
17
|
+
# Add pyproject.toml + README.md for flit install
|
|
18
|
+
ADD pyproject.toml pyproject.toml
|
|
19
|
+
ADD README.md README.md
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
pipeline {
|
|
2
|
+
environment {
|
|
3
|
+
PATH_HOME = '/home/jenkins'
|
|
4
|
+
TEST_REPORT_DIR = '/root/test-reports'
|
|
5
|
+
PYTHONPYCACHEPREFIX = '/tmp/.pytest_cache'
|
|
6
|
+
PYTHONDONTWRITEBYTECODE = '1'
|
|
7
|
+
JENKINS_UIDGID = '1004:1004'
|
|
8
|
+
|
|
9
|
+
MAJOR_VERSION = '0'
|
|
10
|
+
MINOR_VERSION = '5'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
agent none
|
|
14
|
+
|
|
15
|
+
triggers {
|
|
16
|
+
upstream(upstreamProjects: 'pymultirole_plugins/' + BRANCH_NAME.replaceAll('/', '%2F'),\
|
|
17
|
+
threshold: hudson.model.Result.SUCCESS)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
stages {
|
|
21
|
+
stage('Catch build termination') {
|
|
22
|
+
agent {
|
|
23
|
+
node {
|
|
24
|
+
label 'built-in'
|
|
25
|
+
customWorkspace "${PATH_HOME}/${JOB_NAME}"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
stages {
|
|
29
|
+
stage('Analyse build cause') {
|
|
30
|
+
steps {
|
|
31
|
+
script {
|
|
32
|
+
analyseBuildCause()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
stage('Generate new version') {
|
|
40
|
+
when {
|
|
41
|
+
environment name: 'SKIP_JOB', value: '0'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
agent {
|
|
45
|
+
node {
|
|
46
|
+
label 'built-in'
|
|
47
|
+
customWorkspace "${PATH_HOME}/${JOB_NAME}"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
stages {
|
|
52
|
+
stage('Add credentials') {
|
|
53
|
+
steps {
|
|
54
|
+
script {
|
|
55
|
+
// Add password file for flit publishing
|
|
56
|
+
sh "cp ${PATH_HOME}/.passwd-pypi .env"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
stage('Commit new version') {
|
|
62
|
+
steps {
|
|
63
|
+
script {
|
|
64
|
+
println("attempt to publish ${JOB_NAME} with version: ${MAJOR_VERSION}.${MINOR_VERSION}.${BUILD_ID}")
|
|
65
|
+
|
|
66
|
+
// push updates of file __init__.py
|
|
67
|
+
withCredentials([gitUsernamePassword(credentialsId: 'bitbucket-user', gitToolName: 'git-tool')]) {
|
|
68
|
+
sh 'git pull'
|
|
69
|
+
sh "echo '\"\"\"OpenAIVision converter\"\"\"' > pyconverters_openai_vision/__init__.py"
|
|
70
|
+
sh "echo '__version__ = \"${MAJOR_VERSION}.${MINOR_VERSION}.${BUILD_ID}\"' >> pyconverters_openai_vision/__init__.py"
|
|
71
|
+
sh 'git commit pyconverters_openai_vision/__init__.py -m "[Jenkins CI] Commit on version files" || echo "No changes to commit"'
|
|
72
|
+
sh 'git push'
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
stage('Build, test and publish') {
|
|
81
|
+
when {
|
|
82
|
+
beforeAgent true
|
|
83
|
+
environment name: 'SKIP_JOB', value: '0'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
agent {
|
|
87
|
+
// dockerfile agent
|
|
88
|
+
// Mounted volume for Junit reports
|
|
89
|
+
// - docker: /root/test-reports
|
|
90
|
+
// - host : /tmp/_${JOB_NAME}/test-reports
|
|
91
|
+
dockerfile {
|
|
92
|
+
label 'built-in'
|
|
93
|
+
customWorkspace "${PATH_HOME}/${JOB_NAME}"
|
|
94
|
+
filename 'Dockerfile'
|
|
95
|
+
args "-u root --privileged -v /tmp/_${JOB_NAME}/test-reports:${TEST_REPORT_DIR}"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
stages {
|
|
100
|
+
stage('Install flit & flake8') {
|
|
101
|
+
steps {
|
|
102
|
+
// remove any previous tox env
|
|
103
|
+
sh 'rm -rf .tox'
|
|
104
|
+
sh 'python -m pip install pip==22.0.3'
|
|
105
|
+
sh 'pip install --no-cache-dir flit==3.2.0 flake8==3.9.2 flakehell tox'
|
|
106
|
+
sh 'flit install'
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
stage('Test & lint python code') {
|
|
111
|
+
steps {
|
|
112
|
+
// remove any previous results.xml file
|
|
113
|
+
sh "rm -f ${TEST_REPORT_DIR}/results.xml"
|
|
114
|
+
sh 'tox'
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
stage('Publish on PyPI') {
|
|
119
|
+
environment {
|
|
120
|
+
FLIT_USERNAME = getUserName '.env'
|
|
121
|
+
FLIT_PASSWORD = getUserPass '.env'
|
|
122
|
+
}
|
|
123
|
+
steps {
|
|
124
|
+
// remove any previous folder dist
|
|
125
|
+
sh 'rm -rf dist'
|
|
126
|
+
// create (as root) folder dist
|
|
127
|
+
sh 'mkdir dist'
|
|
128
|
+
// pull recent updates of file __init__.py
|
|
129
|
+
withCredentials([gitUsernamePassword(credentialsId: 'bitbucket-user', gitToolName: 'git-tool')]) {
|
|
130
|
+
sh 'git config --global pull.rebase false'
|
|
131
|
+
sh "git config --global --add safe.directory ${WORKSPACE}"
|
|
132
|
+
sh 'git pull'
|
|
133
|
+
}
|
|
134
|
+
// put back owner of .git folder
|
|
135
|
+
sh "chown -R ${JENKINS_UIDGID} ${WORKSPACE}/.git"
|
|
136
|
+
// put back owner of pulled file
|
|
137
|
+
sh "chown ${JENKINS_UIDGID} pyconverters_openai_vision/__init__.py"
|
|
138
|
+
// get git status
|
|
139
|
+
sh 'git status'
|
|
140
|
+
// publish on PyPI
|
|
141
|
+
sh '''
|
|
142
|
+
export COMMIT_VERSION=$( cat pyconverters_openai_vision/__init__.py|grep version|cut -d '"' -f2|tr -s '[:blank:]' )
|
|
143
|
+
export BUILD_VERSION="${MAJOR_VERSION}"."${MINOR_VERSION}"."${BUILD_ID}"
|
|
144
|
+
if [ "${COMMIT_VERSION}" = "${BUILD_VERSION}" ] ; then flit publish ; fi
|
|
145
|
+
'''
|
|
146
|
+
// remove current folder dist
|
|
147
|
+
sh 'rm -rf dist'
|
|
148
|
+
// remove current folder .hypothesis
|
|
149
|
+
sh 'rm -rf .hypothesis'
|
|
150
|
+
// remove current folder .tox
|
|
151
|
+
sh 'rm -rf .tox'
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
post {
|
|
159
|
+
// only triggered when blue or green sign
|
|
160
|
+
success {
|
|
161
|
+
// node is specified here to get an agent
|
|
162
|
+
node('built-in') {
|
|
163
|
+
// keep using customWorkspace to store Junit report
|
|
164
|
+
ws("${PATH_HOME}/${JOB_NAME}") {
|
|
165
|
+
script {
|
|
166
|
+
try {
|
|
167
|
+
sh 'rm -f results.xml'
|
|
168
|
+
sh "cp /tmp/_${JOB_NAME}/test-reports/results.xml results.xml"
|
|
169
|
+
} catch (Exception e) {
|
|
170
|
+
println 'Exception occurred: ' + e.toString()
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
junit 'results.xml'
|
|
174
|
+
} catch (Exception e) {
|
|
175
|
+
println 'Exception occurred: ' + e.toString()
|
|
176
|
+
}
|
|
177
|
+
if (sendEmailNotif("${PATH_HOME}/${JOB_NAME}", "${BUILD_NUMBER}")) {
|
|
178
|
+
println 'sending Success Build notification'
|
|
179
|
+
CUSTOM_SUBJECT = '[CI - Jenkinzz SUCCESS] ' + CUSTOM_SUBJECT
|
|
180
|
+
emailext(
|
|
181
|
+
mimeType: 'text/html',
|
|
182
|
+
subject: CUSTOM_SUBJECT,
|
|
183
|
+
body: '${DEFAULT_CONTENT}',
|
|
184
|
+
replyTo: '${DEFAULT_REPLYTO}',
|
|
185
|
+
to: '${ADMIN_RECIPIENTS}' + ';' + CUSTOM_RECIPIENTS
|
|
186
|
+
)
|
|
187
|
+
switchEmailNotif(false, BUILD_NUMBER)
|
|
188
|
+
} else {
|
|
189
|
+
println 'preventing Success Build notification'
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// triggered when red sign
|
|
196
|
+
failure {
|
|
197
|
+
// node is specified here to get an agent
|
|
198
|
+
node('built-in') {
|
|
199
|
+
// keep using customWorkspace to store Junit report
|
|
200
|
+
ws("${PATH_HOME}/${JOB_NAME}") {
|
|
201
|
+
script {
|
|
202
|
+
try {
|
|
203
|
+
sh 'rm -f results.xml'
|
|
204
|
+
sh "cp /tmp/_${JOB_NAME}/test-reports/results.xml results.xml"
|
|
205
|
+
} catch (Exception e) {
|
|
206
|
+
println 'Exception occurred: ' + e.toString()
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
junit 'results.xml'
|
|
210
|
+
} catch (Exception e) {
|
|
211
|
+
println 'Exception occurred: ' + e.toString()
|
|
212
|
+
}
|
|
213
|
+
println 'sending Failure Build notification'
|
|
214
|
+
CUSTOM_SUBJECT = '[CI - Jenkinzz FAILURE] ' + CUSTOM_SUBJECT
|
|
215
|
+
emailext(
|
|
216
|
+
mimeType: 'text/html',
|
|
217
|
+
subject: CUSTOM_SUBJECT,
|
|
218
|
+
body: '${DEFAULT_CONTENT}',
|
|
219
|
+
replyTo: '${DEFAULT_REPLYTO}',
|
|
220
|
+
to: '${ADMIN_RECIPIENTS}' + ';' + CUSTOM_RECIPIENTS
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// triggered when black sign
|
|
227
|
+
aborted {
|
|
228
|
+
println 'post-declarative message: abort job'
|
|
229
|
+
}
|
|
230
|
+
// trigger every-works
|
|
231
|
+
//always {
|
|
232
|
+
//}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// return FLIT_USERNAME from given file
|
|
237
|
+
def getUserName(path) {
|
|
238
|
+
USERNAME = sh(
|
|
239
|
+
script: "grep FLIT_USERNAME ${path}|cut -d '=' -f2",
|
|
240
|
+
returnStdout: true
|
|
241
|
+
).trim()
|
|
242
|
+
return USERNAME
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// return FLIT_PASSWORD from given file
|
|
246
|
+
def getUserPass(path) {
|
|
247
|
+
USERPASS = sh(
|
|
248
|
+
script: "grep FLIT_PASSWORD ${path}|cut -d '=' -f2",
|
|
249
|
+
returnStdout: true
|
|
250
|
+
).trim()
|
|
251
|
+
return USERPASS
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// create/remove emailNotif file to trigger email notification
|
|
255
|
+
def switchEmailNotif(toggle, build) {
|
|
256
|
+
if (toggle) {
|
|
257
|
+
sh 'echo ' + build + ' > .emailNotif'
|
|
258
|
+
} else {
|
|
259
|
+
if (build == BUILD_NUMBER) {
|
|
260
|
+
sh 'rm -f .emailNotif'
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// return true if emailNotif file present
|
|
266
|
+
boolean sendEmailNotif(path, build) {
|
|
267
|
+
emailNotif = sh(
|
|
268
|
+
script: "find ${path} -name '.emailNotif'|wc -l",
|
|
269
|
+
returnStdout: true
|
|
270
|
+
).trim()
|
|
271
|
+
emailContent = ''
|
|
272
|
+
if (emailNotif == '1') {
|
|
273
|
+
emailContent = sh(
|
|
274
|
+
script: "cat ${path}/.emailNotif",
|
|
275
|
+
returnStdout: true
|
|
276
|
+
).trim()
|
|
277
|
+
}
|
|
278
|
+
return (emailContent == build)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
def analyseBuildCause() {
|
|
282
|
+
upstreamProjects = ['pymultirole_plugins']
|
|
283
|
+
boolean upstreamRunning = false
|
|
284
|
+
String jobName
|
|
285
|
+
// iterate over upstreamProjects
|
|
286
|
+
for (upstream_project in upstreamProjects) {
|
|
287
|
+
Jenkins.instance.getItemByFullName(upstream_project).items.each { repository ->
|
|
288
|
+
boolean isRunning = false
|
|
289
|
+
//repository.parent.name: project
|
|
290
|
+
//repository.name: branch
|
|
291
|
+
if ( repository.name == BRANCH_NAME ) {
|
|
292
|
+
// iterate over all jobs of current repository
|
|
293
|
+
repository.allJobs.each { job ->
|
|
294
|
+
// iterate over all builds of current job
|
|
295
|
+
job.builds.each { build ->
|
|
296
|
+
// determine if a build is running or not
|
|
297
|
+
if ( build.result == (null) ) {
|
|
298
|
+
jobName = build.parent.parent.name
|
|
299
|
+
isRunning = true
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if ( isRunning ) {
|
|
303
|
+
upstreamRunning = true
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Catch if build has been triggered by CI Commit
|
|
311
|
+
// returnStatus = true when string not found -> Team commit
|
|
312
|
+
// returnStatus = false when string is found -> CI commit
|
|
313
|
+
boolean lastCommitIsTeam = sh(
|
|
314
|
+
script: 'git log -1 | grep "\\[Jenkins CI\\]"',
|
|
315
|
+
returnStatus: true
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
// Skip build when upstream detected
|
|
319
|
+
if (upstreamRunning) {
|
|
320
|
+
println 'Skipping build because upstream job detected (' + jobName + ')'
|
|
321
|
+
env.SKIP_JOB = '1'
|
|
322
|
+
switchEmailNotif(false, 0)
|
|
323
|
+
currentBuild.result = 'NOT_BUILT'
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Catch if build has been triggered by User
|
|
327
|
+
boolean isStartedByUser = currentBuild.rawBuild.getCause(hudson.model.Cause$UserIdCause) != null
|
|
328
|
+
if (isStartedByUser && !upstreamRunning) {
|
|
329
|
+
env.SKIP_JOB = '0'
|
|
330
|
+
env.CUSTOM_SUBJECT = JOB_NAME + ' - Manual Build #' + BUILD_NUMBER
|
|
331
|
+
env.CUSTOM_RECIPIENTS = emailextrecipients([[$class: 'RequesterRecipientProvider']])
|
|
332
|
+
switchEmailNotif(true, BUILD_NUMBER)
|
|
333
|
+
println 'Job started by User, proceeding'
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Catch if build has been triggered by Upstream
|
|
337
|
+
boolean isStartedByUpstream = currentBuild.rawBuild.getCause(hudson.model.Cause$UpstreamCause) != null
|
|
338
|
+
if (isStartedByUpstream && !upstreamRunning) {
|
|
339
|
+
int changeSetCount = 0
|
|
340
|
+
int ciSkipCount = 0
|
|
341
|
+
String upstreamFullJobName = ''
|
|
342
|
+
for (Run upstreamBuild : currentBuild.upstreamBuilds) {
|
|
343
|
+
upstreamFullJobName = upstreamBuild.rawBuild.fullDisplayName
|
|
344
|
+
if (upstreamBuild.changeSets != null) {
|
|
345
|
+
def changeLogSets = upstreamBuild.changeSets
|
|
346
|
+
for (int i = 0; i < changeLogSets.size(); i++) {
|
|
347
|
+
changeSetCount++
|
|
348
|
+
def entries = changeLogSets[i].items
|
|
349
|
+
for (int j = 0; j < entries.length; j++) {
|
|
350
|
+
def entry = entries[j]
|
|
351
|
+
if (entry.msg.contains('[Jenkins CI]')) {
|
|
352
|
+
ciSkipCount++
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (changeSetCount > 0 && changeSetCount == ciSkipCount) {
|
|
359
|
+
env.SKIP_JOB = '1'
|
|
360
|
+
switchEmailNotif(false, 0)
|
|
361
|
+
println 'Job started by Upstream [' + upstreamFullJobName + '], with CI commit, skipping'
|
|
362
|
+
currentBuild.result = 'NOT_BUILT'
|
|
363
|
+
} else {
|
|
364
|
+
env.SKIP_JOB = '0'
|
|
365
|
+
env.CUSTOM_SUBJECT = JOB_NAME + ' - Upstream Build #' + BUILD_NUMBER
|
|
366
|
+
env.CUSTOM_RECIPIENTS = emailextrecipients([[$class:'UpstreamComitterRecipientProvider']])
|
|
367
|
+
switchEmailNotif(true, BUILD_NUMBER)
|
|
368
|
+
println 'Job started by Upstream [' + upstreamFullJobName + '], proceeding'
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Catch if build has been triggered by User Commit
|
|
373
|
+
boolean isStartedByCommit = currentBuild.rawBuild.getCause(jenkins.branch.BranchEventCause) != null
|
|
374
|
+
if (isStartedByCommit && lastCommitIsTeam && !upstreamRunning) {
|
|
375
|
+
env.SKIP_JOB = '0'
|
|
376
|
+
env.CUSTOM_SUBJECT = JOB_NAME + ' - SCM Build #' + BUILD_NUMBER
|
|
377
|
+
env.CUSTOM_RECIPIENTS = emailextrecipients([[$class: 'DevelopersRecipientProvider'], [$class:'CulpritsRecipientProvider']])
|
|
378
|
+
switchEmailNotif(true, BUILD_NUMBER)
|
|
379
|
+
println 'Job started by User Commit, proceeding'
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Catch if build has been triggered by cron
|
|
383
|
+
boolean isStartedByCron = currentBuild.rawBuild.getCause(hudson.triggers.TimerTrigger$TimerTriggerCause) != null
|
|
384
|
+
if (isStartedByCron && lastCommitIsTeam && !upstreamRunning) {
|
|
385
|
+
env.SKIP_JOB = '0'
|
|
386
|
+
env.CUSTOM_SUBJECT = JOB_NAME + ' - CRON Build #' + BUILD_NUMBER
|
|
387
|
+
env.CUSTOM_RECIPIENTS = emailextrecipients([[$class: 'DevelopersRecipientProvider'], [$class:'CulpritsRecipientProvider']])
|
|
388
|
+
switchEmailNotif(true, BUILD_NUMBER)
|
|
389
|
+
println 'Job started by Cron, proceeding'
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Catch if build has been triggered by branch discovery
|
|
393
|
+
boolean isStartedByBranchDiscovery = currentBuild.rawBuild.getCause(jenkins.branch.BranchIndexingCause) != null
|
|
394
|
+
if (isStartedByBranchDiscovery && lastCommitIsTeam && !upstreamRunning) {
|
|
395
|
+
env.SKIP_JOB = '0'
|
|
396
|
+
env.CUSTOM_SUBJECT = JOB_NAME + ' - BranchDiscovery Build #' + BUILD_NUMBER
|
|
397
|
+
env.CUSTOM_RECIPIENTS = emailextrecipients([[$class: 'DevelopersRecipientProvider'], [$class:'CulpritsRecipientProvider']])
|
|
398
|
+
switchEmailNotif(true, BUILD_NUMBER)
|
|
399
|
+
println 'Job started by Branch Discovery, proceeding'
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (!lastCommitIsTeam && !upstreamRunning && !isStartedByUser && !isStartedByUpstream) {
|
|
403
|
+
println 'Skipping build because last commit has been done by CI'
|
|
404
|
+
env.SKIP_JOB = '1'
|
|
405
|
+
switchEmailNotif(false, 0)
|
|
406
|
+
//currentBuild.result = 'NOT_BUILT'
|
|
407
|
+
}
|
|
408
|
+
}
|
|
@@ -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,23 @@
|
|
|
1
|
+
## Requirements
|
|
2
|
+
|
|
3
|
+
- Python 3.8+
|
|
4
|
+
- Flit to put Python packages and modules on PyPI
|
|
5
|
+
- Pydantic for the data parts.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
```
|
|
9
|
+
pip install flit
|
|
10
|
+
pip install pymultirole-plugins
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Publish the Python Package to PyPI
|
|
14
|
+
- Increment the version of your package in the `__init__.py` file:
|
|
15
|
+
```
|
|
16
|
+
"""An amazing package!"""
|
|
17
|
+
|
|
18
|
+
__version__ = 'x.y.z'
|
|
19
|
+
```
|
|
20
|
+
- Publish
|
|
21
|
+
```
|
|
22
|
+
flit publish
|
|
23
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main(argv):
|
|
7
|
+
part = argv[0].lower() if len(argv) > 0 else "minor"
|
|
8
|
+
Jenkinsfile = Path("./Jenkinsfile")
|
|
9
|
+
pyprojectfile = Path("./pyproject.toml")
|
|
10
|
+
with Jenkinsfile.open("r", encoding="utf-8") as fin:
|
|
11
|
+
data = fin.read()
|
|
12
|
+
for line in data.split(fin.newlines):
|
|
13
|
+
if "MAJOR_VERSION" in line:
|
|
14
|
+
result = re.search(r'MAJOR_VERSION\s*=\s*"([0-9]+)"', line)
|
|
15
|
+
if result:
|
|
16
|
+
major = int(result.group(1))
|
|
17
|
+
if part == "major":
|
|
18
|
+
to_replace = result.group(0)
|
|
19
|
+
by_replace = f'MAJOR_VERSION = "{major + 1}"'
|
|
20
|
+
if "MINOR_VERSION" in line:
|
|
21
|
+
result = re.search(r'MINOR_VERSION\s*=\s*"([0-9]+)"', line)
|
|
22
|
+
if result:
|
|
23
|
+
minor = int(result.group(1))
|
|
24
|
+
if part == "minor":
|
|
25
|
+
to_replace = result.group(0)
|
|
26
|
+
by_replace = f'MINOR_VERSION = "{minor + 1}"'
|
|
27
|
+
data = data.replace(to_replace, by_replace)
|
|
28
|
+
with Jenkinsfile.open("wt") as fout:
|
|
29
|
+
fout.write(data)
|
|
30
|
+
|
|
31
|
+
depend_string = f"{major}.{minor}.0,<{major}.{minor + 1}.0"
|
|
32
|
+
new_depend_string = f"{major}.{minor + 1}.0,<{major}.{minor + 2}.0"
|
|
33
|
+
with pyprojectfile.open("r", encoding="utf-8") as fin:
|
|
34
|
+
data = fin.read()
|
|
35
|
+
data = data.replace(depend_string, new_depend_string)
|
|
36
|
+
with pyprojectfile.open("wt") as fout:
|
|
37
|
+
fout.write(data)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if __name__ == "__main__":
|
|
41
|
+
main(sys.argv[1:])
|
|
@@ -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,103 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["flit_core >=2,<4"]
|
|
3
|
+
build-backend = "flit_core.buildapi"
|
|
4
|
+
|
|
5
|
+
[tool.flit.metadata]
|
|
6
|
+
module = "pyconverters_openai_vision"
|
|
7
|
+
author = "Olivier Terrier"
|
|
8
|
+
author-email = "olivier.terrier@kairntech.com"
|
|
9
|
+
home-page = "https://kairntech.com/"
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Intended Audience :: Information Technology",
|
|
12
|
+
"Intended Audience :: System Administrators",
|
|
13
|
+
"Operating System :: OS Independent",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python",
|
|
16
|
+
"Topic :: Internet",
|
|
17
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
18
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
19
|
+
"Topic :: Software Development :: Libraries",
|
|
20
|
+
"Topic :: Software Development",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
"Development Status :: 4 - Beta",
|
|
23
|
+
"Environment :: Web Environment",
|
|
24
|
+
"Framework :: AsyncIO",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
27
|
+
"Programming Language :: Python :: 3.8",
|
|
28
|
+
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
|
|
29
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
30
|
+
]
|
|
31
|
+
requires = [
|
|
32
|
+
"pymultirole-plugins>=0.5.0,<0.6.0",
|
|
33
|
+
"openai==1.9.0",
|
|
34
|
+
"Jinja2",
|
|
35
|
+
"tenacity",
|
|
36
|
+
"log-with-context",
|
|
37
|
+
"StrEnum",
|
|
38
|
+
"filetype==1.0.13",
|
|
39
|
+
"requests",
|
|
40
|
+
]
|
|
41
|
+
dist-name = "pyconverters-openai_vision"
|
|
42
|
+
description-file = "README.md"
|
|
43
|
+
requires-python = ">=3.8"
|
|
44
|
+
|
|
45
|
+
[tool.flit.entrypoints."pyconverters.plugins"]
|
|
46
|
+
openai_vision = "pyconverters_openai_vision.openai_vision:OpenAIVisionConverter"
|
|
47
|
+
deepinfra_openai_vision = "pyconverters_openai_vision.openai_vision:DeepInfraOpenAIVisionConverter"
|
|
48
|
+
azure_openai_vision = "pyconverters_openai_vision.openai_vision:AzureOpenAIVisionConverter"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
[tool.flit.metadata.requires-extra]
|
|
52
|
+
test = [
|
|
53
|
+
"pytest>=7.0",
|
|
54
|
+
"pytest-cov",
|
|
55
|
+
"pytest-flake8",
|
|
56
|
+
"pytest-black",
|
|
57
|
+
# "hypothesis",
|
|
58
|
+
"flake8==3.9.2",
|
|
59
|
+
"tox",
|
|
60
|
+
"dirty-equals",
|
|
61
|
+
"werkzeug==2.0.0",
|
|
62
|
+
"flask==2.1.3"
|
|
63
|
+
]
|
|
64
|
+
docs = [
|
|
65
|
+
"sphinx",
|
|
66
|
+
"sphinx-rtd-theme",
|
|
67
|
+
"m2r2", # markdown support
|
|
68
|
+
"sphinxcontrib.apidoc", # run sphinx-apidoc when building docs
|
|
69
|
+
"jupyter_sphinx", # for execution of code snippets in the documentation
|
|
70
|
+
]
|
|
71
|
+
dev = [
|
|
72
|
+
"flit",
|
|
73
|
+
"pre-commit",
|
|
74
|
+
"bump2version",
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
[tool.flakehell]
|
|
78
|
+
exclude = ["README.md"]
|
|
79
|
+
format = "colored"
|
|
80
|
+
#format = "junit-xml"
|
|
81
|
+
max_line_length = 120
|
|
82
|
+
show_source = true
|
|
83
|
+
#whitelist = "../../allowlist.txt"
|
|
84
|
+
extended_default_ignore = []
|
|
85
|
+
|
|
86
|
+
[tool.flakehell.plugins]
|
|
87
|
+
flake8-bandit = ["+*", "-S322"]
|
|
88
|
+
flake8-bugbear = ["+*"]
|
|
89
|
+
flake8-builtins = ["+*", "-A003"]
|
|
90
|
+
flake8-comprehensions = ["+*"]
|
|
91
|
+
#flake8-darglint = ["+*"]
|
|
92
|
+
flake8-docstrings = ["+*"]
|
|
93
|
+
flake8-eradicate = ["+*"]
|
|
94
|
+
flake8-isort = ["+*"]
|
|
95
|
+
flake8-mutable = ["+*"]
|
|
96
|
+
flake8-pytest-style = ["+*"]
|
|
97
|
+
flake8-spellcheck = ["+*"]
|
|
98
|
+
mccabe = ["+*"]
|
|
99
|
+
pep8-naming = ["+*"]
|
|
100
|
+
pycodestyle = ["+*"]
|
|
101
|
+
pyflakes = ["+*"]
|
|
102
|
+
pylint = ["+*"]
|
|
103
|
+
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# setup.py generated by flit for tools that don't yet use PEP 517
|
|
3
|
+
|
|
4
|
+
from distutils.core import setup
|
|
5
|
+
|
|
6
|
+
packages = \
|
|
7
|
+
['pyconverters_openai_vision']
|
|
8
|
+
|
|
9
|
+
package_data = \
|
|
10
|
+
{'': ['*']}
|
|
11
|
+
|
|
12
|
+
install_requires = \
|
|
13
|
+
['pymultirole-plugins>=0.5.0,<0.6.0',
|
|
14
|
+
'openai==1.9.0',
|
|
15
|
+
'Jinja2',
|
|
16
|
+
'tenacity',
|
|
17
|
+
'log-with-context',
|
|
18
|
+
'StrEnum',
|
|
19
|
+
'filetype==1.0.13',
|
|
20
|
+
'requests']
|
|
21
|
+
|
|
22
|
+
extras_require = \
|
|
23
|
+
{'dev': ['flit', 'pre-commit', 'bump2version'],
|
|
24
|
+
'docs': ['sphinx',
|
|
25
|
+
'sphinx-rtd-theme',
|
|
26
|
+
'm2r2',
|
|
27
|
+
'sphinxcontrib.apidoc',
|
|
28
|
+
'jupyter_sphinx'],
|
|
29
|
+
'test': ['pytest>=7.0',
|
|
30
|
+
'pytest-cov',
|
|
31
|
+
'pytest-flake8',
|
|
32
|
+
'pytest-black',
|
|
33
|
+
'flake8==3.9.2',
|
|
34
|
+
'tox',
|
|
35
|
+
'dirty-equals',
|
|
36
|
+
'werkzeug==2.0.0',
|
|
37
|
+
'flask==2.1.3']}
|
|
38
|
+
|
|
39
|
+
entry_points = \
|
|
40
|
+
{'pyconverters.plugins': ['azure_openai_vision = '
|
|
41
|
+
'pyconverters_openai_vision.openai_vision:AzureOpenAIVisionConverter',
|
|
42
|
+
'deepinfra_openai_vision = '
|
|
43
|
+
'pyconverters_openai_vision.openai_vision:DeepInfraOpenAIVisionConverter',
|
|
44
|
+
'openai_vision = '
|
|
45
|
+
'pyconverters_openai_vision.openai_vision:OpenAIVisionConverter']}
|
|
46
|
+
|
|
47
|
+
setup(name='pyconverters-openai_vision',
|
|
48
|
+
version='0.5.2',
|
|
49
|
+
description='OpenAIVision converter',
|
|
50
|
+
author='Olivier Terrier',
|
|
51
|
+
author_email='olivier.terrier@kairntech.com',
|
|
52
|
+
url='https://kairntech.com/',
|
|
53
|
+
packages=packages,
|
|
54
|
+
package_data=package_data,
|
|
55
|
+
install_requires=install_requires,
|
|
56
|
+
extras_require=extras_require,
|
|
57
|
+
entry_points=entry_points,
|
|
58
|
+
python_requires='>=3.8',
|
|
59
|
+
)
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from pymultirole_plugins.v1.schema import Document
|
|
6
|
+
from starlette.datastructures import UploadFile
|
|
7
|
+
|
|
8
|
+
from pyconverters_openai_vision.openai_vision import (
|
|
9
|
+
OpenAIVisionConverter,
|
|
10
|
+
OpenAIVisionParameters, DeepInfraOpenAIVisionParameters, DeepInfraOpenAIVisionConverter
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_openai_vision_basic():
|
|
15
|
+
model = OpenAIVisionConverter.get_model()
|
|
16
|
+
model_class = model.construct().__class__
|
|
17
|
+
assert model_class == OpenAIVisionParameters
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.mark.skip(reason="Not a test")
|
|
21
|
+
def test_openai():
|
|
22
|
+
converter = OpenAIVisionConverter()
|
|
23
|
+
parameters = OpenAIVisionParameters()
|
|
24
|
+
testdir = Path(__file__).parent
|
|
25
|
+
source = Path(testdir, 'data/colducoq.jpg')
|
|
26
|
+
with source.open("rb") as fin:
|
|
27
|
+
docs: List[Document] = converter.convert(UploadFile(source.name, fin, 'image/jpeg'), parameters)
|
|
28
|
+
assert len(docs) == 1
|
|
29
|
+
doc0 = docs[0]
|
|
30
|
+
assert 'dent de crolles' in doc0.text.lower()
|
|
31
|
+
|
|
32
|
+
source = Path(testdir, 'data/webinar.png')
|
|
33
|
+
with source.open("rb") as fin:
|
|
34
|
+
docs: List[Document] = converter.convert(UploadFile(source.name, fin, 'image/png'), parameters)
|
|
35
|
+
assert len(docs) == 1
|
|
36
|
+
doc0 = docs[0]
|
|
37
|
+
assert 'kairntech' in doc0.text.lower()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.skip(reason="Not a test")
|
|
41
|
+
def test_deepinfra():
|
|
42
|
+
converter = DeepInfraOpenAIVisionConverter()
|
|
43
|
+
parameters = DeepInfraOpenAIVisionParameters(model="meta-llama/Llama-3.2-11B-Vision-Instruct")
|
|
44
|
+
testdir = Path(__file__).parent
|
|
45
|
+
source = Path(testdir, 'data/colducoq.jpg')
|
|
46
|
+
with source.open("rb") as fin:
|
|
47
|
+
docs: List[Document] = converter.convert(UploadFile(source.name, fin, 'image/jpeg'), parameters)
|
|
48
|
+
assert len(docs) == 1
|
|
49
|
+
doc0 = docs[0]
|
|
50
|
+
assert 'dent de crolles' in doc0.text.lower()
|
|
51
|
+
|
|
52
|
+
source = Path(testdir, 'data/webinar.png')
|
|
53
|
+
with source.open("rb") as fin:
|
|
54
|
+
docs: List[Document] = converter.convert(UploadFile(source.name, fin, 'image/png'), parameters)
|
|
55
|
+
assert len(docs) == 1
|
|
56
|
+
doc0 = docs[0]
|
|
57
|
+
assert 'generative ai' in doc0.text.lower()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[tox]
|
|
2
|
+
isolated_build = True
|
|
3
|
+
envlist = clean,py38,report
|
|
4
|
+
|
|
5
|
+
[gh-actions]
|
|
6
|
+
python =
|
|
7
|
+
3.8: clean,py38,report
|
|
8
|
+
|
|
9
|
+
[pytest]
|
|
10
|
+
addopts =
|
|
11
|
+
--durations=5
|
|
12
|
+
--flake8
|
|
13
|
+
norecursedirs =
|
|
14
|
+
docs
|
|
15
|
+
|
|
16
|
+
filterwarnings =
|
|
17
|
+
ignore:<class '.*'> is not using a cooperative constructor:pytest.PytestDeprecationWarning
|
|
18
|
+
ignore:The \(fspath. py.path.local\) argument to .* is deprecated.:pytest.PytestDeprecationWarning
|
|
19
|
+
ignore:.* is an Item subclass and should not be a collector.*:pytest.PytestWarning
|
|
20
|
+
|
|
21
|
+
[testenv]
|
|
22
|
+
deps = .[test]
|
|
23
|
+
commands = pytest --junit-xml={env:TEST_REPORT_DIR:.}/{env:TEST_REPORT_FILE:results.xml}
|
|
24
|
+
passenv = *
|
|
25
|
+
depends =
|
|
26
|
+
report: py38
|
|
27
|
+
|
|
28
|
+
[testenv:docs]
|
|
29
|
+
changedir=docs
|
|
30
|
+
deps = .[docs]
|
|
31
|
+
commands = sphinx-build . _build
|
|
32
|
+
|
|
33
|
+
[flake8]
|
|
34
|
+
max-line-length = 140
|
|
35
|
+
ignore =
|
|
36
|
+
E131,
|
|
37
|
+
E126,
|
|
38
|
+
E203,
|
|
39
|
+
E266,
|
|
40
|
+
E501,
|
|
41
|
+
W503,
|
|
42
|
+
W291,
|
|
43
|
+
C901
|
|
44
|
+
per-file-ignores =
|
|
45
|
+
__init__.py: F401
|
|
46
|
+
max-complexity = 18
|
|
47
|
+
select = B, C, E, F, W, T4, B9
|
|
48
|
+
exclude =
|
|
49
|
+
.git,
|
|
50
|
+
.tox,
|
|
51
|
+
__pycache__,
|
|
52
|
+
dist,
|