FEAT: First commit
This commit is contained in:
commit
1bcb5672ad
11
.env.example
Normal file
11
.env.example
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
#DATABASE_CACHING
|
||||||
|
REDIS_HOST=
|
||||||
|
REDIS_PORT=
|
||||||
|
REDIS_DB=
|
||||||
|
REDIS_PASSWORD=
|
||||||
|
#DATABASE SAVING
|
||||||
|
SQLITE_PATH=
|
||||||
|
#SYSTEM
|
||||||
|
SENTRY_DSN=
|
||||||
|
MASTER_KEY=
|
||||||
|
CORS_ALLOW_ORIGINS=*
|
147
.gitignore
vendored
Normal file
147
.gitignore
vendored
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
CMakeFiles/
|
||||||
|
CMakeCache.txt
|
||||||
|
CMakeScripts/
|
||||||
|
CTestTestfile.cmake
|
||||||
|
cmake_install.cmake
|
||||||
|
install_manifest.txt
|
||||||
|
Makefile
|
||||||
|
*.cmake
|
||||||
|
*.make
|
||||||
|
*.txt
|
||||||
|
|
||||||
|
|
||||||
|
.vscode/
|
||||||
|
.cache/
|
||||||
|
.env
|
19
Craft/__init__.py
Normal file
19
Craft/__init__.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from Craft.route import router
|
||||||
|
from Craft.middleware.cors_middlewares import init_cors_middleware
|
||||||
|
from Craft.middleware.logging_middlewares import init_logging_middleware
|
||||||
|
from Craft.middleware.processtime_middlewares import init_processtimemiddleware
|
||||||
|
from Craft.database.sqlite import Sqlite
|
||||||
|
from Craft.module.sentry import Sentry
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
init_cors_middleware(app)
|
||||||
|
init_logging_middleware(app)
|
||||||
|
init_processtimemiddleware(app)
|
||||||
|
|
||||||
|
Sentry()
|
||||||
|
|
||||||
|
app.include_router(router)
|
38
Craft/controller/merge.py
Normal file
38
Craft/controller/merge.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import sentry_sdk
|
||||||
|
from Craft.module.logging import Logger
|
||||||
|
from Craft.model.merge import MergeOutput, MergeInput
|
||||||
|
from Craft.module.ml import Engine
|
||||||
|
from Craft.database.redis.caching import RedisCaching
|
||||||
|
from Craft.database.sqlite.data import SqliteDatabase
|
||||||
|
|
||||||
|
class MergeController:
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = Logger()
|
||||||
|
self.engine = Engine()
|
||||||
|
self.redis_caching = RedisCaching()
|
||||||
|
self.sqlite_database = SqliteDatabase()
|
||||||
|
|
||||||
|
async def merge(self, model: MergeInput) -> MergeOutput:
|
||||||
|
try:
|
||||||
|
cached = await self.redis_caching.cache_get(model.first_word, model.second_word)
|
||||||
|
if cached:
|
||||||
|
return MergeOutput(status=1, response=cached)
|
||||||
|
data = await self.engine.generate(model.first_word, model.second_word)
|
||||||
|
await self.redis_caching.cache_set(model.first_word, model.second_word, data)
|
||||||
|
await self.sqlite_database.sets(model.first_word, model.second_word, data.emoji, data.word)
|
||||||
|
return MergeOutput(status=1, response={"emoji": data.emoji, "word": data.word})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._log_and_capture_exception(e, "Error in Merge Controller")
|
||||||
|
return MergeOutput(status=-1, error=str(e))
|
||||||
|
|
||||||
|
async def increasevalue_backgroundtasks(self, first_word: str, second_word: str) -> None:
|
||||||
|
try:
|
||||||
|
await self.sqlite_database.update(first_word, second_word)
|
||||||
|
except Exception as e:
|
||||||
|
self._log_and_capture_exception(e, "Error in Increase Value BackgroundTasks")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _log_and_capture_exception(self, exception: Exception, message: str) -> None:
|
||||||
|
self.logger.error(f"{message}: {exception}")
|
||||||
|
sentry_sdk.capture_exception(exception)
|
40
Craft/database/redis/__init__.py
Normal file
40
Craft/database/redis/__init__.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import os
|
||||||
|
import redis.asyncio as redis
|
||||||
|
from dotenv import find_dotenv, load_dotenv
|
||||||
|
|
||||||
|
from Craft.database.sqlite import Sqlite
|
||||||
|
load_dotenv(find_dotenv())
|
||||||
|
|
||||||
|
|
||||||
|
class Redis:
|
||||||
|
def __init__(self):
|
||||||
|
self.redis_host: str = os.getenv("REDIS_HOST")
|
||||||
|
self.redis_port: int = os.getenv("REDIS_PORT")
|
||||||
|
self.redis_db: int = os.getenv("REDIS_DB")
|
||||||
|
self.redis_password: str = os.getenv("REDIS_PASSWORD")
|
||||||
|
|
||||||
|
self.redis_pool = redis.ConnectionPool(
|
||||||
|
host=self.redis_host,
|
||||||
|
port=self.redis_port,
|
||||||
|
db=self.redis_db,
|
||||||
|
password=self.redis_password
|
||||||
|
)
|
||||||
|
self.redis_instance = redis.Redis(connection_pool=self.redis_pool)
|
||||||
|
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
return self.redis_instance
|
||||||
|
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc, tb):
|
||||||
|
await self.redis_instance.close()
|
||||||
|
|
||||||
|
|
||||||
|
async def sync_data(self) -> None:
|
||||||
|
await self.redis_instance.flushdb()
|
||||||
|
async with Sqlite() as sqlite:
|
||||||
|
await sqlite.execute("SELECT * FROM Craft")
|
||||||
|
data = await sqlite.fetchall()
|
||||||
|
for row in data:
|
||||||
|
key1, key2, value_emoji, value_word, _ = row
|
||||||
|
self.redis_instance.hmset(f"CraftCached:{key1}:{key2}", {"emoji": value_emoji, "word": value_word})
|
82
Craft/database/redis/caching.py
Normal file
82
Craft/database/redis/caching.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import asyncio
|
||||||
|
import sentry_sdk
|
||||||
|
|
||||||
|
from Craft.database.redis import Redis
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
class RedisCaching:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _create_cache_key(key1: str, key2: str) -> str:
|
||||||
|
return f"CraftCached:{key1}:{key2}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _validate_key(key: str) -> None:
|
||||||
|
if not isinstance(key, str) or not key:
|
||||||
|
raise ValueError("Invalid key")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _validate_value(value: SimpleNamespace) -> None:
|
||||||
|
if not isinstance(value, SimpleNamespace) or not hasattr(value, 'emoji') or not hasattr(value, 'word'):
|
||||||
|
raise ValueError("Invalid value")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def cache_get(key1: str, key2: str) -> dict:
|
||||||
|
RedisCaching._validate_key(key1)
|
||||||
|
RedisCaching._validate_key(key2)
|
||||||
|
key = RedisCaching._create_cache_key(key1, key2)
|
||||||
|
async with Redis() as redis:
|
||||||
|
try:
|
||||||
|
data = await redis.hgetall(key)
|
||||||
|
if not data:
|
||||||
|
key = RedisCaching._create_cache_key(key2, key1)
|
||||||
|
data = await redis.hgetall(key)
|
||||||
|
return data
|
||||||
|
except Exception as e:
|
||||||
|
sentry_sdk.capture_exception(e)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def cache_set(key1: str, key2: str, value: SimpleNamespace) -> None:
|
||||||
|
RedisCaching._validate_key(key1)
|
||||||
|
RedisCaching._validate_key(key2)
|
||||||
|
RedisCaching._validate_value(value)
|
||||||
|
key = RedisCaching._create_cache_key(key1, key2)
|
||||||
|
|
||||||
|
async with Redis() as redis:
|
||||||
|
try:
|
||||||
|
await redis.hset(key, mapping={"emoji": value.emoji, "word": value.word})
|
||||||
|
except Exception as e:
|
||||||
|
sentry_sdk.capture_exception(e)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def cache_delete(key1: str, key2: str) -> None:
|
||||||
|
RedisCaching._validate_key(key1)
|
||||||
|
RedisCaching._validate_key(key2)
|
||||||
|
key = RedisCaching._create_cache_key(key1, key2)
|
||||||
|
|
||||||
|
async with Redis() as redis:
|
||||||
|
try:
|
||||||
|
await redis.delete(key)
|
||||||
|
except Exception as e:
|
||||||
|
sentry_sdk.capture_exception(e)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def cache_keys() -> list:
|
||||||
|
async with Redis() as redis:
|
||||||
|
try:
|
||||||
|
return await redis.keys("CraftCached:*")
|
||||||
|
except Exception as e:
|
||||||
|
sentry_sdk.capture_exception(e)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def cache_flush() -> None:
|
||||||
|
async with Redis() as redis:
|
||||||
|
try:
|
||||||
|
await redis.flushdb()
|
||||||
|
except Exception as e:
|
||||||
|
sentry_sdk.capture_exception(e)
|
||||||
|
raise e
|
37
Craft/database/sqlite/__init__.py
Normal file
37
Craft/database/sqlite/__init__.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import os
|
||||||
|
import aiosqlite
|
||||||
|
|
||||||
|
from dotenv import find_dotenv, load_dotenv
|
||||||
|
|
||||||
|
load_dotenv(find_dotenv())
|
||||||
|
|
||||||
|
|
||||||
|
class Sqlite:
|
||||||
|
def __init__(self):
|
||||||
|
self.db_path = os.getenv("SQLITE_PATH")
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
self.conn = await aiosqlite.connect(self.db_path)
|
||||||
|
return await self.conn.cursor()
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc, tb):
|
||||||
|
await self.conn.commit()
|
||||||
|
await self.conn.close()
|
||||||
|
|
||||||
|
async def init_database(self):
|
||||||
|
async with self as cursor:
|
||||||
|
await cursor.execute(
|
||||||
|
"""CREATE TABLE IF NOT EXISTS Craft (
|
||||||
|
key1 VARCHAR(255),
|
||||||
|
key2 VARCHAR(255),
|
||||||
|
value_emoji VARCHAR(255),
|
||||||
|
value_word VARCHAR(255),
|
||||||
|
used_count INTEGER,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (key1, key2)
|
||||||
|
)"""
|
||||||
|
)
|
||||||
|
await self.conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
|
33
Craft/database/sqlite/data.py
Normal file
33
Craft/database/sqlite/data.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
from Craft.database.sqlite import Sqlite
|
||||||
|
from typing import Optional, List, Tuple
|
||||||
|
|
||||||
|
class SqliteDatabase:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get(key1: str, key2: str) -> Optional[Tuple]:
|
||||||
|
async with Sqlite() as sqlite:
|
||||||
|
await sqlite.execute("SELECT * FROM Craft WHERE key1 = ? AND key2 = ?", (key1, key2))
|
||||||
|
data = await sqlite.fetchone()
|
||||||
|
return data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def sets(key1: str, key2: str, value_emoji: str, value_word: str) -> None:
|
||||||
|
async with Sqlite() as sqlite:
|
||||||
|
await sqlite.execute("INSERT INTO Craft (key1, key2, value_emoji, value_word, used_count) VALUES (?, ?, ?, ?, ?)", (key1, key2, value_emoji, value_word, 1))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def update(key1: str, key2: str) -> None:
|
||||||
|
async with Sqlite() as sqlite:
|
||||||
|
await sqlite.execute("UPDATE Craft SET used_count = used_count + 1 WHERE key1 = ? AND key2 = ?", (key1, key2))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def delete(key1: str, key2: str) -> None:
|
||||||
|
async with Sqlite() as sqlite:
|
||||||
|
await sqlite.execute("DELETE FROM Craft WHERE key1 = ? AND key2 = ?", (key1, key2))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def keys() -> List[Tuple]:
|
||||||
|
async with Sqlite() as sqlite:
|
||||||
|
await sqlite.execute("SELECT * FROM Craft")
|
||||||
|
data = await sqlite.fetchall()
|
||||||
|
return data
|
46
Craft/enums.py
Normal file
46
Craft/enums.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class LoggingLevel(Enum):
|
||||||
|
DEBUG = "DEBUG"
|
||||||
|
INFO = "INFO"
|
||||||
|
WARNING = "WARNING"
|
||||||
|
ERROR = "ERROR"
|
||||||
|
CRITICAL = "CRITICAL"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_level(cls, level: int):
|
||||||
|
for key, value in cls.__dict__.items():
|
||||||
|
if value == level:
|
||||||
|
return value
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_level_value(cls, level: str):
|
||||||
|
return getattr(cls, level.upper(), None)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
def __int__(self):
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class LoggingColor(Enum):
|
||||||
|
DEBUG = "blue"
|
||||||
|
INFO = "green"
|
||||||
|
WARNING = "yellow"
|
||||||
|
ERROR = "red"
|
||||||
|
CRITICAL = "red"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_color(cls, level: int):
|
||||||
|
for key, value in cls.__dict__.items():
|
||||||
|
if value == level:
|
||||||
|
return value
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_color_value(cls, level: str):
|
||||||
|
return getattr(cls, level.upper(), None)
|
18
Craft/middleware/cors_middlewares.py
Normal file
18
Craft/middleware/cors_middlewares.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from dotenv import find_dotenv, load_dotenv
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
load_dotenv(find_dotenv())
|
||||||
|
|
||||||
|
def init_cors_middleware(app: FastAPI):
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=os.getenv("CORS_ALLOW_ORIGINS").split(","),
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
32
Craft/middleware/logging_middlewares.py
Normal file
32
Craft/middleware/logging_middlewares.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
from fastapi import FastAPI, Request, HTTPException, Depends, status, Response
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from Craft.enums import LoggingLevel
|
||||||
|
from Craft.module.logging import Logger
|
||||||
|
|
||||||
|
|
||||||
|
def init_logging_middleware(app: FastAPI):
|
||||||
|
@app.middleware("http")
|
||||||
|
async def logging_middleware(request: Request, call_next):
|
||||||
|
try:
|
||||||
|
logger = Logger()
|
||||||
|
request_body = await request.body()
|
||||||
|
request = Request(
|
||||||
|
scope=request.scope,
|
||||||
|
receive=lambda: {"type": "http.request", "body": request_body},
|
||||||
|
)
|
||||||
|
response = await call_next(request)
|
||||||
|
#oneline_body = request_body.decode("utf-8").replace("\n", "")
|
||||||
|
logger.info(
|
||||||
|
f"Request: {request.method} {request.url.path} {request.client.host} {request.client.port}"
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=500, content={"message": "Internal Server Error"}
|
||||||
|
)
|
||||||
|
|
||||||
|
return app
|
26
Craft/middleware/processtime_middlewares.py
Normal file
26
Craft/middleware/processtime_middlewares.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import fastapi
|
||||||
|
import datetime
|
||||||
|
from fastapi import FastAPI, Request
|
||||||
|
from fastapi.middleware import Middleware
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
|
from typing import Any, Dict, Callable, Coroutine
|
||||||
|
|
||||||
|
|
||||||
|
def init_processtimemiddleware(app: FastAPI):
|
||||||
|
@app.middleware("http")
|
||||||
|
async def process_time_middleware(request: Request, call_next):
|
||||||
|
try:
|
||||||
|
start_time = datetime.datetime.now()
|
||||||
|
response = await call_next(request)
|
||||||
|
end_time = datetime.datetime.now()
|
||||||
|
response.headers["process-Time"] = (
|
||||||
|
str(round((end_time - start_time).total_seconds(), 1)) + "s"
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=500, content={"message": "Internal Server Error"}
|
||||||
|
)
|
||||||
|
|
||||||
|
return app
|
0
Craft/model/__init__.py
Normal file
0
Craft/model/__init__.py
Normal file
14
Craft/model/merge.py
Normal file
14
Craft/model/merge.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import Optional, Dict
|
||||||
|
|
||||||
|
|
||||||
|
class MergeInput(BaseModel):
|
||||||
|
first_word: str = Field(..., title="First word", description="The first word to be merged")
|
||||||
|
second_word: str = Field(..., title="Second word", description="The second word to be merged")
|
||||||
|
|
||||||
|
class MergeOutput(BaseModel):
|
||||||
|
status: int = Field(..., title="Status", description="The status of the response")
|
||||||
|
response: Optional[Dict[str, str]] = Field(None, title="Response", description="The response data if any")
|
||||||
|
error: Optional[str] = Field(None, title="Error", description="The error message if any")
|
||||||
|
|
||||||
|
|
28
Craft/module/logging.py
Normal file
28
Craft/module/logging.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import loguru
|
||||||
|
|
||||||
|
from Craft.enums import LoggingLevel, LoggingColor
|
||||||
|
|
||||||
|
|
||||||
|
class Logger:
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = loguru.logger
|
||||||
|
self.logger.add("logs/latest.log", level="DEBUG", colorize=True, format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>")
|
||||||
|
|
||||||
|
def log(self, level: int, message: str):
|
||||||
|
self.logger.log(level, message, color=LoggingColor.get_color(level))
|
||||||
|
|
||||||
|
def debug(self, message: str):
|
||||||
|
self.log(str(LoggingLevel.DEBUG), message)
|
||||||
|
|
||||||
|
def info(self, message: str):
|
||||||
|
self.log(str(LoggingLevel.INFO), message)
|
||||||
|
|
||||||
|
def warning(self, message: str):
|
||||||
|
self.log(str(LoggingLevel.WARNING), message)
|
||||||
|
|
||||||
|
def error(self, message: str):
|
||||||
|
self.log(str(LoggingLevel.ERROR), message)
|
||||||
|
|
||||||
|
def critical(self, message: str):
|
||||||
|
self.log(str(LoggingLevel.CRITICAL), message)
|
||||||
|
|
54
Craft/module/ml/__init__.py
Normal file
54
Craft/module/ml/__init__.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import asyncio
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
from dotenv import find_dotenv, load_dotenv
|
||||||
|
from ollama import generate
|
||||||
|
from Craft.module.ml.prompt import generate_system, generate_user, generate_system_eng, generate_user_eng
|
||||||
|
from Craft.module.ml.util import extract_emoji_and_text
|
||||||
|
|
||||||
|
load_dotenv(find_dotenv())
|
||||||
|
|
||||||
|
class Engine:
|
||||||
|
def __init__(self):
|
||||||
|
self.key = None
|
||||||
|
|
||||||
|
def _generate(self, first_word: str, second_word: str, eng_result: str) -> dict:
|
||||||
|
return generate(
|
||||||
|
model="infcraft:latest",
|
||||||
|
system=generate_system(first_word, second_word, eng_result),
|
||||||
|
prompt=generate_user(first_word, second_word),
|
||||||
|
keep_alive=60*60*24,
|
||||||
|
context=None,
|
||||||
|
options={
|
||||||
|
"seed": 0,
|
||||||
|
"temperature": 0.4,
|
||||||
|
"top_p": 0.85,
|
||||||
|
"top_k": 0.1,
|
||||||
|
"max_tokens": 32,
|
||||||
|
"main_gpu": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _generate_eng(self, first_word: str, second_word: str) -> dict:
|
||||||
|
system = await generate_system_eng(first_word, second_word)
|
||||||
|
prompt = await generate_user_eng(first_word, second_word)
|
||||||
|
return generate(
|
||||||
|
model="mistral:latest",
|
||||||
|
system=system,
|
||||||
|
prompt=prompt,
|
||||||
|
keep_alive=60*60*24,
|
||||||
|
options={
|
||||||
|
"seed": 0,
|
||||||
|
"temperature": 0.8,
|
||||||
|
"top_p": 1,
|
||||||
|
"top_k": 0.1,
|
||||||
|
"max_tokens": 64,
|
||||||
|
"main_gpu": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def generate(self, first_word: str, second_word: str) -> str:
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
eng_result = await self._generate_eng(first_word, second_word)
|
||||||
|
with ThreadPoolExecutor() as executor:
|
||||||
|
data = await loop.run_in_executor(executor, self._generate, first_word, second_word, eng_result['response'])
|
||||||
|
return extract_emoji_and_text(data['response'])
|
63
Craft/module/ml/prompt.py
Normal file
63
Craft/module/ml/prompt.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import aiogoogletrans
|
||||||
|
import re
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
translator = aiogoogletrans.Translator()
|
||||||
|
def generate_system(first_word: str, second_word: str, eng_result: Optional[str] = None) -> str:
|
||||||
|
first_word = first_word.strip()
|
||||||
|
second_word = second_word.strip()
|
||||||
|
prompt = (
|
||||||
|
"당신은 Infinite Craft라는 게임의 핵심 부분을 담당하는 도우미입니다. 주어진 두 개의 요소와 이모지를 결합 및 활용하여 연관된 새로운 요소를 만들어야 합니다." +
|
||||||
|
"이 게임은 한국인을 타깃으로 하고 있기 때문에 한국 정서나 문화 또는 사회에 대한 지식을 사용해야합니다." +
|
||||||
|
"무조건 항상 입력에 따라 이러한 결과를 생성해야 합니다. 입력 형식은 '(이모지) (요소) + (이모지) (요소)'입니다." +
|
||||||
|
"그리고 당신의 결과는 항상 '(이모지) (요소)' 형식이어야 합니다." +
|
||||||
|
"그리고 항상 이모지와 요소가 최대한 관련이 있도록 하고 일관되게 유지하세요. " +
|
||||||
|
"이모지와 요소는 예시에 아무런 제한되지 않으며, 무엇이든 만들 수 있습니다. 주어진 형식으로 항상 응답하세요." +
|
||||||
|
"가장 중요한 규칙은 답변에 '{},{}'단어 자체를 직접적으로는 사용 할 수 없다는 것입니다. 즉, 단어를 결합한 결과를 답하십시오." +
|
||||||
|
"답변은 한글로 이루어진 명사(고유명사나 다른 한 요소 가능) 여야 합니다." +
|
||||||
|
"두 요소의 순서는 중요하지 않으며, 두 요소 모두 똑같이 중요합니다." +
|
||||||
|
"답변은 무조건 두 요소와 그 요소의 맥락또는 의미와 관련되어야 합니다." +
|
||||||
|
"답변은 물건, 재료, 사람, 회사, 동물, 직업, 음식, 장소, 객체, 감정, 사건, 개념, 자연 현상, 한국의 사회현상, 한국의 특정 밈, 트랜드, 게임, 신체 부위, 차량, 스포츠, 의류, 가구, 기술, 건물, 악기, 음료, 식물, 학문등 기타 명사일 수 있습니다." +
|
||||||
|
"답변을 생성할때 정확도를 향상시키기 위해 영어 답변을 참고하여 생성하십시오. " +
|
||||||
|
"영어 답변: '{}'" +
|
||||||
|
"답변 예시:" +
|
||||||
|
"'🌬️ 바람 + 🌱 식물' = 🌼 민들레'" +
|
||||||
|
"'🌍 지구 + 💧 물' = 🌱 식물" +
|
||||||
|
"'🌍 지구 + 🔥 불' = 🌋 용암" +
|
||||||
|
"'🌋 용암 + 🌋 용암' = 🌋 화산" +
|
||||||
|
"'💧 물 + 🌬️ 바람' = 🌊 파도 " +
|
||||||
|
"'🍄 버섯 + 🎮 닌텐도' = 🎮 마리오" +
|
||||||
|
"'💙 파랑 + 💽 저장소' = 🎮 블루아카이브" +
|
||||||
|
"<end_of_turn>"
|
||||||
|
).format(re.sub(r'[^\w\s]', '', first_word), re.sub(r'[^\w\s]', '', second_word), eng_result)
|
||||||
|
return prompt
|
||||||
|
|
||||||
|
def generate_user(first_word: str, second_word: str) -> str:
|
||||||
|
prompt = "{} + {}".format(first_word, second_word)
|
||||||
|
return prompt
|
||||||
|
|
||||||
|
async def generate_system_eng(first_word: str, second_word: str) -> str:
|
||||||
|
first_word = first_word.strip()
|
||||||
|
second_word = second_word.strip()
|
||||||
|
word = f"{first_word},{second_word}"
|
||||||
|
translated = await translator.translate(word, dest='en')
|
||||||
|
first_word, second_word = translated.text.split(',')
|
||||||
|
prompt = (
|
||||||
|
"You are a helpful assistant that helps people to craft new things by combining two words into a new word. " +
|
||||||
|
"The most important rules that you have to follow with every single answer that you are not allowed to use the words '{} and {}' as part of your answer and that you are only allowed to answer with one thing. " +
|
||||||
|
"DO NOT INCLUDE THE WORDS '{} and {}' as part of the answer!!!!! The words '{} and {}' may NOT be part of the answer. " +
|
||||||
|
"No sentences, no phrases, no multiple words, no punctuation, no special characters, no numbers, no emojis, no URLs, no code, no commands, no programming" +
|
||||||
|
"The answer has to be a noun. " +
|
||||||
|
"The order of the both words does not matter, both are equally important. " +
|
||||||
|
"The answer has to be related to both words and the context of the words. " +
|
||||||
|
"The answer can either be a combination of the words or the role of one word in relation to the other. " +
|
||||||
|
"Answers can be things, materials, people, companies, animals, occupations, food, places, objects, emotions, events, concepts, natural phenomena, body parts, vehicles, sports, clothing, furniture, technology, buildings, technology, instruments, beverages, plants, academic subjects and everything else you can think of that is a noun."
|
||||||
|
).format(first_word, second_word, first_word, second_word, first_word, second_word)
|
||||||
|
return prompt
|
||||||
|
|
||||||
|
async def generate_user_eng(first_word: str, second_word: str) -> str:
|
||||||
|
word = f"{first_word},{second_word}"
|
||||||
|
translated = await translator.translate(word, dest='en')
|
||||||
|
first_word, second_word = translated.text.split(',')
|
||||||
|
prompt = "Reply with the result of what would happen if you combine '{} and {}'. The answer has to be related to both words and the context of the words and may not contain the words themselves.".format(first_word, second_word)
|
||||||
|
return prompt
|
9
Craft/module/ml/util.py
Normal file
9
Craft/module/ml/util.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import re
|
||||||
|
import emoji
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
def extract_emoji_and_text(input_string):
|
||||||
|
emoji_data = emoji.emoji_list(input_string)
|
||||||
|
emojis = emoji_data[0]['emoji'] if emoji_data else None
|
||||||
|
text = re.sub(r'[^a-zA-Z0-9ㄱ-ㅎ가-힣]', '', input_string)
|
||||||
|
return SimpleNamespace(emoji=emojis, word=text)
|
27
Craft/module/sentry.py
Normal file
27
Craft/module/sentry.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import os
|
||||||
|
import sentry_sdk
|
||||||
|
|
||||||
|
from dotenv import find_dotenv, load_dotenv
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from sentry_sdk.integrations.fastapi import FastApiIntegration
|
||||||
|
from sentry_sdk.integrations.loguru import LoguruIntegration
|
||||||
|
from sentry_sdk.integrations.redis import RedisIntegration
|
||||||
|
|
||||||
|
from Craft.enums import LoggingLevel
|
||||||
|
|
||||||
|
load_dotenv(find_dotenv())
|
||||||
|
|
||||||
|
class Sentry:
|
||||||
|
def __init__(self):
|
||||||
|
self.sentry_dsn = os.getenv("SENTRY_DSN")
|
||||||
|
sentry_sdk.init(
|
||||||
|
dsn=self.sentry_dsn,
|
||||||
|
integrations=[
|
||||||
|
FastApiIntegration(),
|
||||||
|
LoguruIntegration(),
|
||||||
|
RedisIntegration(),
|
||||||
|
],
|
||||||
|
traces_sample_rate=1.0,
|
||||||
|
profiles_sample_rate=1.0,
|
||||||
|
)
|
16
Craft/module/templates.py
Normal file
16
Craft/module/templates.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
INDEX_TEMPLATE: str = """
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<title>CraftINF</title>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-800">
|
||||||
|
<div class="flex flex-col items-center justify-center h-screen">
|
||||||
|
<h1 class="text-4xl text-white">Welcome to CraftINF</h1>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
9
Craft/route/__init__.py
Normal file
9
Craft/route/__init__.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from Craft.route.default import app as default_router
|
||||||
|
from Craft.route.merge import app as merge_router
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
router.include_router(default_router, tags=["Default"])
|
||||||
|
router.include_router(merge_router, tags=["AI"], prefix="/v1")
|
17
Craft/route/default.py
Normal file
17
Craft/route/default.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from fastapi import APIRouter, Request, HTTPException, Depends, status, Response
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
from fastapi.openapi.docs import get_swagger_ui_html
|
||||||
|
|
||||||
|
from Craft.module.templates import INDEX_TEMPLATE
|
||||||
|
app = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/", response_class=HTMLResponse)
|
||||||
|
async def index(request: Request):
|
||||||
|
return HTMLResponse(INDEX_TEMPLATE)
|
||||||
|
|
||||||
|
@app.get("/docs", response_class=HTMLResponse)
|
||||||
|
async def docs(request: Request):
|
||||||
|
return get_swagger_ui_html(openapi_url="/openapi.json", title="docs")
|
||||||
|
|
||||||
|
|
14
Craft/route/merge.py
Normal file
14
Craft/route/merge.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
from fastapi.background import BackgroundTasks
|
||||||
|
|
||||||
|
from Craft.controller.merge import MergeController
|
||||||
|
from Craft.model.merge import MergeOutput, MergeInput
|
||||||
|
|
||||||
|
app = APIRouter()
|
||||||
|
|
||||||
|
@app.post("/merge", response_model=MergeOutput)
|
||||||
|
async def merge(model: MergeInput, background_tasks: BackgroundTasks):
|
||||||
|
background_tasks.add_task(MergeController().increasevalue_backgroundtasks, model.first_word, model.second_word)
|
||||||
|
return await MergeController().merge(model)
|
||||||
|
|
10
infcraft.modelfile
Normal file
10
infcraft.modelfile
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
FROM gemma2:27b-instruct-q3_K_M
|
||||||
|
TEMPLATE "{{ if .System }}<bos><start_of_turn>system
|
||||||
|
{{ .System }}{{ end }}
|
||||||
|
<start_of_turn>user
|
||||||
|
{{ .Prompt }}<end_of_turn>
|
||||||
|
<start_of_turn>model
|
||||||
|
{{ .Response }}<end_of_turn>
|
||||||
|
"
|
||||||
|
PARAMETER stop <start_of_turn>
|
||||||
|
PARAMETER stop <end_of_turn>
|
1397
poetry.lock
generated
Normal file
1397
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
pyproject.toml
Normal file
25
pyproject.toml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
[tool.poetry]
|
||||||
|
name = "kocraft"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Korean Infinite Craft Backend"
|
||||||
|
authors = ["tmddn3070 <tmddn3070@gmail.com>"]
|
||||||
|
license = "apache-2"
|
||||||
|
readme = "README.md"
|
||||||
|
package-mode = false
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.10"
|
||||||
|
fastapi = "^0.111.0"
|
||||||
|
uvicorn = {extras = ["standard"], version = "^0.30.1"}
|
||||||
|
redis = {extras = ["hiredis"], version = "^5.0.7"}
|
||||||
|
sentry-sdk = {extras = ["fastapi", "loguru"], version = "^2.9.0"}
|
||||||
|
python-dotenv = "^1.0.1"
|
||||||
|
loguru = "^0.7.2"
|
||||||
|
ollama = "^0.2.1"
|
||||||
|
emoji = "^2.12.1"
|
||||||
|
aiosqlite = "^0.20.0"
|
||||||
|
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
67
test.py
Normal file
67
test.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import asyncio
|
||||||
|
import emoji
|
||||||
|
import aiogoogletrans
|
||||||
|
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
from dotenv import find_dotenv, load_dotenv
|
||||||
|
from ollama import generate
|
||||||
|
|
||||||
|
from Craft.module.ml.prompt import generate_system, generate_user, generate_system_eng, generate_user_eng
|
||||||
|
from Craft.module.ml.util import extract_emoji_and_text
|
||||||
|
load_dotenv(find_dotenv())
|
||||||
|
|
||||||
|
translator = aiogoogletrans.Translator()
|
||||||
|
|
||||||
|
class Engine:
|
||||||
|
def __init__(self):
|
||||||
|
self.key = None
|
||||||
|
|
||||||
|
|
||||||
|
def _generate(self, first_word: str, second_word: str, eng_result: str) -> str:
|
||||||
|
gen_data = generate(
|
||||||
|
model="infcraft:minial",
|
||||||
|
system=generate_system(first_word, second_word, eng_result),
|
||||||
|
prompt=generate_user(first_word, second_word),
|
||||||
|
keep_alive=60*60*24,
|
||||||
|
context=None,
|
||||||
|
options={
|
||||||
|
"seed" : 0,
|
||||||
|
"temperature" : 0.2,
|
||||||
|
"top_p" : 0.85,
|
||||||
|
"top_k" : 0.1,
|
||||||
|
"max_tokens" : 32,
|
||||||
|
"main_gpu" : 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# 반환된 값의 response에서 이모지만 추출
|
||||||
|
return gen_data
|
||||||
|
|
||||||
|
async def _generate_eng(self, first_word: str, second_word: str) -> str:
|
||||||
|
gen_data = generate(
|
||||||
|
model="mistral:latest",
|
||||||
|
system=await generate_system_eng(first_word, second_word),
|
||||||
|
prompt=await generate_user_eng(first_word, second_word),
|
||||||
|
keep_alive=60*60*24,
|
||||||
|
options={
|
||||||
|
"seed" : 0,
|
||||||
|
"temperature" : 0.2,
|
||||||
|
"top_p" : 1,
|
||||||
|
"top_k" : 0.1,
|
||||||
|
"max_tokens" : 64,
|
||||||
|
"main_gpu" : 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return gen_data
|
||||||
|
|
||||||
|
async def generate(self, first_word: str, second_word: str) -> str:
|
||||||
|
with ThreadPoolExecutor() as executor:
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
eng_result = await self._generate_eng(first_word, second_word)
|
||||||
|
data = await loop.run_in_executor(executor, self._generate, first_word, second_word, eng_result['response'])
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
data = asyncio.run(Engine().generate("👮 윤석열", "👮 이재명"))
|
||||||
|
print(extract_emoji_and_text(data['response']))
|
Loading…
Reference in New Issue
Block a user