commands.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. import datetime
  2. import logging
  3. import random
  4. import string
  5. import time
  6. import click
  7. from flask import current_app
  8. from werkzeug.exceptions import NotFound
  9. from core.index.index import IndexBuilder
  10. from libs.password import password_pattern, valid_password, hash_password
  11. from libs.helper import email as email_validate
  12. from extensions.ext_database import db
  13. from libs.rsa import generate_key_pair
  14. from models.account import InvitationCode, Tenant
  15. from models.dataset import Dataset, DatasetQuery, Document, DocumentSegment
  16. from models.model import Account
  17. import secrets
  18. import base64
  19. from models.provider import Provider, ProviderName
  20. from services.provider_service import ProviderService
  21. @click.command('reset-password', help='Reset the account password.')
  22. @click.option('--email', prompt=True, help='The email address of the account whose password you need to reset')
  23. @click.option('--new-password', prompt=True, help='the new password.')
  24. @click.option('--password-confirm', prompt=True, help='the new password confirm.')
  25. def reset_password(email, new_password, password_confirm):
  26. if str(new_password).strip() != str(password_confirm).strip():
  27. click.echo(click.style('sorry. The two passwords do not match.', fg='red'))
  28. return
  29. account = db.session.query(Account). \
  30. filter(Account.email == email). \
  31. one_or_none()
  32. if not account:
  33. click.echo(click.style('sorry. the account: [{}] not exist .'.format(email), fg='red'))
  34. return
  35. try:
  36. valid_password(new_password)
  37. except:
  38. click.echo(
  39. click.style('sorry. The passwords must match {} '.format(password_pattern), fg='red'))
  40. return
  41. # generate password salt
  42. salt = secrets.token_bytes(16)
  43. base64_salt = base64.b64encode(salt).decode()
  44. # encrypt password with salt
  45. password_hashed = hash_password(new_password, salt)
  46. base64_password_hashed = base64.b64encode(password_hashed).decode()
  47. account.password = base64_password_hashed
  48. account.password_salt = base64_salt
  49. db.session.commit()
  50. click.echo(click.style('Congratulations!, password has been reset.', fg='green'))
  51. @click.command('reset-email', help='Reset the account email.')
  52. @click.option('--email', prompt=True, help='The old email address of the account whose email you need to reset')
  53. @click.option('--new-email', prompt=True, help='the new email.')
  54. @click.option('--email-confirm', prompt=True, help='the new email confirm.')
  55. def reset_email(email, new_email, email_confirm):
  56. if str(new_email).strip() != str(email_confirm).strip():
  57. click.echo(click.style('Sorry, new email and confirm email do not match.', fg='red'))
  58. return
  59. account = db.session.query(Account). \
  60. filter(Account.email == email). \
  61. one_or_none()
  62. if not account:
  63. click.echo(click.style('sorry. the account: [{}] not exist .'.format(email), fg='red'))
  64. return
  65. try:
  66. email_validate(new_email)
  67. except:
  68. click.echo(
  69. click.style('sorry. {} is not a valid email. '.format(email), fg='red'))
  70. return
  71. account.email = new_email
  72. db.session.commit()
  73. click.echo(click.style('Congratulations!, email has been reset.', fg='green'))
  74. @click.command('reset-encrypt-key-pair', help='Reset the asymmetric key pair of workspace for encrypt LLM credentials. '
  75. 'After the reset, all LLM credentials will become invalid, '
  76. 'requiring re-entry.'
  77. 'Only support SELF_HOSTED mode.')
  78. @click.confirmation_option(prompt=click.style('Are you sure you want to reset encrypt key pair?'
  79. ' this operation cannot be rolled back!', fg='red'))
  80. def reset_encrypt_key_pair():
  81. if current_app.config['EDITION'] != 'SELF_HOSTED':
  82. click.echo(click.style('Sorry, only support SELF_HOSTED mode.', fg='red'))
  83. return
  84. tenant = db.session.query(Tenant).first()
  85. if not tenant:
  86. click.echo(click.style('Sorry, no workspace found. Please enter /install to initialize.', fg='red'))
  87. return
  88. tenant.encrypt_public_key = generate_key_pair(tenant.id)
  89. db.session.query(Provider).filter(Provider.provider_type == 'custom').delete()
  90. db.session.commit()
  91. click.echo(click.style('Congratulations! '
  92. 'the asymmetric key pair of workspace {} has been reset.'.format(tenant.id), fg='green'))
  93. @click.command('generate-invitation-codes', help='Generate invitation codes.')
  94. @click.option('--batch', help='The batch of invitation codes.')
  95. @click.option('--count', prompt=True, help='Invitation codes count.')
  96. def generate_invitation_codes(batch, count):
  97. if not batch:
  98. now = datetime.datetime.now()
  99. batch = now.strftime('%Y%m%d%H%M%S')
  100. if not count or int(count) <= 0:
  101. click.echo(click.style('sorry. the count must be greater than 0.', fg='red'))
  102. return
  103. count = int(count)
  104. click.echo('Start generate {} invitation codes for batch {}.'.format(count, batch))
  105. codes = ''
  106. for i in range(count):
  107. code = generate_invitation_code()
  108. invitation_code = InvitationCode(
  109. code=code,
  110. batch=batch
  111. )
  112. db.session.add(invitation_code)
  113. click.echo(code)
  114. codes += code + "\n"
  115. db.session.commit()
  116. filename = 'storage/invitation-codes-{}.txt'.format(batch)
  117. with open(filename, 'w') as f:
  118. f.write(codes)
  119. click.echo(click.style(
  120. 'Congratulations! Generated {} invitation codes for batch {} and saved to the file \'{}\''.format(count, batch,
  121. filename),
  122. fg='green'))
  123. def generate_invitation_code():
  124. code = generate_upper_string()
  125. while db.session.query(InvitationCode).filter(InvitationCode.code == code).count() > 0:
  126. code = generate_upper_string()
  127. return code
  128. def generate_upper_string():
  129. letters_digits = string.ascii_uppercase + string.digits
  130. result = ""
  131. for i in range(8):
  132. result += random.choice(letters_digits)
  133. return result
  134. @click.command('recreate-all-dataset-indexes', help='Recreate all dataset indexes.')
  135. def recreate_all_dataset_indexes():
  136. click.echo(click.style('Start recreate all dataset indexes.', fg='green'))
  137. recreate_count = 0
  138. page = 1
  139. while True:
  140. try:
  141. datasets = db.session.query(Dataset).filter(Dataset.indexing_technique == 'high_quality') \
  142. .order_by(Dataset.created_at.desc()).paginate(page=page, per_page=50)
  143. except NotFound:
  144. break
  145. page += 1
  146. for dataset in datasets:
  147. try:
  148. click.echo('Recreating dataset index: {}'.format(dataset.id))
  149. index = IndexBuilder.get_index(dataset, 'high_quality')
  150. if index and index._is_origin():
  151. index.recreate_dataset(dataset)
  152. recreate_count += 1
  153. else:
  154. click.echo('passed.')
  155. except Exception as e:
  156. click.echo(
  157. click.style('Recreate dataset index error: {} {}'.format(e.__class__.__name__, str(e)), fg='red'))
  158. continue
  159. click.echo(click.style('Congratulations! Recreate {} dataset indexes.'.format(recreate_count), fg='green'))
  160. @click.command('clean-unused-dataset-indexes', help='Clean unused dataset indexes.')
  161. def clean_unused_dataset_indexes():
  162. click.echo(click.style('Start clean unused dataset indexes.', fg='green'))
  163. clean_days = int(current_app.config.get('CLEAN_DAY_SETTING'))
  164. start_at = time.perf_counter()
  165. thirty_days_ago = datetime.datetime.now() - datetime.timedelta(days=clean_days)
  166. page = 1
  167. while True:
  168. try:
  169. datasets = db.session.query(Dataset).filter(Dataset.created_at < thirty_days_ago) \
  170. .order_by(Dataset.created_at.desc()).paginate(page=page, per_page=50)
  171. except NotFound:
  172. break
  173. page += 1
  174. for dataset in datasets:
  175. dataset_query = db.session.query(DatasetQuery).filter(
  176. DatasetQuery.created_at > thirty_days_ago,
  177. DatasetQuery.dataset_id == dataset.id
  178. ).all()
  179. if not dataset_query or len(dataset_query) == 0:
  180. documents = db.session.query(Document).filter(
  181. Document.dataset_id == dataset.id,
  182. Document.indexing_status == 'completed',
  183. Document.enabled == True,
  184. Document.archived == False,
  185. Document.updated_at > thirty_days_ago
  186. ).all()
  187. if not documents or len(documents) == 0:
  188. try:
  189. # remove index
  190. vector_index = IndexBuilder.get_index(dataset, 'high_quality')
  191. kw_index = IndexBuilder.get_index(dataset, 'economy')
  192. # delete from vector index
  193. if vector_index:
  194. vector_index.delete()
  195. kw_index.delete()
  196. # update document
  197. update_params = {
  198. Document.enabled: False
  199. }
  200. Document.query.filter_by(dataset_id=dataset.id).update(update_params)
  201. db.session.commit()
  202. click.echo(click.style('Cleaned unused dataset {} from db success!'.format(dataset.id),
  203. fg='green'))
  204. except Exception as e:
  205. click.echo(
  206. click.style('clean dataset index error: {} {}'.format(e.__class__.__name__, str(e)),
  207. fg='red'))
  208. end_at = time.perf_counter()
  209. click.echo(click.style('Cleaned unused dataset from db success latency: {}'.format(end_at - start_at), fg='green'))
  210. @click.command('sync-anthropic-hosted-providers', help='Sync anthropic hosted providers.')
  211. def sync_anthropic_hosted_providers():
  212. click.echo(click.style('Start sync anthropic hosted providers.', fg='green'))
  213. count = 0
  214. page = 1
  215. while True:
  216. try:
  217. tenants = db.session.query(Tenant).order_by(Tenant.created_at.desc()).paginate(page=page, per_page=50)
  218. except NotFound:
  219. break
  220. page += 1
  221. for tenant in tenants:
  222. try:
  223. click.echo('Syncing tenant anthropic hosted provider: {}'.format(tenant.id))
  224. ProviderService.create_system_provider(
  225. tenant,
  226. ProviderName.ANTHROPIC.value,
  227. current_app.config['ANTHROPIC_HOSTED_QUOTA_LIMIT'],
  228. True
  229. )
  230. count += 1
  231. except Exception as e:
  232. click.echo(click.style(
  233. 'Sync tenant anthropic hosted provider error: {} {}'.format(e.__class__.__name__, str(e)),
  234. fg='red'))
  235. continue
  236. click.echo(click.style('Congratulations! Synced {} anthropic hosted providers.'.format(count), fg='green'))
  237. def register_commands(app):
  238. app.cli.add_command(reset_password)
  239. app.cli.add_command(reset_email)
  240. app.cli.add_command(generate_invitation_codes)
  241. app.cli.add_command(reset_encrypt_key_pair)
  242. app.cli.add_command(recreate_all_dataset_indexes)
  243. app.cli.add_command(sync_anthropic_hosted_providers)
  244. app.cli.add_command(clean_unused_dataset_indexes)