cube-studio/myapp/views/view_chat.py
2024-06-30 11:34:35 +08:00

1371 lines
70 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import base64
import uuid
import random
import re
import shutil
import logging
from myapp.models.model_chat import Chat, ChatLog
import requests
import time
from myapp.forms import MySelect2Widget, MyBS3TextFieldWidget
import multiprocessing
from flask import Flask, render_template, send_file
import pandas as pd
from myapp.exceptions import MyappException
from sqlalchemy.exc import InvalidRequestError
import datetime
from flask import Response,flash,g
from flask_appbuilder import action
from myapp.views.baseSQLA import MyappSQLAInterface as SQLAInterface
from wtforms.validators import DataRequired, Regexp
from myapp import app, appbuilder
from wtforms import StringField, SelectField
from flask_appbuilder.fieldwidgets import BS3TextFieldWidget, Select2Widget
from myapp.forms import MyBS3TextAreaFieldWidget, MySelect2Widget
from flask import jsonify, Markup, make_response, stream_with_context
from .baseApi import MyappModelRestApi
from flask import g, request, redirect
import urllib
import json, os, sys
import emoji,re
from werkzeug.utils import secure_filename
import pysnooper
from sqlalchemy import or_
from flask_babel import gettext as __
from flask_babel import lazy_gettext as _
from myapp import app, appbuilder, db
from flask_appbuilder import expose
import threading
import queue
from .base import (
DeleteMixin,
MyappFilter,
MyappModelView,
)
from myapp import cache
conf = app.config
logging.getLogger("sseclient").setLevel(logging.INFO)
max_len=2000
class Chat_Filter(MyappFilter):
# @pysnooper.snoop()
def apply(self, query, func):
user_roles = [role.name.lower() for role in list(self.get_user_roles())]
if "admin" in user_roles:
return query
return query.filter(
or_(
self.model.owner.contains(g.user.username),
self.model.owner.contains('*')
)
)
default_icon = '<svg t="1708691376697" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4274" width="50" height="50"><path d="M512 127.0272c-133.4144 0-192.4864 72.1792-192.4864 192.4864V656.384h384.9856V319.5136c-0.0128-120.3072-78.72-192.4864-192.4992-192.4864z" fill="#A67C52" p-id="4275"></path><path d="M560.128 487.9488h-96.256l-24.0512 96.2432v312.8064h144.3584V584.192z" fill="#DBB59A" p-id="4276"></path><path d="M223.2576 704.4992c-45.2352 45.2352-48.128 192.4864-48.128 192.4864H512L415.7568 608.256s-169.8944 73.6256-192.4992 96.2432z m577.4848 0C778.112 681.8688 608.256 608.256 608.256 608.256L512 896.9984h336.8576s-2.88-147.264-48.1152-192.4992z" fill="#48A0DC" p-id="4277"></path><path d="M584.1792 584.192L512 896.9984h24.064l72.1792-168.4352 120.3072-24.064-144.3712-120.3072z m-288.7296 120.3072l120.3072 24.064 72.1792 168.4352H512L439.8208 584.192l-144.3712 120.3072z" fill="#FFFFFF" p-id="4278"></path><path d="M578.2144 270.976c-18.8288 41.6384-83.7888 72.6016-162.4576 72.6016h-47.7824c1.0496 47.1424 5.44 83.7248 23.7312 120.3072 24.064 48.128 73.1648 96.2432 120.3072 96.2432S608.256 512 632.32 463.8848c21.4272-42.8416 23.7696-85.696 24.0256-145.5232-1.4208-27.0464-52.48-47.3856-78.1312-47.3856z" fill="#F6CBAD" p-id="4279"></path><path d="M723.6864 283.8912c-21.0176-75.904-107.8016-132.8-211.6864-132.8s-190.6688 56.896-211.6864 132.8c-16.0512 8.8064-28.928 21.504-28.928 35.6224v48.128c0 26.5728 45.6064 48.128 72.1792 48.128v-96.2432c0-66.4448 75.4048-120.3072 168.4352-120.3072s168.4352 53.8624 168.4352 120.3072v48.128c0 51.5712-32.448 95.552-78.0288 112.6656-6.4384-9.8816-17.5872-16.4224-30.2464-16.4224h-48.128c-19.9296 0-36.096 16.1664-36.096 36.096 0 19.9296 16.1664 36.096 36.096 36.096H572.16c3.52 0 6.912-0.512 10.1248-1.4464 70.9888-9.3312 128.0512-62.8608 142.6432-132.0576 15.4752-8.768 27.6992-21.1712 27.6992-34.9184v-48.128c-0.0128-14.144-12.8896-26.8416-28.9408-35.648z" fill="#4D4D4D" p-id="4280"></path></svg>'
icon_choices=[
'<svg t="1682394317506" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2833" width="50" height="50"><path d="M431.207059 2.199998C335.414129 13.19899 257.420186 72.593947 219.024215 163.78688l-6.199996 14.797989-19.997985 5.799996C104.233299 210.582846 38.840347 279.776795 15.041364 372.369727c-6.999995 27.39698-8.999993 71.393948-4.199997 99.990927 7.399995 44.996967 26.597981 88.592935 53.795961 121.989911l9.198993 11.399991-5.199996 19.597986c-6.799995 26.597981-8.598994 74.593945-3.799997 103.190924 14.799989 87.392936 75.193945 163.58688 155.587886 196.383857 46.395966 18.998986 95.99193 24.797982 142.187895 16.798987l11.599992-1.999998 18.597986 17.598987c30.396978 28.596979 66.593951 48.395965 108.789921 59.994956 25.998981 6.999995 83.193939 8.999993 111.391918 3.599997 53.194961-9.799993 98.391928-33.797975 137.1889-72.794946 27.996979-28.196979 51.194963-64.393953 59.794956-93.591932 2.199998-6.999995 3.599997-8.599994 8.798993-9.799993 12.798991-2.598998 42.595969-13.39799 56.194959-20.196985 35.996974-17.998987 72.793947-49.195964 94.792931-80.593941 19.797985-28.197979 36.196973-65.993952 44.395967-102.990924 1.799999-7.799994 2.799998-24.997982 2.799998-48.995965 0-33.997975-0.6-38.796972-5.799996-58.995956-9.998993-38.795972-25.997981-71.993947-48.395964-100.190927l-10.198993-12.799991 4.399997-17.597987c26.79698-102.790925-16.798988-217.181841-105.391923-276.576797-30.996977-20.598985-58.194957-31.997977-95.59193-40.196971-22.397984-4.999996-70.993948-5.799996-91.991932-1.799998-12.399991 2.399998-12.99999 2.399998-15.799989-1.599999-4.598997-7.199995-34.795975-31.596977-52.794961-42.995969C548.196973 9.598993 486.603019-4.199997 431.207059 2.199998z m45.395967 67.793951c25.197982 2.399998 40.39697 6.399995 61.394955 16.198988 16.797988 7.799994 41.995969 23.397983 41.995969 25.997981 0 0.799999-45.595967 27.79798-101.390926 59.794956-55.995959 32.196976-104.591923 60.794955-108.19092 63.394954-14.799989 10.998992-14.399989 8.399994-14.59999 97.591928-0.2 43.995968-0.999999 110.389919-1.599998 147.387892l-1.199 67.393951-42.596968-24.397982-42.595969-24.397982 0.599999-134.988902c0.799999-154.386887 0.2-147.987892 19.597986-187.383862 29.797978-60.395956 86.792936-100.191927 151.987889-106.591922 8.199994-0.799999 15.398989-1.599999 15.998988-1.599999 0.6-0.2 9.798993 0.6 20.597985 1.599999z m268.977803 82.992939c73.393946 15.399989 132.189903 74.193946 147.387892 147.987892 3.599997 16.998988 4.599997 62.394954 1.599999 67.79495-1.199999 2.399998-22.797983-9.399993-108.590921-59.394957-105.391923-61.394955-107.191921-62.394954-117.989913-62.394954-10.799992 0-13.19999 1.399999-137.989899 73.593946l-126.989907 73.393946-0.599-49.395963c-0.2-27.19798 0.2-49.995963 1-50.795963 3.799997-3.599997 209.182847-121.189911 223.581836-127.989906 35.796974-16.797988 77.992943-21.397984 118.589913-12.798991z m-537.955606 362.369735c3.199998 4.599997 37.596972 25.398981 130.389904 78.993942 69.393949 39.796971 125.988908 72.993947 125.988908 73.593946 0 0.6-5.599996 4.199997-12.598991 8.199994-6.799995 3.799997-25.997981 14.797989-42.596968 24.397982l-30.196978 17.597987-107.790921-62.194954c-59.194957-34.196975-114.589916-67.393951-122.78991-73.793946-29.397978-22.597983-56.395959-63.793953-66.194952-101.190926-6.199995-24.197982-7.199995-60.794955-2.199998-84.992938 7.599994-36.996973 23.397983-66.994951 49.195964-93.792931 17.398987-17.997987 33.197976-29.396978 55.195959-40.195971l16.997988-8.199994 0.999999 127.589907 0.999999 127.589906 4.599997 6.398996zM750.379825 367.169731c56.394959 32.596976 108.389921 62.994954 115.589916 67.593951 43.396968 28.597979 73.593946 75.793944 81.99294 127.989906 3.599997 21.597984 1.599999 61.994955-3.999997 80.992941-8.998993 31.397977-24.996982 58.995957-47.594966 82.593939-17.598987 18.397987-48.195965 38.995971-65.794951 44.395967l-4.599997 1.399999v-124.189909c0-138.188899 0.4-133.389902-13.59899-143.387895-4.399997-2.999998-62.393954-37.196973-128.988906-75.593944-66.594951-38.596972-121.189911-70.393948-121.189911-70.993948-0.2-0.799999 83.592939-49.795964 85.192938-49.995964 0.4 0 46.595966 26.597981 102.991924 59.194957z m-181.385867 50.195963l54.99596 31.596977v127.989906l-55.19596 31.596977-55.194959 31.797977-39.196971-22.598983c-21.797984-12.398991-46.795966-26.99698-55.994959-32.196977l-16.398988-9.799993 0.399999-63.393953 0.6-63.394954 53.99496-31.396977c29.797978-17.198987 54.79596-31.397977 55.59596-31.397977 0.799999-0.2 26.197981 13.99999 56.394958 31.197977z m147.587892 85.592938l41.39697 23.797982v127.389907c0 139.787898-0.4 146.187893-11.999991 178.384869-11.597992 31.796977-36.595973 65.394952-64.593953 86.592937-6.799995 5.199996-21.397984 13.79899-32.396976 18.997986-51.995962 24.997982-109.59092 25.597981-162.586881 1.799999-12.598991-5.799996-40.39697-23.397983-40.396971-25.797982 0-0.6 46.996966-28.196979 104.191924-61.194955 57.394958-32.996976 107.190921-62.794954 110.789919-66.193951 3.799997-3.799997 7.399995-9.999993 8.799993-15.399989 1.599999-6.398995 2.199998-50.994963 2.199999-151.386889 0-78.392943 0.799999-141.987896 1.599999-141.587896 0.799999 0.2 20.197985 11.398992 42.995968 24.597982zM622.590919 732.139464c-3.799997 3.599997-205.38285 119.189913-221.781838 126.989907-26.597981 12.798991-47.995965 17.397987-79.792941 17.397987-19.798985 0-30.197978-0.999999-43.596968-4.199997-68.59395-16.997988-120.589912-66.193952-140.587897-133.787902-5.599996-18.798986-8.599994-57.395958-5.999996-75.193945l1.399999-9.199993 50.395963 29.197979c174.185872 100.391926 165.185879 95.59193 176.185871 95.591929 9.598993-0.2 16.597988-3.799997 137.1879-73.393946l126.989907-73.393946 0.599999 49.395964c0.2 26.99798-0.2 49.795964-0.999999 50.595963z" p-id="2834"></path></svg>',
'<svg t="1686402272731" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2683" width="50" height="50"><path d="M0 895.984494l477.825318 128.015506V256.031012L0 128.015506zM887.425318 0l-384.542701 192.023259L119.456329 0v95.887583L503.378801 198.473652l384.542701-102.462023zM545.802544 256.031012v767.968988l478.197456-128.015506V128.015506z" fill="#006934" p-id="2684"></path></svg>',
'<svg t="1686402383408" class="icon" viewBox="0 0 1068 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9266" width="50" height="50"><path d="M553.2394 778.224h-0.05c-7.99 0-16.092-0.08-24.08-0.231-139.472-2.679-270.492-28.227-368.938-71.938C55.6134 659.625-1.2666 597.068 0.0224 529.92c1.252-65.252 57.605-124.407 158.69-166.561 94.734-39.506 220.97-61.264 355.443-61.264 7.972 0 16.073 0.077 24.073 0.226 139.475 2.673 270.495 28.221 368.947 71.943 104.553 46.43 161.437 108.98 160.144 176.124-1.248 65.262-57.613 124.412-158.693 166.567-94.715 39.508-220.944 61.269-355.386 61.269m-39.085-431.16c-128.673 0-248.76 20.528-338.137 57.802C93.6154 439.232 45.8614 485.124 44.9814 530.786c-0.904 47.198 47.734 96.106 133.445 134.166 93.081 41.336 217.934 65.515 351.543 68.075 7.702 0.152 15.512 0.224 23.224 0.224h0.046c128.648 0 248.713-20.528 338.087-57.796 82.392-34.364 130.153-80.26 131.03-125.926 0.906-47.205-47.732-96.111-133.44-134.162-93.09-41.346-217.94-65.518-351.546-68.08a1224.355 1224.355 0 0 0-23.214-0.223" p-id="9267"></path><path d="M756.9434 1005.515c-59.176 0-131.026-32.34-207.793-93.514-75.843-60.441-149.83-143.674-213.94-240.688-76.91-116.372-130.195-238.778-150.027-344.65-21.076-112.447-1.83-194.769 54.203-231.8C259.5284 81.556 283.4234 74.8 310.3924 74.8c59.177 0 131.035 32.332 207.792 93.514 75.848 60.446 149.832 143.676 213.941 240.694 76.904 116.374 130.182 238.765 150.027 344.642 21.07 112.456 1.82 194.771-54.209 231.803-20.144 13.311-44.028 20.062-71 20.062M310.3934 119.77c-18.002 0-33.549 4.246-46.209 12.614-39.39 26.032-52.073 93.829-34.802 186 18.758 100.116 69.668 216.65 143.35 328.13 61.543 93.136 132.243 172.772 204.446 230.318 67.73 53.982 131.58 83.713 179.766 83.713 18 0 33.55-4.247 46.205-12.606 39.388-26.034 52.08-93.831 34.798-186.008-18.754-100.11-69.662-216.642-143.339-328.127-61.548-93.135-132.243-172.777-204.45-230.319-67.738-53.98-131.576-83.715-179.766-83.715" p-id="9268"></path><path d="M336.2474 1022.571c-21.815 0-41.914-4.884-59.874-14.774-58.847-32.38-84.69-112.869-72.783-226.66 11.211-107.126 54.414-233.438 121.658-355.652C398.6414 292.103 493.0914 178.773 584.3814 114.555c10.16-7.147 24.19-4.707 31.334 5.448 7.145 10.163 4.707 24.185-5.456 31.332-85.885 60.418-175.404 168.245-245.604 295.836-64.417 117.082-105.735 237.347-116.342 338.652-9.757 93.266 8.37 159.819 49.734 182.574 41.363 22.758 107.278 2.454 180.845-55.713 79.893-63.172 159.371-162.448 223.786-279.529 70.2-127.587 113.367-260.914 118.428-365.804 0.605-12.404 11.138-21.978 23.546-21.378 12.402 0.6 21.976 11.14 21.38 23.544-5.38 111.497-50.557 251.94-123.949 385.319-67.24 122.217-150.808 226.318-235.3 293.128-62.346 49.294-120.885 74.607-170.536 74.607" p-id="9269"></path><path d="M655.3074 113.592c0 29.6 12.182 59.013 33.111 79.95 20.93 20.929 50.348 33.11 79.952 33.11 29.602 0 59.014-12.181 79.945-33.11 20.934-20.937 33.112-50.352 33.112-79.95 0-29.602-12.178-59.021-33.112-79.95C827.3854 12.717 797.9724 0.528 768.3714 0.528c-29.604 0-59.023 12.189-79.952 33.112-20.93 20.93-33.11 50.349-33.11 79.95" p-id="9270"></path></svg>',
'<svg t="1686402633537" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="20717" width="50" height="50"><path d="M433.501009 539.493067c0 28.299636-22.999704 51.299341-51.299341 51.29934-28.299636 0-51.299341-22.999704-51.299341-51.29934 0-28.299636 22.999704-51.299341 51.299341-51.299341 28.399635 0 51.299341 22.999704 51.299341 51.299341z m208.297323-51.299341c-28.299636 0-51.299341 22.999704-51.299341 51.299341 0 28.299636 22.999704 51.299341 51.299341 51.29934 28.299636 0 51.299341-22.999704 51.299341-51.29934 0-28.299636-22.999704-51.299341-51.299341-51.299341zM860.895516 696.991043v-5.799926c-5.899924 17.09978-12.899834 33.299572-20.499736 48.99937 1.399982 9.699875 3.999949 23.099703 8.399892 38.599504 21.099729 32.599581 80.598964 74.199046 121.398439 84.898909-21.999717 9.29988-48.499377 13.19983-74.299045 9.199882 23.599697 29.699618 56.899269 56.699271 103.798666 72.699066-109.69859 98.898729-290.496267 101.098701-416.794643 24.299687-24.499685 7.199907-48.399378 10.999859-70.79909 10.999859s-46.399404-3.799951-70.79909-10.999859c-126.398376 76.799013-307.096053 74.599041-416.794644-24.299687 47.899384-16.399789 81.498953-44.099433 105.098649-74.499043-20.899731 0.79999-41.699464-2.999961-59.499235-10.599864 29.899616-7.7999 69.799103-32.299585 97.29875-57.599259 9.599877-25.499672 14.299816-48.199381 16.399789-62.699195-7.599902-15.799797-14.699811-31.999589-20.499737-48.99937v5.799926h-61.999203c-48.899372 0-88.598861-39.69949-88.598861-88.598862v-97.498747c0-33.699567 18.799758-62.99919 46.499402-77.998997 0.899988-138.398221 41.299469-246.496832 120.098457-321.195873C257.203275 37.599517 369.201835 0 512 0c142.798165 0 254.796725 37.599517 332.99572 111.698564 78.798987 74.69904 119.098469 182.797651 120.098457 321.195873 27.699644 14.999807 46.499402 44.299431 46.499402 77.998997v97.498747c0 48.899372-39.69949 88.598861-88.598861 88.598862h-62.099202zM823.99599 539.893062c0-22.799707-1.199985-44.199432-2.899962-64.999165-39.999486-36.59953-113.098547-64.199175-205.397361-73.89905 14.599812 13.099832 27.19965 30.599607 33.899565 55.599285-53.599311-39.199496-165.997867-51.299341-203.397386-93.3988-59.399237-39.299495-74.999036-95.69877-75.49903-72.399069-2.399969 111.798563-81.69895 198.997443-169.697819 211.297284-0.599992 12.299842-0.999987 24.699683-0.999987 37.699516 0 47.299392 7.699901 90.398838 20.799732 129.198339 49.399365 59.599234 131.098315 76.499017 203.397386 80.998959 13.399828-21.299726 43.499441-36.299533 78.59899-36.299533 47.49939 0 86.098893 27.399648 86.098894 61.099215s-38.499505 61.099215-86.098894 61.099215c-36.59953 0-67.69913-16.199792-80.198969-38.999499-50.499351-2.899963-105.998638-11.499852-155.498002-34.899552C336.702253 864.588889 444.600866 918.788192 512 918.788192c105.898639 0 311.99599-133.698282 311.99599-378.89513z m53.599312-117.598489h34.099561C906.094935 180.797676 768.196707 53.199316 512 53.199316S117.905065 180.797676 112.305137 422.294573h34.099561c12.499839-81.398954 38.799501-148.398093 78.798988-199.797432C289.002866 140.498194 385.501626 98.898729 512 98.898729c126.598373 0 223.097133 41.599465 286.796314 123.598412 39.999486 51.399339 66.399147 118.398478 78.798988 199.797432z" fill="#626264" p-id="20718"></path></svg>'
]
prompt_default= __('''
你是一个AI助手以下```中的内容是你已知的知识。
```
{{knowledge}}
```
你的任务是根据上面给出的知识,回答用户的问题。当你回答时,你的回复必须遵循以下约束:
1. 只回复以上知识中包含的信息。
2. 当你回答问题需要一些额外知识的时候,只能使用非常确定的知识和信息,以确保不会误导用户。
3. 如果你无法确切回答用户问题的答案,请直接回复"不知道",并给出原因。
4. 使用中文回答。
你需要回答:
{{query}}
'''.strip())
class Chat_View_Base():
datamodel = SQLAInterface(Chat)
route_base = '/chat_modelview/api'
label_title = _('智能体配置')
base_order = ("id", "desc")
order_columns = ['id']
base_filters = [["id", Chat_Filter, lambda: []]] # 设置权限过滤器
spec_label_columns = {
"chat_type": _("交互类型"),
"hello": _("欢迎语"),
"tips": _("输入示例"),
"knowledge": _("知识库"),
"service_type": _("接口类型"),
"service_config": _("接口配置"),
"session_num": _("上下文条数"),
"prompt": _("提示词模板")
}
list_columns = ['name', 'icon', 'label', 'chat_type', 'service_type', 'owner', 'session_num', 'hello']
cols_width = {
"name": {"type": "ellip1", "width": 100},
"label": {"type": "ellip2", "width": 150},
"chat_type": {"type": "ellip1", "width": 100},
"hello": {"type": "ellip1", "width": 200},
"tips": {"type": "ellip1", "width": 200},
"service_type": {"type": "ellip1", "width": 100},
"owner": {"type": "ellip1", "width": 200},
"session_num":{"type": "ellip1", "width": 100},
"knowledge": {"type": "ellip1", "width": 200},
"prompt": {"type": "ellip1", "width": 200},
"service_config": {"type": "ellip1", "width": 200},
}
default_icon = '<svg t="1683877543698" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4469" width="50" height="50"><path d="M894.1 355.6h-1.7C853 177.6 687.6 51.4 498.1 54.9S148.2 190.5 115.9 369.7c-35.2 5.6-61.1 36-61.1 71.7v143.4c0.9 40.4 34.3 72.5 74.7 71.7 21.7-0.3 42.2-10 56-26.7 33.6 84.5 99.9 152 183.8 187 1.1-2 2.3-3.9 3.7-5.7 0.9-1.5 2.4-2.6 4.1-3 1.3 0 2.5 0.5 3.6 1.2a318.46 318.46 0 0 1-105.3-187.1c-5.1-44.4 24.1-85.4 67.6-95.2 64.3-11.7 128.1-24.7 192.4-35.9 37.9-5.3 70.4-29.8 85.7-64.9 6.8-15.9 11-32.8 12.5-50 0.5-3.1 2.9-5.6 5.9-6.2 3.1-0.7 6.4 0.5 8.2 3l1.7-1.1c25.4 35.9 74.7 114.4 82.7 197.2 8.2 94.8 3.7 160-71.4 226.5-1.1 1.1-1.7 2.6-1.7 4.1 0.1 2 1.1 3.8 2.8 4.8h4.8l3.2-1.8c75.6-40.4 132.8-108.2 159.9-189.5 11.4 16.1 28.5 27.1 47.8 30.8C846 783.9 716.9 871.6 557.2 884.9c-12-28.6-42.5-44.8-72.9-38.6-33.6 5.4-56.6 37-51.2 70.6 4.4 27.6 26.8 48.8 54.5 51.6 30.6 4.6 60.3-13 70.8-42.2 184.9-14.5 333.2-120.8 364.2-286.9 27.8-10.8 46.3-37.4 46.6-67.2V428.7c-0.1-19.5-8.1-38.2-22.3-51.6-14.5-13.8-33.8-21.4-53.8-21.3l1-0.2zM825.9 397c-71.1-176.9-272.1-262.7-449-191.7-86.8 34.9-155.7 103.4-191 190-2.5-2.8-5.2-5.4-8-7.9 25.3-154.6 163.8-268.6 326.8-269.2s302.3 112.6 328.7 267c-2.9 3.8-5.4 7.7-7.5 11.8z" fill="#2c2c2c" p-id="4470"></path></svg>'
knowledge_config = '''
{
"type": "api|file",
"url":"", # api请求地址
"headers": {}, # api请求的附加header
"data": {}, # api请求的附加data
"file":"/mnt/$username/", # 文件地址,或者目录地址,可以多个文件
"upload_url": "", # 知识库的上传地址
"recall_url": "", # 召回地址
}
'''
service_config = '''
openai接口类型
{
"llm_url": "", # 请求的url
"llm_headers": {
"xxxxx": "xxxxxx" # 额外添加的header
},
"llm_tokens": [], # chatgpt的token池
"llm_data": {
"xxxxx": "xxxxxx" # 额外添加的json参数
},
"stream": "false" # 是否流式响应
}
aihub接口类型
{
"url": "aihub应用请求地址",
"data": {
"prompt": "$text" # 输入变量名和输入变量类型
# $text为用户输入的文本
# $image为用户输入的图片
# $audio为用户输入的音频
# $video为用户输入的视频
},
"output": "image", # 支持textmarkdownimageaudiovideo
"req_num":1, # 请求多少次模型默认1次在文生图中可以使用4次
"stream": "true"
}
'''
options_demo={"xAxis":{"type":"category","data":["Mon","Tue","Wed","Thu","Fri","Sat","Sun"]},"yAxis":{"type":"value"},"series":[{"data":[150,230,224,218,135,147,260],"type":"line"}]}
test_api_resonse = {
# "text":"这里是文本响应体",
"echart": json.dumps(options_demo)
}
add_fieldsets = [
(
_('基础配置'),
{"fields": ['name','icon','label','doc','owner'], "expanded": True},
),
(
_('提示词配置'),
{"fields": ['chat_type','hello','tips','knowledge','prompt','session_num'], "expanded": True},
),
(
_('模型服务'),
{"fields": ['service_type','service_config','expand'], "expanded": True},
)
]
edit_fieldsets=add_fieldsets
add_form_extra_fields = {
"name": StringField(
label= _('名称'),
description= _('英文名(小写字母、数字、- 组成)最长50个字符'),
default='',
widget=BS3TextFieldWidget(),
validators=[DataRequired(),Regexp("^[a-z][a-z0-9\-]*[a-z0-9]$")]
),
"label": StringField(
label= _('标签'),
default='',
description = _('中文名'),
widget=BS3TextFieldWidget(),
validators=[DataRequired()]
),
"icon": StringField(
label= _('图标'),
default=default_icon, # random.choice(icon_choices),
description= _('svg格式图标图标宽高设置为50*50<a target="_blank" href="https://www.iconfont.cn/">iconfont</a>'),
widget=BS3TextFieldWidget(),
# choices=[[str(x),Markup(icon_choices[x])] for x in range(len(icon_choices))],
validators=[DataRequired()]
),
"owner": StringField(
label= _('责任人'),
default='*',
description= _('可见用户,*表示所有用户可见,将责任人列为第一管理员,逗号分割多个责任人'),
widget=BS3TextFieldWidget(),
validators=[DataRequired()]
),
"chat_type": SelectField(
label= _('对话类型'),
description='',
default='text',
widget=MySelect2Widget(),
choices=[['text', _('文本对话')], ['digital_human', _("数字人")]],
validators=[]
),
"service_type": SelectField(
label= _('服务类型'),
description= _('接口类型并不一定是openai只需要符合http请求响应格式即可'),
widget=Select2Widget(),
default='openai',
choices=[[x, x] for x in ["openai",'aihub','chatbi','autogpt',_('召回列表')]],
validators=[]
),
"service_config": StringField(
label= _('接口配置'),
default=json.dumps({
"llm_url": "",
"llm_tokens": [],
"stream": "true"
},indent=4,ensure_ascii=False),
description= _('接口配置,每种接口类型配置参数不同'),
widget=MyBS3TextAreaFieldWidget(rows=5, tips=Markup('<pre><code>' + service_config + "</code></pre>")),
validators=[DataRequired()]
),
"knowledge": StringField(
label= _('知识库'),
default=json.dumps({
"type": "file",
"file": [__("文件地址")]
},indent=4,ensure_ascii=False),
description= _('先验知识配置。如果先验字符串少于1800个字符可以直接填写字符串否则需要使用json配置'),
widget=MyBS3TextAreaFieldWidget(rows=5, tips=Markup('<pre><code>' + knowledge_config + "</code></pre>")),
validators=[]
),
"prompt":StringField(
label= _('提示词'),
default=prompt_default,
description= _('提示词模板,包含{{knowledge}}知识库召回内容,{{history}}为多轮对话,{{query}}为用户的问题'),
widget=MyBS3TextAreaFieldWidget(rows=5),
validators=[]
),
"tips": StringField(
label= _('输入示例'),
default='',
description= _('提示输入,多个提示输入,多行配置'),
widget=MyBS3TextAreaFieldWidget(rows=3),
validators=[]
),
"expand": StringField(
label= _('扩展'),
default=json.dumps({
"index":int(time.time())
},indent=4,ensure_ascii=False),
description= _('配置扩展参数,"index":控制显示顺序,"isPublic":控制是否为公共应用'),
widget=MyBS3TextAreaFieldWidget(),
validators=[]
),
}
from copy import deepcopy
edit_form_extra_fields = add_form_extra_fields
# @pysnooper.snoop()
def pre_update_web(self, chat=None):
pass
self.edit_form_extra_fields['name'] = StringField(
_('名称'),
description=_('英文名(小写字母、数字、- 组成)最长50个字符'),
default='',
widget=MyBS3TextFieldWidget(readonly=True if chat else False),
validators=[DataRequired(), Regexp("^[a-z][a-z0-9\-]*[a-z0-9]$")]
)
def pre_add_web(self):
self.default_filter = {
"expand": '"isPublic": true'
}
self.pre_update_web()
# 如果传上来的有文件
# @pysnooper.snoop()
def pre_add_req(self, req_json=None):
# 针对chat界面的页面处理
# chat界面前端会有files参数
if req_json and 'files' in req_json:
expand = json.loads(req_json.get('expand', '{}'))
name=req_json.get('name','')
# 在添加的时候做一些特殊处理
if request.method=='POST':
name = f'{g.user.username}-faq-{uuid.uuid4().hex[:4]}'
req_json['name'] = name
req_json['hello'] = __('自动为您创建的私人对话,不使用上下文,左下角可以清理会话和修改知识库配置')
req_json['session_num'] = '0'
req_json['icon'] = default_icon
files_path = []
files = req_json['files']
if type(files) != list:
files = [files]
exist_knowledge = {}
if name:
chat = db.session.query(Chat).filter_by(name=name).first()
if chat:
try:
exist_knowledge = json.loads(chat.knowledge)
except:
exist_knowledge = {}
file_arr = []
for file in files:
file_name = file.get('name', '')
file_type = file.get("type", '')
file_content = file.get("content", '') # 最优最新一次上传的才有这个。
file_arr.append({
"name": file_name,
"type": file_type
})
# 拼接文件保存路径
file_path = f'/data/k8s/kubeflow/global/knowledge/{name}/{file_name}'
files_path.append(file_path)
# 如果有新创建的文件内容
if file_content:
content = base64.b64decode(file_content)
os.makedirs(os.path.dirname(file_path), exist_ok=True)
file = open(file_path, mode='wb')
file.write(content)
file.close()
knowledge = {
"type": "file",
"file": files_path,
# "file_arr": file_arr,
"upload_url": "http://chat-embedding.aihub:80/aihub/chat-embedding/api/upload_files",
"recall_url": "http://chat-embedding.aihub:80/aihub/chat-embedding/api/recall"
}
expand['fileMetaList']=file_arr
if exist_knowledge.get('status',''):
knowledge['status']=exist_knowledge.get('status','')
knowledge['update_time'] = exist_knowledge.get('update_time','')
knowledge['upload_url'] = exist_knowledge.get('upload_url', '')
knowledge['recall_url'] = exist_knowledge.get('recall_url', '')
req_json['knowledge'] = json.dumps(knowledge, indent=4, ensure_ascii=False)
req_json['expand']=json.dumps(expand, indent=4, ensure_ascii=False)
del req_json['files']
return req_json
pre_update_req = pre_add_req
# @pysnooper.snoop(watch_explode=('req_json',))
# def pre_update_req(self, req_json=None):
# print(g.user.username)
# owner = req_json.get('owner','')
# if g.user.username in owner:
# self.pre_add_req(req_json)
# else:
# flash('只有创建者或管理员可以配置', 'warning')
# raise MyappException('just creator can add/edit')
# @pysnooper.snoop(watch_explode=('item',))
def pre_add(self, item):
if not item.knowledge or not item.knowledge.strip():
item.knowledge = '{}'
if not item.owner:
item.owner = g.user.username
if not item.icon:
item.icon = default_icon # random.choice(icon_choices)
if not item.chat_type:
item.chat_type = 'text'
if not item.service_type:
item.service_type = 'openai'
if not item.service_config or not item.service_config.strip():
service_config = {
"llm_url": "",
"llm_tokens": [],
"stream": "true"
}
item.service_config = json.dumps(service_config)
service_config = json.loads(item.service_config) if item.service_config.strip()else {}
expand = json.loads(item.expand) if item.expand.strip() else {}
knowledge = json.loads(item.knowledge) if item.knowledge.strip() else {}
# 配置扩展字段
if item.expand and item.expand.strip():
item.expand=json.dumps(json.loads(item.expand),indent=4,ensure_ascii=False)
try:
expand = json.loads(item.expand) if item.expand else {}
expand['isPublic'] = expand.get('isPublic',True)
# 把之前的属性更新上,避免更新的时候少填了什么属性
src_expand = self.src_item_json.get("expand",'{}')
if src_expand:
src_expand = json.loads(src_expand)
src_expand.update(expand)
expand = src_expand
item.expand = json.dumps(expand, indent=4, ensure_ascii=False)
except Exception as e:
print(e)
# 如果是私有应用添加一些file_arr
if not expand.get('isPublic', True):
fileMetaList = expand.get('fileMetaList',[])
files = knowledge.get('file',[])
# 如果有文件,但是没有文件属性信息,则更新
if not fileMetaList and files:
expand['fileMetaList'] = []
if type(files)!=list:
files = [files]
for file in files:
name = os.path.basename(file)
if '.' in name:
ext = name[name.rindex('.')+1:]
file_map={
"map":"application/octet-stream",
"csv":"text/csv",
"pdf":"application/pdf",
"txt":"text/plain"
}
file_attr = {
"name": name,
"type": file_map[ext]
}
expand['fileMetaList'].append(file_attr)
# 如果有知识库
if knowledge.get('file',[]) or knowledge.get('url',''):
item.prompt = prompt_default
# 如果没有,就自动多轮对话
else:
knowledge['status']='在线'
knowledge['update_time'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
item.session_num=10
item.prompt = '''
{{history}}
Human:{{query}}
AI:
'''.strip()
# item.icon = item.icon.replace('width="200"','width="50"').replace('height="200"','height="50"')
item.icon = re.sub(r'width="\d+(\.\d+)?(px)?"', f'width="50px"', item.icon)
item.icon = re.sub(r'height="\d+(\.\d+)?(px)?"', f'height="50px"', item.icon)
if '{{query}}' not in item.prompt:
item.prompt = item.prompt+"\n{{query}}\n"
item.service_config = json.dumps(service_config, indent=4, ensure_ascii=False)
item.expand = json.dumps(expand, indent=4, ensure_ascii=False)
try:
item.knowledge = json.dumps(knowledge, indent=4, ensure_ascii=False)
except Exception as e:
print(e)
# @pysnooper.snoop()
def pre_update(self, item):
if g.user.username in self.src_item_json.get('owner','') or g.user.is_admin():
self.pre_add(item)
else:
flash(__('只有创建者或管理员可以配置'), 'warning')
raise MyappException('just creator can add/edit')
# @pysnooper.snoop(watch_explode=('item',))
def post_add(self, item):
try:
if not self.src_item_json:
self.src_item_json = {}
src_file = json.loads(self.src_item_json.get('knowledge', '{}')).get("file", '')
last_time = json.loads(self.src_item_json.get('knowledge', '{}')).get("update_time",'')
if last_time:
last_time = datetime.datetime.strptime(last_time,'%Y-%m-%d %H:%M:%S')
knowledge_config = json.loads(item.knowledge) if item.knowledge else {}
exist_file = knowledge_config.get("file", '')
# 文件变了,或者更新时间过期了,都要重新更新
if exist_file and (src_file != exist_file or not last_time or (datetime.datetime.now()-last_time).total_seconds()>3600):
self.upload_knowledge(chat=item, knowledge_config=knowledge_config)
except Exception as e:
print(e)
def post_update(self, item):
self.post_add(item)
# 按配置的索引进行排序
def post_list(self, items):
from myapp.utils import core
return core.sort_expand_index(items)
# print(_response['data'])
# _response['data'] = sorted(_response['data'],key=lambda chat:float(json.loads(chat.get('expand','{}').get("index",1))))
@action("copy", "复制", confirmation= '复制所选记录?', icon="fa-copy", multiple=True, single=True)
def copy(self, chats):
if not isinstance(chats, list):
chats = [chats]
try:
for chat in chats:
new_chat = chat.clone()
new_chat.name = new_chat.name+"-copy"
new_chat.created_on = datetime.datetime.now()
new_chat.changed_on = datetime.datetime.now()
db.session.add(new_chat)
db.session.commit()
except InvalidRequestError:
db.session.rollback()
except Exception as e:
print(e)
raise e
return redirect(request.referrer)
@expose('/chat/<chat_name>', methods=['POST', 'GET'])
# @pysnooper.snoop()
def chat(self, chat_name, args=None):
if chat_name == 'chatbi':
files = os.listdir('myapp/utils/echart/')
files = ['area-stack.json', 'rose.json', 'mix-line-bar.json', 'pie-nest.json', 'bar-stack.json',
'candlestick-simple.json', 'graph-simple.json', 'tree-polyline.json', 'sankey-simple.json',
'radar.json', 'sunburst-visualMap.json', 'parallel-aqi.json', 'funnel.json',
'sunburst-visualMap.json', 'scatter-effect.json']
files = [os.path.join('myapp/utils/echart/',file) for file in files if '.json' in file]
return {
"status": 0,
"finish": False,
"message": 'success',
"result": [
{
"text":"未配置后端模型,这里生成示例看板\n\n",
# "echart": json.dumps(options_demo)
"echart":open(random.choice(files)).read()
}
]
}
if not args:
args = request.get_json(silent=True)
if not args:
args = {}
session_id = args.get('session_id', 'xxxxxx')
request_id = args.get('request_id', str(datetime.datetime.now().timestamp()))
search_text = args.get('search_text', '')
search_audio = args.get('search_audio', None)
search_image = args.get('search_image', None)
search_video = args.get('search_video', None)
username = args.get('username', '')
enable_tts = args.get('enable_tts', False)
if not username:
username = g.user.username
if g:
g.after_message=''
stream = args.get('stream', False)
if str(stream).lower()=='false':
stream = False
begin_time = datetime.datetime.now()
chat = db.session.query(Chat).filter_by(name=chat_name).first()
if not chat:
return jsonify({
"status": 1,
"message": __('聊天不存在'),
"result": []
})
# 如果超过一定聊天数目,则禁止
# if username not in conf.get('ADMIN_USER').split(','):
# log_num = db.session.query(ChatLog).filter(ChatLog.username==username).filter(ChatLog.answer_status=='成功').filter(ChatLog.created_on>datetime.datetime.now().strftime('%Y-%m-%d')).all()
# if len(log_num)>10:
# return jsonify({
# "status": 1,
# "finish": 0,
# "message": '聊天次数达到上限每人每天仅限10次',
# "result": [{"text":"聊天次数达到上限每人每天仅限10次"}]
# })
stream_config = json.loads(chat.service_config).get('stream', True)
if stream_config==False or str(stream_config).lower() == 'false':
stream = False
enable_history = args.get('history', True)
chatlog=None
# 添加数据库记录
try:
text = emoji.demojize(search_text)
search_text = re.sub(':\S+?:', ' ', text) # 去除表情
chatlog = ChatLog(
username=str(username),
chat_id=chat.id,
query=search_text,
answer="",
manual_feedback="",
answer_status="created",
answer_cost='0',
err_msg="",
created_on=str(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())),
changed_on=str(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
)
db.session.add(chatlog)
db.session.commit()
except Exception as e:
db.session.rollback()
print(e)
return_result = []
return_message = ''
return_status = 1
finish = ''
answer_status = 'making'
err_msg = ''
history = []
try:
if enable_history and int(chat.session_num):
history = cache.get('chat_' + session_id) # 所有需要的上下文
if not history:
history = []
except Exception as e:
print(e)
if chat.service_type.lower() == 'openai' or chat.service_type.lower() == 'chatgpt4' or chat.service_type.lower() == 'chatgpt3.5':
if stream:
if chatlog:
chatlog.answer_status = 'push chatgpt'
db.session.commit()
res = self.chatgpt(
chat=chat,
session_id=session_id,
search_text=search_text,
enable_history=enable_history,
history=history,
chatlog_id=chatlog.id,
stream=True
)
if chatlog:
chatlog.answer_status = '成功'
db.session.commit()
return res
else:
if chatlog:
chatlog.answer_status = 'push chatgpt'
db.session.commit()
return_status, text = self.chatgpt(
chat=chat,
session_id=session_id,
search_text=search_text,
enable_history=enable_history,
history=history,
chatlog_id=chatlog.id,
stream=False
)
return_message = __('失败') if return_status else __("成功")
answer_status = return_message
return_result = [
{
"text": text
}
]
if chat.service_type.lower() == 'openai' or chat.service_type.lower() == 'chatgpt4' or chat.service_type.lower() == 'chatgpt3.5':
if stream:
if chatlog:
chatlog.answer_status = 'push chatgpt'
db.session.commit()
res = self.chatgpt(
chat=chat,
session_id=session_id,
search_text=search_text,
enable_history=enable_history,
history=history,
chatlog_id=chatlog.id,
stream=True
)
if chatlog:
chatlog.answer_status = __('成功')
db.session.commit()
return res
else:
if chatlog:
chatlog.answer_status = 'push chatgpt'
db.session.commit()
return_status, text = self.chatgpt(
chat=chat,
session_id=session_id,
search_text=search_text,
enable_history=enable_history,
history=history,
chatlog_id=chatlog.id,
stream=False
)
return_message = __('失败') if return_status else __("成功")
answer_status = return_message
return_result = [
{
"text": text
}
]
# 仅返回召回列表
if __('召回列表') in chat.service_type.lower():
knowledge = self.get_remote_knowledge(chat, search_text,score=True)
knowledge = [__("内容:\n\n ") + x['context'].replace('\n','\n ') + "\n\n" + __("得分:\n\n ") + str(x.get('score', '')) + "\n\n" + __("文件:\n\n ") + str(x.get('file', '')) for x in knowledge]
if knowledge:
text = '\n\n-------\n'.join(knowledge)
return_message = __("成功")
answer_status = return_message
return_result = [
{
"text": text
}
]
else:
return_result = [
{
"text": __('召回内容为空')
}
]
# 多轮召回方式
if __('多轮') in chat.service_type.lower():
knowledge = self.get_remote_knowledge(chat, search_text)
if knowledge:
return_message = __("成功")
answer_status = return_message
return_result = [
{
"text": text
} for text in knowledge
]
else:
return_result = [
{
"text": __('未找到相关内容')
}
]
if chat.service_type.lower() == 'aihub':
return_status, return_res = self.aigc4(chat=chat, search_text=search_text)
if not return_status:
return return_res
# 添加数据库记录
if chatlog:
try:
canswar = "\n".join(item.get('text','') for item in return_result)
chatlog.query = search_text
# chatlog.answer = canswar # 内容太多了
chatlog.answer_cost = str((datetime.datetime.now()-begin_time).total_seconds())
chatlog.answer_status=answer_status,
chatlog.err_msg = return_message
db.session.commit()
# 正确响应的话,才设置为历史状态
if history != None and not return_status:
history.append((search_text, canswar))
history = history[0 - int(chat.session_num):]
try:
cache.set('chat_' + session_id, history, timeout=300) # 人连续对话的时间跨度
except Exception as e:
print(e)
except Exception as e:
db.session.rollback()
print(e)
return {
"status": return_status,
"finish": finish,
"message": return_message,
"result": [x for x in return_result if x]
}
# @pysnooper.snoop()
def upload_knowledge(self,chat,knowledge_config):
"""
上传文件到远程服务
@param chat: 场景对象
@param knowledge_config: 知识库配置
@return:
"""
# 没有任何值就是空的
files=[]
if not knowledge_config:
return ''
knowledge_type = knowledge_config.get("type", 'str')
if knowledge_type == 'str':
knowledge = knowledge_config.get("content", '')
if knowledge:
file_path = f'knowledge/{chat.name}/{str(time.time()*1000)}'
os.makedirs(os.path.dirname(file_path),exist_ok=True)
file = open(file_path,mode='w')
file.write(knowledge)
file.close()
files.append(file_path)
if knowledge_type == 'api':
url = knowledge_config.get("url", '')
if not url:
return ''
headers = knowledge_config.get("headers", {})
data = knowledge_config.get("data", {})
if data:
res = requests.post(url, headers=headers, json=data,verify=False)
else:
res = requests.get(url, headers=headers,verify=False)
if res.status_code == 200:
# 获取文件名和文件格式
filename = os.path.basename(url)
file_format = os.path.splitext(filename)[1]
# 拼接文件保存路径
file_path = f'knowledge/{chat.name}/{str(time.time() * 1000)}'
if file_format:
file_path = file_path+"."+file_format
os.makedirs(os.path.dirname(file_path), exist_ok=True)
file = open(file_path, mode='wb')
file.write(res.content)
file.close()
files.append(file_path)
if knowledge_type == 'file':
file_paths = knowledge_config.get("file", '')
if type(file_paths)!=list:
file_paths = [file_paths]
for file_path in file_paths:
if re.match('^/mnt', file_path):
file_path = "/data/k8s/kubeflow/pipeline/workspace" + file_path.replace("/mnt", '')
if os.path.exists(file_path):
if os.path.isfile(file_path):
files.append(file_path)
if os.path.isdir(file_path):
for root, dirs_temp, files_temp in os.walk(file_path):
for name in files_temp:
one_file_path = os.path.join(root, name)
# print(one_file_path)
if os.path.isfile(one_file_path):
files.append(one_file_path)
if knowledge_type == 'sql':
return ''
service_config = json.loads(chat.service_config)
upload_url = knowledge_config.get("upload_url", '')
if files:
if '127.0.0.1' in request.host_url:
print('发现的知识库文件:',files)
knowledge = json.loads(chat.knowledge) if chat.knowledge else {}
knowledge['update_time'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
knowledge['status'] = "uploading"
chat.knowledge = json.dumps(knowledge,indent=4,ensure_ascii=False)
db.session.commit()
files_content = [('files', (os.path.basename(file), open(file, 'rb'))) for file in files]
data = {"chat_id": chat.name}
response = requests.post(upload_url, files=files_content, data=data,verify=False)
print('上传私有知识响应:',json.dumps(json.loads(response.text), ensure_ascii=False, indent=4))
knowledge['update_time'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
knowledge['status'] = "online"
chat.knowledge = json.dumps(knowledge, indent=4, ensure_ascii=False)
db.session.commit()
else:
from myapp.tasks.async_task import upload_knowledge
kwargs = {
"files": files,
"chat_id":chat.name,
"upload_url":upload_url,
# 可以根据不同的配置来决定对数据做什么处理,比如
# "config":{
# "file":{
# "cube-studio.csv":{
# "embedding_columns": ["问题"],
# "llm_columns": ['问题', '答案'],
# "keywork_columns": [],
# "summary_columns": []
# }
# }
# }
}
upload_knowledge.apply_async(kwargs=kwargs)
all_chat_knowledge = {}
# 根据配置获取远程的先验知识
# @pysnooper.snoop()
def get_remote_knowledge(self,chat,search_text,score=False):
"""
召回服务
@param chat: 场景对象
@param search_text: 搜索文本
@return: 获取召回的前3个文本
"""
knowledge=[]
try:
service_config = json.loads(chat.service_config)
knowledge_config = json.loads(chat.knowledge)
# 时间过时就发过去重新更新知识库
update_time = knowledge_config.get("update_time",'')
if update_time:
update_time = datetime.datetime.strptime(update_time,'%Y-%m-%d %H:%M:%S')
if not update_time or (datetime.datetime.now()-update_time).total_seconds()>3600 or knowledge_config.get("status","")!='在线':
self.upload_knowledge(chat=chat,knowledge_config=knowledge_config)
# 进行召回
recall_url = knowledge_config.get("recall_url", '')
if recall_url:
data={
"knowledge_base_id":chat.name,
"question":search_text,
"history":[]
}
headers={
"Content-Type": "application/json",
"Accept": "application/json"
}
res = requests.post(recall_url,json=data,headers=headers,timeout=5,verify=False)
if res.status_code==200:
recall_result = res.json()
print('召回响应',json.dumps(recall_result,indent=4,ensure_ascii=False))
if 'result' in recall_result:
knowledge= recall_result['result']
if not score:
knowledge = [x['context'] for x in knowledge]
knowledge = knowledge[0:3]
else:
source_documents = recall_result.get('source_documents',[])
# source_documents = sorted(source_documents,key=lambda item:float(item.get('score',1))) # 按分数值排序,应该走排序算法
source_documents = source_documents[:3] # 只去前面3个
all_sources = []
for index,item in enumerate(source_documents):
if int(item.get('score', 1)) > float(knowledge_config.get("min_score",0)): # 根据最小分数值确定
source = item.get('source', '')
if source:
all_sources.append(source)
knowledge.append(item['context'])
all_sources = [x.strip() for x in list(set(all_sources)) if x.strip()]
after_message = ''
if all_sources:
for index,source in enumerate(all_sources):
source_url = request.host_url.rstrip('/') + f"/aitalk_modelview/api/file/{chat.name}/" + source.lstrip('/')
after_message += f'[文档{index}]({source_url}) '
# g.after_message = g.after_message + f"\n\n{after_message}"
except Exception as e:
print(e)
return knowledge
# @pysnooper.snoop()
# 获取header和url
def get_llm_url_header(self,chat,stream=False):
"""
获取访问地址和有效token
@param chat:
@param stream:
@return:
"""
url = json.loads(chat.service_config).get("llm_url", '')
headers = json.loads(chat.service_config).get("llm_headers", {})
if stream:
headers['Accept'] = 'text/event-stream'
else:
headers['Accept'] = 'application/json'
if not url:
llm_url = conf.get('CHATGPT_CHAT_URL', 'https://api.openai.com/v1/chat/completions')
if llm_url:
if type(llm_url) == list:
llm_url = random.choice(llm_url)
else:
llm_url = llm_url
url=llm_url
llm_tokens = json.loads(chat.service_config).get("llm_tokens", [])
llm_token = ''
if llm_tokens:
if type(llm_tokens) != list:
llm_tokens = [llm_tokens]
# 如果有过多错误的token则直接废弃
error_token = json.loads(chat.service_config).get("miss_tokens",{})
if error_token:
right_llm_tokens= [token for token in llm_token if int(error_token.get(token,0))<100]
if right_llm_tokens:
llm_tokens=right_llm_tokens
llm_token = random.choice(llm_tokens)
headers['Authorization'] = 'Bearer ' + llm_token # openai的接口
headers['api-key'] = llm_token # 微软的接口
else:
llm_tokens = conf.get('CHATGPT_TOKEN','')
if llm_tokens:
if type(llm_tokens)==list:
llm_token = random.choice(llm_tokens)
else:
llm_token = llm_tokens
headers['Authorization'] = 'Bearer ' + llm_token # openai的接口
headers['api-key']=llm_token # 微软的接口
return url,headers,llm_token
# 组织提问词
# @pysnooper.snoop(watch_explode=('system_content'))
def generate_prompt(self,chat, search_text, enable_history, history=[]):
messages = chat.prompt
messages = messages.replace('{{query}}', search_text)
# 获取知识库
if '{{knowledge}}' in chat.prompt:
knowledge = chat.knowledge # 直接使用原文作为知识库
try:
knowledge_config = json.loads(chat.knowledge)
try:
knowledge = self.get_remote_knowledge(chat, search_text)
except Exception as e1:
print(e1)
except Exception as e:
print(e)
if type(knowledge) != list:
knowledge = [str(knowledge)]
knowledge = [x for x in knowledge if x.strip()]
# 拼接请求体
print('召回知识库', json.dumps(knowledge, indent=4, ensure_ascii=False))
added_knowledge = []
# 添加私有知识库要满足token限制
for item in knowledge:
# 至少要保留前置语句,后置语句,搜索语句。
if sum([len(x) for x in added_knowledge]) < (max_len - len(messages) - len(item)):
added_knowledge.append(item)
added_knowledge = '\n\n'.join(added_knowledge)
messages = messages.replace('{{knowledge}}', added_knowledge)
if '{{history}}' in chat.prompt:
# 拼接上下文
# 先从后往前加看看个数是不是超过了门槛
added_history=[]
if enable_history and history:
for index in range(len(history) - 1, -1, -1):
faq = history[index]
added_faq="Human: %s\nAI: %s"%(faq[0],faq[1])
added_history_len = sum([len(x) for x in added_faq])
if len(added_faq) < (max_len-len(messages)-added_history_len):
added_history.insert(0,added_faq)
else:
break
added_history = '\n'.join(added_history)
messages = messages.replace('{{history}}', added_history)
print(messages)
return [{'role': 'user', 'content': messages}]
# 生成openai相应格式
def make_openai_res(self,message,stream=True):
back = {
"id": "chatcmpl-7OPUNz80uRGVKLcBMW8aKZT9dg938",
"object": "chat.completion.chunk" if stream else 'chat.completion',
"created": int(time.time()),
"model": "gpt-4-turbo-2024-04-09",
"choices": [
{
"index": 0,
"finish_reason": None,
"delta": {
"role": "assistant",
"content":message
},
"message":{
"role": "assistant",
"content": message
}
}
],
"usage": None
}
return json.dumps(back)
@expose('/chat/chatgpt/<chat_name>', methods=['POST', 'GET'])
# @pysnooper.snoop()
def chatgpt_api(self, chat_name):
"""
为调用chatgpt单独提供的接口
@param chat_name:
@return:
"""
args = request.get_json(silent=True)
chat = db.session.query(Chat).filter_by(name=chat_name).first()
session_id = args.get('session_id', 'xxxxxx')
request_id = args.get('request_id', str(datetime.datetime.now().timestamp()))
search_text = args.get('search_text', '')
return_status, text = self.chatgpt(
chat=chat,
session_id=session_id,
search_text=search_text,
enable_history=False,
history=[],
chatlog_id=None,
stream=False
)
return jsonify({
"status": return_status,
"message": __('失败') if return_status else __("成功"),
"result": [
{
"text": text
}
]
})
# 调用chatgpt接口
# @pysnooper.snoop()
def chatgpt(self, chat, session_id, search_text, enable_history,history=[], chatlog_id=None, stream=True):
max_retry=3
for i in range(0,max_retry):
url, headers, llm_token = self.get_llm_url_header(chat, stream)
message = self.generate_prompt(chat=chat, search_text=search_text, enable_history=enable_history, history=history)
service_config = json.loads(chat.service_config)
data = {
'model': 'gpt-4-turbo-2024-04-09',
'messages': message,
'temperature': service_config.get("temperature",1), # 问答发散度 0-2 越高越发散 较高的值如0.8将使输出更随机较低的值如0.2)将使其更集中和确定性
'top_p': service_config.get("top_p",0.5), # 同temperature如果设置 0.1 意味着只考虑构成前 10% 概率质量的 tokens
'n': 1, # top n可选值
'stream': stream,
'stop': 'elit proident sint', #
'max_tokens': service_config.get("max_tokens",2500), # 最大返回数
'presence_penalty': service_config.get("presence_penalty",1), # [控制主题的重复度]-2.0(抓住一个主题使劲谈论) ~ 2.0(最大程度避免谈论重复的主题) 之间的数字,正值会根据到目前为止是否出现在文本中来惩罚新 tokens从而增加模型谈论新主题的可能性
'frequency_penalty': 0, # [重复度惩罚因子], -2.0(可以尽情出现相同的词汇) ~ 2.0 (尽量不要出现相同的词汇)
'user': 'user',
}
data.update(json.loads(chat.service_config).get("llm_data", {}))
if stream:
# 返回流响应
import sseclient
res = requests.post(
url,
headers=headers,
json=data,
stream=stream,
verify=False
)
if res.status_code != 200 and i<(max_retry-1):
continue
client = sseclient.SSEClient(res)
# @pysnooper.snoop(watch_explode='message')
def generate(history):
back_message = ''
for event in client.events():
message = event.data
finish = False
if message != '[DONE]':
choices = json.loads(event.data)['choices']
if choices:
message = choices[0].get('delta', {}).get('content', '')
else:
message=''
print(message, flush=True, end='')
# print(message)
if message == '[DONE]':
finish = True
back_message = back_message+g.after_message
if chatlog_id:
chatlog = db.session.query(ChatLog).filter_by(id=int(chatlog_id)).first()
chatlog.answer_status = '成功'
# chatlog.answer = back_message # 内容太多了
db.session.commit()
if history != None:
history.append((search_text, back_message))
history = history[0 - int(chat.session_num):]
try:
cache.set('chat_' + session_id, history, timeout=300) # 人连续对话的时间跨度
except Exception as e:
print(e)
else:
back_message = back_message + message
# 随机乱码,用来避免内容中包含此内容,实现每次返回内容的分隔
back = "TQJXQKT0POF6P4D:" + json.dumps(
{
"message": "success",
"status": 0,
"finish":finish,
"result": [
{"text": back_message},
]
}, ensure_ascii=False
) + "\n\n"
yield back
response = Response(stream_with_context(generate(history=history if enable_history else None)),mimetype='text/event-stream')
response.headers["Cache-Control"] = "no-cache"
response.headers["Connection"] = 'keep-alive'
response.status_code = res.status_code
if response.status_code ==401:
service_config = json.loads(chat.service_config)
# if 'miss_tokens' not in service_config:
# service_config['miss_tokens']={}
# service_config['miss_tokens'][llm_token]=service_config['miss_tokens'].get(llm_token,0)+1
chat.service_config = json.dumps(service_config,ensure_ascii=False,indent=4)
db.session.commit()
return response
# 返回普通响应
else:
# print(url)
# print(headers)
# print(data)
res = requests.post(
url,
headers=headers,
json=data,
verify=False
)
if res.status_code != 200 and i < (max_retry - 1):
continue
if res.status_code == 200 or res.status_code == 201:
# print(res.text)
mes = res.json()['choices'][0]['message']['content']
print(mes)
return 0, mes
else:
service_config = json.loads(chat.service_config)
# if 'miss_tokens' not in service_config:
# service_config['miss_tokens'] = {}
# service_config['miss_tokens'][llm_token] = service_config['miss_tokens'].get(llm_token,0) + 1
chat.service_config = json.dumps(service_config, ensure_ascii=False, indent=4)
db.session.commit()
return 1, f'请求{url}失败'
# @pysnooper.snoop()
def aigc4(self, chat, search_text):
"""
aigc 文本转图片
@param chat:
@param search_text:
@return:
"""
try:
url = json.loads(chat.service_config).get("aigc_url", '')
if 'http:' not in url and 'https://' not in url:
url = urllib.parse.urljoin(request.host_url, url)
pic_num = json.loads(chat.service_config).get("pic_num", 4)
headers = json.loads(chat.service_config).get("aigc_headers", {})
data = {
"text": search_text,
"prompt": search_text,
"steps":50
}
data.update(json.loads(chat.service_config).get("aigc_data", {}))
# @pysnooper.snoop()
from myapp.utils.core import pic2html
def generate():
all_result_image = []
for i in range(pic_num):
# 示例输入
time.sleep(1)
status, image = 0,f'https://cube-studio.oss-cn-hangzhou.aliyuncs.com/aihub/aigc/aigc{i+1}.jpeg'
if not status:
all_result_image.append(image)
back_message = "未配置后端模型为您生成4张示例图片\n"+pic2html(all_result_image,pic_num)
# print(back_message)
back = "TQJXQKT0POF6P4D:" + json.dumps(
{
"message": "success",
"status": 0,
"finish": False,
"result": [
{"text": back_message},
]
}, ensure_ascii=False
) + "\n\n"
yield back
response = Response(stream_with_context(generate()),mimetype='text/event-stream')
response.headers["Cache-Control"] = "no-cache"
response.headers["Connection"] = 'keep-alive'
return 0,response
except Exception as e:
return 1, 'aigc报错' + str(e)
# 添加api
class Chat_View(Chat_View_Base, MyappModelRestApi):
datamodel = SQLAInterface(Chat)
# 添加api
class Chat_View_Api(Chat_View_Base, MyappModelRestApi):
datamodel = SQLAInterface(Chat)
route_base = '/aitalk_modelview/api'
list_columns = ['id','name', 'icon', 'label', 'chat_type', 'service_type', 'owner', 'session_num', 'hello', 'tips','knowledge','service_config','expand']
# info接口响应修正
# @pysnooper.snoop()
def pre_list_res(self, _response):
# 把提示语进行分割
for chat in _response['data']:
chat['tips'] = [x for x in chat['tips'].split('\n') if x] if chat['tips'] else []
try:
service_config = chat.get('service_config', '{}')
if service_config:
chat['service_config'] = json.loads(service_config)
except Exception as e:
print(e)
try:
knowledge = chat.get('knowledge', '{}')
if knowledge:
chat['knowledge'] = json.loads(knowledge)
except Exception as e:
print(e)
try:
expand = chat.get('expand', '{}')
if expand:
chat['expand'] = json.loads(expand)
except Exception as e:
print(e)
return _response
appbuilder.add_api(Chat_View)
appbuilder.add_api(Chat_View_Api)