parent
a9b942981d
commit
a71f2863ac
@ -0,0 +1,291 @@
|
|||||||
|
from flask_login import current_user
|
||||||
|
from flask_restful import Resource, reqparse, marshal_with, marshal
|
||||||
|
from werkzeug.exceptions import Forbidden
|
||||||
|
|
||||||
|
from controllers.console import api
|
||||||
|
from controllers.console.app.error import NoFileUploadedError
|
||||||
|
from controllers.console.datasets.error import TooManyFilesError
|
||||||
|
from controllers.console.setup import setup_required
|
||||||
|
from controllers.console.wraps import account_initialization_required, cloud_edition_billing_resource_check
|
||||||
|
from extensions.ext_redis import redis_client
|
||||||
|
from fields.annotation_fields import annotation_list_fields, annotation_hit_history_list_fields, annotation_fields, \
|
||||||
|
annotation_hit_history_fields
|
||||||
|
from libs.login import login_required
|
||||||
|
from services.annotation_service import AppAnnotationService
|
||||||
|
from flask import request
|
||||||
|
|
||||||
|
|
||||||
|
class AnnotationReplyActionApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@cloud_edition_billing_resource_check('annotation')
|
||||||
|
def post(self, app_id, action):
|
||||||
|
# The role of the current user in the ta table must be admin or owner
|
||||||
|
if current_user.current_tenant.current_role not in ['admin', 'owner']:
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
app_id = str(app_id)
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument('score_threshold', required=True, type=float, location='json')
|
||||||
|
parser.add_argument('embedding_provider_name', required=True, type=str, location='json')
|
||||||
|
parser.add_argument('embedding_model_name', required=True, type=str, location='json')
|
||||||
|
args = parser.parse_args()
|
||||||
|
if action == 'enable':
|
||||||
|
result = AppAnnotationService.enable_app_annotation(args, app_id)
|
||||||
|
elif action == 'disable':
|
||||||
|
result = AppAnnotationService.disable_app_annotation(app_id)
|
||||||
|
else:
|
||||||
|
raise ValueError('Unsupported annotation reply action')
|
||||||
|
return result, 200
|
||||||
|
|
||||||
|
|
||||||
|
class AppAnnotationSettingDetailApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
def get(self, app_id):
|
||||||
|
# The role of the current user in the ta table must be admin or owner
|
||||||
|
if current_user.current_tenant.current_role not in ['admin', 'owner']:
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
app_id = str(app_id)
|
||||||
|
result = AppAnnotationService.get_app_annotation_setting_by_app_id(app_id)
|
||||||
|
return result, 200
|
||||||
|
|
||||||
|
|
||||||
|
class AppAnnotationSettingUpdateApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
def post(self, app_id, annotation_setting_id):
|
||||||
|
# The role of the current user in the ta table must be admin or owner
|
||||||
|
if current_user.current_tenant.current_role not in ['admin', 'owner']:
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
app_id = str(app_id)
|
||||||
|
annotation_setting_id = str(annotation_setting_id)
|
||||||
|
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument('score_threshold', required=True, type=float, location='json')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
result = AppAnnotationService.update_app_annotation_setting(app_id, annotation_setting_id, args)
|
||||||
|
return result, 200
|
||||||
|
|
||||||
|
|
||||||
|
class AnnotationReplyActionStatusApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@cloud_edition_billing_resource_check('annotation')
|
||||||
|
def get(self, app_id, job_id, action):
|
||||||
|
# The role of the current user in the ta table must be admin or owner
|
||||||
|
if current_user.current_tenant.current_role not in ['admin', 'owner']:
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
job_id = str(job_id)
|
||||||
|
app_annotation_job_key = '{}_app_annotation_job_{}'.format(action, str(job_id))
|
||||||
|
cache_result = redis_client.get(app_annotation_job_key)
|
||||||
|
if cache_result is None:
|
||||||
|
raise ValueError("The job is not exist.")
|
||||||
|
|
||||||
|
job_status = cache_result.decode()
|
||||||
|
error_msg = ''
|
||||||
|
if job_status == 'error':
|
||||||
|
app_annotation_error_key = '{}_app_annotation_error_{}'.format(action, str(job_id))
|
||||||
|
error_msg = redis_client.get(app_annotation_error_key).decode()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'job_id': job_id,
|
||||||
|
'job_status': job_status,
|
||||||
|
'error_msg': error_msg
|
||||||
|
}, 200
|
||||||
|
|
||||||
|
|
||||||
|
class AnnotationListApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
def get(self, app_id):
|
||||||
|
# The role of the current user in the ta table must be admin or owner
|
||||||
|
if current_user.current_tenant.current_role not in ['admin', 'owner']:
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
page = request.args.get('page', default=1, type=int)
|
||||||
|
limit = request.args.get('limit', default=20, type=int)
|
||||||
|
keyword = request.args.get('keyword', default=None, type=str)
|
||||||
|
|
||||||
|
app_id = str(app_id)
|
||||||
|
annotation_list, total = AppAnnotationService.get_annotation_list_by_app_id(app_id, page, limit, keyword)
|
||||||
|
response = {
|
||||||
|
'data': marshal(annotation_list, annotation_fields),
|
||||||
|
'has_more': len(annotation_list) == limit,
|
||||||
|
'limit': limit,
|
||||||
|
'total': total,
|
||||||
|
'page': page
|
||||||
|
}
|
||||||
|
return response, 200
|
||||||
|
|
||||||
|
|
||||||
|
class AnnotationExportApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
def get(self, app_id):
|
||||||
|
# The role of the current user in the ta table must be admin or owner
|
||||||
|
if current_user.current_tenant.current_role not in ['admin', 'owner']:
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
app_id = str(app_id)
|
||||||
|
annotation_list = AppAnnotationService.export_annotation_list_by_app_id(app_id)
|
||||||
|
response = {
|
||||||
|
'data': marshal(annotation_list, annotation_fields)
|
||||||
|
}
|
||||||
|
return response, 200
|
||||||
|
|
||||||
|
|
||||||
|
class AnnotationCreateApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@cloud_edition_billing_resource_check('annotation')
|
||||||
|
@marshal_with(annotation_fields)
|
||||||
|
def post(self, app_id):
|
||||||
|
# The role of the current user in the ta table must be admin or owner
|
||||||
|
if current_user.current_tenant.current_role not in ['admin', 'owner']:
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
app_id = str(app_id)
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument('question', required=True, type=str, location='json')
|
||||||
|
parser.add_argument('answer', required=True, type=str, location='json')
|
||||||
|
args = parser.parse_args()
|
||||||
|
annotation = AppAnnotationService.insert_app_annotation_directly(args, app_id)
|
||||||
|
return annotation
|
||||||
|
|
||||||
|
|
||||||
|
class AnnotationUpdateDeleteApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@cloud_edition_billing_resource_check('annotation')
|
||||||
|
@marshal_with(annotation_fields)
|
||||||
|
def post(self, app_id, annotation_id):
|
||||||
|
# The role of the current user in the ta table must be admin or owner
|
||||||
|
if current_user.current_tenant.current_role not in ['admin', 'owner']:
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
app_id = str(app_id)
|
||||||
|
annotation_id = str(annotation_id)
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument('question', required=True, type=str, location='json')
|
||||||
|
parser.add_argument('answer', required=True, type=str, location='json')
|
||||||
|
args = parser.parse_args()
|
||||||
|
annotation = AppAnnotationService.update_app_annotation_directly(args, app_id, annotation_id)
|
||||||
|
return annotation
|
||||||
|
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@cloud_edition_billing_resource_check('annotation')
|
||||||
|
def delete(self, app_id, annotation_id):
|
||||||
|
# The role of the current user in the ta table must be admin or owner
|
||||||
|
if current_user.current_tenant.current_role not in ['admin', 'owner']:
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
app_id = str(app_id)
|
||||||
|
annotation_id = str(annotation_id)
|
||||||
|
AppAnnotationService.delete_app_annotation(app_id, annotation_id)
|
||||||
|
return {'result': 'success'}, 200
|
||||||
|
|
||||||
|
|
||||||
|
class AnnotationBatchImportApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@cloud_edition_billing_resource_check('annotation')
|
||||||
|
def post(self, app_id):
|
||||||
|
# The role of the current user in the ta table must be admin or owner
|
||||||
|
if current_user.current_tenant.current_role not in ['admin', 'owner']:
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
app_id = str(app_id)
|
||||||
|
# get file from request
|
||||||
|
file = request.files['file']
|
||||||
|
# check file
|
||||||
|
if 'file' not in request.files:
|
||||||
|
raise NoFileUploadedError()
|
||||||
|
|
||||||
|
if len(request.files) > 1:
|
||||||
|
raise TooManyFilesError()
|
||||||
|
# check file type
|
||||||
|
if not file.filename.endswith('.csv'):
|
||||||
|
raise ValueError("Invalid file type. Only CSV files are allowed")
|
||||||
|
return AppAnnotationService.batch_import_app_annotations(app_id, file)
|
||||||
|
|
||||||
|
|
||||||
|
class AnnotationBatchImportStatusApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@cloud_edition_billing_resource_check('annotation')
|
||||||
|
def get(self, app_id, job_id):
|
||||||
|
# The role of the current user in the ta table must be admin or owner
|
||||||
|
if current_user.current_tenant.current_role not in ['admin', 'owner']:
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
job_id = str(job_id)
|
||||||
|
indexing_cache_key = 'app_annotation_batch_import_{}'.format(str(job_id))
|
||||||
|
cache_result = redis_client.get(indexing_cache_key)
|
||||||
|
if cache_result is None:
|
||||||
|
raise ValueError("The job is not exist.")
|
||||||
|
job_status = cache_result.decode()
|
||||||
|
error_msg = ''
|
||||||
|
if job_status == 'error':
|
||||||
|
indexing_error_msg_key = 'app_annotation_batch_import_error_msg_{}'.format(str(job_id))
|
||||||
|
error_msg = redis_client.get(indexing_error_msg_key).decode()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'job_id': job_id,
|
||||||
|
'job_status': job_status,
|
||||||
|
'error_msg': error_msg
|
||||||
|
}, 200
|
||||||
|
|
||||||
|
|
||||||
|
class AnnotationHitHistoryListApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
def get(self, app_id, annotation_id):
|
||||||
|
# The role of the current user in the table must be admin or owner
|
||||||
|
if current_user.current_tenant.current_role not in ['admin', 'owner']:
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
page = request.args.get('page', default=1, type=int)
|
||||||
|
limit = request.args.get('limit', default=20, type=int)
|
||||||
|
app_id = str(app_id)
|
||||||
|
annotation_id = str(annotation_id)
|
||||||
|
annotation_hit_history_list, total = AppAnnotationService.get_annotation_hit_histories(app_id, annotation_id,
|
||||||
|
page, limit)
|
||||||
|
response = {
|
||||||
|
'data': marshal(annotation_hit_history_list, annotation_hit_history_fields),
|
||||||
|
'has_more': len(annotation_hit_history_list) == limit,
|
||||||
|
'limit': limit,
|
||||||
|
'total': total,
|
||||||
|
'page': page
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
api.add_resource(AnnotationReplyActionApi, '/apps/<uuid:app_id>/annotation-reply/<string:action>')
|
||||||
|
api.add_resource(AnnotationReplyActionStatusApi,
|
||||||
|
'/apps/<uuid:app_id>/annotation-reply/<string:action>/status/<uuid:job_id>')
|
||||||
|
api.add_resource(AnnotationListApi, '/apps/<uuid:app_id>/annotations')
|
||||||
|
api.add_resource(AnnotationExportApi, '/apps/<uuid:app_id>/annotations/export')
|
||||||
|
api.add_resource(AnnotationUpdateDeleteApi, '/apps/<uuid:app_id>/annotations/<uuid:annotation_id>')
|
||||||
|
api.add_resource(AnnotationBatchImportApi, '/apps/<uuid:app_id>/annotations/batch-import')
|
||||||
|
api.add_resource(AnnotationBatchImportStatusApi, '/apps/<uuid:app_id>/annotations/batch-import-status/<uuid:job_id>')
|
||||||
|
api.add_resource(AnnotationHitHistoryListApi, '/apps/<uuid:app_id>/annotations/<uuid:annotation_id>/hit-histories')
|
||||||
|
api.add_resource(AppAnnotationSettingDetailApi, '/apps/<uuid:app_id>/annotation-setting')
|
||||||
|
api.add_resource(AppAnnotationSettingUpdateApi, '/apps/<uuid:app_id>/annotation-settings/<uuid:annotation_setting_id>')
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
from flask_restful import fields
|
||||||
|
from libs.helper import TimestampField
|
||||||
|
|
||||||
|
account_fields = {
|
||||||
|
'id': fields.String,
|
||||||
|
'name': fields.String,
|
||||||
|
'email': fields.String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
annotation_fields = {
|
||||||
|
"id": fields.String,
|
||||||
|
"question": fields.String,
|
||||||
|
"answer": fields.Raw(attribute='content'),
|
||||||
|
"hit_count": fields.Integer,
|
||||||
|
"created_at": TimestampField,
|
||||||
|
# 'account': fields.Nested(account_fields, allow_null=True)
|
||||||
|
}
|
||||||
|
|
||||||
|
annotation_list_fields = {
|
||||||
|
"data": fields.List(fields.Nested(annotation_fields)),
|
||||||
|
}
|
||||||
|
|
||||||
|
annotation_hit_history_fields = {
|
||||||
|
"id": fields.String,
|
||||||
|
"source": fields.String,
|
||||||
|
"score": fields.Float,
|
||||||
|
"question": fields.String,
|
||||||
|
"created_at": TimestampField,
|
||||||
|
"match": fields.String(attribute='annotation_question'),
|
||||||
|
"response": fields.String(attribute='annotation_content')
|
||||||
|
}
|
||||||
|
|
||||||
|
annotation_hit_history_list_fields = {
|
||||||
|
"data": fields.List(fields.Nested(annotation_hit_history_fields)),
|
||||||
|
}
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
"""add_app_anntation_setting
|
||||||
|
|
||||||
|
Revision ID: 246ba09cbbdb
|
||||||
|
Revises: 714aafe25d39
|
||||||
|
Create Date: 2023-12-14 11:26:12.287264
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '246ba09cbbdb'
|
||||||
|
down_revision = '714aafe25d39'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('app_annotation_settings',
|
||||||
|
sa.Column('id', postgresql.UUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
|
||||||
|
sa.Column('app_id', postgresql.UUID(), nullable=False),
|
||||||
|
sa.Column('score_threshold', sa.Float(), server_default=sa.text('0'), nullable=False),
|
||||||
|
sa.Column('collection_binding_id', postgresql.UUID(), nullable=False),
|
||||||
|
sa.Column('created_user_id', postgresql.UUID(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False),
|
||||||
|
sa.Column('updated_user_id', postgresql.UUID(), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id', name='app_annotation_settings_pkey')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('app_annotation_settings', schema=None) as batch_op:
|
||||||
|
batch_op.create_index('app_annotation_settings_app_idx', ['app_id'], unique=False)
|
||||||
|
|
||||||
|
with op.batch_alter_table('app_model_configs', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('annotation_reply')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('app_model_configs', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('annotation_reply', sa.TEXT(), autoincrement=False, nullable=True))
|
||||||
|
|
||||||
|
with op.batch_alter_table('app_annotation_settings', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index('app_annotation_settings_app_idx')
|
||||||
|
|
||||||
|
op.drop_table('app_annotation_settings')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
"""add-annotation-histoiry-score
|
||||||
|
|
||||||
|
Revision ID: 46976cc39132
|
||||||
|
Revises: e1901f623fd0
|
||||||
|
Create Date: 2023-12-13 04:39:59.302971
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '46976cc39132'
|
||||||
|
down_revision = 'e1901f623fd0'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('app_annotation_hit_histories', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('score', sa.Float(), server_default=sa.text('0'), nullable=False))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('app_annotation_hit_histories', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('score')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
"""add_anntation_history_match_response
|
||||||
|
|
||||||
|
Revision ID: 714aafe25d39
|
||||||
|
Revises: f2a6fc85e260
|
||||||
|
Create Date: 2023-12-14 06:38:02.972527
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '714aafe25d39'
|
||||||
|
down_revision = 'f2a6fc85e260'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('app_annotation_hit_histories', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('annotation_question', sa.Text(), nullable=False))
|
||||||
|
batch_op.add_column(sa.Column('annotation_content', sa.Text(), nullable=False))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('app_annotation_hit_histories', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('annotation_content')
|
||||||
|
batch_op.drop_column('annotation_question')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
"""add-annotation-reply
|
||||||
|
|
||||||
|
Revision ID: e1901f623fd0
|
||||||
|
Revises: fca025d3b60f
|
||||||
|
Create Date: 2023-12-12 06:58:41.054544
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'e1901f623fd0'
|
||||||
|
down_revision = 'fca025d3b60f'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('app_annotation_hit_histories',
|
||||||
|
sa.Column('id', postgresql.UUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
|
||||||
|
sa.Column('app_id', postgresql.UUID(), nullable=False),
|
||||||
|
sa.Column('annotation_id', postgresql.UUID(), nullable=False),
|
||||||
|
sa.Column('source', sa.Text(), nullable=False),
|
||||||
|
sa.Column('question', sa.Text(), nullable=False),
|
||||||
|
sa.Column('account_id', postgresql.UUID(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id', name='app_annotation_hit_histories_pkey')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('app_annotation_hit_histories', schema=None) as batch_op:
|
||||||
|
batch_op.create_index('app_annotation_hit_histories_account_idx', ['account_id'], unique=False)
|
||||||
|
batch_op.create_index('app_annotation_hit_histories_annotation_idx', ['annotation_id'], unique=False)
|
||||||
|
batch_op.create_index('app_annotation_hit_histories_app_idx', ['app_id'], unique=False)
|
||||||
|
|
||||||
|
with op.batch_alter_table('app_model_configs', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('annotation_reply', sa.Text(), nullable=True))
|
||||||
|
|
||||||
|
with op.batch_alter_table('dataset_collection_bindings', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('type', sa.String(length=40), server_default=sa.text("'dataset'::character varying"), nullable=False))
|
||||||
|
|
||||||
|
with op.batch_alter_table('message_annotations', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('question', sa.Text(), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('hit_count', sa.Integer(), server_default=sa.text('0'), nullable=False))
|
||||||
|
batch_op.alter_column('conversation_id',
|
||||||
|
existing_type=postgresql.UUID(),
|
||||||
|
nullable=True)
|
||||||
|
batch_op.alter_column('message_id',
|
||||||
|
existing_type=postgresql.UUID(),
|
||||||
|
nullable=True)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('message_annotations', schema=None) as batch_op:
|
||||||
|
batch_op.alter_column('message_id',
|
||||||
|
existing_type=postgresql.UUID(),
|
||||||
|
nullable=False)
|
||||||
|
batch_op.alter_column('conversation_id',
|
||||||
|
existing_type=postgresql.UUID(),
|
||||||
|
nullable=False)
|
||||||
|
batch_op.drop_column('hit_count')
|
||||||
|
batch_op.drop_column('question')
|
||||||
|
|
||||||
|
with op.batch_alter_table('dataset_collection_bindings', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('type')
|
||||||
|
|
||||||
|
with op.batch_alter_table('app_model_configs', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('annotation_reply')
|
||||||
|
|
||||||
|
with op.batch_alter_table('app_annotation_hit_histories', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index('app_annotation_hit_histories_app_idx')
|
||||||
|
batch_op.drop_index('app_annotation_hit_histories_annotation_idx')
|
||||||
|
batch_op.drop_index('app_annotation_hit_histories_account_idx')
|
||||||
|
|
||||||
|
op.drop_table('app_annotation_hit_histories')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
"""add_anntation_history_message_id
|
||||||
|
|
||||||
|
Revision ID: f2a6fc85e260
|
||||||
|
Revises: 46976cc39132
|
||||||
|
Create Date: 2023-12-13 11:09:29.329584
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'f2a6fc85e260'
|
||||||
|
down_revision = '46976cc39132'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('app_annotation_hit_histories', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('message_id', postgresql.UUID(), nullable=False))
|
||||||
|
batch_op.create_index('app_annotation_hit_histories_message_idx', ['message_id'], unique=False)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('app_annotation_hit_histories', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index('app_annotation_hit_histories_message_idx')
|
||||||
|
batch_op.drop_column('message_id')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@ -0,0 +1,426 @@
|
|||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from flask_login import current_user
|
||||||
|
from sqlalchemy import or_
|
||||||
|
from werkzeug.datastructures import FileStorage
|
||||||
|
from werkzeug.exceptions import NotFound
|
||||||
|
|
||||||
|
from extensions.ext_database import db
|
||||||
|
from extensions.ext_redis import redis_client
|
||||||
|
from models.model import MessageAnnotation, Message, App, AppAnnotationHitHistory, AppAnnotationSetting
|
||||||
|
from tasks.annotation.add_annotation_to_index_task import add_annotation_to_index_task
|
||||||
|
from tasks.annotation.enable_annotation_reply_task import enable_annotation_reply_task
|
||||||
|
from tasks.annotation.disable_annotation_reply_task import disable_annotation_reply_task
|
||||||
|
from tasks.annotation.update_annotation_to_index_task import update_annotation_to_index_task
|
||||||
|
from tasks.annotation.delete_annotation_index_task import delete_annotation_index_task
|
||||||
|
from tasks.annotation.batch_import_annotations_task import batch_import_annotations_task
|
||||||
|
|
||||||
|
|
||||||
|
class AppAnnotationService:
|
||||||
|
@classmethod
|
||||||
|
def up_insert_app_annotation_from_message(cls, args: dict, app_id: str) -> MessageAnnotation:
|
||||||
|
# get app info
|
||||||
|
app = db.session.query(App).filter(
|
||||||
|
App.id == app_id,
|
||||||
|
App.tenant_id == current_user.current_tenant_id,
|
||||||
|
App.status == 'normal'
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not app:
|
||||||
|
raise NotFound("App not found")
|
||||||
|
if 'message_id' in args and args['message_id']:
|
||||||
|
message_id = str(args['message_id'])
|
||||||
|
# get message info
|
||||||
|
message = db.session.query(Message).filter(
|
||||||
|
Message.id == message_id,
|
||||||
|
Message.app_id == app.id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not message:
|
||||||
|
raise NotFound("Message Not Exists.")
|
||||||
|
|
||||||
|
annotation = message.annotation
|
||||||
|
# save the message annotation
|
||||||
|
if annotation:
|
||||||
|
annotation.content = args['answer']
|
||||||
|
annotation.question = args['question']
|
||||||
|
else:
|
||||||
|
annotation = MessageAnnotation(
|
||||||
|
app_id=app.id,
|
||||||
|
conversation_id=message.conversation_id,
|
||||||
|
message_id=message.id,
|
||||||
|
content=args['answer'],
|
||||||
|
question=args['question'],
|
||||||
|
account_id=current_user.id
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
annotation = MessageAnnotation(
|
||||||
|
app_id=app.id,
|
||||||
|
content=args['answer'],
|
||||||
|
question=args['question'],
|
||||||
|
account_id=current_user.id
|
||||||
|
)
|
||||||
|
db.session.add(annotation)
|
||||||
|
db.session.commit()
|
||||||
|
# if annotation reply is enabled , add annotation to index
|
||||||
|
annotation_setting = db.session.query(AppAnnotationSetting).filter(
|
||||||
|
AppAnnotationSetting.app_id == app_id).first()
|
||||||
|
if annotation_setting:
|
||||||
|
add_annotation_to_index_task.delay(annotation.id, args['question'], current_user.current_tenant_id,
|
||||||
|
app_id, annotation_setting.collection_binding_id)
|
||||||
|
return annotation
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def enable_app_annotation(cls, args: dict, app_id: str) -> dict:
|
||||||
|
enable_app_annotation_key = 'enable_app_annotation_{}'.format(str(app_id))
|
||||||
|
cache_result = redis_client.get(enable_app_annotation_key)
|
||||||
|
if cache_result is not None:
|
||||||
|
return {
|
||||||
|
'job_id': cache_result,
|
||||||
|
'job_status': 'processing'
|
||||||
|
}
|
||||||
|
|
||||||
|
# async job
|
||||||
|
job_id = str(uuid.uuid4())
|
||||||
|
enable_app_annotation_job_key = 'enable_app_annotation_job_{}'.format(str(job_id))
|
||||||
|
# send batch add segments task
|
||||||
|
redis_client.setnx(enable_app_annotation_job_key, 'waiting')
|
||||||
|
enable_annotation_reply_task.delay(str(job_id), app_id, current_user.id, current_user.current_tenant_id,
|
||||||
|
args['score_threshold'],
|
||||||
|
args['embedding_provider_name'], args['embedding_model_name'])
|
||||||
|
return {
|
||||||
|
'job_id': job_id,
|
||||||
|
'job_status': 'waiting'
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def disable_app_annotation(cls, app_id: str) -> dict:
|
||||||
|
disable_app_annotation_key = 'disable_app_annotation_{}'.format(str(app_id))
|
||||||
|
cache_result = redis_client.get(disable_app_annotation_key)
|
||||||
|
if cache_result is not None:
|
||||||
|
return {
|
||||||
|
'job_id': cache_result,
|
||||||
|
'job_status': 'processing'
|
||||||
|
}
|
||||||
|
|
||||||
|
# async job
|
||||||
|
job_id = str(uuid.uuid4())
|
||||||
|
disable_app_annotation_job_key = 'disable_app_annotation_job_{}'.format(str(job_id))
|
||||||
|
# send batch add segments task
|
||||||
|
redis_client.setnx(disable_app_annotation_job_key, 'waiting')
|
||||||
|
disable_annotation_reply_task.delay(str(job_id), app_id, current_user.current_tenant_id)
|
||||||
|
return {
|
||||||
|
'job_id': job_id,
|
||||||
|
'job_status': 'waiting'
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_annotation_list_by_app_id(cls, app_id: str, page: int, limit: int, keyword: str):
|
||||||
|
# get app info
|
||||||
|
app = db.session.query(App).filter(
|
||||||
|
App.id == app_id,
|
||||||
|
App.tenant_id == current_user.current_tenant_id,
|
||||||
|
App.status == 'normal'
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not app:
|
||||||
|
raise NotFound("App not found")
|
||||||
|
if keyword:
|
||||||
|
annotations = (db.session.query(MessageAnnotation)
|
||||||
|
.filter(MessageAnnotation.app_id == app_id)
|
||||||
|
.filter(
|
||||||
|
or_(
|
||||||
|
MessageAnnotation.question.ilike('%{}%'.format(keyword)),
|
||||||
|
MessageAnnotation.content.ilike('%{}%'.format(keyword))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.order_by(MessageAnnotation.created_at.desc())
|
||||||
|
.paginate(page=page, per_page=limit, max_per_page=100, error_out=False))
|
||||||
|
else:
|
||||||
|
annotations = (db.session.query(MessageAnnotation)
|
||||||
|
.filter(MessageAnnotation.app_id == app_id)
|
||||||
|
.order_by(MessageAnnotation.created_at.desc())
|
||||||
|
.paginate(page=page, per_page=limit, max_per_page=100, error_out=False))
|
||||||
|
return annotations.items, annotations.total
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_annotation_list_by_app_id(cls, app_id: str):
|
||||||
|
# get app info
|
||||||
|
app = db.session.query(App).filter(
|
||||||
|
App.id == app_id,
|
||||||
|
App.tenant_id == current_user.current_tenant_id,
|
||||||
|
App.status == 'normal'
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not app:
|
||||||
|
raise NotFound("App not found")
|
||||||
|
annotations = (db.session.query(MessageAnnotation)
|
||||||
|
.filter(MessageAnnotation.app_id == app_id)
|
||||||
|
.order_by(MessageAnnotation.created_at.desc()).all())
|
||||||
|
return annotations
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def insert_app_annotation_directly(cls, args: dict, app_id: str) -> MessageAnnotation:
|
||||||
|
# get app info
|
||||||
|
app = db.session.query(App).filter(
|
||||||
|
App.id == app_id,
|
||||||
|
App.tenant_id == current_user.current_tenant_id,
|
||||||
|
App.status == 'normal'
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not app:
|
||||||
|
raise NotFound("App not found")
|
||||||
|
|
||||||
|
annotation = MessageAnnotation(
|
||||||
|
app_id=app.id,
|
||||||
|
content=args['answer'],
|
||||||
|
question=args['question'],
|
||||||
|
account_id=current_user.id
|
||||||
|
)
|
||||||
|
db.session.add(annotation)
|
||||||
|
db.session.commit()
|
||||||
|
# if annotation reply is enabled , add annotation to index
|
||||||
|
annotation_setting = db.session.query(AppAnnotationSetting).filter(
|
||||||
|
AppAnnotationSetting.app_id == app_id).first()
|
||||||
|
if annotation_setting:
|
||||||
|
add_annotation_to_index_task.delay(annotation.id, args['question'], current_user.current_tenant_id,
|
||||||
|
app_id, annotation_setting.collection_binding_id)
|
||||||
|
return annotation
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def update_app_annotation_directly(cls, args: dict, app_id: str, annotation_id: str):
|
||||||
|
# get app info
|
||||||
|
app = db.session.query(App).filter(
|
||||||
|
App.id == app_id,
|
||||||
|
App.tenant_id == current_user.current_tenant_id,
|
||||||
|
App.status == 'normal'
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not app:
|
||||||
|
raise NotFound("App not found")
|
||||||
|
|
||||||
|
annotation = db.session.query(MessageAnnotation).filter(MessageAnnotation.id == annotation_id).first()
|
||||||
|
|
||||||
|
if not annotation:
|
||||||
|
raise NotFound("Annotation not found")
|
||||||
|
|
||||||
|
annotation.content = args['answer']
|
||||||
|
annotation.question = args['question']
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
# if annotation reply is enabled , add annotation to index
|
||||||
|
app_annotation_setting = db.session.query(AppAnnotationSetting).filter(
|
||||||
|
AppAnnotationSetting.app_id == app_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if app_annotation_setting:
|
||||||
|
update_annotation_to_index_task.delay(annotation.id, annotation.question,
|
||||||
|
current_user.current_tenant_id,
|
||||||
|
app_id, app_annotation_setting.collection_binding_id)
|
||||||
|
|
||||||
|
return annotation
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def delete_app_annotation(cls, app_id: str, annotation_id: str):
|
||||||
|
# get app info
|
||||||
|
app = db.session.query(App).filter(
|
||||||
|
App.id == app_id,
|
||||||
|
App.tenant_id == current_user.current_tenant_id,
|
||||||
|
App.status == 'normal'
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not app:
|
||||||
|
raise NotFound("App not found")
|
||||||
|
|
||||||
|
annotation = db.session.query(MessageAnnotation).filter(MessageAnnotation.id == annotation_id).first()
|
||||||
|
|
||||||
|
if not annotation:
|
||||||
|
raise NotFound("Annotation not found")
|
||||||
|
|
||||||
|
db.session.delete(annotation)
|
||||||
|
|
||||||
|
annotation_hit_histories = (db.session.query(AppAnnotationHitHistory)
|
||||||
|
.filter(AppAnnotationHitHistory.annotation_id == annotation_id)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
if annotation_hit_histories:
|
||||||
|
for annotation_hit_history in annotation_hit_histories:
|
||||||
|
db.session.delete(annotation_hit_history)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
# if annotation reply is enabled , delete annotation index
|
||||||
|
app_annotation_setting = db.session.query(AppAnnotationSetting).filter(
|
||||||
|
AppAnnotationSetting.app_id == app_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if app_annotation_setting:
|
||||||
|
delete_annotation_index_task.delay(annotation.id, app_id,
|
||||||
|
current_user.current_tenant_id,
|
||||||
|
app_annotation_setting.collection_binding_id)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def batch_import_app_annotations(cls, app_id, file: FileStorage) -> dict:
|
||||||
|
# get app info
|
||||||
|
app = db.session.query(App).filter(
|
||||||
|
App.id == app_id,
|
||||||
|
App.tenant_id == current_user.current_tenant_id,
|
||||||
|
App.status == 'normal'
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not app:
|
||||||
|
raise NotFound("App not found")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Skip the first row
|
||||||
|
df = pd.read_csv(file)
|
||||||
|
result = []
|
||||||
|
for index, row in df.iterrows():
|
||||||
|
content = {
|
||||||
|
'question': row[0],
|
||||||
|
'answer': row[1]
|
||||||
|
}
|
||||||
|
result.append(content)
|
||||||
|
if len(result) == 0:
|
||||||
|
raise ValueError("The CSV file is empty.")
|
||||||
|
# async job
|
||||||
|
job_id = str(uuid.uuid4())
|
||||||
|
indexing_cache_key = 'app_annotation_batch_import_{}'.format(str(job_id))
|
||||||
|
# send batch add segments task
|
||||||
|
redis_client.setnx(indexing_cache_key, 'waiting')
|
||||||
|
batch_import_annotations_task.delay(str(job_id), result, app_id,
|
||||||
|
current_user.current_tenant_id, current_user.id)
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
'error_msg': str(e)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
'job_id': job_id,
|
||||||
|
'job_status': 'waiting'
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_annotation_hit_histories(cls, app_id: str, annotation_id: str, page, limit):
|
||||||
|
# get app info
|
||||||
|
app = db.session.query(App).filter(
|
||||||
|
App.id == app_id,
|
||||||
|
App.tenant_id == current_user.current_tenant_id,
|
||||||
|
App.status == 'normal'
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not app:
|
||||||
|
raise NotFound("App not found")
|
||||||
|
|
||||||
|
annotation = db.session.query(MessageAnnotation).filter(MessageAnnotation.id == annotation_id).first()
|
||||||
|
|
||||||
|
if not annotation:
|
||||||
|
raise NotFound("Annotation not found")
|
||||||
|
|
||||||
|
annotation_hit_histories = (db.session.query(AppAnnotationHitHistory)
|
||||||
|
.filter(AppAnnotationHitHistory.app_id == app_id,
|
||||||
|
AppAnnotationHitHistory.annotation_id == annotation_id,
|
||||||
|
)
|
||||||
|
.order_by(AppAnnotationHitHistory.created_at.desc())
|
||||||
|
.paginate(page=page, per_page=limit, max_per_page=100, error_out=False))
|
||||||
|
return annotation_hit_histories.items, annotation_hit_histories.total
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_annotation_by_id(cls, annotation_id: str) -> MessageAnnotation | None:
|
||||||
|
annotation = db.session.query(MessageAnnotation).filter(MessageAnnotation.id == annotation_id).first()
|
||||||
|
|
||||||
|
if not annotation:
|
||||||
|
return None
|
||||||
|
return annotation
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_annotation_history(cls, annotation_id: str, app_id: str, annotation_question: str,
|
||||||
|
annotation_content: str, query: str, user_id: str,
|
||||||
|
message_id: str, from_source: str, score: float):
|
||||||
|
# add hit count to annotation
|
||||||
|
db.session.query(MessageAnnotation).filter(
|
||||||
|
MessageAnnotation.id == annotation_id
|
||||||
|
).update(
|
||||||
|
{MessageAnnotation.hit_count: MessageAnnotation.hit_count + 1},
|
||||||
|
synchronize_session=False
|
||||||
|
)
|
||||||
|
|
||||||
|
annotation_hit_history = AppAnnotationHitHistory(
|
||||||
|
annotation_id=annotation_id,
|
||||||
|
app_id=app_id,
|
||||||
|
account_id=user_id,
|
||||||
|
question=query,
|
||||||
|
source=from_source,
|
||||||
|
score=score,
|
||||||
|
message_id=message_id,
|
||||||
|
annotation_question=annotation_question,
|
||||||
|
annotation_content=annotation_content
|
||||||
|
)
|
||||||
|
db.session.add(annotation_hit_history)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_app_annotation_setting_by_app_id(cls, app_id: str):
|
||||||
|
# get app info
|
||||||
|
app = db.session.query(App).filter(
|
||||||
|
App.id == app_id,
|
||||||
|
App.tenant_id == current_user.current_tenant_id,
|
||||||
|
App.status == 'normal'
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not app:
|
||||||
|
raise NotFound("App not found")
|
||||||
|
|
||||||
|
annotation_setting = db.session.query(AppAnnotationSetting).filter(
|
||||||
|
AppAnnotationSetting.app_id == app_id).first()
|
||||||
|
if annotation_setting:
|
||||||
|
collection_binding_detail = annotation_setting.collection_binding_detail
|
||||||
|
return {
|
||||||
|
"id": annotation_setting.id,
|
||||||
|
"enabled": True,
|
||||||
|
"score_threshold": annotation_setting.score_threshold,
|
||||||
|
"embedding_model": {
|
||||||
|
"embedding_provider_name": collection_binding_detail.provider_name,
|
||||||
|
"embedding_model_name": collection_binding_detail.model_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"enabled": False
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def update_app_annotation_setting(cls, app_id: str, annotation_setting_id: str, args: dict):
|
||||||
|
# get app info
|
||||||
|
app = db.session.query(App).filter(
|
||||||
|
App.id == app_id,
|
||||||
|
App.tenant_id == current_user.current_tenant_id,
|
||||||
|
App.status == 'normal'
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not app:
|
||||||
|
raise NotFound("App not found")
|
||||||
|
|
||||||
|
annotation_setting = db.session.query(AppAnnotationSetting).filter(
|
||||||
|
AppAnnotationSetting.app_id == app_id,
|
||||||
|
AppAnnotationSetting.id == annotation_setting_id,
|
||||||
|
).first()
|
||||||
|
if not annotation_setting:
|
||||||
|
raise NotFound("App annotation not found")
|
||||||
|
annotation_setting.score_threshold = args['score_threshold']
|
||||||
|
annotation_setting.updated_user_id = current_user.id
|
||||||
|
annotation_setting.updated_at = datetime.datetime.utcnow()
|
||||||
|
db.session.add(annotation_setting)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
collection_binding_detail = annotation_setting.collection_binding_detail
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": annotation_setting.id,
|
||||||
|
"enabled": True,
|
||||||
|
"score_threshold": annotation_setting.score_threshold,
|
||||||
|
"embedding_model": {
|
||||||
|
"embedding_provider_name": collection_binding_detail.provider_name,
|
||||||
|
"embedding_model_name": collection_binding_detail.model_name
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
import click
|
||||||
|
from celery import shared_task
|
||||||
|
from langchain.schema import Document
|
||||||
|
|
||||||
|
from core.index.index import IndexBuilder
|
||||||
|
|
||||||
|
from models.dataset import Dataset
|
||||||
|
from services.dataset_service import DatasetCollectionBindingService
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(queue='dataset')
|
||||||
|
def add_annotation_to_index_task(annotation_id: str, question: str, tenant_id: str, app_id: str,
|
||||||
|
collection_binding_id: str):
|
||||||
|
"""
|
||||||
|
Add annotation to index.
|
||||||
|
:param annotation_id: annotation id
|
||||||
|
:param question: question
|
||||||
|
:param tenant_id: tenant id
|
||||||
|
:param app_id: app id
|
||||||
|
:param collection_binding_id: embedding binding id
|
||||||
|
|
||||||
|
Usage: clean_dataset_task.delay(dataset_id, tenant_id, indexing_technique, index_struct)
|
||||||
|
"""
|
||||||
|
logging.info(click.style('Start build index for annotation: {}'.format(annotation_id), fg='green'))
|
||||||
|
start_at = time.perf_counter()
|
||||||
|
|
||||||
|
try:
|
||||||
|
dataset_collection_binding = DatasetCollectionBindingService.get_dataset_collection_binding_by_id_and_type(
|
||||||
|
collection_binding_id,
|
||||||
|
'annotation'
|
||||||
|
)
|
||||||
|
dataset = Dataset(
|
||||||
|
id=app_id,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
indexing_technique='high_quality',
|
||||||
|
collection_binding_id=dataset_collection_binding.id
|
||||||
|
)
|
||||||
|
|
||||||
|
document = Document(
|
||||||
|
page_content=question,
|
||||||
|
metadata={
|
||||||
|
"annotation_id": annotation_id,
|
||||||
|
"app_id": app_id,
|
||||||
|
"doc_id": annotation_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
index = IndexBuilder.get_index(dataset, 'high_quality')
|
||||||
|
if index:
|
||||||
|
index.add_texts([document])
|
||||||
|
end_at = time.perf_counter()
|
||||||
|
logging.info(
|
||||||
|
click.style(
|
||||||
|
'Build index successful for annotation: {} latency: {}'.format(annotation_id, end_at - start_at),
|
||||||
|
fg='green'))
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Build index for annotation failed")
|
||||||
@ -0,0 +1,99 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
import click
|
||||||
|
from celery import shared_task
|
||||||
|
from langchain.schema import Document
|
||||||
|
from werkzeug.exceptions import NotFound
|
||||||
|
|
||||||
|
from core.index.index import IndexBuilder
|
||||||
|
from extensions.ext_database import db
|
||||||
|
from extensions.ext_redis import redis_client
|
||||||
|
from models.dataset import Dataset
|
||||||
|
from models.model import MessageAnnotation, App, AppAnnotationSetting
|
||||||
|
from services.dataset_service import DatasetCollectionBindingService
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(queue='dataset')
|
||||||
|
def batch_import_annotations_task(job_id: str, content_list: list[dict], app_id: str, tenant_id: str,
|
||||||
|
user_id: str):
|
||||||
|
"""
|
||||||
|
Add annotation to index.
|
||||||
|
:param job_id: job_id
|
||||||
|
:param content_list: content list
|
||||||
|
:param tenant_id: tenant id
|
||||||
|
:param app_id: app id
|
||||||
|
:param user_id: user_id
|
||||||
|
|
||||||
|
"""
|
||||||
|
logging.info(click.style('Start batch import annotation: {}'.format(job_id), fg='green'))
|
||||||
|
start_at = time.perf_counter()
|
||||||
|
indexing_cache_key = 'app_annotation_batch_import_{}'.format(str(job_id))
|
||||||
|
# get app info
|
||||||
|
app = db.session.query(App).filter(
|
||||||
|
App.id == app_id,
|
||||||
|
App.tenant_id == tenant_id,
|
||||||
|
App.status == 'normal'
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if app:
|
||||||
|
try:
|
||||||
|
documents = []
|
||||||
|
for content in content_list:
|
||||||
|
annotation = MessageAnnotation(
|
||||||
|
app_id=app.id,
|
||||||
|
content=content['answer'],
|
||||||
|
question=content['question'],
|
||||||
|
account_id=user_id
|
||||||
|
)
|
||||||
|
db.session.add(annotation)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
document = Document(
|
||||||
|
page_content=content['question'],
|
||||||
|
metadata={
|
||||||
|
"annotation_id": annotation.id,
|
||||||
|
"app_id": app_id,
|
||||||
|
"doc_id": annotation.id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
documents.append(document)
|
||||||
|
# if annotation reply is enabled , batch add annotations' index
|
||||||
|
app_annotation_setting = db.session.query(AppAnnotationSetting).filter(
|
||||||
|
AppAnnotationSetting.app_id == app_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if app_annotation_setting:
|
||||||
|
dataset_collection_binding = DatasetCollectionBindingService.get_dataset_collection_binding_by_id_and_type(
|
||||||
|
app_annotation_setting.collection_binding_id,
|
||||||
|
'annotation'
|
||||||
|
)
|
||||||
|
if not dataset_collection_binding:
|
||||||
|
raise NotFound("App annotation setting not found")
|
||||||
|
dataset = Dataset(
|
||||||
|
id=app_id,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
indexing_technique='high_quality',
|
||||||
|
embedding_model_provider=dataset_collection_binding.provider_name,
|
||||||
|
embedding_model=dataset_collection_binding.model_name,
|
||||||
|
collection_binding_id=dataset_collection_binding.id
|
||||||
|
)
|
||||||
|
|
||||||
|
index = IndexBuilder.get_index(dataset, 'high_quality')
|
||||||
|
if index:
|
||||||
|
index.add_texts(documents)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
redis_client.setex(indexing_cache_key, 600, 'completed')
|
||||||
|
end_at = time.perf_counter()
|
||||||
|
logging.info(
|
||||||
|
click.style(
|
||||||
|
'Build index successful for batch import annotation: {} latency: {}'.format(job_id, end_at - start_at),
|
||||||
|
fg='green'))
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
redis_client.setex(indexing_cache_key, 600, 'error')
|
||||||
|
indexing_error_msg_key = 'app_annotation_batch_import_error_msg_{}'.format(str(job_id))
|
||||||
|
redis_client.setex(indexing_error_msg_key, 600, str(e))
|
||||||
|
logging.exception("Build index for batch import annotations failed")
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
import click
|
||||||
|
from celery import shared_task
|
||||||
|
from core.index.index import IndexBuilder
|
||||||
|
from models.dataset import Dataset
|
||||||
|
from services.dataset_service import DatasetCollectionBindingService
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(queue='dataset')
|
||||||
|
def delete_annotation_index_task(annotation_id: str, app_id: str, tenant_id: str,
|
||||||
|
collection_binding_id: str):
|
||||||
|
"""
|
||||||
|
Async delete annotation index task
|
||||||
|
"""
|
||||||
|
logging.info(click.style('Start delete app annotation index: {}'.format(app_id), fg='green'))
|
||||||
|
start_at = time.perf_counter()
|
||||||
|
try:
|
||||||
|
dataset_collection_binding = DatasetCollectionBindingService.get_dataset_collection_binding_by_id_and_type(
|
||||||
|
collection_binding_id,
|
||||||
|
'annotation'
|
||||||
|
)
|
||||||
|
|
||||||
|
dataset = Dataset(
|
||||||
|
id=app_id,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
indexing_technique='high_quality',
|
||||||
|
collection_binding_id=dataset_collection_binding.id
|
||||||
|
)
|
||||||
|
|
||||||
|
vector_index = IndexBuilder.get_default_high_quality_index(dataset)
|
||||||
|
if vector_index:
|
||||||
|
try:
|
||||||
|
vector_index.delete_by_metadata_field('annotation_id', annotation_id)
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Delete annotation index failed when annotation deleted.")
|
||||||
|
end_at = time.perf_counter()
|
||||||
|
logging.info(
|
||||||
|
click.style('App annotations index deleted : {} latency: {}'.format(app_id, end_at - start_at),
|
||||||
|
fg='green'))
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception("Annotation deleted index failed:{}".format(str(e)))
|
||||||
|
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
import click
|
||||||
|
from celery import shared_task
|
||||||
|
from werkzeug.exceptions import NotFound
|
||||||
|
|
||||||
|
from core.index.index import IndexBuilder
|
||||||
|
from extensions.ext_database import db
|
||||||
|
from extensions.ext_redis import redis_client
|
||||||
|
from models.dataset import Dataset
|
||||||
|
from models.model import MessageAnnotation, App, AppAnnotationSetting
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(queue='dataset')
|
||||||
|
def disable_annotation_reply_task(job_id: str, app_id: str, tenant_id: str):
|
||||||
|
"""
|
||||||
|
Async enable annotation reply task
|
||||||
|
"""
|
||||||
|
logging.info(click.style('Start delete app annotations index: {}'.format(app_id), fg='green'))
|
||||||
|
start_at = time.perf_counter()
|
||||||
|
# get app info
|
||||||
|
app = db.session.query(App).filter(
|
||||||
|
App.id == app_id,
|
||||||
|
App.tenant_id == tenant_id,
|
||||||
|
App.status == 'normal'
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not app:
|
||||||
|
raise NotFound("App not found")
|
||||||
|
|
||||||
|
app_annotation_setting = db.session.query(AppAnnotationSetting).filter(
|
||||||
|
AppAnnotationSetting.app_id == app_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not app_annotation_setting:
|
||||||
|
raise NotFound("App annotation setting not found")
|
||||||
|
|
||||||
|
disable_app_annotation_key = 'disable_app_annotation_{}'.format(str(app_id))
|
||||||
|
disable_app_annotation_job_key = 'disable_app_annotation_job_{}'.format(str(job_id))
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
dataset = Dataset(
|
||||||
|
id=app_id,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
indexing_technique='high_quality',
|
||||||
|
collection_binding_id=app_annotation_setting.collection_binding_id
|
||||||
|
)
|
||||||
|
|
||||||
|
vector_index = IndexBuilder.get_default_high_quality_index(dataset)
|
||||||
|
if vector_index:
|
||||||
|
try:
|
||||||
|
vector_index.delete_by_metadata_field('app_id', app_id)
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Delete doc index failed when dataset deleted.")
|
||||||
|
redis_client.setex(disable_app_annotation_job_key, 600, 'completed')
|
||||||
|
|
||||||
|
# delete annotation setting
|
||||||
|
db.session.delete(app_annotation_setting)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
end_at = time.perf_counter()
|
||||||
|
logging.info(
|
||||||
|
click.style('App annotations index deleted : {} latency: {}'.format(app_id, end_at - start_at),
|
||||||
|
fg='green'))
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception("Annotation batch deleted index failed:{}".format(str(e)))
|
||||||
|
redis_client.setex(disable_app_annotation_job_key, 600, 'error')
|
||||||
|
disable_app_annotation_error_key = 'disable_app_annotation_error_{}'.format(str(job_id))
|
||||||
|
redis_client.setex(disable_app_annotation_error_key, 600, str(e))
|
||||||
|
finally:
|
||||||
|
redis_client.delete(disable_app_annotation_key)
|
||||||
@ -0,0 +1,106 @@
|
|||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
import click
|
||||||
|
from celery import shared_task
|
||||||
|
from langchain.schema import Document
|
||||||
|
from werkzeug.exceptions import NotFound
|
||||||
|
|
||||||
|
from core.index.index import IndexBuilder
|
||||||
|
from extensions.ext_database import db
|
||||||
|
from extensions.ext_redis import redis_client
|
||||||
|
from models.dataset import Dataset
|
||||||
|
from models.model import MessageAnnotation, App, AppAnnotationSetting
|
||||||
|
from services.dataset_service import DatasetCollectionBindingService
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(queue='dataset')
|
||||||
|
def enable_annotation_reply_task(job_id: str, app_id: str, user_id: str, tenant_id: str, score_threshold: float,
|
||||||
|
embedding_provider_name: str, embedding_model_name: str):
|
||||||
|
"""
|
||||||
|
Async enable annotation reply task
|
||||||
|
"""
|
||||||
|
logging.info(click.style('Start add app annotation to index: {}'.format(app_id), fg='green'))
|
||||||
|
start_at = time.perf_counter()
|
||||||
|
# get app info
|
||||||
|
app = db.session.query(App).filter(
|
||||||
|
App.id == app_id,
|
||||||
|
App.tenant_id == tenant_id,
|
||||||
|
App.status == 'normal'
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not app:
|
||||||
|
raise NotFound("App not found")
|
||||||
|
|
||||||
|
annotations = db.session.query(MessageAnnotation).filter(MessageAnnotation.app_id == app_id).all()
|
||||||
|
enable_app_annotation_key = 'enable_app_annotation_{}'.format(str(app_id))
|
||||||
|
enable_app_annotation_job_key = 'enable_app_annotation_job_{}'.format(str(job_id))
|
||||||
|
|
||||||
|
try:
|
||||||
|
documents = []
|
||||||
|
dataset_collection_binding = DatasetCollectionBindingService.get_dataset_collection_binding(
|
||||||
|
embedding_provider_name,
|
||||||
|
embedding_model_name,
|
||||||
|
'annotation'
|
||||||
|
)
|
||||||
|
annotation_setting = db.session.query(AppAnnotationSetting).filter(
|
||||||
|
AppAnnotationSetting.app_id == app_id).first()
|
||||||
|
if annotation_setting:
|
||||||
|
annotation_setting.score_threshold = score_threshold
|
||||||
|
annotation_setting.collection_binding_id = dataset_collection_binding.id
|
||||||
|
annotation_setting.updated_user_id = user_id
|
||||||
|
annotation_setting.updated_at = datetime.datetime.utcnow()
|
||||||
|
db.session.add(annotation_setting)
|
||||||
|
else:
|
||||||
|
new_app_annotation_setting = AppAnnotationSetting(
|
||||||
|
app_id=app_id,
|
||||||
|
score_threshold=score_threshold,
|
||||||
|
collection_binding_id=dataset_collection_binding.id,
|
||||||
|
created_user_id=user_id,
|
||||||
|
updated_user_id=user_id
|
||||||
|
)
|
||||||
|
db.session.add(new_app_annotation_setting)
|
||||||
|
|
||||||
|
dataset = Dataset(
|
||||||
|
id=app_id,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
indexing_technique='high_quality',
|
||||||
|
embedding_model_provider=embedding_provider_name,
|
||||||
|
embedding_model=embedding_model_name,
|
||||||
|
collection_binding_id=dataset_collection_binding.id
|
||||||
|
)
|
||||||
|
if annotations:
|
||||||
|
for annotation in annotations:
|
||||||
|
document = Document(
|
||||||
|
page_content=annotation.question,
|
||||||
|
metadata={
|
||||||
|
"annotation_id": annotation.id,
|
||||||
|
"app_id": app_id,
|
||||||
|
"doc_id": annotation.id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
documents.append(document)
|
||||||
|
index = IndexBuilder.get_index(dataset, 'high_quality')
|
||||||
|
if index:
|
||||||
|
try:
|
||||||
|
index.delete_by_metadata_field('app_id', app_id)
|
||||||
|
except Exception as e:
|
||||||
|
logging.info(
|
||||||
|
click.style('Delete annotation index error: {}'.format(str(e)),
|
||||||
|
fg='red'))
|
||||||
|
index.add_texts(documents)
|
||||||
|
db.session.commit()
|
||||||
|
redis_client.setex(enable_app_annotation_job_key, 600, 'completed')
|
||||||
|
end_at = time.perf_counter()
|
||||||
|
logging.info(
|
||||||
|
click.style('App annotations added to index: {} latency: {}'.format(app_id, end_at - start_at),
|
||||||
|
fg='green'))
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception("Annotation batch created index failed:{}".format(str(e)))
|
||||||
|
redis_client.setex(enable_app_annotation_job_key, 600, 'error')
|
||||||
|
enable_app_annotation_error_key = 'enable_app_annotation_error_{}'.format(str(job_id))
|
||||||
|
redis_client.setex(enable_app_annotation_error_key, 600, str(e))
|
||||||
|
db.session.rollback()
|
||||||
|
finally:
|
||||||
|
redis_client.delete(enable_app_annotation_key)
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
import click
|
||||||
|
from celery import shared_task
|
||||||
|
from langchain.schema import Document
|
||||||
|
|
||||||
|
from core.index.index import IndexBuilder
|
||||||
|
|
||||||
|
from models.dataset import Dataset
|
||||||
|
from services.dataset_service import DatasetCollectionBindingService
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(queue='dataset')
|
||||||
|
def update_annotation_to_index_task(annotation_id: str, question: str, tenant_id: str, app_id: str,
|
||||||
|
collection_binding_id: str):
|
||||||
|
"""
|
||||||
|
Update annotation to index.
|
||||||
|
:param annotation_id: annotation id
|
||||||
|
:param question: question
|
||||||
|
:param tenant_id: tenant id
|
||||||
|
:param app_id: app id
|
||||||
|
:param collection_binding_id: embedding binding id
|
||||||
|
|
||||||
|
Usage: clean_dataset_task.delay(dataset_id, tenant_id, indexing_technique, index_struct)
|
||||||
|
"""
|
||||||
|
logging.info(click.style('Start update index for annotation: {}'.format(annotation_id), fg='green'))
|
||||||
|
start_at = time.perf_counter()
|
||||||
|
|
||||||
|
try:
|
||||||
|
dataset_collection_binding = DatasetCollectionBindingService.get_dataset_collection_binding_by_id_and_type(
|
||||||
|
collection_binding_id,
|
||||||
|
'annotation'
|
||||||
|
)
|
||||||
|
|
||||||
|
dataset = Dataset(
|
||||||
|
id=app_id,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
indexing_technique='high_quality',
|
||||||
|
embedding_model_provider=dataset_collection_binding.provider_name,
|
||||||
|
embedding_model=dataset_collection_binding.model_name,
|
||||||
|
collection_binding_id=dataset_collection_binding.id
|
||||||
|
)
|
||||||
|
|
||||||
|
document = Document(
|
||||||
|
page_content=question,
|
||||||
|
metadata={
|
||||||
|
"annotation_id": annotation_id,
|
||||||
|
"app_id": app_id,
|
||||||
|
"doc_id": annotation_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
index = IndexBuilder.get_index(dataset, 'high_quality')
|
||||||
|
if index:
|
||||||
|
index.delete_by_metadata_field('annotation_id', annotation_id)
|
||||||
|
index.add_texts([document])
|
||||||
|
end_at = time.perf_counter()
|
||||||
|
logging.info(
|
||||||
|
click.style(
|
||||||
|
'Build index successful for annotation: {} latency: {}'.format(annotation_id, end_at - start_at),
|
||||||
|
fg='green'))
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Build index for annotation failed")
|
||||||
Loading…
Reference in New Issue