zinoubm commited on
Commit
66340f1
·
1 Parent(s): 3de8ba7

initial commit

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .coverage +0 -0
  2. Dockerfile +29 -0
  3. __pycache__/main.cpython-310.pyc +0 -0
  4. alembic.ini +100 -0
  5. alembic/README +1 -0
  6. alembic/__pycache__/env.cpython-310.pyc +0 -0
  7. alembic/env.py +80 -0
  8. alembic/script.py.mako +25 -0
  9. alembic/versions/__pycache__/5c89a726934c_add_item_migration.cpython-310.pyc +0 -0
  10. alembic/versions/__pycache__/7e09fa75df7a_add_initial_migration.cpython-310.pyc +0 -0
  11. alembic/versions/__pycache__/98ffdabb3a7a_adding_docs.cpython-310.pyc +0 -0
  12. alembic/versions/__pycache__/ae0b63479948_init_db.cpython-310.pyc +0 -0
  13. alembic/versions/__pycache__/afef0f70285c_adding_oauth_support.cpython-310.pyc +0 -0
  14. alembic/versions/__pycache__/d8d827ad33ee_init_db.cpython-310.pyc +0 -0
  15. alembic/versions/__pycache__/db6822e47425_init_db.cpython-310.pyc +0 -0
  16. alembic/versions/d8d827ad33ee_init_db.py +66 -0
  17. app/__pycache__/db.cpython-310.pyc +0 -0
  18. app/__pycache__/factory.cpython-310.pyc +0 -0
  19. app/api/__init__.py +10 -0
  20. app/api/__pycache__/__init__.cpython-310.pyc +0 -0
  21. app/api/__pycache__/documents.cpython-310.pyc +0 -0
  22. app/api/__pycache__/queries.cpython-310.pyc +0 -0
  23. app/api/__pycache__/users.cpython-310.pyc +0 -0
  24. app/api/__pycache__/utils.cpython-310.pyc +0 -0
  25. app/api/documents.py +75 -0
  26. app/api/queries.py +92 -0
  27. app/api/users.py +30 -0
  28. app/api/utils.py +17 -0
  29. app/core/__pycache__/config.cpython-310.pyc +0 -0
  30. app/core/__pycache__/logger.cpython-310.pyc +0 -0
  31. app/core/config.py +52 -0
  32. app/core/logger.py +4 -0
  33. app/db.py +20 -0
  34. app/deps/__pycache__/db.cpython-310.pyc +0 -0
  35. app/deps/__pycache__/users.cpython-310.pyc +0 -0
  36. app/deps/db.py +11 -0
  37. app/deps/qdrant.py +5 -0
  38. app/deps/request_params.py +46 -0
  39. app/deps/users.py +51 -0
  40. app/email/send_mail.py +2 -0
  41. app/factory.py +116 -0
  42. app/models/__init__.py +3 -0
  43. app/models/__pycache__/__init__.cpython-310.pyc +0 -0
  44. app/models/__pycache__/document.cpython-310.pyc +0 -0
  45. app/models/__pycache__/user.cpython-310.pyc +0 -0
  46. app/models/document.py +27 -0
  47. app/models/user.py +44 -0
  48. app/openai/__pycache__/base.cpython-310.pyc +0 -0
  49. app/openai/__pycache__/core.cpython-310.pyc +0 -0
  50. app/openai/base.py +82 -0
.coverage ADDED
Binary file (53.2 kB). View file
 
Dockerfile ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ARG PYTHON_VER=3.10
2
+
3
+ FROM python:${PYTHON_VER} AS base
4
+
5
+ WORKDIR /app
6
+
7
+ ENV PYTHONUNBUFFERED=1
8
+
9
+ # Install Poetry
10
+ RUN curl -sSL https://install.python-poetry.org | POETRY_HOME=/opt/poetry python && \
11
+ cd /usr/local/bin && \
12
+ ln -s /opt/poetry/bin/poetry && \
13
+ poetry config virtualenvs.create false
14
+
15
+ COPY ./pyproject.toml ./poetry.lock* /app/
16
+
17
+ RUN poetry install --no-root
18
+
19
+ COPY . /app
20
+
21
+ FROM python:3.10-slim
22
+
23
+ WORKDIR /app
24
+
25
+ COPY --from=base /app /app
26
+ COPY --from=base /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
27
+ COPY --from=base /usr/local/bin /usr/local/bin
28
+
29
+ CMD uvicorn --host 0.0.0.0 --port 7860 main:app
__pycache__/main.cpython-310.pyc ADDED
Binary file (485 Bytes). View file
 
alembic.ini ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # A generic, single database configuration.
2
+
3
+ [alembic]
4
+ # path to migration scripts
5
+ script_location = alembic
6
+
7
+ # template used to generate migration files
8
+ # file_template = %%(rev)s_%%(slug)s
9
+
10
+ # sys.path path, will be prepended to sys.path if present.
11
+ # defaults to the current working directory.
12
+ prepend_sys_path = .
13
+
14
+ # timezone to use when rendering the date within the migration file
15
+ # as well as the filename.
16
+ # If specified, requires the python-dateutil library that can be
17
+ # installed by adding `alembic[tz]` to the pip requirements
18
+ # string value is passed to dateutil.tz.gettz()
19
+ # leave blank for localtime
20
+ # timezone =
21
+
22
+ # max length of characters to apply to the
23
+ # "slug" field
24
+ # truncate_slug_length = 40
25
+
26
+ # set to 'true' to run the environment during
27
+ # the 'revision' command, regardless of autogenerate
28
+ # revision_environment = false
29
+
30
+ # set to 'true' to allow .pyc and .pyo files without
31
+ # a source .py file to be detected as revisions in the
32
+ # versions/ directory
33
+ # sourceless = false
34
+
35
+ # version location specification; This defaults
36
+ # to alembic/versions. When using multiple version
37
+ # directories, initial revisions must be specified with --version-path.
38
+ # The path separator used here should be the separator specified by "version_path_separator"
39
+ # version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
40
+
41
+ # version path separator; As mentioned above, this is the character used to split
42
+ # version_locations. Valid values are:
43
+ #
44
+ # version_path_separator = :
45
+ # version_path_separator = ;
46
+ # version_path_separator = space
47
+ version_path_separator = os # default: use os.pathsep
48
+
49
+ # the output encoding used when revision files
50
+ # are written from script.py.mako
51
+ # output_encoding = utf-8
52
+
53
+ sqlalchemy.url = driver://user:pass@localhost/dbname
54
+
55
+
56
+ [post_write_hooks]
57
+ # post_write_hooks defines scripts or Python functions that are run
58
+ # on newly generated revision scripts. See the documentation for further
59
+ # detail and examples
60
+
61
+ # format using "black" - use the console_scripts runner, against the "black" entrypoint
62
+ # hooks = black
63
+ # black.type = console_scripts
64
+ # black.entrypoint = black
65
+ # black.options = -l 79 REVISION_SCRIPT_FILENAME
66
+
67
+ # Logging configuration
68
+ [loggers]
69
+ keys = root,sqlalchemy,alembic
70
+
71
+ [handlers]
72
+ keys = console
73
+
74
+ [formatters]
75
+ keys = generic
76
+
77
+ [logger_root]
78
+ level = WARN
79
+ handlers = console
80
+ qualname =
81
+
82
+ [logger_sqlalchemy]
83
+ level = WARN
84
+ handlers =
85
+ qualname = sqlalchemy.engine
86
+
87
+ [logger_alembic]
88
+ level = INFO
89
+ handlers =
90
+ qualname = alembic
91
+
92
+ [handler_console]
93
+ class = StreamHandler
94
+ args = (sys.stderr,)
95
+ level = NOTSET
96
+ formatter = generic
97
+
98
+ [formatter_generic]
99
+ format = %(levelname)-5.5s [%(name)s] %(message)s
100
+ datefmt = %H:%M:%S
alembic/README ADDED
@@ -0,0 +1 @@
 
 
1
+ Generic single-database configuration.
alembic/__pycache__/env.cpython-310.pyc ADDED
Binary file (1.86 kB). View file
 
alembic/env.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from logging.config import fileConfig
2
+
3
+ from alembic import context
4
+ from sqlalchemy import engine_from_config, pool
5
+
6
+ # models import is necessary for autogenerate to work
7
+ from app import models
8
+ from app.core.config import settings
9
+ from app.db import Base
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
+ fileConfig(config.config_file_name)
18
+
19
+ # add your model's MetaData object here
20
+ # for 'autogenerate' support
21
+ # from myapp import mymodel
22
+ # target_metadata = mymodel.Base.metadata
23
+ target_metadata = Base.metadata
24
+
25
+ # other values from the config, defined by the needs of env.py,
26
+ # can be acquired:
27
+ # my_important_option = config.get_main_option("my_important_option")
28
+ # ... etc.
29
+
30
+ config.set_section_option("alembic", "sqlalchemy.url", settings.DATABASE_URL)
31
+
32
+
33
+ def run_migrations_offline():
34
+ """Run migrations in 'offline' mode.
35
+
36
+ This configures the context with just a URL
37
+ and not an Engine, though an Engine is acceptable
38
+ here as well. By skipping the Engine creation
39
+ we don't even need a DBAPI to be available.
40
+
41
+ Calls to context.execute() here emit the given string to the
42
+ script output.
43
+
44
+ """
45
+ url = config.get_main_option("sqlalchemy.url")
46
+ context.configure(
47
+ url=url,
48
+ target_metadata=target_metadata,
49
+ literal_binds=True,
50
+ dialect_opts={"paramstyle": "named"},
51
+ )
52
+
53
+ with context.begin_transaction():
54
+ context.run_migrations()
55
+
56
+
57
+ def run_migrations_online():
58
+ """Run migrations in 'online' mode.
59
+
60
+ In this scenario we need to create an Engine
61
+ and associate a connection with the context.
62
+
63
+ """
64
+ connectable = engine_from_config(
65
+ config.get_section(config.config_ini_section),
66
+ prefix="sqlalchemy.",
67
+ poolclass=pool.NullPool,
68
+ )
69
+
70
+ with connectable.connect() as connection:
71
+ context.configure(connection=connection, target_metadata=target_metadata)
72
+
73
+ with context.begin_transaction():
74
+ context.run_migrations()
75
+
76
+
77
+ if context.is_offline_mode():
78
+ run_migrations_offline()
79
+ else:
80
+ run_migrations_online()
alembic/script.py.mako ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+ import fastapi_users_db_sqlalchemy
11
+ ${imports if imports else ""}
12
+
13
+ # revision identifiers, used by Alembic.
14
+ revision = ${repr(up_revision)}
15
+ down_revision = ${repr(down_revision)}
16
+ branch_labels = ${repr(branch_labels)}
17
+ depends_on = ${repr(depends_on)}
18
+
19
+
20
+ def upgrade():
21
+ ${upgrades if upgrades else "pass"}
22
+
23
+
24
+ def downgrade():
25
+ ${downgrades if downgrades else "pass"}
alembic/versions/__pycache__/5c89a726934c_add_item_migration.cpython-310.pyc ADDED
Binary file (1.53 kB). View file
 
alembic/versions/__pycache__/7e09fa75df7a_add_initial_migration.cpython-310.pyc ADDED
Binary file (1.3 kB). View file
 
alembic/versions/__pycache__/98ffdabb3a7a_adding_docs.cpython-310.pyc ADDED
Binary file (666 Bytes). View file
 
alembic/versions/__pycache__/ae0b63479948_init_db.cpython-310.pyc ADDED
Binary file (1.46 kB). View file
 
alembic/versions/__pycache__/afef0f70285c_adding_oauth_support.cpython-310.pyc ADDED
Binary file (2.63 kB). View file
 
alembic/versions/__pycache__/d8d827ad33ee_init_db.cpython-310.pyc ADDED
Binary file (1.99 kB). View file
 
alembic/versions/__pycache__/db6822e47425_init_db.cpython-310.pyc ADDED
Binary file (1.24 kB). View file
 
alembic/versions/d8d827ad33ee_init_db.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """init db
2
+
3
+ Revision ID: d8d827ad33ee
4
+ Revises:
5
+ Create Date: 2023-07-14 04:15:22.710735
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+ import fastapi_users_db_sqlalchemy
11
+
12
+
13
+ # revision identifiers, used by Alembic.
14
+ revision = 'd8d827ad33ee'
15
+ down_revision = None
16
+ branch_labels = None
17
+ depends_on = None
18
+
19
+
20
+ def upgrade():
21
+ # ### commands auto generated by Alembic - please adjust! ###
22
+ op.create_table('user',
23
+ sa.Column('created', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
24
+ sa.Column('id', fastapi_users_db_sqlalchemy.generics.GUID(), nullable=False),
25
+ sa.Column('email', sa.String(length=320), nullable=False),
26
+ sa.Column('hashed_password', sa.String(length=1024), nullable=False),
27
+ sa.Column('is_active', sa.Boolean(), nullable=False),
28
+ sa.Column('is_superuser', sa.Boolean(), nullable=False),
29
+ sa.Column('is_verified', sa.Boolean(), nullable=False),
30
+ sa.PrimaryKeyConstraint('id')
31
+ )
32
+ op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True)
33
+ op.create_table('document',
34
+ sa.Column('id', sa.Integer(), nullable=False),
35
+ sa.Column('user_id', fastapi_users_db_sqlalchemy.generics.GUID(), nullable=False),
36
+ sa.Column('name', sa.String(), nullable=False),
37
+ sa.Column('created', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
38
+ sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
39
+ sa.PrimaryKeyConstraint('id')
40
+ )
41
+ op.create_table('oauth_account',
42
+ sa.Column('id', fastapi_users_db_sqlalchemy.generics.GUID(), nullable=False),
43
+ sa.Column('user_id', fastapi_users_db_sqlalchemy.generics.GUID(), nullable=False),
44
+ sa.Column('oauth_name', sa.String(length=100), nullable=False),
45
+ sa.Column('access_token', sa.String(length=1024), nullable=False),
46
+ sa.Column('expires_at', sa.Integer(), nullable=True),
47
+ sa.Column('refresh_token', sa.String(length=1024), nullable=True),
48
+ sa.Column('account_id', sa.String(length=320), nullable=False),
49
+ sa.Column('account_email', sa.String(length=320), nullable=False),
50
+ sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='cascade'),
51
+ sa.PrimaryKeyConstraint('id')
52
+ )
53
+ op.create_index(op.f('ix_oauth_account_account_id'), 'oauth_account', ['account_id'], unique=False)
54
+ op.create_index(op.f('ix_oauth_account_oauth_name'), 'oauth_account', ['oauth_name'], unique=False)
55
+ # ### end Alembic commands ###
56
+
57
+
58
+ def downgrade():
59
+ # ### commands auto generated by Alembic - please adjust! ###
60
+ op.drop_index(op.f('ix_oauth_account_oauth_name'), table_name='oauth_account')
61
+ op.drop_index(op.f('ix_oauth_account_account_id'), table_name='oauth_account')
62
+ op.drop_table('oauth_account')
63
+ op.drop_table('document')
64
+ op.drop_index(op.f('ix_user_email'), table_name='user')
65
+ op.drop_table('user')
66
+ # ### end Alembic commands ###
app/__pycache__/db.cpython-310.pyc ADDED
Binary file (734 Bytes). View file
 
app/__pycache__/factory.cpython-310.pyc ADDED
Binary file (3.54 kB). View file
 
app/api/__init__.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter
2
+
3
+ from app.api import users, utils, queries, documents
4
+
5
+ api_router = APIRouter()
6
+
7
+ api_router.include_router(utils.router, tags=["utils"])
8
+ api_router.include_router(users.router, tags=["users"])
9
+ api_router.include_router(queries.router, tags=["queries"])
10
+ api_router.include_router(documents.router, tags=["documents"])
app/api/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (401 Bytes). View file
 
app/api/__pycache__/documents.cpython-310.pyc ADDED
Binary file (2.58 kB). View file
 
app/api/__pycache__/queries.cpython-310.pyc ADDED
Binary file (1.81 kB). View file
 
app/api/__pycache__/users.cpython-310.pyc ADDED
Binary file (1.19 kB). View file
 
app/api/__pycache__/utils.cpython-310.pyc ADDED
Binary file (501 Bytes). View file
 
app/api/documents.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any
2
+ from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
3
+ from starlette.responses import Response
4
+ from app.schemas.document import UpsertResponse
5
+ from app.parser.parser import get_document_from_file
6
+ from app.parser.chunk import chunk_text
7
+ import requests
8
+
9
+ from app.deps.users import current_user
10
+ from app.models.user import User
11
+ from app.parser.parser import get_document_from_file
12
+ from sqlalchemy.ext.asyncio.session import AsyncSession
13
+ from app.deps.db import get_async_session
14
+
15
+ from app.models.document import Document
16
+ from uuid import uuid4
17
+
18
+ from app.vectorstore.qdrant import QdrantManager
19
+ from app.openai.base import OpenAiManager
20
+ from sqlalchemy.future import select
21
+
22
+
23
+ router = APIRouter(prefix="/documents")
24
+
25
+
26
+ @router.post(
27
+ "/upsert-file",
28
+ response_model=UpsertResponse,
29
+ )
30
+ async def upsert_file(
31
+ file: UploadFile = File(...),
32
+ session: AsyncSession = Depends(get_async_session),
33
+ user: User = Depends(current_user),
34
+ ):
35
+ document = await get_document_from_file(file)
36
+ chunks = chunk_text(document.text, max_size=2000)
37
+ db_document = Document(user_id=str(user.id).replace("-", ""), name=file.filename)
38
+ session.add(db_document)
39
+
40
+ await session.commit()
41
+
42
+ openai_manager = OpenAiManager()
43
+ ids = [uuid4().hex for chunk in chunks]
44
+ payloads = [
45
+ {
46
+ "user_id": str(user.id).replace("-", ""),
47
+ "document_id": db_document.id,
48
+ "chunk": chunk,
49
+ }
50
+ for chunk in chunks
51
+ ]
52
+ embeddings = openai_manager.get_embeddings(chunks)
53
+
54
+ vector_manager = QdrantManager()
55
+ response = vector_manager.upsert_points(ids, payloads, embeddings)
56
+
57
+ return UpsertResponse(id=db_document.id)
58
+
59
+
60
+ @router.get("/")
61
+ async def get_documents(
62
+ session: AsyncSession = Depends(get_async_session),
63
+ user: User = Depends(current_user),
64
+ ):
65
+ statement = select(Document).where(
66
+ Document.user_id == str(user.id).replace("-", "")
67
+ )
68
+ db_documents = await session.execute(statement)
69
+
70
+ documents = [
71
+ {"id": document.id, "name": document.name}
72
+ for document in db_documents.scalars().all()
73
+ ]
74
+
75
+ return {"documents": documents}
app/api/queries.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any
2
+ from fastapi import APIRouter, Depends, HTTPException
3
+ from starlette.responses import Response
4
+ import requests
5
+
6
+
7
+ from app.deps.users import current_user
8
+ from app.models.user import User
9
+
10
+ from app.vectorstore.qdrant import qdrant_manager
11
+ from app.openai.base import openai_manager
12
+ from app.openai.core import ask, filter, summarize
13
+
14
+ from pydantic import BaseModel
15
+
16
+
17
+ class QueryRequest(BaseModel):
18
+ query: str
19
+ document_id: int
20
+
21
+
22
+ class QueryResponse(BaseModel):
23
+ answer: str
24
+ document_id: int
25
+
26
+
27
+ router = APIRouter(prefix="/queries")
28
+
29
+
30
+ @router.post("/")
31
+ async def query(
32
+ query_request: QueryRequest,
33
+ response: Response,
34
+ user: User = Depends(current_user),
35
+ ) -> QueryResponse:
36
+ # add check that this user actually owns this document
37
+
38
+ query_vector = openai_manager.get_embedding(query_request.query)
39
+
40
+ # print(">>>>>>>>>>>>")
41
+ # print("query vector")
42
+ # print(query_vector)
43
+
44
+ # print(">>>>>>>>>>>>>>>")
45
+ # print("document_id: ", query_request.document_id)
46
+ # print("--------------")
47
+
48
+ # print(">>>>>>>>>>>>")
49
+ # print("user_id")
50
+ # print(user.id.hex)
51
+
52
+ points = qdrant_manager.search_point(
53
+ query_vector=query_vector,
54
+ user_id=str(user.id.hex),
55
+ document_id=int(query_request.document_id),
56
+ limit=1000,
57
+ )
58
+
59
+ # print(">>>>>>>>>>>>")
60
+ # print("points")
61
+ # print(points)
62
+
63
+ context = "\n\n\n".join([point.payload["chunk"] for point in points])
64
+ # print(">>>>>>>>>>>>")
65
+ # print("context")
66
+ # print(context)
67
+
68
+ # filter_response = filter(context, query_request.query, openai_manager)
69
+ # print(">>>>>>>>>>>>>>>>")
70
+ # print("filter resopnse")
71
+ # print(filter_response)
72
+ # print("----------------")
73
+ # remove later
74
+ filter_response = True
75
+ if filter_response:
76
+ answer = ask(
77
+ context,
78
+ query_request.query,
79
+ openai_manager,
80
+ )
81
+
82
+ query_response = QueryResponse(
83
+ answer=answer, document_id=query_request.document_id
84
+ )
85
+
86
+ else:
87
+ query_response = QueryResponse(
88
+ answer="Sorry, Your question is out of Context!",
89
+ document_id=query_request.document_id,
90
+ )
91
+
92
+ return query_response
app/api/users.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, List
2
+
3
+ from fastapi.params import Depends
4
+ from fastapi.routing import APIRouter
5
+ from sqlalchemy import func, select
6
+ from sqlalchemy.ext.asyncio.session import AsyncSession
7
+ from starlette.responses import Response
8
+
9
+ from app.deps.db import get_async_session
10
+ from app.deps.users import current_superuser
11
+ from app.models.user import User
12
+ from app.schemas.user import UserRead
13
+
14
+ router = APIRouter()
15
+
16
+
17
+ @router.get("/users", response_model=List[UserRead])
18
+ async def get_users(
19
+ response: Response,
20
+ session: AsyncSession = Depends(get_async_session),
21
+ user: User = Depends(current_superuser),
22
+ skip: int = 0,
23
+ limit: int = 100,
24
+ ) -> Any:
25
+ total = await session.scalar(select(func.count(User.id)))
26
+ users = (
27
+ (await session.execute(select(User).offset(skip).limit(limit))).scalars().all()
28
+ )
29
+ response.headers["Content-Range"] = f"{skip}-{skip + len(users)}/{total}"
30
+ return users
app/api/utils.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any
2
+
3
+ from fastapi import APIRouter
4
+
5
+ from app.schemas.msg import Msg
6
+
7
+ router = APIRouter()
8
+
9
+
10
+ @router.get(
11
+ "/hello-world",
12
+ response_model=Msg,
13
+ status_code=200,
14
+ include_in_schema=False,
15
+ )
16
+ def test_hello_world() -> Any:
17
+ return {"msg": "Hello world!"}
app/core/__pycache__/config.cpython-310.pyc ADDED
Binary file (1.8 kB). View file
 
app/core/__pycache__/logger.cpython-310.pyc ADDED
Binary file (220 Bytes). View file
 
app/core/config.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from typing import Any, Dict, List, Optional
3
+
4
+ from pydantic import BaseSettings, HttpUrl, PostgresDsn, validator
5
+ from pydantic.networks import AnyHttpUrl
6
+
7
+
8
+ class Settings(BaseSettings):
9
+ PROJECT_NAME: str = "QueryX"
10
+
11
+ SENTRY_DSN: Optional[HttpUrl] = None
12
+
13
+ API_PATH: str = "/api/v1"
14
+
15
+ ACCESS_TOKEN_EXPIRE_MINUTES: int = 7 * 24 * 60 # 7 days
16
+
17
+ BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = [
18
+ "http://0.0.0.0:9000",
19
+ "http://0.0.0.0:8000",
20
+ ]
21
+
22
+ # The following variables need to be defined in environment
23
+
24
+ TEST_DATABASE_URL: Optional[PostgresDsn]
25
+ DATABASE_URL: PostgresDsn
26
+ ASYNC_DATABASE_URL: Optional[PostgresDsn]
27
+
28
+ @validator("DATABASE_URL", pre=True)
29
+ def build_test_database_url(cls, v: Optional[str], values: Dict[str, Any]):
30
+ """Overrides DATABASE_URL with TEST_DATABASE_URL in test environment."""
31
+ url = v
32
+ if "pytest" in sys.modules:
33
+ if not values.get("TEST_DATABASE_URL"):
34
+ raise Exception(
35
+ "pytest detected, but TEST_DATABASE_URL is not set in environment"
36
+ )
37
+ url = values["TEST_DATABASE_URL"]
38
+ if url:
39
+ return url.replace("postgres://", "postgresql://")
40
+ return url
41
+
42
+ @validator("ASYNC_DATABASE_URL")
43
+ def build_async_database_url(cls, v: Optional[str], values: Dict[str, Any]):
44
+ """Builds ASYNC_DATABASE_URL from DATABASE_URL."""
45
+ v = values["DATABASE_URL"]
46
+ return v.replace("postgresql", "postgresql+asyncpg", 1) if v else v
47
+
48
+ SECRET_KEY: str
49
+ # END: required environment variables
50
+
51
+
52
+ settings = Settings()
app/core/logger.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ import logging
2
+
3
+ logger = logging.getLogger(__name__)
4
+ logging.basicConfig(level=logging.INFO)
app/db.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any
2
+
3
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
4
+ from sqlalchemy.orm import DeclarativeBase
5
+
6
+ from app.core.config import settings
7
+
8
+ async_engine = create_async_engine(str(settings.ASYNC_DATABASE_URL), pool_pre_ping=True)
9
+
10
+ async_session_maker = async_sessionmaker(
11
+ async_engine,
12
+ class_=AsyncSession,
13
+ expire_on_commit=False,
14
+ autocommit=False,
15
+ autoflush=False,
16
+ )
17
+
18
+
19
+ class Base(DeclarativeBase):
20
+ id: Any
app/deps/__pycache__/db.cpython-310.pyc ADDED
Binary file (513 Bytes). View file
 
app/deps/__pycache__/users.cpython-310.pyc ADDED
Binary file (1.78 kB). View file
 
app/deps/db.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import AsyncGenerator
2
+
3
+ from sqlalchemy.ext.asyncio.session import AsyncSession
4
+
5
+ from app.db import async_session_maker
6
+
7
+
8
+ async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
9
+ async with async_session_maker() as session:
10
+ yield session
11
+ await session.close()
app/deps/qdrant.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import qdrant_client
2
+
3
+
4
+ def get_qdrant_client():
5
+ return qdrant_client.QdrantClient(host=host, port=port, api_key=qdrant_api_key)
app/deps/request_params.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from typing import Callable, Optional, Type
3
+
4
+ from fastapi import HTTPException, Query
5
+ from sqlalchemy import UnaryExpression, asc, desc
6
+
7
+ from app.db import Base
8
+ from app.schemas.request_params import RequestParams
9
+
10
+
11
+ def parse_react_admin_params(model: Type[Base]) -> Callable:
12
+ """Parses sort and range parameters coming from a react-admin request"""
13
+
14
+ def inner(
15
+ sort_: Optional[str] = Query(
16
+ None,
17
+ alias="sort",
18
+ description='Format: `["field_name", "direction"]`',
19
+ example='["id", "ASC"]',
20
+ ),
21
+ range_: Optional[str] = Query(
22
+ None,
23
+ alias="range",
24
+ description="Format: `[start, end]`",
25
+ example="[0, 10]",
26
+ ),
27
+ ) -> RequestParams:
28
+ skip, limit = 0, 10
29
+ if range_:
30
+ start, end = json.loads(range_)
31
+ skip, limit = start, (end - start + 1)
32
+
33
+ order_by: UnaryExpression = desc(model.id)
34
+ if sort_:
35
+ sort_column, sort_order = json.loads(sort_)
36
+ if sort_order.lower() == "asc":
37
+ direction = asc
38
+ elif sort_order.lower() == "desc":
39
+ direction = desc
40
+ else:
41
+ raise HTTPException(400, f"Invalid sort direction {sort_order}")
42
+ order_by = direction(model.__table__.c[sort_column])
43
+
44
+ return RequestParams(skip=skip, limit=limit, order_by=order_by)
45
+
46
+ return inner
app/deps/users.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+
3
+ from fastapi import Depends
4
+ from fastapi_users import FastAPIUsers
5
+ from fastapi_users.authentication import (
6
+ AuthenticationBackend,
7
+ BearerTransport,
8
+ JWTStrategy,
9
+ )
10
+ from fastapi_users.manager import BaseUserManager, UUIDIDMixin
11
+ from fastapi_users_db_sqlalchemy import SQLAlchemyUserDatabase
12
+ from sqlalchemy.ext.asyncio import AsyncSession
13
+
14
+ from app.core.config import settings
15
+ from app.deps.db import get_async_session
16
+ from app.models.user import OAuthAccount, User as UserModel
17
+
18
+ bearer_transport = BearerTransport(tokenUrl="auth/jwt/login")
19
+
20
+
21
+ def get_jwt_strategy() -> JWTStrategy:
22
+ return JWTStrategy(
23
+ secret=settings.SECRET_KEY,
24
+ lifetime_seconds=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
25
+ )
26
+
27
+
28
+ jwt_authentication = AuthenticationBackend(
29
+ name="jwt",
30
+ transport=bearer_transport,
31
+ get_strategy=get_jwt_strategy,
32
+ )
33
+
34
+
35
+ class UserManager(UUIDIDMixin, BaseUserManager[UserModel, uuid.UUID]):
36
+ reset_password_token_secret = settings.SECRET_KEY
37
+ verification_token_secret = settings.SECRET_KEY
38
+
39
+
40
+ def get_user_db(session: AsyncSession = Depends(get_async_session)):
41
+ yield SQLAlchemyUserDatabase(session, UserModel, OAuthAccount)
42
+
43
+
44
+ def get_user_manager(user_db=Depends(get_user_db)):
45
+ yield UserManager(user_db)
46
+
47
+
48
+ fastapi_users = FastAPIUsers(get_user_manager, [jwt_authentication])
49
+
50
+ current_user = fastapi_users.current_user(active=True)
51
+ current_superuser = fastapi_users.current_user(active=True, superuser=True)
app/email/send_mail.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ def send_email():
2
+ raise NotImplementedError
app/factory.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.routing import APIRoute
3
+ from fastapi.staticfiles import StaticFiles
4
+ from fastapi_users import FastAPIUsers
5
+ from starlette.middleware.cors import CORSMiddleware
6
+ from starlette.requests import Request
7
+ from starlette.responses import FileResponse
8
+
9
+ from app.api import api_router
10
+ from app.core.config import settings
11
+ from app.deps.users import fastapi_users, jwt_authentication
12
+ from app.schemas.user import UserCreate, UserRead, UserUpdate
13
+ import os
14
+
15
+
16
+ from httpx_oauth.clients.google import GoogleOAuth2 # google client
17
+
18
+ # for dev only
19
+ os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
20
+
21
+
22
+ def create_app():
23
+ description = f"{settings.PROJECT_NAME} API"
24
+ app = FastAPI(
25
+ title=settings.PROJECT_NAME,
26
+ openapi_url=f"{settings.API_PATH}/openapi.json",
27
+ docs_url="/docs/",
28
+ description=description,
29
+ redoc_url=None,
30
+ )
31
+ setup_routers(app, fastapi_users)
32
+ setup_cors_middleware(app)
33
+ serve_static_app(app)
34
+ return app
35
+
36
+
37
+ def setup_routers(app: FastAPI, fastapi_users: FastAPIUsers) -> None:
38
+ app.include_router(api_router, prefix=settings.API_PATH)
39
+ app.include_router(
40
+ fastapi_users.get_auth_router(
41
+ jwt_authentication,
42
+ requires_verification=False,
43
+ ),
44
+ prefix=f"{settings.API_PATH}/auth/jwt",
45
+ tags=["auth"],
46
+ )
47
+ app.include_router(
48
+ fastapi_users.get_register_router(UserRead, UserCreate),
49
+ prefix=f"{settings.API_PATH}/auth",
50
+ tags=["auth"],
51
+ )
52
+ app.include_router(
53
+ fastapi_users.get_users_router(
54
+ UserRead, UserUpdate, requires_verification=False
55
+ ),
56
+ prefix=f"{settings.API_PATH}/users",
57
+ tags=["users"],
58
+ )
59
+
60
+ google_oauth_client = GoogleOAuth2(
61
+ os.getenv("GOOGLE_OAUTH_CLIENT_ID", ""),
62
+ os.getenv("GOOGLE_OAUTH_CLIENT_SECRET", ""),
63
+ )
64
+
65
+ app.include_router(
66
+ fastapi_users.get_oauth_router(
67
+ google_oauth_client, jwt_authentication, "SECRET"
68
+ ),
69
+ prefix="/auth/google",
70
+ tags=["auth"],
71
+ )
72
+ # The following operation needs to be at the end of this function
73
+ use_route_names_as_operation_ids(app)
74
+
75
+
76
+ def serve_static_app(app):
77
+ app.mount("/", StaticFiles(directory="static"), name="static")
78
+
79
+ @app.middleware("http")
80
+ async def _add_404_middleware(request: Request, call_next):
81
+ """Serves static assets on 404"""
82
+ response = await call_next(request)
83
+ path = request["path"]
84
+ if path.startswith(settings.API_PATH) or path.startswith("/docs"):
85
+ return response
86
+ if response.status_code == 404:
87
+ return FileResponse("static/index.html")
88
+ return response
89
+
90
+
91
+ def setup_cors_middleware(app):
92
+ if settings.BACKEND_CORS_ORIGINS:
93
+ app.add_middleware(
94
+ CORSMiddleware,
95
+ allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS],
96
+ allow_credentials=True,
97
+ allow_methods=["*"],
98
+ expose_headers=["Content-Range", "Range"],
99
+ allow_headers=["Authorization", "Range", "Content-Range"],
100
+ )
101
+
102
+
103
+ def use_route_names_as_operation_ids(app: FastAPI) -> None:
104
+ """
105
+ Simplify operation IDs so that generated API clients have simpler function
106
+ names.
107
+
108
+ Should be called only after all routes have been added.
109
+ """
110
+ route_names = set()
111
+ for route in app.routes:
112
+ if isinstance(route, APIRoute):
113
+ if route.name in route_names:
114
+ raise Exception("Route function names should be unique")
115
+ route.operation_id = route.name
116
+ route_names.add(route.name)
app/models/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Import all models here so alembic can discover them
2
+ from app.models.user import User
3
+ from app.models.document import Document
app/models/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (216 Bytes). View file
 
app/models/__pycache__/document.cpython-310.pyc ADDED
Binary file (1.17 kB). View file
 
app/models/__pycache__/user.cpython-310.pyc ADDED
Binary file (1.42 kB). View file
 
app/models/document.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from typing import TYPE_CHECKING
3
+ from uuid import UUID
4
+
5
+ from fastapi_users_db_sqlalchemy import GUID
6
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
7
+ from sqlalchemy.sql.functions import func
8
+ from sqlalchemy.sql.schema import ForeignKey
9
+ from sqlalchemy.sql.sqltypes import DateTime
10
+
11
+ from app.db import Base
12
+
13
+ if TYPE_CHECKING:
14
+ from app.models.user import User
15
+
16
+
17
+ class Document(Base):
18
+ __tablename__ = "document"
19
+
20
+ id: Mapped[int] = mapped_column(primary_key=True)
21
+ user_id: Mapped[UUID] = mapped_column(GUID, ForeignKey("user.id"))
22
+ user: Mapped["User"] = relationship(back_populates="documents")
23
+ name: Mapped[str]
24
+
25
+ created: Mapped[datetime] = mapped_column(
26
+ DateTime(timezone=True), server_default=func.now()
27
+ )
app/models/user.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from typing import TYPE_CHECKING, List
3
+
4
+
5
+ from fastapi_users_db_sqlalchemy import (
6
+ SQLAlchemyBaseUserTableUUID,
7
+ SQLAlchemyBaseOAuthAccountTableUUID,
8
+ )
9
+
10
+ from sqlalchemy import DateTime
11
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
12
+ from sqlalchemy.sql.functions import func
13
+
14
+ from app.db import Base
15
+
16
+
17
+ if TYPE_CHECKING:
18
+ from app.models.document import Document
19
+
20
+
21
+ class OAuthAccount(SQLAlchemyBaseOAuthAccountTableUUID, Base):
22
+ pass
23
+
24
+
25
+ class User(SQLAlchemyBaseUserTableUUID, Base):
26
+ __tablename__ = "user"
27
+
28
+ oauth_accounts: Mapped[List[OAuthAccount]] = relationship(
29
+ "OAuthAccount", lazy="joined"
30
+ )
31
+
32
+ created: Mapped[datetime] = mapped_column(
33
+ DateTime(timezone=True), server_default=func.now()
34
+ )
35
+
36
+ documents: Mapped["Document"] = relationship(
37
+ back_populates="user", cascade="all, delete"
38
+ )
39
+
40
+ def __repr__(self):
41
+ return f"User(id={self.id!r}, name={self.email!r})"
42
+
43
+
44
+ # add activated column
app/openai/__pycache__/base.cpython-310.pyc ADDED
Binary file (2.46 kB). View file
 
app/openai/__pycache__/core.cpython-310.pyc ADDED
Binary file (1.23 kB). View file
 
app/openai/base.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ # from dotenv import load_dotenv
4
+ import openai
5
+
6
+
7
+ class OpenAiManager:
8
+ def __init__(self):
9
+ # load_dotenv()
10
+ openai.api_key = os.getenv("OPENAI_API_KEY")
11
+
12
+ def get_completion(
13
+ self,
14
+ prompt,
15
+ model="text-davinci-003",
16
+ max_tokens=128,
17
+ temperature=0,
18
+ ):
19
+ response = None
20
+ try:
21
+ response = openai.Completion.create(
22
+ prompt=prompt,
23
+ max_tokens=max_tokens,
24
+ model=model,
25
+ temperature=temperature,
26
+ )["choices"][0]["text"]
27
+
28
+ except Exception as err:
29
+ print(f"Sorry, There was a problem \n\n {err}")
30
+
31
+ return response
32
+
33
+ def get_chat_completion(self, prompt, model="gpt-3.5-turbo"):
34
+ response = None
35
+ try:
36
+ response = (
37
+ openai.ChatCompletion.create(
38
+ model=model,
39
+ messages=[
40
+ {
41
+ "role": "system",
42
+ "content": prompt,
43
+ }
44
+ ],
45
+ )
46
+ .choices[0]
47
+ .message.content.strip()
48
+ )
49
+
50
+ except Exception as err:
51
+ print(f"Sorry, There was a problem \n\n {err}")
52
+
53
+ return response
54
+
55
+ def get_embedding(self, prompt, model="text-embedding-ada-002"):
56
+ prompt = prompt.replace("\n", " ")
57
+
58
+ embedding = None
59
+ try:
60
+ embedding = openai.Embedding.create(input=[prompt], model=model)["data"][0][
61
+ "embedding"
62
+ ]
63
+
64
+ except Exception as err:
65
+ print(f"Sorry, There was a problem {err}")
66
+
67
+ return embedding
68
+
69
+ def get_embeddings(self, prompts, model="text-embedding-ada-002"):
70
+ prompts = [prompt.replace("\n", " ") for prompt in prompts]
71
+
72
+ embeddings = None
73
+ try:
74
+ embeddings = openai.Embedding.create(input=prompts, model=model)["data"]
75
+
76
+ except Exception as err:
77
+ print(f"Sorry, There was a problem {err}")
78
+
79
+ return [embedding["embedding"] for embedding in embeddings]
80
+
81
+
82
+ openai_manager = OpenAiManager()