Spaces:
Sleeping
Sleeping
update
Browse files- Dockerfile +1 -1
- alembic.ini +119 -0
- common/db.py +1 -9
- common/db_schemas.py +10 -0
- common/dependencies.py +6 -0
- components/dbo/alembic/README +1 -0
- components/dbo/alembic/autoupdate_db.py +58 -0
- components/dbo/alembic/env.py +81 -0
- components/dbo/alembic/script.py.mako +28 -0
- components/dbo/alembic/versions/12bb1ebae3ff_logs_refactoring.py +56 -0
- components/dbo/alembic/versions/6635b061c086_init.py +32 -0
- components/dbo/models/feedback.py +0 -2
- components/dbo/models/log.py +7 -8
- components/services/document.py +1 -0
- components/services/log.py +106 -0
- config_hf.yaml +39 -0
- docker-compose-example.yaml +1 -1
- main.py +25 -16
- requirements.txt +2 -1
- routes/dataset.py +24 -24
- routes/llm.py +39 -5
- routes/log.py +15 -102
- schemas/entity.py +1 -0
- schemas/log.py +47 -8
Dockerfile
CHANGED
@@ -2,7 +2,7 @@ FROM nvidia/cuda:12.6.0-runtime-ubuntu22.04
|
|
2 |
|
3 |
ARG PORT=7860
|
4 |
ENV PORT=${PORT}
|
5 |
-
ENV CONFIG_PATH=
|
6 |
ENV SQLALCHEMY_DATABASE_URL=sqlite:////data/logs.db
|
7 |
|
8 |
ENV PYTHONUNBUFFERED=1
|
|
|
2 |
|
3 |
ARG PORT=7860
|
4 |
ENV PORT=${PORT}
|
5 |
+
ENV CONFIG_PATH=config_hf.yaml
|
6 |
ENV SQLALCHEMY_DATABASE_URL=sqlite:////data/logs.db
|
7 |
|
8 |
ENV PYTHONUNBUFFERED=1
|
alembic.ini
ADDED
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# A generic, single database configuration.
|
2 |
+
|
3 |
+
[alembic]
|
4 |
+
# path to migration scripts
|
5 |
+
# Use forward slashes (/) also on windows to provide an os agnostic path
|
6 |
+
script_location = components/dbo/alembic
|
7 |
+
|
8 |
+
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
9 |
+
# Uncomment the line below if you want the files to be prepended with date and time
|
10 |
+
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
11 |
+
# for all available tokens
|
12 |
+
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
13 |
+
|
14 |
+
# sys.path path, will be prepended to sys.path if present.
|
15 |
+
# defaults to the current working directory.
|
16 |
+
prepend_sys_path = .
|
17 |
+
|
18 |
+
# timezone to use when rendering the date within the migration file
|
19 |
+
# as well as the filename.
|
20 |
+
# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
|
21 |
+
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
|
22 |
+
# string value is passed to ZoneInfo()
|
23 |
+
# leave blank for localtime
|
24 |
+
# timezone =
|
25 |
+
|
26 |
+
# max length of characters to apply to the "slug" field
|
27 |
+
# truncate_slug_length = 40
|
28 |
+
|
29 |
+
# set to 'true' to run the environment during
|
30 |
+
# the 'revision' command, regardless of autogenerate
|
31 |
+
# revision_environment = false
|
32 |
+
|
33 |
+
# set to 'true' to allow .pyc and .pyo files without
|
34 |
+
# a source .py file to be detected as revisions in the
|
35 |
+
# versions/ directory
|
36 |
+
# sourceless = false
|
37 |
+
|
38 |
+
# version location specification; This defaults
|
39 |
+
# to alembic/versions. When using multiple version
|
40 |
+
# directories, initial revisions must be specified with --version-path.
|
41 |
+
# The path separator used here should be the separator specified by "version_path_separator" below.
|
42 |
+
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
|
43 |
+
|
44 |
+
# version path separator; As mentioned above, this is the character used to split
|
45 |
+
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
|
46 |
+
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
|
47 |
+
# Valid values for version_path_separator are:
|
48 |
+
#
|
49 |
+
# version_path_separator = :
|
50 |
+
# version_path_separator = ;
|
51 |
+
# version_path_separator = space
|
52 |
+
# version_path_separator = newline
|
53 |
+
#
|
54 |
+
# Use os.pathsep. Default configuration used for new projects.
|
55 |
+
version_path_separator = os
|
56 |
+
|
57 |
+
# set to 'true' to search source files recursively
|
58 |
+
# in each "version_locations" directory
|
59 |
+
# new in Alembic version 1.10
|
60 |
+
# recursive_version_locations = false
|
61 |
+
|
62 |
+
# the output encoding used when revision files
|
63 |
+
# are written from script.py.mako
|
64 |
+
# output_encoding = utf-8
|
65 |
+
|
66 |
+
sqlalchemy.url = sqlite:///../data/logs.db
|
67 |
+
|
68 |
+
|
69 |
+
[post_write_hooks]
|
70 |
+
# post_write_hooks defines scripts or Python functions that are run
|
71 |
+
# on newly generated revision scripts. See the documentation for further
|
72 |
+
# detail and examples
|
73 |
+
|
74 |
+
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
75 |
+
# hooks = black
|
76 |
+
# black.type = console_scripts
|
77 |
+
# black.entrypoint = black
|
78 |
+
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
79 |
+
|
80 |
+
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
|
81 |
+
# hooks = ruff
|
82 |
+
# ruff.type = exec
|
83 |
+
# ruff.executable = %(here)s/.venv/bin/ruff
|
84 |
+
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
85 |
+
|
86 |
+
# Logging configuration
|
87 |
+
[loggers]
|
88 |
+
keys = root,sqlalchemy,alembic
|
89 |
+
|
90 |
+
[handlers]
|
91 |
+
keys = console
|
92 |
+
|
93 |
+
[formatters]
|
94 |
+
keys = generic
|
95 |
+
|
96 |
+
[logger_root]
|
97 |
+
level = WARNING
|
98 |
+
handlers = console
|
99 |
+
qualname =
|
100 |
+
|
101 |
+
[logger_sqlalchemy]
|
102 |
+
level = WARNING
|
103 |
+
handlers =
|
104 |
+
qualname = sqlalchemy.engine
|
105 |
+
|
106 |
+
[logger_alembic]
|
107 |
+
level = INFO
|
108 |
+
handlers =
|
109 |
+
qualname = alembic
|
110 |
+
|
111 |
+
[handler_console]
|
112 |
+
class = StreamHandler
|
113 |
+
args = (sys.stderr,)
|
114 |
+
level = NOTSET
|
115 |
+
formatter = generic
|
116 |
+
|
117 |
+
[formatter_generic]
|
118 |
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
119 |
+
datefmt = %H:%M:%S
|
common/db.py
CHANGED
@@ -8,15 +8,7 @@ from sqlalchemy.orm import sessionmaker, scoped_session, Session
|
|
8 |
|
9 |
from common.configuration import Configuration
|
10 |
from components.dbo.models.base import Base
|
11 |
-
import
|
12 |
-
import components.dbo.models.acronym
|
13 |
-
import components.dbo.models.dataset
|
14 |
-
import components.dbo.models.dataset_document
|
15 |
-
import components.dbo.models.document
|
16 |
-
import components.dbo.models.log
|
17 |
-
import components.dbo.models.llm_prompt
|
18 |
-
import components.dbo.models.llm_config
|
19 |
-
import components.dbo.models.entity
|
20 |
|
21 |
CONFIG_PATH = os.environ.get('CONFIG_PATH', './config_dev.yaml')
|
22 |
config = Configuration(CONFIG_PATH)
|
|
|
8 |
|
9 |
from common.configuration import Configuration
|
10 |
from components.dbo.models.base import Base
|
11 |
+
import common.db_schemas
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
|
13 |
CONFIG_PATH = os.environ.get('CONFIG_PATH', './config_dev.yaml')
|
14 |
config = Configuration(CONFIG_PATH)
|
common/db_schemas.py
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from components.dbo.models.base import Base
|
2 |
+
import components.dbo.models.feedback
|
3 |
+
import components.dbo.models.acronym
|
4 |
+
import components.dbo.models.dataset
|
5 |
+
import components.dbo.models.dataset_document
|
6 |
+
import components.dbo.models.document
|
7 |
+
import components.dbo.models.log
|
8 |
+
import components.dbo.models.llm_prompt
|
9 |
+
import components.dbo.models.llm_config
|
10 |
+
import components.dbo.models.entity
|
common/dependencies.py
CHANGED
@@ -4,6 +4,7 @@ from logging import Logger
|
|
4 |
from typing import Annotated
|
5 |
|
6 |
from fastapi import Depends
|
|
|
7 |
from ntr_text_fragmentation import InjectionBuilder
|
8 |
from sqlalchemy.orm import Session, sessionmaker
|
9 |
|
@@ -34,6 +35,11 @@ def get_logger() -> Logger:
|
|
34 |
return logging.getLogger(__name__)
|
35 |
|
36 |
|
|
|
|
|
|
|
|
|
|
|
37 |
def get_embedding_extractor(
|
38 |
config: Annotated[Configuration, Depends(get_config)],
|
39 |
) -> EmbeddingExtractor:
|
|
|
4 |
from typing import Annotated
|
5 |
|
6 |
from fastapi import Depends
|
7 |
+
from components.services.log import LogService
|
8 |
from ntr_text_fragmentation import InjectionBuilder
|
9 |
from sqlalchemy.orm import Session, sessionmaker
|
10 |
|
|
|
35 |
return logging.getLogger(__name__)
|
36 |
|
37 |
|
38 |
+
def get_log_service(
|
39 |
+
db: Annotated[sessionmaker, Depends(get_db)],
|
40 |
+
) -> LogService:
|
41 |
+
return LogService(db)
|
42 |
+
|
43 |
def get_embedding_extractor(
|
44 |
config: Annotated[Configuration, Depends(get_config)],
|
45 |
) -> EmbeddingExtractor:
|
components/dbo/alembic/README
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
Generic single-database configuration.
|
components/dbo/alembic/autoupdate_db.py
ADDED
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import re
|
3 |
+
import logging
|
4 |
+
|
5 |
+
from sqlalchemy import inspect
|
6 |
+
from sqlalchemy.sql import text
|
7 |
+
from alembic.config import Config
|
8 |
+
from alembic import command
|
9 |
+
|
10 |
+
import common.dependencies as DI
|
11 |
+
|
12 |
+
|
13 |
+
logger = logging.getLogger(__name__)
|
14 |
+
|
15 |
+
def get_old_versions():
|
16 |
+
old_versions = list()
|
17 |
+
migration_dir = 'components/dbo/alembic/versions'
|
18 |
+
for file in os.listdir(migration_dir):
|
19 |
+
if not file.endswith('.py'):
|
20 |
+
continue
|
21 |
+
file_path = os.path.join(migration_dir, file)
|
22 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
23 |
+
content = f.read()
|
24 |
+
match = re.search(
|
25 |
+
r"^(down_revision: Union\[str, None\] = )(None|'[^']*')",
|
26 |
+
content,
|
27 |
+
re.MULTILINE)
|
28 |
+
if match:
|
29 |
+
old_versions.append(match.group(2).replace("'", ""))
|
30 |
+
return old_versions
|
31 |
+
|
32 |
+
|
33 |
+
def get_cur_version():
|
34 |
+
session_factory = DI.get_db()
|
35 |
+
session: Session = session_factory()
|
36 |
+
|
37 |
+
try:
|
38 |
+
inspector = inspect(session.bind)
|
39 |
+
if 'alembic_version' not in inspector.get_table_names():
|
40 |
+
return None
|
41 |
+
|
42 |
+
result = session.execute(text("SELECT version_num FROM alembic_version")).scalar()
|
43 |
+
return result
|
44 |
+
|
45 |
+
finally:
|
46 |
+
session.close()
|
47 |
+
|
48 |
+
|
49 |
+
def update():
|
50 |
+
old_versions = get_old_versions()
|
51 |
+
cur_version = get_cur_version()
|
52 |
+
if cur_version not in old_versions and cur_version is not None:
|
53 |
+
return
|
54 |
+
|
55 |
+
|
56 |
+
logger.info(f"Updating the database from migration {cur_version}")
|
57 |
+
config = Config("alembic.ini")
|
58 |
+
command.upgrade(config, "head")
|
components/dbo/alembic/env.py
ADDED
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from logging.config import fileConfig
|
2 |
+
|
3 |
+
from sqlalchemy import engine_from_config
|
4 |
+
from sqlalchemy import pool
|
5 |
+
|
6 |
+
from alembic import context
|
7 |
+
|
8 |
+
from components.dbo.models.base import Base
|
9 |
+
import common.db_schemas
|
10 |
+
|
11 |
+
# this is the Alembic Config object, which provides
|
12 |
+
# access to the values within the .ini file in use.
|
13 |
+
config = context.config
|
14 |
+
|
15 |
+
# Interpret the config file for Python logging.
|
16 |
+
# This line sets up loggers basically.
|
17 |
+
if config.config_file_name is not None:
|
18 |
+
fileConfig(config.config_file_name)
|
19 |
+
|
20 |
+
# add your model's MetaData object here
|
21 |
+
# for 'autogenerate' support
|
22 |
+
# from myapp import mymodel
|
23 |
+
# target_metadata = mymodel.Base.metadata
|
24 |
+
target_metadata = Base.metadata
|
25 |
+
|
26 |
+
# other values from the config, defined by the needs of env.py,
|
27 |
+
# can be acquired:
|
28 |
+
# my_important_option = config.get_main_option("my_important_option")
|
29 |
+
# ... etc.
|
30 |
+
|
31 |
+
|
32 |
+
def run_migrations_offline() -> None:
|
33 |
+
"""Run migrations in 'offline' mode.
|
34 |
+
|
35 |
+
This configures the context with just a URL
|
36 |
+
and not an Engine, though an Engine is acceptable
|
37 |
+
here as well. By skipping the Engine creation
|
38 |
+
we don't even need a DBAPI to be available.
|
39 |
+
|
40 |
+
Calls to context.execute() here emit the given string to the
|
41 |
+
script output.
|
42 |
+
|
43 |
+
"""
|
44 |
+
url = config.get_main_option("sqlalchemy.url")
|
45 |
+
context.configure(
|
46 |
+
url=url,
|
47 |
+
target_metadata=target_metadata,
|
48 |
+
literal_binds=True,
|
49 |
+
dialect_opts={"paramstyle": "named"},
|
50 |
+
)
|
51 |
+
|
52 |
+
with context.begin_transaction():
|
53 |
+
context.run_migrations()
|
54 |
+
|
55 |
+
|
56 |
+
def run_migrations_online() -> None:
|
57 |
+
"""Run migrations in 'online' mode.
|
58 |
+
|
59 |
+
In this scenario we need to create an Engine
|
60 |
+
and associate a connection with the context.
|
61 |
+
|
62 |
+
"""
|
63 |
+
connectable = engine_from_config(
|
64 |
+
config.get_section(config.config_ini_section, {}),
|
65 |
+
prefix="sqlalchemy.",
|
66 |
+
poolclass=pool.NullPool,
|
67 |
+
)
|
68 |
+
|
69 |
+
with connectable.connect() as connection:
|
70 |
+
context.configure(
|
71 |
+
connection=connection, target_metadata=target_metadata
|
72 |
+
)
|
73 |
+
|
74 |
+
with context.begin_transaction():
|
75 |
+
context.run_migrations()
|
76 |
+
|
77 |
+
|
78 |
+
if context.is_offline_mode():
|
79 |
+
run_migrations_offline()
|
80 |
+
else:
|
81 |
+
run_migrations_online()
|
components/dbo/alembic/script.py.mako
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""${message}
|
2 |
+
|
3 |
+
Revision ID: ${up_revision}
|
4 |
+
Revises: ${down_revision | comma,n}
|
5 |
+
Create Date: ${create_date}
|
6 |
+
|
7 |
+
"""
|
8 |
+
from typing import Sequence, Union
|
9 |
+
|
10 |
+
from alembic import op
|
11 |
+
import sqlalchemy as sa
|
12 |
+
${imports if imports else ""}
|
13 |
+
|
14 |
+
# revision identifiers, used by Alembic.
|
15 |
+
revision: str = ${repr(up_revision)}
|
16 |
+
down_revision: Union[str, None] = ${repr(down_revision)}
|
17 |
+
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
18 |
+
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
19 |
+
|
20 |
+
|
21 |
+
def upgrade() -> None:
|
22 |
+
"""Upgrade schema."""
|
23 |
+
${upgrades if upgrades else "pass"}
|
24 |
+
|
25 |
+
|
26 |
+
def downgrade() -> None:
|
27 |
+
"""Downgrade schema."""
|
28 |
+
${downgrades if downgrades else "pass"}
|
components/dbo/alembic/versions/12bb1ebae3ff_logs_refactoring.py
ADDED
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Logs refactoring
|
2 |
+
|
3 |
+
Revision ID: 12bb1ebae3ff
|
4 |
+
Revises: 6635b061c086
|
5 |
+
Create Date: 2025-04-16 12:00:40.247356
|
6 |
+
|
7 |
+
"""
|
8 |
+
from typing import Sequence, Union
|
9 |
+
|
10 |
+
from alembic import op
|
11 |
+
import sqlalchemy as sa
|
12 |
+
|
13 |
+
|
14 |
+
# revision identifiers, used by Alembic.
|
15 |
+
revision: str = '12bb1ebae3ff'
|
16 |
+
down_revision: Union[str, None] = '6635b061c086'
|
17 |
+
branch_labels: Union[str, Sequence[str], None] = None
|
18 |
+
depends_on: Union[str, Sequence[str], None] = None
|
19 |
+
|
20 |
+
|
21 |
+
def upgrade() -> None:
|
22 |
+
"""Upgrade schema."""
|
23 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
24 |
+
op.add_column('log', sa.Column('user_request', sa.String(), nullable=True))
|
25 |
+
op.add_column('log', sa.Column('qe_result', sa.String(), nullable=True))
|
26 |
+
op.add_column('log', sa.Column('search_result', sa.String(), nullable=True))
|
27 |
+
op.add_column('log', sa.Column('llm_result', sa.String(), nullable=True))
|
28 |
+
op.add_column('log', sa.Column('llm_settings', sa.String(), nullable=True))
|
29 |
+
op.add_column('log', sa.Column('user_name', sa.String(), nullable=True))
|
30 |
+
op.add_column('log', sa.Column('error', sa.String(), nullable=True))
|
31 |
+
op.drop_column('log', 'query_type')
|
32 |
+
op.drop_column('log', 'llm_classifier')
|
33 |
+
op.drop_column('log', 'llmResponse')
|
34 |
+
op.drop_column('log', 'userRequest')
|
35 |
+
op.drop_column('log', 'userName')
|
36 |
+
op.drop_column('log', 'llmPrompt')
|
37 |
+
# ### end Alembic commands ###
|
38 |
+
|
39 |
+
|
40 |
+
def downgrade() -> None:
|
41 |
+
"""Downgrade schema."""
|
42 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
43 |
+
op.add_column('log', sa.Column('llmPrompt', sa.VARCHAR(), nullable=True))
|
44 |
+
op.add_column('log', sa.Column('userName', sa.VARCHAR(), nullable=True))
|
45 |
+
op.add_column('log', sa.Column('userRequest', sa.VARCHAR(), nullable=True))
|
46 |
+
op.add_column('log', sa.Column('llmResponse', sa.VARCHAR(), nullable=True))
|
47 |
+
op.add_column('log', sa.Column('llm_classifier', sa.VARCHAR(), nullable=True))
|
48 |
+
op.add_column('log', sa.Column('query_type', sa.VARCHAR(), nullable=True))
|
49 |
+
op.drop_column('log', 'error')
|
50 |
+
op.drop_column('log', 'user_name')
|
51 |
+
op.drop_column('log', 'llm_settings')
|
52 |
+
op.drop_column('log', 'llm_result')
|
53 |
+
op.drop_column('log', 'search_result')
|
54 |
+
op.drop_column('log', 'qe_result')
|
55 |
+
op.drop_column('log', 'user_request')
|
56 |
+
# ### end Alembic commands ###
|
components/dbo/alembic/versions/6635b061c086_init.py
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Init
|
2 |
+
|
3 |
+
Revision ID: 6635b061c086
|
4 |
+
Revises:
|
5 |
+
Create Date: 2025-04-09 09:21:08.157225
|
6 |
+
|
7 |
+
"""
|
8 |
+
from typing import Sequence, Union
|
9 |
+
|
10 |
+
from alembic import op
|
11 |
+
import sqlalchemy as sa
|
12 |
+
|
13 |
+
|
14 |
+
# revision identifiers, used by Alembic.
|
15 |
+
revision: str = '6635b061c086'
|
16 |
+
down_revision: Union[str, None] = None
|
17 |
+
branch_labels: Union[str, Sequence[str], None] = None
|
18 |
+
depends_on: Union[str, Sequence[str], None] = None
|
19 |
+
|
20 |
+
|
21 |
+
def upgrade() -> None:
|
22 |
+
"""Upgrade schema."""
|
23 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
24 |
+
pass
|
25 |
+
# ### end Alembic commands ###
|
26 |
+
|
27 |
+
|
28 |
+
def downgrade() -> None:
|
29 |
+
"""Downgrade schema."""
|
30 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
31 |
+
pass
|
32 |
+
# ### end Alembic commands ###
|
components/dbo/models/feedback.py
CHANGED
@@ -23,5 +23,3 @@ class Feedback(Base):
|
|
23 |
llmEstimate = mapped_column(Integer)
|
24 |
|
25 |
log_id = mapped_column(Integer, ForeignKey('log.id'), index=True)
|
26 |
-
|
27 |
-
log = relationship("Log", back_populates="feedback")
|
|
|
23 |
llmEstimate = mapped_column(Integer)
|
24 |
|
25 |
log_id = mapped_column(Integer, ForeignKey('log.id'), index=True)
|
|
|
|
components/dbo/models/log.py
CHANGED
@@ -9,11 +9,10 @@ from components.dbo.models.base import Base
|
|
9 |
class Log(Base):
|
10 |
__tablename__ = 'log'
|
11 |
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
feedback = relationship("Feedback", back_populates="log")
|
|
|
9 |
class Log(Base):
|
10 |
__tablename__ = 'log'
|
11 |
|
12 |
+
user_request = mapped_column(String)
|
13 |
+
qe_result = mapped_column(String)
|
14 |
+
search_result = mapped_column(String)
|
15 |
+
llm_result = mapped_column(String)
|
16 |
+
llm_settings = mapped_column(String)
|
17 |
+
user_name = mapped_column(String)
|
18 |
+
error = mapped_column(String)
|
|
components/services/document.py
CHANGED
@@ -93,6 +93,7 @@ class DocumentService:
|
|
93 |
file_location.parent.mkdir(parents=True, exist_ok=True)
|
94 |
with open(file_location, 'wb') as buffer:
|
95 |
buffer.write(file.file.read())
|
|
|
96 |
|
97 |
source_format = get_source_format(file.filename)
|
98 |
|
|
|
93 |
file_location.parent.mkdir(parents=True, exist_ok=True)
|
94 |
with open(file_location, 'wb') as buffer:
|
95 |
buffer.write(file.file.read())
|
96 |
+
file.file.close()
|
97 |
|
98 |
source_format = get_source_format(file.filename)
|
99 |
|
components/services/log.py
ADDED
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
|
3 |
+
from fastapi import HTTPException
|
4 |
+
from sqlalchemy.orm import Session
|
5 |
+
|
6 |
+
from components.dbo.models.log import Log as LogSQL
|
7 |
+
from schemas.log import LogCreateSchema, LogFilterSchema, LogSchema, PaginatedLogResponse
|
8 |
+
|
9 |
+
|
10 |
+
logger = logging.getLogger(__name__)
|
11 |
+
|
12 |
+
|
13 |
+
class LogService:
|
14 |
+
"""
|
15 |
+
Сервис для работы с параметрами LLM.
|
16 |
+
"""
|
17 |
+
|
18 |
+
def __init__(self, db: Session):
|
19 |
+
logger.info("LogService initializing")
|
20 |
+
self.db = db
|
21 |
+
|
22 |
+
|
23 |
+
def create(self, log_schema: LogCreateSchema):
|
24 |
+
logger.info("Creating a new log")
|
25 |
+
with self.db() as session:
|
26 |
+
new_log: LogSQL = LogSQL(**log_schema.model_dump())
|
27 |
+
session.add(new_log)
|
28 |
+
session.commit()
|
29 |
+
session.refresh(new_log)
|
30 |
+
|
31 |
+
return LogSchema(**new_log.to_dict())
|
32 |
+
|
33 |
+
|
34 |
+
def get_list(self, filters: LogFilterSchema) -> PaginatedLogResponse:
|
35 |
+
logger.info(f"Fetching logs with filters: {filters.model_dump(exclude_none=True)}")
|
36 |
+
with self.db() as session:
|
37 |
+
query = session.query(LogSQL)
|
38 |
+
|
39 |
+
# Применение фильтра по user_name
|
40 |
+
if filters.user_name:
|
41 |
+
query = query.filter(LogSQL.user_name == filters.user_name)
|
42 |
+
|
43 |
+
# Применение фильтра по диапазону date_created
|
44 |
+
if filters.date_from:
|
45 |
+
query = query.filter(LogSQL.date_created >= filters.date_from)
|
46 |
+
if filters.date_to:
|
47 |
+
query = query.filter(LogSQL.date_created <= filters.date_to)
|
48 |
+
|
49 |
+
total = query.count()
|
50 |
+
|
51 |
+
# Применение пагинации
|
52 |
+
offset = (filters.page - 1) * filters.page_size
|
53 |
+
logs = query.offset(offset).limit(filters.page_size).all()
|
54 |
+
|
55 |
+
# Вычисление общего количества страниц
|
56 |
+
total_pages = (total + filters.page_size - 1) // filters.page_size
|
57 |
+
|
58 |
+
# Формирование ответа
|
59 |
+
return PaginatedLogResponse(
|
60 |
+
data=[LogSchema(**log.to_dict()) for log in logs],
|
61 |
+
total=total,
|
62 |
+
page=filters.page,
|
63 |
+
page_size=filters.page_size,
|
64 |
+
total_pages=total_pages
|
65 |
+
)
|
66 |
+
|
67 |
+
def get_by_id(self, id: int) -> LogSchema:
|
68 |
+
with self.db() as session:
|
69 |
+
log: LogSQL = session.query(LogSQL).filter(LogSQL.id == id).first()
|
70 |
+
|
71 |
+
if not log:
|
72 |
+
raise HTTPException(
|
73 |
+
status_code=400, detail=f"Item with id {id} not found"
|
74 |
+
)
|
75 |
+
|
76 |
+
return LogSchema(**log.to_dict())
|
77 |
+
|
78 |
+
|
79 |
+
def update(self, id: int, new_log: LogSchema):
|
80 |
+
logger.info("Updating log")
|
81 |
+
with self.db() as session:
|
82 |
+
log: LogSQL = session.query(LogSQL).filter(LogSQL.id == id).first()
|
83 |
+
|
84 |
+
if not log:
|
85 |
+
raise HTTPException(
|
86 |
+
status_code=400, detail=f"Item with id {id} not found"
|
87 |
+
)
|
88 |
+
|
89 |
+
update_data = new_log.model_dump(exclude_unset=True)
|
90 |
+
|
91 |
+
for key, value in update_data.items():
|
92 |
+
if hasattr(log, key):
|
93 |
+
setattr(log, key, value)
|
94 |
+
|
95 |
+
|
96 |
+
session.commit()
|
97 |
+
session.refresh(log)
|
98 |
+
return log
|
99 |
+
|
100 |
+
|
101 |
+
def delete(self, id: int):
|
102 |
+
logger.info("Deleting log: {id}")
|
103 |
+
with self.db() as session:
|
104 |
+
log_to_del: LogSQL = session.query(LogSQL).get(id)
|
105 |
+
session.delete(log_to_del)
|
106 |
+
session.commit()
|
config_hf.yaml
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
common:
|
2 |
+
log_file_path: !ENV ${LOG_FILE_PATH:/data/logs/common.log}
|
3 |
+
log_sql_path: !ENV ${SQLALCHEMY_DATABASE_URL:sqlite:////data/logs.db}
|
4 |
+
log_level: !ENV ${LOG_LEVEL:INFO}
|
5 |
+
|
6 |
+
bd:
|
7 |
+
entities:
|
8 |
+
# Варианты: fixed_size, sentence, paragraph, blm_sentence, blm_paragraph
|
9 |
+
strategy_name: !ENV ${ENTITIES_STRATEGY_NAME:paragraph}
|
10 |
+
strategy_params:
|
11 |
+
# words_per_chunk: 50
|
12 |
+
# overlap_words: 25
|
13 |
+
# respect_sentence_boundaries: true
|
14 |
+
process_tables: true
|
15 |
+
neighbors_max_distance: 1
|
16 |
+
|
17 |
+
search:
|
18 |
+
use_qe: true
|
19 |
+
use_vector_search: true
|
20 |
+
vectorizer_path: !ENV ${EMBEDDING_MODEL_PATH:BAAI/bge-m3}
|
21 |
+
device: !ENV ${DEVICE:cuda}
|
22 |
+
max_entities_per_message: 150
|
23 |
+
max_entities_per_dialogue: 300
|
24 |
+
|
25 |
+
files:
|
26 |
+
empty_start: true
|
27 |
+
documents_path: /data/documents
|
28 |
+
|
29 |
+
llm:
|
30 |
+
base_url: !ENV ${LLM_BASE_URL:https://api.deepinfra.com}
|
31 |
+
api_key_env: !ENV ${API_KEY_ENV:DEEPINFRA_API_KEY}
|
32 |
+
model: !ENV ${MODEL_NAME:meta-llama/Llama-3.3-70B-Instruct}
|
33 |
+
tokenizer_name: !ENV ${TOKENIZER_NAME:unsloth/Llama-3.3-70B-Instruct}
|
34 |
+
temperature: 0.14
|
35 |
+
top_p: 0.95
|
36 |
+
min_p: 0.05
|
37 |
+
frequency_penalty: -0.001
|
38 |
+
presence_penalty: 1.3
|
39 |
+
seed: 42
|
docker-compose-example.yaml
CHANGED
@@ -7,7 +7,7 @@ services:
|
|
7 |
args:
|
8 |
PORT: ${PORT:-8885}
|
9 |
environment:
|
10 |
-
- CONFIG_PATH=/app/
|
11 |
- SQLALCHEMY_DATABASE_URL=sqlite:////data/logs.db # Путь к БД
|
12 |
- PORT=${PORT:-8885}
|
13 |
- HF_HOME=/data/hf_cache
|
|
|
7 |
args:
|
8 |
PORT: ${PORT:-8885}
|
9 |
environment:
|
10 |
+
- CONFIG_PATH=/app/config_hf.yaml # Конфиг
|
11 |
- SQLALCHEMY_DATABASE_URL=sqlite:////data/logs.db # Путь к БД
|
12 |
- PORT=${PORT:-8885}
|
13 |
- HF_HOME=/data/hf_cache
|
main.py
CHANGED
@@ -1,8 +1,8 @@
|
|
1 |
import logging
|
2 |
import os
|
3 |
-
from contextlib import asynccontextmanager
|
4 |
from pathlib import Path
|
5 |
-
from typing import Annotated
|
6 |
|
7 |
import dotenv
|
8 |
import uvicorn
|
@@ -10,29 +10,34 @@ from fastapi import FastAPI
|
|
10 |
from fastapi.middleware.cors import CORSMiddleware
|
11 |
from transformers import AutoModel, AutoTokenizer
|
12 |
|
13 |
-
|
14 |
-
from common import dependencies as DI
|
15 |
from common.common import configure_logging
|
16 |
from common.configuration import Configuration
|
|
|
17 |
from routes.dataset import router as dataset_router
|
18 |
from routes.document import router as document_router
|
19 |
from routes.entity import router as entity_router
|
|
|
20 |
from routes.llm import router as llm_router
|
21 |
from routes.llm_config import router as llm_config_router
|
22 |
from routes.llm_prompt import router as llm_prompt_router
|
|
|
23 |
from routes.auth import router as auth_router
|
|
|
24 |
|
25 |
-
#
|
26 |
-
|
|
|
|
|
27 |
|
28 |
# Загружаем переменные из .env
|
29 |
dotenv.load_dotenv()
|
30 |
|
31 |
-
|
32 |
-
|
33 |
-
# from routes.log import router as log_router
|
34 |
|
35 |
CONFIG_PATH = os.environ.get('CONFIG_PATH', 'config_dev.yaml')
|
|
|
36 |
print("config path: ")
|
37 |
print(CONFIG_PATH)
|
38 |
config = Configuration(CONFIG_PATH)
|
@@ -48,10 +53,14 @@ configure_logging(
|
|
48 |
tmp_path = Path(os.environ.get("APP_TMP_PATH", '.')) / 'tmp.json'
|
49 |
tmp_path.unlink(missing_ok=True)
|
50 |
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
|
|
|
|
|
|
|
|
55 |
|
56 |
app = FastAPI(title="Assistant control panel")
|
57 |
|
@@ -66,20 +75,20 @@ app.add_middleware(
|
|
66 |
)
|
67 |
|
68 |
app.include_router(llm_router)
|
69 |
-
# app.include_router(log_router)
|
70 |
-
# app.include_router(feedback_router)
|
71 |
app.include_router(dataset_router)
|
72 |
app.include_router(document_router)
|
73 |
app.include_router(llm_config_router)
|
74 |
app.include_router(llm_prompt_router)
|
75 |
app.include_router(entity_router)
|
|
|
76 |
app.include_router(auth_router)
|
|
|
77 |
|
78 |
if __name__ == "__main__":
|
79 |
uvicorn.run(
|
80 |
"main:app",
|
81 |
host="localhost",
|
82 |
-
port=
|
83 |
reload=False,
|
84 |
workers=2
|
85 |
)
|
|
|
1 |
import logging
|
2 |
import os
|
3 |
+
from contextlib import asynccontextmanager # noqa: F401
|
4 |
from pathlib import Path
|
5 |
+
from typing import Annotated # noqa: F401
|
6 |
|
7 |
import dotenv
|
8 |
import uvicorn
|
|
|
10 |
from fastapi.middleware.cors import CORSMiddleware
|
11 |
from transformers import AutoModel, AutoTokenizer
|
12 |
|
13 |
+
from common import dependencies as DI # noqa: F401
|
|
|
14 |
from common.common import configure_logging
|
15 |
from common.configuration import Configuration
|
16 |
+
from routes.auth import router as auth_router
|
17 |
from routes.dataset import router as dataset_router
|
18 |
from routes.document import router as document_router
|
19 |
from routes.entity import router as entity_router
|
20 |
+
from routes.evaluation import router as evaluation_router
|
21 |
from routes.llm import router as llm_router
|
22 |
from routes.llm_config import router as llm_config_router
|
23 |
from routes.llm_prompt import router as llm_prompt_router
|
24 |
+
from routes.log import router as log_router
|
25 |
from routes.auth import router as auth_router
|
26 |
+
from components.dbo.alembic import autoupdate_db
|
27 |
|
28 |
+
# Защита от автоудаления линтером
|
29 |
+
_ = DI
|
30 |
+
_ = Annotated
|
31 |
+
_ = asynccontextmanager
|
32 |
|
33 |
# Загружаем переменные из .env
|
34 |
dotenv.load_dotenv()
|
35 |
|
36 |
+
autoupdate_db.update()
|
37 |
+
|
|
|
38 |
|
39 |
CONFIG_PATH = os.environ.get('CONFIG_PATH', 'config_dev.yaml')
|
40 |
+
|
41 |
print("config path: ")
|
42 |
print(CONFIG_PATH)
|
43 |
config = Configuration(CONFIG_PATH)
|
|
|
53 |
tmp_path = Path(os.environ.get("APP_TMP_PATH", '.')) / 'tmp.json'
|
54 |
tmp_path.unlink(missing_ok=True)
|
55 |
|
56 |
+
try:
|
57 |
+
print("Downloading model to cache...")
|
58 |
+
AutoTokenizer.from_pretrained(config.db_config.search.vectorizer_path)
|
59 |
+
AutoModel.from_pretrained(config.db_config.search.vectorizer_path)
|
60 |
+
print("Model cached successfully.")
|
61 |
+
except Exception as e:
|
62 |
+
logger.error(f"Error downloading model from huggingface {config.db_config.search.vectorizer_path}: {str(e)}")
|
63 |
+
|
64 |
|
65 |
app = FastAPI(title="Assistant control panel")
|
66 |
|
|
|
75 |
)
|
76 |
|
77 |
app.include_router(llm_router)
|
|
|
|
|
78 |
app.include_router(dataset_router)
|
79 |
app.include_router(document_router)
|
80 |
app.include_router(llm_config_router)
|
81 |
app.include_router(llm_prompt_router)
|
82 |
app.include_router(entity_router)
|
83 |
+
app.include_router(evaluation_router)
|
84 |
app.include_router(auth_router)
|
85 |
+
app.include_router(log_router)
|
86 |
|
87 |
if __name__ == "__main__":
|
88 |
uvicorn.run(
|
89 |
"main:app",
|
90 |
host="localhost",
|
91 |
+
port=7860,
|
92 |
reload=False,
|
93 |
workers=2
|
94 |
)
|
requirements.txt
CHANGED
@@ -24,4 +24,5 @@ uvicorn==0.34.0
|
|
24 |
python-multipart==0.0.20
|
25 |
python-dotenv==1.1.0
|
26 |
pyjwt==2.10.1
|
27 |
-
fuzzywuzzy[speedup]
|
|
|
|
24 |
python-multipart==0.0.20
|
25 |
python-dotenv==1.1.0
|
26 |
pyjwt==2.10.1
|
27 |
+
fuzzywuzzy[speedup]
|
28 |
+
alembic==1.15.2
|
routes/dataset.py
CHANGED
@@ -42,31 +42,31 @@ async def get_processing(dataset_service: Annotated[DatasetService, Depends(DI.g
|
|
42 |
|
43 |
|
44 |
|
45 |
-
def try_create_default_dataset(dataset_service: DatasetService):
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
|
60 |
-
@router.get('/try_init_default_dataset')
|
61 |
-
async def try_init_default_dataset(dataset_service: Annotated[DatasetService, Depends(DI.get_dataset_service)],
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
|
71 |
|
72 |
@router.get('/{dataset_id}')
|
|
|
42 |
|
43 |
|
44 |
|
45 |
+
# def try_create_default_dataset(dataset_service: DatasetService):
|
46 |
+
# """
|
47 |
+
# Создаёт датасет по умолчанию, если такого нет.
|
48 |
+
# """
|
49 |
+
|
50 |
+
# if not dataset_service.get_default_dataset():
|
51 |
+
# print('creating default dataset')
|
52 |
+
# if dataset_service.config.db_config.files.empty_start:
|
53 |
+
# dataset_service.create_empty_dataset(is_default=True)
|
54 |
+
# else:
|
55 |
+
# dataset_service.create_dataset_from_directory(
|
56 |
+
# is_default=True,
|
57 |
+
# directory_with_documents=dataset_service.config.db_config.files.documents_path,
|
58 |
+
# )
|
59 |
|
60 |
+
# @router.get('/try_init_default_dataset')
|
61 |
+
# async def try_init_default_dataset(dataset_service: Annotated[DatasetService, Depends(DI.get_dataset_service)],
|
62 |
+
# current_user: Annotated[any, Depends(auth.get_current_user)]):
|
63 |
+
# logger.info(f"Handling GET request try_init_default_dataset")
|
64 |
+
# try_create_default_dataset(dataset_service)
|
65 |
+
# try:
|
66 |
+
# return {"ok": True}
|
67 |
+
# except Exception as e:
|
68 |
+
# logger.error(f"Error creating default dataset: {str(e)}")
|
69 |
+
# raise
|
70 |
|
71 |
|
72 |
@router.get('/{dataset_id}')
|
routes/llm.py
CHANGED
@@ -18,6 +18,8 @@ from components.services.dialogue import DialogueService
|
|
18 |
from components.services.entity import EntityService
|
19 |
from components.services.llm_config import LLMConfigService
|
20 |
from components.services.llm_prompt import LlmPromptService
|
|
|
|
|
21 |
|
22 |
router = APIRouter(prefix='/llm', tags=['LLM chat'])
|
23 |
logger = logging.getLogger(__name__)
|
@@ -53,7 +55,6 @@ def get_last_user_message(chat_request: ChatRequest) -> Optional[Message]:
|
|
53 |
msg
|
54 |
for msg in reversed(chat_request.history)
|
55 |
if msg.role == "user"
|
56 |
-
and (msg.searchResults is None or not msg.searchResults)
|
57 |
),
|
58 |
None,
|
59 |
)
|
@@ -165,12 +166,22 @@ async def sse_generator(request: ChatRequest, llm_api: DeepInfraApi, system_prom
|
|
165 |
predict_params: LlmPredictParams,
|
166 |
dataset_service: DatasetService,
|
167 |
entity_service: EntityService,
|
168 |
-
dialogue_service: DialogueService
|
|
|
|
|
169 |
"""
|
170 |
Генератор для стриминга ответа LLM через SSE.
|
171 |
"""
|
|
|
|
|
|
|
172 |
try:
|
173 |
old_history = request.history
|
|
|
|
|
|
|
|
|
|
|
174 |
new_history = [Message(
|
175 |
role=msg.role,
|
176 |
content=msg.content,
|
@@ -182,6 +193,10 @@ async def sse_generator(request: ChatRequest, llm_api: DeepInfraApi, system_prom
|
|
182 |
|
183 |
|
184 |
qe_result = await dialogue_service.get_qe_result(request.history)
|
|
|
|
|
|
|
|
|
185 |
try_insert_reasoning(request, qe_result.debug_message)
|
186 |
|
187 |
# qe_debug_event = {
|
@@ -200,6 +215,9 @@ async def sse_generator(request: ChatRequest, llm_api: DeepInfraApi, system_prom
|
|
200 |
}
|
201 |
yield f"data: {json.dumps(qe_event, ensure_ascii=False)}\n\n"
|
202 |
except Exception as e:
|
|
|
|
|
|
|
203 |
logger.error(f"Error in SSE chat stream while dialogue_service.get_qe_result: {str(e)}", stack_info=True)
|
204 |
yield "data: {\"event\": \"error\", \"data\":\""+str(e)+"\" }\n\n"
|
205 |
qe_result = dialogue_service.get_qe_result_from_chat(request.history)
|
@@ -216,6 +234,9 @@ async def sse_generator(request: ChatRequest, llm_api: DeepInfraApi, system_prom
|
|
216 |
)
|
217 |
text_chunks = await entity_service.build_text_async(chunk_ids, dataset.id, scores)
|
218 |
|
|
|
|
|
|
|
219 |
search_results_event = {
|
220 |
"event": "search_results",
|
221 |
"data": {
|
@@ -229,23 +250,35 @@ async def sse_generator(request: ChatRequest, llm_api: DeepInfraApi, system_prom
|
|
229 |
|
230 |
try_insert_search_results(request, text_chunks)
|
231 |
except Exception as e:
|
|
|
|
|
|
|
232 |
logger.error(f"Error in SSE chat stream while searching: {str(e)}", stack_info=True)
|
233 |
yield "data: {\"event\": \"error\", \"data\":\""+str(e)+"\" }\n\n"
|
|
|
|
|
234 |
try:
|
235 |
# Сворачиваем историю в первое сообщение
|
236 |
collapsed_request = collapse_history_to_first_message(request)
|
237 |
-
|
|
|
|
|
238 |
# Стриминг токенов ответа
|
239 |
async for token in llm_api.get_predict_chat_generator(collapsed_request, system_prompt, predict_params):
|
240 |
token_event = {"event": "token", "data": token}
|
241 |
-
|
|
|
|
|
242 |
yield f"data: {json.dumps(token_event, ensure_ascii=False)}\n\n"
|
243 |
|
244 |
# Финальное событие
|
245 |
yield "data: {\"event\": \"done\"}\n\n"
|
246 |
except Exception as e:
|
|
|
247 |
logger.error(f"Error in SSE chat stream while generating response: {str(e)}", stack_info=True)
|
248 |
yield "data: {\"event\": \"error\", \"data\":\""+str(e)+"\" }\n\n"
|
|
|
|
|
249 |
|
250 |
|
251 |
@router.post("/chat/stream")
|
@@ -258,6 +291,7 @@ async def chat_stream(
|
|
258 |
entity_service: Annotated[EntityService, Depends(DI.get_entity_service)],
|
259 |
dataset_service: Annotated[DatasetService, Depends(DI.get_dataset_service)],
|
260 |
dialogue_service: Annotated[DialogueService, Depends(DI.get_dialogue_service)],
|
|
|
261 |
current_user: Annotated[any, Depends(auth.get_current_user)]
|
262 |
):
|
263 |
try:
|
@@ -282,7 +316,7 @@ async def chat_stream(
|
|
282 |
"Access-Control-Allow-Origin": "*",
|
283 |
}
|
284 |
return StreamingResponse(
|
285 |
-
sse_generator(request, llm_api, system_prompt.text, predict_params, dataset_service, entity_service, dialogue_service),
|
286 |
media_type="text/event-stream",
|
287 |
headers=headers
|
288 |
)
|
|
|
18 |
from components.services.entity import EntityService
|
19 |
from components.services.llm_config import LLMConfigService
|
20 |
from components.services.llm_prompt import LlmPromptService
|
21 |
+
from components.services.log import LogService
|
22 |
+
from schemas.log import LogCreateSchema
|
23 |
|
24 |
router = APIRouter(prefix='/llm', tags=['LLM chat'])
|
25 |
logger = logging.getLogger(__name__)
|
|
|
55 |
msg
|
56 |
for msg in reversed(chat_request.history)
|
57 |
if msg.role == "user"
|
|
|
58 |
),
|
59 |
None,
|
60 |
)
|
|
|
166 |
predict_params: LlmPredictParams,
|
167 |
dataset_service: DatasetService,
|
168 |
entity_service: EntityService,
|
169 |
+
dialogue_service: DialogueService,
|
170 |
+
log_service: LogService,
|
171 |
+
current_user: auth.User) -> AsyncGenerator[str, None]:
|
172 |
"""
|
173 |
Генератор для стриминга ответа LLM через SSE.
|
174 |
"""
|
175 |
+
# Создаем экземпляр "сквозного" лога через весь процесс
|
176 |
+
log = LogCreateSchema(user_name=current_user.username)
|
177 |
+
|
178 |
try:
|
179 |
old_history = request.history
|
180 |
+
|
181 |
+
# Сохраняем последнее сообщение в лог как исходный пользовательский запрос
|
182 |
+
last_message = get_last_user_message(request)
|
183 |
+
log.user_request = last_message.content if last_message is not None else None
|
184 |
+
|
185 |
new_history = [Message(
|
186 |
role=msg.role,
|
187 |
content=msg.content,
|
|
|
193 |
|
194 |
|
195 |
qe_result = await dialogue_service.get_qe_result(request.history)
|
196 |
+
|
197 |
+
# Запись результата qe в лог
|
198 |
+
log.qe_result = qe_result.model_dump_json()
|
199 |
+
|
200 |
try_insert_reasoning(request, qe_result.debug_message)
|
201 |
|
202 |
# qe_debug_event = {
|
|
|
215 |
}
|
216 |
yield f"data: {json.dumps(qe_event, ensure_ascii=False)}\n\n"
|
217 |
except Exception as e:
|
218 |
+
log.error = "Error in QE block: " + str(e)
|
219 |
+
log_service.create(log)
|
220 |
+
|
221 |
logger.error(f"Error in SSE chat stream while dialogue_service.get_qe_result: {str(e)}", stack_info=True)
|
222 |
yield "data: {\"event\": \"error\", \"data\":\""+str(e)+"\" }\n\n"
|
223 |
qe_result = dialogue_service.get_qe_result_from_chat(request.history)
|
|
|
234 |
)
|
235 |
text_chunks = await entity_service.build_text_async(chunk_ids, dataset.id, scores)
|
236 |
|
237 |
+
# Запись результатов поиска в лог
|
238 |
+
log.search_result = text_chunks
|
239 |
+
|
240 |
search_results_event = {
|
241 |
"event": "search_results",
|
242 |
"data": {
|
|
|
250 |
|
251 |
try_insert_search_results(request, text_chunks)
|
252 |
except Exception as e:
|
253 |
+
log.error = "Error in vector search block: " + str(e)
|
254 |
+
log_service.create(log)
|
255 |
+
|
256 |
logger.error(f"Error in SSE chat stream while searching: {str(e)}", stack_info=True)
|
257 |
yield "data: {\"event\": \"error\", \"data\":\""+str(e)+"\" }\n\n"
|
258 |
+
|
259 |
+
log_error = None
|
260 |
try:
|
261 |
# Сворачиваем историю в первое сообщение
|
262 |
collapsed_request = collapse_history_to_first_message(request)
|
263 |
+
|
264 |
+
log.llm_result = ''
|
265 |
+
|
266 |
# Стриминг токенов ответа
|
267 |
async for token in llm_api.get_predict_chat_generator(collapsed_request, system_prompt, predict_params):
|
268 |
token_event = {"event": "token", "data": token}
|
269 |
+
|
270 |
+
log.llm_result += token
|
271 |
+
|
272 |
yield f"data: {json.dumps(token_event, ensure_ascii=False)}\n\n"
|
273 |
|
274 |
# Финальное событие
|
275 |
yield "data: {\"event\": \"done\"}\n\n"
|
276 |
except Exception as e:
|
277 |
+
log.error = "Error in llm inference block: " + str(e)
|
278 |
logger.error(f"Error in SSE chat stream while generating response: {str(e)}", stack_info=True)
|
279 |
yield "data: {\"event\": \"error\", \"data\":\""+str(e)+"\" }\n\n"
|
280 |
+
finally:
|
281 |
+
log_service.create(log)
|
282 |
|
283 |
|
284 |
@router.post("/chat/stream")
|
|
|
291 |
entity_service: Annotated[EntityService, Depends(DI.get_entity_service)],
|
292 |
dataset_service: Annotated[DatasetService, Depends(DI.get_dataset_service)],
|
293 |
dialogue_service: Annotated[DialogueService, Depends(DI.get_dialogue_service)],
|
294 |
+
log_service: Annotated[LogService, Depends(DI.get_log_service)],
|
295 |
current_user: Annotated[any, Depends(auth.get_current_user)]
|
296 |
):
|
297 |
try:
|
|
|
316 |
"Access-Control-Allow-Origin": "*",
|
317 |
}
|
318 |
return StreamingResponse(
|
319 |
+
sse_generator(request, llm_api, system_prompt.text, predict_params, dataset_service, entity_service, dialogue_service, log_service, current_user),
|
320 |
media_type="text/event-stream",
|
321 |
headers=headers
|
322 |
)
|
routes/log.py
CHANGED
@@ -1,119 +1,32 @@
|
|
1 |
import logging
|
2 |
from datetime import datetime
|
3 |
-
from typing import Annotated, Optional
|
4 |
|
5 |
-
from fastapi import APIRouter, Depends, Query
|
6 |
-
from
|
7 |
-
from starlette import status
|
8 |
|
9 |
from common import auth
|
10 |
from common.common import configure_logging
|
11 |
-
from
|
12 |
-
from
|
13 |
-
from components.dbo.models.log import Log
|
14 |
-
from schemas.log import LogCreate
|
15 |
import common.dependencies as DI
|
16 |
-
from sqlalchemy.orm import sessionmaker
|
17 |
|
18 |
router = APIRouter(tags=['Logs'])
|
19 |
|
20 |
logger = logging.getLogger(__name__)
|
21 |
configure_logging()
|
22 |
-
|
23 |
-
|
24 |
-
@router.get('/logs', status_code=status.HTTP_200_OK)
|
25 |
async def get_all_logs(
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
date_end: Optional[datetime] = Query(None, alias="date_end")
|
30 |
):
|
31 |
-
logger.info(f'GET /logs
|
32 |
-
logger.info(f'GET /logs: start_date={date_start}, end_date={date_end}')
|
33 |
-
feedback_alias = aliased(Feedback)
|
34 |
-
|
35 |
-
query = db.query(Log)
|
36 |
-
|
37 |
-
if date_start and date_end:
|
38 |
-
query = query.filter(Log.dateCreated.between(date_start, date_end))
|
39 |
-
elif date_start:
|
40 |
-
query = query.filter(Log.dateCreated >= date_start)
|
41 |
-
elif date_end:
|
42 |
-
query = query.filter(Log.dateCreated <= date_end)
|
43 |
-
|
44 |
-
query = query.outerjoin(feedback_alias, Log.id == feedback_alias.log_id)
|
45 |
-
|
46 |
-
logs_with_feedback = query.all()
|
47 |
-
|
48 |
-
combined_logs = []
|
49 |
-
for log in logs_with_feedback:
|
50 |
-
if log.feedback:
|
51 |
-
for feedback in log.feedback:
|
52 |
-
combined_logs.append(
|
53 |
-
{
|
54 |
-
"log_id": log.id,
|
55 |
-
"llmPrompt": log.llmPrompt,
|
56 |
-
"llmResponse": log.llmResponse,
|
57 |
-
"llm_classifier": log.llm_classifier,
|
58 |
-
"dateCreated": log.dateCreated,
|
59 |
-
"userRequest": log.userRequest,
|
60 |
-
"userName": log.userName,
|
61 |
-
"query_type": log.query_type,
|
62 |
-
"feedback_id": feedback.feedback_id,
|
63 |
-
"userComment": feedback.userComment,
|
64 |
-
"userScore": feedback.userScore,
|
65 |
-
"manualEstimate": feedback.manualEstimate,
|
66 |
-
"llmEstimate": feedback.llmEstimate,
|
67 |
-
}
|
68 |
-
)
|
69 |
-
else:
|
70 |
-
combined_logs.append(
|
71 |
-
{
|
72 |
-
"log_id": log.id,
|
73 |
-
"llmPrompt": log.llmPrompt,
|
74 |
-
"llmResponse": log.llmResponse,
|
75 |
-
"llm_classifier": log.llm_classifier,
|
76 |
-
"dateCreated": log.dateCreated,
|
77 |
-
"userRequest": log.userRequest,
|
78 |
-
"userName": log.userName,
|
79 |
-
"query_type": log.query_type,
|
80 |
-
"feedback_id": None,
|
81 |
-
"userComment": None,
|
82 |
-
"userScore": None,
|
83 |
-
"manualEstimate": None,
|
84 |
-
"llmEstimate": None,
|
85 |
-
}
|
86 |
-
)
|
87 |
-
return combined_logs
|
88 |
|
89 |
-
|
90 |
-
@router.get('/log/{log_id}', status_code=status.HTTP_200_OK)
|
91 |
-
async def get_log(db: Annotated[sessionmaker, Depends(DI.get_db)],
|
92 |
-
current_user: Annotated[any, Depends(auth.get_current_user)], log_id):
|
93 |
-
log = db.query(Log).filter(Log.id == log_id).first()
|
94 |
-
if log is None:
|
95 |
-
raise LogNotFoundException(log_id)
|
96 |
-
return log
|
97 |
-
|
98 |
-
|
99 |
-
@router.post('/log', status_code=status.HTTP_201_CREATED)
|
100 |
-
async def create_log(log: LogCreate, db: Annotated[sessionmaker, Depends(DI.get_db)]):
|
101 |
-
logger.info("Handling POST request to /log")
|
102 |
try:
|
103 |
-
|
104 |
-
|
105 |
-
llmResponse=log.llmResponse,
|
106 |
-
llm_classifier=log.llm_classifier,
|
107 |
-
userRequest=log.userRequest,
|
108 |
-
userName=log.userName,
|
109 |
-
)
|
110 |
-
|
111 |
-
db.add(new_log)
|
112 |
-
db.commit()
|
113 |
-
db.refresh(new_log)
|
114 |
-
|
115 |
-
logger.info(f"Successfully created log with ID: {new_log.id}")
|
116 |
-
return new_log
|
117 |
-
except Exception as e:
|
118 |
-
logger.error(f"Error creating log: {str(e)}")
|
119 |
raise e
|
|
|
|
|
|
1 |
import logging
|
2 |
from datetime import datetime
|
3 |
+
from typing import Annotated, List, Optional
|
4 |
|
5 |
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
6 |
+
from pydantic import BaseModel
|
|
|
7 |
|
8 |
from common import auth
|
9 |
from common.common import configure_logging
|
10 |
+
from components.services.log import LogService
|
11 |
+
from schemas.log import LogCreateSchema, LogFilterSchema, LogSchema, PaginatedLogResponse
|
|
|
|
|
12 |
import common.dependencies as DI
|
|
|
13 |
|
14 |
router = APIRouter(tags=['Logs'])
|
15 |
|
16 |
logger = logging.getLogger(__name__)
|
17 |
configure_logging()
|
18 |
+
|
19 |
+
@router.get('/logs', response_model=PaginatedLogResponse)
|
|
|
20 |
async def get_all_logs(
|
21 |
+
filters: Annotated[LogFilterSchema, Depends()],
|
22 |
+
log_service: Annotated[LogService, Depends(DI.get_log_service)],
|
23 |
+
current_user: Annotated[any, Depends(auth.get_current_user)]
|
|
|
24 |
):
|
25 |
+
logger.info(f'GET /logs')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
try:
|
28 |
+
return log_service.get_list(filters)
|
29 |
+
except HTTPException as e:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
30 |
raise e
|
31 |
+
except Exception as e:
|
32 |
+
raise HTTPException(status_code=500, detail=str(e))
|
schemas/entity.py
CHANGED
@@ -41,6 +41,7 @@ class EntityTextRequest(BaseModel):
|
|
41 |
chunk_scores: Optional[dict[str, float]] = None
|
42 |
include_tables: bool = True
|
43 |
max_documents: Optional[int] = None
|
|
|
44 |
|
45 |
|
46 |
class EntityTextResponse(BaseModel):
|
|
|
41 |
chunk_scores: Optional[dict[str, float]] = None
|
42 |
include_tables: bool = True
|
43 |
max_documents: Optional[int] = None
|
44 |
+
dataset_id: int
|
45 |
|
46 |
|
47 |
class EntityTextResponse(BaseModel):
|
schemas/log.py
CHANGED
@@ -1,12 +1,51 @@
|
|
1 |
-
from
|
|
|
2 |
|
3 |
from pydantic import BaseModel
|
4 |
|
5 |
|
6 |
-
class
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime
|
2 |
+
from typing import List, Optional
|
3 |
|
4 |
from pydantic import BaseModel
|
5 |
|
6 |
|
7 |
+
class LogSchema(BaseModel):
|
8 |
+
id: int
|
9 |
+
date_created: datetime
|
10 |
+
user_request: Optional[str] = None
|
11 |
+
qe_result: Optional[str] = None
|
12 |
+
search_result: Optional[str] = None
|
13 |
+
llm_result: Optional[str] = None
|
14 |
+
llm_settings: Optional[str] = None
|
15 |
+
user_name: Optional[str] = None
|
16 |
+
error: Optional[str] = None
|
17 |
+
|
18 |
+
class LogCreateSchema(BaseModel):
|
19 |
+
user_request: Optional[str] = None
|
20 |
+
qe_result: Optional[str] = None
|
21 |
+
search_result: Optional[str] = None
|
22 |
+
llm_result: Optional[str] = None
|
23 |
+
llm_settings: Optional[str] = None
|
24 |
+
user_name: Optional[str] = None
|
25 |
+
error: Optional[str] = None
|
26 |
+
|
27 |
+
class LogFilterSchema(BaseModel):
|
28 |
+
user_name: Optional[str] = None
|
29 |
+
date_from: Optional[datetime] = None
|
30 |
+
date_to: Optional[datetime] = None
|
31 |
+
|
32 |
+
page: int = 1 # Номер страницы, по умолчанию 1
|
33 |
+
page_size: int = 50 # Размер страницы, по умолчанию 50
|
34 |
+
|
35 |
+
class Config:
|
36 |
+
json_schema_extra = {
|
37 |
+
"example": {
|
38 |
+
"user_name": "demo",
|
39 |
+
"date_from": "2024-01-01T00:00:00",
|
40 |
+
"date_to": "2026-12-31T23:59:59",
|
41 |
+
"page": 1,
|
42 |
+
"page_size": 50
|
43 |
+
}
|
44 |
+
}
|
45 |
+
|
46 |
+
class PaginatedLogResponse(BaseModel):
|
47 |
+
data: List[LogSchema]
|
48 |
+
total: int
|
49 |
+
page: int
|
50 |
+
page_size: int
|
51 |
+
total_pages: int
|