muryshev commited on
Commit
9390ea2
·
1 Parent(s): e11aef6
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=config_dev.yaml
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 components.dbo.models.feedback
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
- llmPrompt = mapped_column(String)
13
- llmResponse = mapped_column(String)
14
- llm_classifier = mapped_column(String)
15
- userRequest = mapped_column(String)
16
- query_type = mapped_column(String)
17
- userName = mapped_column(String)
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/config_dev.yaml # Конфиг
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
- # from routes.acronym import router as acronym_router
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
- # from main_before import config
26
-
 
 
27
 
28
  # Загружаем переменные из .env
29
  dotenv.load_dotenv()
30
 
31
- # from routes.feedback import router as feedback_router
32
- # from routes.llm import router as llm_router
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
- print("Downloading model to cache...")
52
- AutoTokenizer.from_pretrained(config.db_config.search.vectorizer_path)
53
- AutoModel.from_pretrained(config.db_config.search.vectorizer_path)
54
- print("Model cached successfully.")
 
 
 
 
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=8885,
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
- 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}')
 
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) -> AsyncGenerator[str, None]:
 
 
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
- # logger.info(f"Streaming token: {token}")
 
 
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 sqlalchemy.orm import aliased
7
- from starlette import status
8
 
9
  from common import auth
10
  from common.common import configure_logging
11
- from common.exceptions import LogNotFoundException
12
- from components.dbo.models.feedback import Feedback
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
- db: Annotated[sessionmaker, Depends(DI.get_db)],
27
- current_user: Annotated[any, Depends(auth.get_current_user)],
28
- date_start: Optional[datetime] = Query(None, alias="date_start"),
29
- date_end: Optional[datetime] = Query(None, alias="date_end")
30
  ):
31
- logger.info(f'GET /logs: start')
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
- new_log = Log(
104
- llmPrompt=log.llmPrompt,
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 typing import Optional
 
2
 
3
  from pydantic import BaseModel
4
 
5
 
6
- class LogCreate(BaseModel):
7
- llmPrompt: Optional[str] = None
8
- llmResponse: Optional[str] = None
9
- userRequest: str
10
- llm_classifier: Optional[str] = None
11
- query_type: Optional[str] = None
12
- userName: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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