Add new features
This commit is contained in:
parent
2d8e1c1758
commit
2c3f19e67c
24 changed files with 485 additions and 214 deletions
|
|
@ -133,11 +133,13 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"source": [
|
"source": [
|
||||||
"result = predict_domain(\"\"\"\n",
|
"result = predict_domain(\n",
|
||||||
|
" \"\"\"\n",
|
||||||
" State-of-the-art methods for zero-shot visual recognition formulate learning as a joint embedding problem of images and side information. In these formulations the current best complement to visual features are attributes: manually encoded vectors describing shared characteristics among categories. Despite good performance, attributes have limitations: (1) finer-grained recognition requires commensurately more, and (2) attributes do not provide a natural language interface. We propose to overcome these limitations by training neural language models from scratch; i.e. without pre-training and only consuming words and characters. Our proposed models train end-to-end to align with the fine-grained and category-specific content of images. Natural language provides a flexible and compact way of encoding only the salient visual aspects for distinguishing categories. By training on raw text, our model can do inference on raw text as well, providing humans a familiar mode both for annotation and retrieval. Our model achieves strong performance on zero-shot text-based image retrieval and significantly outperforms the attribute-based state-of-the-art for zero-shot classification on the CaltechUCSD Birds 200-2011 dataset. \"\"\"\n",
|
" State-of-the-art methods for zero-shot visual recognition formulate learning as a joint embedding problem of images and side information. In these formulations the current best complement to visual features are attributes: manually encoded vectors describing shared characteristics among categories. Despite good performance, attributes have limitations: (1) finer-grained recognition requires commensurately more, and (2) attributes do not provide a natural language interface. We propose to overcome these limitations by training neural language models from scratch; i.e. without pre-training and only consuming words and characters. Our proposed models train end-to-end to align with the fine-grained and category-specific content of images. Natural language provides a flexible and compact way of encoding only the salient visual aspects for distinguishing categories. By training on raw text, our model can do inference on raw text as well, providing humans a familiar mode both for annotation and retrieval. Our model achieves strong performance on zero-shot text-based image retrieval and significantly outperforms the attribute-based state-of-the-art for zero-shot classification on the CaltechUCSD Birds 200-2011 dataset. \"\"\"\n",
|
||||||
")\n",
|
")\n",
|
||||||
"\n",
|
"\n",
|
||||||
"from pprint import pprint\n",
|
"from pprint import pprint\n",
|
||||||
|
"\n",
|
||||||
"pprint(result.dict(), width=120)"
|
"pprint(result.dict(), width=120)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ install_requires =
|
||||||
plotly >= 5.8.0
|
plotly >= 5.8.0
|
||||||
dash >= 2.4.0
|
dash >= 2.4.0
|
||||||
uvicorn[standard] >= 0.17.0
|
uvicorn[standard] >= 0.17.0
|
||||||
|
watchdog >= 2.1.0
|
||||||
|
|
||||||
[options.package_data]
|
[options.package_data]
|
||||||
* = *.json, *.yaml, *.yml, *.css
|
* = *.json, *.yaml, *.yml, *.css
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,29 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
from importlib import import_module
|
import time
|
||||||
|
from importlib import import_module, reload
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from uvicorn.config import LOGGING_CONFIG
|
from uvicorn.config import LOGGING_CONFIG, Config
|
||||||
|
from uvicorn.subprocess import get_subprocess
|
||||||
|
from uvicorn.supervisors.basereload import BaseReload
|
||||||
|
from watchdog.events import FileSystemEvent, PatternMatchingEventHandler
|
||||||
|
from watchdog.observers import Observer
|
||||||
|
|
||||||
from .great_ai.context import get_context
|
from .great_ai.context.configure import _is_in_production_mode
|
||||||
from .great_ai.exceptions import MissingArgumentError
|
from .great_ai.deploy import GreatAI
|
||||||
|
from .great_ai.exceptions import ArgumentValidationError, MissingArgumentError
|
||||||
from .parse_arguments import parse_arguments
|
from .parse_arguments import parse_arguments
|
||||||
from .utilities.logger import get_logger
|
from .utilities.logger import get_logger
|
||||||
|
|
||||||
logger = get_logger("GreatAI-Server")
|
logger = get_logger("GreatAI-Server")
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
GREAT_AI_LOGGING_CONFIG = {
|
||||||
args = parse_arguments()
|
|
||||||
|
|
||||||
file_name = re.sub(r"\.py$", "", args.file_name)
|
|
||||||
function_name = args.function_name
|
|
||||||
|
|
||||||
module = import_module(file_name)
|
|
||||||
|
|
||||||
if not function_name:
|
|
||||||
logger.warning("Argument function_name not provided, trying to guess it")
|
|
||||||
|
|
||||||
if not function_name and "app" in module.__dict__:
|
|
||||||
function_name = "app"
|
|
||||||
|
|
||||||
if not function_name and file_name in module.__dict__:
|
|
||||||
function_name = file_name
|
|
||||||
|
|
||||||
if function_name:
|
|
||||||
logger.warning(f"Found `{function_name}` as the value of function_name")
|
|
||||||
else:
|
|
||||||
raise MissingArgumentError("Argument function_name could not be guessed")
|
|
||||||
|
|
||||||
app_name = f"{file_name}:{function_name}"
|
|
||||||
logger.info(f"Starting uvicorn server with app={app_name}")
|
|
||||||
|
|
||||||
uvicorn.run(
|
|
||||||
app_name,
|
|
||||||
host=args.host,
|
|
||||||
port=args.port,
|
|
||||||
timeout_keep_alive=args.timeout_keep_alive,
|
|
||||||
workers=args.workers,
|
|
||||||
reload=not get_context().is_production,
|
|
||||||
log_config={
|
|
||||||
**LOGGING_CONFIG,
|
**LOGGING_CONFIG,
|
||||||
"formatters": {
|
"formatters": {
|
||||||
"default": {
|
"default": {
|
||||||
|
|
@ -58,14 +35,159 @@ def main() -> None:
|
||||||
"fmt": "%(asctime)s | %(levelname)8s | %(message)s", # noqa: E501
|
"fmt": "%(asctime)s | %(levelname)8s | %(message)s", # noqa: E501
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
args = parse_arguments()
|
||||||
|
should_auto_reload = not _is_in_production_mode(logger=None)
|
||||||
|
|
||||||
|
if args.workers > 1 and should_auto_reload:
|
||||||
|
raise ArgumentValidationError(
|
||||||
|
"Cannot use auto-reload with multiple workers: set the `--workers=1` CLI argument,"
|
||||||
|
+ "or set the ENVIRONMENT environment variable to `production`."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
common_config = dict(
|
||||||
|
host=args.host,
|
||||||
|
port=args.port,
|
||||||
|
timeout_keep_alive=args.timeout_keep_alive,
|
||||||
|
workers=args.workers,
|
||||||
|
server_header=False,
|
||||||
|
reload=False,
|
||||||
|
log_config=GREAT_AI_LOGGING_CONFIG,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not should_auto_reload:
|
||||||
|
file_name = get_script_name(args.file_name)
|
||||||
|
app = find_app(file_name)
|
||||||
|
|
||||||
|
logger.info(f"Starting uvicorn server with app={app}")
|
||||||
|
|
||||||
|
uvicorn.run(app, **common_config) # this will never return
|
||||||
|
|
||||||
|
class EventHandler(PatternMatchingEventHandler):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__(patterns=["*.py", "*.ipynb"], ignore_patterns=["__*.py"])
|
||||||
|
self.server: Optional[GreatAIReload] = None
|
||||||
|
self.restart()
|
||||||
|
|
||||||
|
def on_closed(self, event: FileSystemEvent) -> None:
|
||||||
|
logger.warning(f"File {event.src_path} has triggered a restart")
|
||||||
|
self.restart()
|
||||||
|
|
||||||
|
def restart(self) -> None:
|
||||||
|
file_name = get_script_name(args.file_name)
|
||||||
|
app = find_app(file_name)
|
||||||
|
if app is None:
|
||||||
|
logger.warning("Auto-reloading skipped")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.stop_server()
|
||||||
|
|
||||||
|
config = Config(app, **common_config)
|
||||||
|
socket = config.bind_socket()
|
||||||
|
self.server = GreatAIReload(
|
||||||
|
config, target=uvicorn.Server(config=config).run, sockets=[socket]
|
||||||
|
)
|
||||||
|
self.server.startup()
|
||||||
|
|
||||||
|
def stop_server(self) -> None:
|
||||||
|
if self.server:
|
||||||
|
self.server.shutdown()
|
||||||
|
|
||||||
|
restart_handler = EventHandler()
|
||||||
|
observer = Observer()
|
||||||
|
observer.schedule(restart_handler, path=".", recursive=True)
|
||||||
|
observer.start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
time.sleep(50)
|
||||||
|
finally:
|
||||||
|
observer.stop()
|
||||||
|
restart_handler.stop_server()
|
||||||
|
if args.file_name.endswith(".ipynb"):
|
||||||
|
Path(get_script_name_of_notebook(args.file_name)).unlink(missing_ok=True)
|
||||||
|
observer.join()
|
||||||
|
|
||||||
|
|
||||||
|
def get_script_name(file_name_argument: str) -> str:
|
||||||
|
if file_name_argument.endswith(".ipynb"):
|
||||||
|
logger.info("Converting notebook to Python script")
|
||||||
|
try:
|
||||||
|
from nbconvert import PythonExporter
|
||||||
|
|
||||||
|
exporter = PythonExporter()
|
||||||
|
content, _ = exporter.from_filename(file_name_argument)
|
||||||
|
file_name_argument = get_script_name_of_notebook(file_name_argument)
|
||||||
|
with open(file_name_argument, "w", encoding="utf-8") as f:
|
||||||
|
f.write(content)
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError(
|
||||||
|
"Install `nbconvert` to be able to use Jupyter notebook files or use a regular Python file instead"
|
||||||
|
)
|
||||||
|
|
||||||
|
return re.sub(r"\.(py|ipynb)$", "", file_name_argument)
|
||||||
|
|
||||||
|
|
||||||
|
def get_script_name_of_notebook(notebook_name: str) -> str:
|
||||||
|
base_name = re.sub(r"\.ipynb$", "", notebook_name)
|
||||||
|
return f"__{base_name}__.py"
|
||||||
|
|
||||||
|
|
||||||
|
module = None
|
||||||
|
|
||||||
|
|
||||||
|
def find_app(file_name: str) -> Optional[str]:
|
||||||
|
global module
|
||||||
|
|
||||||
|
logging.disable(logging.CRITICAL)
|
||||||
|
try:
|
||||||
|
if module is None:
|
||||||
|
module = import_module(file_name)
|
||||||
|
else:
|
||||||
|
module = reload(module)
|
||||||
|
except Exception:
|
||||||
|
logging.disable(logging.NOTSET)
|
||||||
|
logger.exception("Could not load file because of an exception: fix your code")
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
logging.disable(logging.NOTSET)
|
||||||
|
|
||||||
|
for name, value in module.__dict__.items():
|
||||||
|
if isinstance(value, GreatAI):
|
||||||
|
app_name = name
|
||||||
|
|
||||||
|
if app_name:
|
||||||
|
logger.info(f"Found `{app_name}` to be the GreatAI app ")
|
||||||
|
else:
|
||||||
|
raise MissingArgumentError(
|
||||||
|
"GreatAI app could not be found, make sure to use `@GreatAI.deploy` on your prediction function"
|
||||||
|
)
|
||||||
|
|
||||||
|
return f"{file_name}:{app_name}.app"
|
||||||
|
|
||||||
|
|
||||||
|
class GreatAIReload(BaseReload):
|
||||||
|
def startup(self) -> None:
|
||||||
|
self.process = get_subprocess(
|
||||||
|
config=self.config, target=self.target, sockets=self.sockets
|
||||||
|
)
|
||||||
|
self.process.start()
|
||||||
|
|
||||||
|
def shutdown(self) -> None:
|
||||||
|
self.process.terminate()
|
||||||
|
self.process.join()
|
||||||
|
|
||||||
|
for sock in self.sockets:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
try:
|
try:
|
||||||
main()
|
main()
|
||||||
except (MissingArgumentError, ModuleNotFoundError) as e:
|
|
||||||
logger.error(e)
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
exit()
|
exit()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
from .context import configure
|
from .context import configure
|
||||||
from .deploy import GreatAI
|
from .deploy import GreatAI
|
||||||
from .exceptions import ArgumentValidationError, MissingArgumentError
|
|
||||||
from .models import save_model, use_model
|
from .models import save_model, use_model
|
||||||
from .output_models import ClassificationOutput, RegressionOutput
|
from .output_models import ClassificationOutput, RegressionOutput
|
||||||
from .parameters import log_metric, parameter
|
from .parameters import log_metric, parameter
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
ENV_VAR_KEY = "ENVIRONMENT"
|
ENV_VAR_KEY = "ENVIRONMENT"
|
||||||
PRODUCTION_KEY = "production"
|
PRODUCTION_KEY = "production"
|
||||||
DEFAULT_TRACING_DB_FILENAME = "tracing_database.json"
|
DEFAULT_TRACING_DB_FILENAME = "tracing_database.json"
|
||||||
|
METRICS_PATH = "/metrics"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
from logging import INFO, Logger
|
from logging import DEBUG, Logger
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import great_ai.great_ai.context.context as context
|
import great_ai.great_ai.context.context as context
|
||||||
from great_ai.open_s3 import LargeFile
|
from great_ai.open_s3 import LargeFile
|
||||||
|
|
@ -12,12 +13,15 @@ from ..persistence import ParallelTinyDbDriver, PersistenceDriver
|
||||||
|
|
||||||
|
|
||||||
def configure(
|
def configure(
|
||||||
log_level: int = INFO,
|
version: str = "0.0.1",
|
||||||
|
log_level: int = DEBUG,
|
||||||
s3_config: Path = Path("s3.ini"),
|
s3_config: Path = Path("s3.ini"),
|
||||||
seed: int = 42,
|
seed: int = 42,
|
||||||
persistence_driver: PersistenceDriver = ParallelTinyDbDriver(
|
persistence_driver: PersistenceDriver = ParallelTinyDbDriver(
|
||||||
Path(DEFAULT_TRACING_DB_FILENAME)
|
Path(DEFAULT_TRACING_DB_FILENAME)
|
||||||
),
|
),
|
||||||
|
should_log_exception_stack: Optional[bool] = None,
|
||||||
|
prediction_cache_size: int = 512,
|
||||||
) -> None:
|
) -> None:
|
||||||
logger = get_logger("great_ai", level=log_level)
|
logger = get_logger("great_ai", level=log_level)
|
||||||
|
|
||||||
|
|
@ -27,9 +31,7 @@ def configure(
|
||||||
+ 'Make sure to call "configure()" before importing your application code.'
|
+ 'Make sure to call "configure()" before importing your application code.'
|
||||||
)
|
)
|
||||||
|
|
||||||
is_production = _is_in_production_mode(
|
is_production = _is_in_production_mode(logger=logger)
|
||||||
logger=logger,
|
|
||||||
)
|
|
||||||
_initialize_large_file(s3_config, logger=logger)
|
_initialize_large_file(s3_config, logger=logger)
|
||||||
_set_seed(seed)
|
_set_seed(seed)
|
||||||
|
|
||||||
|
|
@ -39,34 +41,38 @@ def configure(
|
||||||
)
|
)
|
||||||
|
|
||||||
context._context = context.Context(
|
context._context = context.Context(
|
||||||
metrics_path="/metrics",
|
version=version,
|
||||||
persistence=persistence_driver,
|
persistence=persistence_driver,
|
||||||
is_production=is_production,
|
is_production=is_production,
|
||||||
logger=logger,
|
logger=logger,
|
||||||
|
should_log_exception_stack=not is_production
|
||||||
|
if should_log_exception_stack is None
|
||||||
|
else should_log_exception_stack,
|
||||||
|
prediction_cache_size=prediction_cache_size,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info("Options: configured ✅")
|
logger.info("Options: configured ✅")
|
||||||
|
|
||||||
|
|
||||||
def _is_in_production_mode(logger: Logger) -> bool:
|
def _is_in_production_mode(logger: Optional[Logger]) -> bool:
|
||||||
environment = os.environ.get(ENV_VAR_KEY)
|
environment = os.environ.get(ENV_VAR_KEY)
|
||||||
|
|
||||||
if environment is None:
|
if environment is None:
|
||||||
logger.info(
|
if logger:
|
||||||
f"Environment variable {ENV_VAR_KEY} is not set, defaulting to development mode"
|
logger.warning(
|
||||||
|
f"Environment variable {ENV_VAR_KEY} is not set, defaulting to development mode ‼️"
|
||||||
)
|
)
|
||||||
is_production = False
|
is_production = False
|
||||||
else:
|
else:
|
||||||
is_production = environment.lower() == PRODUCTION_KEY
|
is_production = environment.lower() == PRODUCTION_KEY
|
||||||
|
if logger:
|
||||||
if not is_production:
|
if not is_production:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Value of {ENV_VAR_KEY} is `{environment}` which is not equal to `{PRODUCTION_KEY}`"
|
f"Value of {ENV_VAR_KEY} is `{environment}` which is not equal to `{PRODUCTION_KEY}`"
|
||||||
|
+ "defaulting to development mode ‼️"
|
||||||
)
|
)
|
||||||
|
|
||||||
if is_production:
|
|
||||||
logger.info("Running in production mode ✅")
|
|
||||||
else:
|
else:
|
||||||
logger.warning("Running in development mode ‼️")
|
logger.info("Running in production mode ✅")
|
||||||
|
|
||||||
return is_production
|
return is_production
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,12 @@ from ..persistence import PersistenceDriver
|
||||||
|
|
||||||
|
|
||||||
class Context(BaseModel):
|
class Context(BaseModel):
|
||||||
metrics_path: str
|
version: str
|
||||||
persistence: PersistenceDriver
|
persistence: PersistenceDriver
|
||||||
is_production: bool
|
is_production: bool
|
||||||
logger: Logger
|
logger: Logger
|
||||||
|
should_log_exception_stack: bool
|
||||||
|
prediction_cache_size: int
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
arbitrary_types_allowed = True
|
arbitrary_types_allowed = True
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ from flask import Flask
|
||||||
|
|
||||||
from great_ai.utilities.unique import unique
|
from great_ai.utilities.unique import unique
|
||||||
|
|
||||||
|
from ..constants import METRICS_PATH
|
||||||
from ..context import get_context
|
from ..context import get_context
|
||||||
from ..helper import snake_case_to_text, text_to_hex_color
|
from ..helper import snake_case_to_text, text_to_hex_color
|
||||||
from ..views import SortBy
|
from ..views import SortBy
|
||||||
|
|
@ -23,7 +24,7 @@ def create_dash_app(function_name: str, function_docs: str) -> Flask:
|
||||||
flask_app = Flask(__name__)
|
flask_app = Flask(__name__)
|
||||||
app = Dash(
|
app = Dash(
|
||||||
function_name,
|
function_name,
|
||||||
requests_pathname_prefix=get_context().metrics_path + "/",
|
requests_pathname_prefix=METRICS_PATH + "/",
|
||||||
server=flask_app,
|
server=flask_app,
|
||||||
title=snake_case_to_text(function_name),
|
title=snake_case_to_text(function_name),
|
||||||
update_title=None,
|
update_title=None,
|
||||||
|
|
@ -103,10 +104,11 @@ def create_dash_app(function_name: str, function_docs: str) -> Flask:
|
||||||
|
|
||||||
@app.callback(
|
@app.callback(
|
||||||
Output(execution_time_histogram, "figure"),
|
Output(execution_time_histogram, "figure"),
|
||||||
|
Output(parallel_coords, "figure"),
|
||||||
Input(table, "filter_query"),
|
Input(table, "filter_query"),
|
||||||
Input(interval, "n_intervals"),
|
Input(interval, "n_intervals"),
|
||||||
)
|
)
|
||||||
def update_execution_times(filter: str, _n_intervals: int) -> go.Figure:
|
def update_charts(filter: str, n_intervals: int) -> go.Figure:
|
||||||
conjunctive_filters = [
|
conjunctive_filters = [
|
||||||
get_filter_from_datatable(f) for f in filter.split(" && ")
|
get_filter_from_datatable(f) for f in filter.split(" && ")
|
||||||
]
|
]
|
||||||
|
|
@ -117,7 +119,7 @@ def create_dash_app(function_name: str, function_docs: str) -> Flask:
|
||||||
)
|
)
|
||||||
|
|
||||||
if not rows:
|
if not rows:
|
||||||
return go.Figure()
|
return go.Figure(), go.Figure()
|
||||||
|
|
||||||
df = pd.DataFrame(rows)
|
df = pd.DataFrame(rows)
|
||||||
|
|
||||||
|
|
@ -136,28 +138,9 @@ def create_dash_app(function_name: str, function_docs: str) -> Flask:
|
||||||
margin=dict(l=0, r=0, b=0, t=0, pad=0),
|
margin=dict(l=0, r=0, b=0, t=0, pad=0),
|
||||||
)
|
)
|
||||||
|
|
||||||
return fig
|
return (
|
||||||
|
fig,
|
||||||
@app.callback(
|
go.Figure(
|
||||||
Output(parallel_coords, "figure"),
|
|
||||||
Input(table, "filter_query"),
|
|
||||||
Input(interval, "n_intervals"),
|
|
||||||
)
|
|
||||||
def update_parallel_coords(filter: str, _n_intervals: int) -> go.Figure:
|
|
||||||
conjunctive_filters = [
|
|
||||||
get_filter_from_datatable(f) for f in filter.split(" && ")
|
|
||||||
]
|
|
||||||
non_null_conjunctive_filters = [f for f in conjunctive_filters if f is not None]
|
|
||||||
|
|
||||||
rows = get_context().persistence.query(
|
|
||||||
conjunctive_filters=non_null_conjunctive_filters
|
|
||||||
)
|
|
||||||
|
|
||||||
if not rows:
|
|
||||||
return go.Figure()
|
|
||||||
|
|
||||||
df = pd.DataFrame(rows)
|
|
||||||
return go.Figure(
|
|
||||||
go.Parcoords(
|
go.Parcoords(
|
||||||
dimensions=[
|
dimensions=[
|
||||||
get_dimension_descriptor(df, c)
|
get_dimension_descriptor(df, c)
|
||||||
|
|
@ -166,6 +149,7 @@ def create_dash_app(function_name: str, function_docs: str) -> Flask:
|
||||||
],
|
],
|
||||||
line_color=accent_color,
|
line_color=accent_color,
|
||||||
)
|
)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return flask_app
|
return flask_app
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import inspect
|
import inspect
|
||||||
from functools import partial
|
from functools import lru_cache, partial, wraps
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
|
|
@ -14,7 +14,7 @@ from typing import (
|
||||||
cast,
|
cast,
|
||||||
)
|
)
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException, status
|
from fastapi import APIRouter, FastAPI, HTTPException, status
|
||||||
from fastapi.middleware.wsgi import WSGIMiddleware
|
from fastapi.middleware.wsgi import WSGIMiddleware
|
||||||
from fastapi.openapi.docs import get_swagger_ui_html
|
from fastapi.openapi.docs import get_swagger_ui_html
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
|
|
@ -22,47 +22,59 @@ from fastapi.staticfiles import StaticFiles
|
||||||
from pydantic import BaseModel, create_model
|
from pydantic import BaseModel, create_model
|
||||||
from starlette.responses import HTMLResponse
|
from starlette.responses import HTMLResponse
|
||||||
|
|
||||||
from great_ai.great_ai.helper.use_http_exceptions import use_http_exceptions
|
|
||||||
from great_ai.utilities.parallel_map import parallel_map
|
from great_ai.utilities.parallel_map import parallel_map
|
||||||
|
|
||||||
|
from ..constants import METRICS_PATH
|
||||||
from ..context import get_context
|
from ..context import get_context
|
||||||
from ..dashboard import create_dash_app
|
from ..dashboard import create_dash_app
|
||||||
from ..helper import get_function_metadata_store, snake_case_to_text
|
from ..helper import (
|
||||||
|
freeze_arguments,
|
||||||
|
get_function_metadata_store,
|
||||||
|
snake_case_to_text,
|
||||||
|
use_http_exceptions,
|
||||||
|
)
|
||||||
from ..parameters import automatically_decorate_parameters
|
from ..parameters import automatically_decorate_parameters
|
||||||
from ..tracing import TracingContext
|
from ..tracing import TracingContext
|
||||||
from ..views import EvaluationFeedbackRequest, HealthCheckResponse, Query, Trace
|
from ..views import (
|
||||||
|
ApiMetadata,
|
||||||
|
EvaluationFeedbackRequest,
|
||||||
|
HealthCheckResponse,
|
||||||
|
Query,
|
||||||
|
Trace,
|
||||||
|
)
|
||||||
|
|
||||||
PATH = Path(__file__).parent.resolve()
|
PATH = Path(__file__).parent.resolve()
|
||||||
|
|
||||||
|
|
||||||
class GreatAI(FastAPI):
|
class GreatAI:
|
||||||
def __init__(self, func: Callable[..., Any], *args: Any, **kwargs: Any):
|
def __init__(self, func: Callable[..., Any]):
|
||||||
self._func = automatically_decorate_parameters(func)
|
self._func = automatically_decorate_parameters(func)
|
||||||
|
self._func = freeze_arguments(
|
||||||
|
lru_cache(get_context().prediction_cache_size)(self._func)
|
||||||
|
)
|
||||||
|
|
||||||
schema = self._get_schema()
|
get_function_metadata_store(self._func).is_finalised = True
|
||||||
|
wraps(func)(self)
|
||||||
|
|
||||||
def process_single(input_value: schema) -> Trace: # type: ignore
|
self.app = FastAPI(
|
||||||
with TracingContext() as t:
|
title=self.name,
|
||||||
result = self._func(**cast(BaseModel, input_value).dict())
|
version=self.version,
|
||||||
output = t.finalise(output=result)
|
|
||||||
return output
|
|
||||||
|
|
||||||
self.process_single = process_single
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
*args,
|
|
||||||
title=snake_case_to_text(func.__name__),
|
|
||||||
description=self.documentation,
|
description=self.documentation,
|
||||||
docs_url=None,
|
docs_url=None,
|
||||||
version=get_function_metadata_store(func).version,
|
|
||||||
redoc_url=None,
|
redoc_url=None,
|
||||||
**kwargs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __call__(self, *args: Any, **kwargs: Any) -> Trace:
|
||||||
|
with TracingContext() as t:
|
||||||
|
result = self._func(*args, **kwargs)
|
||||||
|
output = t.finalise(output=result)
|
||||||
|
return output
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def deploy(
|
def deploy(
|
||||||
func: Optional[Callable[..., Any]] = None,
|
func: Optional[Callable[..., Any]] = None,
|
||||||
*,
|
*,
|
||||||
|
disable_rest_api: bool = False,
|
||||||
disable_docs: bool = False,
|
disable_docs: bool = False,
|
||||||
disable_metrics: bool = False,
|
disable_metrics: bool = False,
|
||||||
) -> Union[Callable[[Callable[..., Any]], "GreatAI"], "GreatAI"]:
|
) -> Union[Callable[[Callable[..., Any]], "GreatAI"], "GreatAI"]:
|
||||||
|
|
@ -71,15 +83,21 @@ class GreatAI(FastAPI):
|
||||||
Callable[..., Any],
|
Callable[..., Any],
|
||||||
partial(
|
partial(
|
||||||
GreatAI.deploy,
|
GreatAI.deploy,
|
||||||
|
disable_http=disable_rest_api,
|
||||||
disable_docs=disable_docs,
|
disable_docs=disable_docs,
|
||||||
disable_metrics=disable_metrics,
|
disable_metrics=disable_metrics,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return GreatAI(func)._bootstrap_rest_api(
|
instance = GreatAI(func)
|
||||||
|
|
||||||
|
if not disable_rest_api:
|
||||||
|
instance._bootstrap_rest_api(
|
||||||
disable_docs=disable_docs, disable_metrics=disable_metrics
|
disable_docs=disable_docs, disable_metrics=disable_metrics
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
def process_batch(
|
def process_batch(
|
||||||
self,
|
self,
|
||||||
batch: Iterable[Any],
|
batch: Iterable[Any],
|
||||||
|
|
@ -89,14 +107,67 @@ class GreatAI(FastAPI):
|
||||||
concurrency = 1
|
concurrency = 1
|
||||||
get_context().logger.warning("Concurrency is ignored")
|
get_context().logger.warning("Concurrency is ignored")
|
||||||
|
|
||||||
return parallel_map(self.process_single, batch, concurrency=concurrency)
|
return parallel_map(self, batch, concurrency=concurrency)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return snake_case_to_text(self._func.__name__)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def version(self) -> str:
|
||||||
|
return f"{get_context().version}+{get_function_metadata_store(self._func).model_versions}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def documentation(self) -> str:
|
def documentation(self) -> str:
|
||||||
return (
|
return (
|
||||||
f"GreatAI wrapper for interacting with the '{self._func.__name__}' function.\n"
|
f"GreatAI wrapper for interacting with the `{self._func.__name__}` function.\n\n"
|
||||||
+ (self._func.__doc__ or "")
|
+ (
|
||||||
|
"\n".join(
|
||||||
|
line.strip()
|
||||||
|
for line in (self._func.__doc__ or "").split("\n")
|
||||||
|
if line.strip()
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _bootstrap_rest_api(self, disable_docs: bool, disable_metrics: bool) -> None:
|
||||||
|
self._bootstrap_prediction_endpoints()
|
||||||
|
self._bootstrap_feedback_endpoints()
|
||||||
|
self._bootstrap_meta_endpoints()
|
||||||
|
|
||||||
|
if not disable_docs:
|
||||||
|
self._bootstrap_docs_endpoints()
|
||||||
|
|
||||||
|
if not disable_metrics:
|
||||||
|
self._bootstrap_metrics_endpoints()
|
||||||
|
|
||||||
|
def _bootstrap_prediction_endpoints(self) -> None:
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/predictions",
|
||||||
|
tags=["predictions"],
|
||||||
|
)
|
||||||
|
|
||||||
|
schema = self._get_schema()
|
||||||
|
|
||||||
|
@router.post("/", status_code=status.HTTP_200_OK, response_model=Trace)
|
||||||
|
@use_http_exceptions
|
||||||
|
def predict(input_value: schema) -> Trace: # type: ignore
|
||||||
|
return self(**cast(BaseModel, input_value).dict())
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/:prediction_id", response_model=Trace, status_code=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
def get_prediction(prediction_id: str) -> Trace:
|
||||||
|
result = get_context().persistence.get_trace(prediction_id)
|
||||||
|
if result is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||||
|
return result
|
||||||
|
|
||||||
|
@router.delete("/:prediction_id", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
def delete_prediction(prediction_id: str) -> None:
|
||||||
|
get_context().persistence.delete_trace(prediction_id)
|
||||||
|
|
||||||
|
self.app.include_router(router)
|
||||||
|
|
||||||
def _get_schema(self) -> Type[BaseModel]:
|
def _get_schema(self) -> Type[BaseModel]:
|
||||||
signature = inspect.signature(self._func)
|
signature = inspect.signature(self._func)
|
||||||
|
|
@ -112,51 +183,74 @@ class GreatAI(FastAPI):
|
||||||
schema: Type[BaseModel] = create_model("InputModel", **parameters) # type: ignore
|
schema: Type[BaseModel] = create_model("InputModel", **parameters) # type: ignore
|
||||||
return schema
|
return schema
|
||||||
|
|
||||||
def _bootstrap_rest_api(
|
def _bootstrap_feedback_endpoints(self) -> None:
|
||||||
self, disable_docs: bool, disable_metrics: bool
|
router = APIRouter(
|
||||||
) -> "GreatAI":
|
prefix="/predictions/:prediction_id/feedback",
|
||||||
self.post("/evaluations", status_code=status.HTTP_200_OK, response_model=Trace)(
|
tags=["feedback"],
|
||||||
use_http_exceptions(self.process_single)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@self.get("/evaluations/:evaluation_id", status_code=status.HTTP_200_OK)
|
@router.put("/", status_code=status.HTTP_202_ACCEPTED)
|
||||||
def get_evaluation(evaluation_id: str) -> Trace:
|
def set_feedback(prediction_id: str, input: EvaluationFeedbackRequest) -> None:
|
||||||
result = get_context().persistence.get_trace(evaluation_id)
|
get_context().persistence.add_feedback(prediction_id, input.evaluation)
|
||||||
if result is None:
|
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
|
||||||
return result
|
|
||||||
|
|
||||||
@self.post(
|
@router.get("/", status_code=status.HTTP_200_OK)
|
||||||
"/evaluations/:evaluation_id/feedback", status_code=status.HTTP_202_ACCEPTED
|
def get_feedback(prediction_id: str) -> Any:
|
||||||
|
return get_context().persistence.get_feedback(prediction_id)
|
||||||
|
|
||||||
|
@router.delete("/", status_code=status.HTTP_200_OK)
|
||||||
|
def delete_feedback(prediction_id: str) -> Any:
|
||||||
|
get_context().persistence.delete_feedback(prediction_id)
|
||||||
|
|
||||||
|
self.app.include_router(router)
|
||||||
|
|
||||||
|
def _bootstrap_meta_endpoints(self) -> None:
|
||||||
|
router = APIRouter(
|
||||||
|
tags=["meta"],
|
||||||
)
|
)
|
||||||
def give_feedback(evaluation_id: str, input: EvaluationFeedbackRequest) -> None:
|
|
||||||
get_context().persistence.add_evaluation(evaluation_id, input.evaluation)
|
|
||||||
|
|
||||||
if not disable_docs:
|
@router.get("/health", status_code=status.HTTP_200_OK)
|
||||||
|
def check_health() -> HealthCheckResponse:
|
||||||
|
return HealthCheckResponse(is_healthy=True)
|
||||||
|
|
||||||
@self.get("/docs", include_in_schema=False)
|
@router.get(
|
||||||
|
"/version", response_model=ApiMetadata, status_code=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
def get_version() -> ApiMetadata:
|
||||||
|
return ApiMetadata(
|
||||||
|
name=self.name, version=self.version, documentation=self.documentation
|
||||||
|
)
|
||||||
|
|
||||||
|
self.app.include_router(router)
|
||||||
|
|
||||||
|
def _bootstrap_docs_endpoints(self) -> None:
|
||||||
|
@self.app.get("/docs", include_in_schema=False)
|
||||||
def custom_swagger_ui_html() -> HTMLResponse:
|
def custom_swagger_ui_html() -> HTMLResponse:
|
||||||
return get_swagger_ui_html(openapi_url="openapi.json", title=self.title)
|
return get_swagger_ui_html(openapi_url="openapi.json", title=self.app.title)
|
||||||
|
|
||||||
@self.get("/docs/index.html", include_in_schema=False)
|
@self.app.get("/docs/index.html", include_in_schema=False)
|
||||||
def redirect_to_docs() -> RedirectResponse:
|
def redirect_to_docs() -> RedirectResponse:
|
||||||
return RedirectResponse("/docs")
|
return RedirectResponse("/docs")
|
||||||
|
|
||||||
if not disable_metrics:
|
def _bootstrap_metrics_endpoints(self) -> None:
|
||||||
dash_app = create_dash_app(self._func.__name__, self.documentation)
|
dash_app = create_dash_app(self._func.__name__, self.documentation)
|
||||||
self.mount(get_context().metrics_path, WSGIMiddleware(dash_app))
|
self.app.mount(METRICS_PATH, WSGIMiddleware(dash_app))
|
||||||
|
|
||||||
@self.get("/", include_in_schema=False)
|
@self.app.get("/", include_in_schema=False)
|
||||||
def redirect_to_entrypoint() -> RedirectResponse:
|
def redirect_to_entrypoint() -> RedirectResponse:
|
||||||
return RedirectResponse("/metrics")
|
return RedirectResponse("/metrics")
|
||||||
|
|
||||||
self.mount(
|
self.app.mount(
|
||||||
"/assets",
|
"/assets",
|
||||||
StaticFiles(directory=PATH / "../dashboard/assets"),
|
StaticFiles(directory=PATH / "../dashboard/assets"),
|
||||||
name="static",
|
name="static",
|
||||||
)
|
)
|
||||||
|
|
||||||
@self.post("/query", status_code=status.HTTP_200_OK)
|
router = APIRouter(
|
||||||
|
prefix="/metrics",
|
||||||
|
tags=["metrics"],
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.post("/query", status_code=status.HTTP_200_OK)
|
||||||
def query_metrics(query: Query) -> List[Dict[str, Any]]:
|
def query_metrics(query: Query) -> List[Dict[str, Any]]:
|
||||||
return get_context().persistence.query(
|
return get_context().persistence.query(
|
||||||
conjunctive_filters=query.filter,
|
conjunctive_filters=query.filter,
|
||||||
|
|
@ -165,8 +259,4 @@ class GreatAI(FastAPI):
|
||||||
take=query.take,
|
take=query.take,
|
||||||
)
|
)
|
||||||
|
|
||||||
@self.get("/health", status_code=status.HTTP_200_OK)
|
self.app.include_router(router)
|
||||||
def check_health() -> HealthCheckResponse:
|
|
||||||
return HealthCheckResponse(is_healthy=True)
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
from .get_args import get_args
|
from .assert_function_is_not_finalised import assert_function_is_not_finalised
|
||||||
|
from .freeze_arguments import freeze_arguments
|
||||||
|
from .get_arguments import get_arguments
|
||||||
from .get_function_metadata_store import get_function_metadata_store
|
from .get_function_metadata_store import get_function_metadata_store
|
||||||
from .snake_case_to_text import snake_case_to_text
|
from .snake_case_to_text import snake_case_to_text
|
||||||
from .strip_lines import strip_lines
|
from .strip_lines import strip_lines
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
from ..context import get_context
|
||||||
|
from .get_function_metadata_store import get_function_metadata_store
|
||||||
|
|
||||||
|
|
||||||
|
def assert_function_is_not_finalised(func: Callable[..., Any]) -> None:
|
||||||
|
error_message = (
|
||||||
|
"The outer-most (first) decorator has to be `@GreatAI.deploy`. "
|
||||||
|
+ f"In the case of `{func.__name__}`, it is not: fix this by moving `@GreatAI.deploy` to the top."
|
||||||
|
)
|
||||||
|
|
||||||
|
if get_function_metadata_store(func).is_finalised:
|
||||||
|
get_context().logger.error(error_message)
|
||||||
|
exit(-1)
|
||||||
25
great_ai/src/great_ai/great_ai/helper/freeze_arguments.py
Normal file
25
great_ai/src/great_ai/great_ai/helper/freeze_arguments.py
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
from functools import wraps
|
||||||
|
from typing import Any, Callable, Dict, List
|
||||||
|
|
||||||
|
|
||||||
|
class FrozenDict(dict):
|
||||||
|
def __hash__(self) -> int: # type: ignore
|
||||||
|
return hash(frozenset(self.items()))
|
||||||
|
|
||||||
|
|
||||||
|
def freeze_arguments(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||||
|
"""Transform mutable dictionary
|
||||||
|
Into immutable
|
||||||
|
Useful to be compatible with cache
|
||||||
|
source: https://stackoverflow.com/questions/6358481/using-functools-lru-cache-with-dictionary-arguments
|
||||||
|
"""
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args: List[Any], **kwargs: Dict[str, Any]) -> Any:
|
||||||
|
args = tuple(FrozenDict(arg) if isinstance(arg, dict) else arg for arg in args)
|
||||||
|
kwargs = {
|
||||||
|
k: FrozenDict(v) if isinstance(v, dict) else v for k, v in kwargs.items()
|
||||||
|
}
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
@ -2,14 +2,23 @@ import inspect
|
||||||
from typing import Any, Callable, Dict, Mapping, Sequence
|
from typing import Any, Callable, Dict, Mapping, Sequence
|
||||||
|
|
||||||
|
|
||||||
def get_args(
|
def get_arguments(
|
||||||
func: Callable[..., Any], args: Sequence[Any], kwargs: Mapping[str, Any]
|
func: Callable[..., Any], args: Sequence[Any], kwargs: Mapping[str, Any]
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Return mapping from parameter names to actual argument values"""
|
"""Return mapping from parameter names to actual argument values"""
|
||||||
|
|
||||||
signature = inspect.signature(func)
|
signature = inspect.signature(func)
|
||||||
|
|
||||||
|
defaults = {
|
||||||
|
p.name: p.default
|
||||||
|
for p in signature.parameters.values()
|
||||||
|
if p.default != inspect._empty
|
||||||
|
}
|
||||||
|
|
||||||
filter_keys = [
|
filter_keys = [
|
||||||
param.name
|
param.name
|
||||||
for param in signature.parameters.values()
|
for param in signature.parameters.values()
|
||||||
if param.kind == param.POSITIONAL_OR_KEYWORD
|
if param.kind == param.POSITIONAL_OR_KEYWORD
|
||||||
]
|
]
|
||||||
return {**dict(zip(filter_keys, args)), **kwargs}
|
|
||||||
|
return {**defaults, **dict(zip(filter_keys, args)), **kwargs}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Any, Callable, Dict, List, Literal, Union
|
from typing import Any, Callable, Dict, List, Literal, Union
|
||||||
|
|
||||||
from ..helper import get_function_metadata_store
|
from ..helper import assert_function_is_not_finalised, get_function_metadata_store
|
||||||
from ..tracing import TracingContext
|
from ..tracing import TracingContext
|
||||||
from ..views import Model
|
from ..views import Model
|
||||||
from .load_model import load_model
|
from .load_model import load_model
|
||||||
|
|
@ -14,7 +14,9 @@ def use_model(
|
||||||
return_path: bool = False,
|
return_path: bool = False,
|
||||||
model_kwarg_name: str = "model",
|
model_kwarg_name: str = "model",
|
||||||
) -> Callable[..., Any]:
|
) -> Callable[..., Any]:
|
||||||
assert isinstance(version, int) or version == "latest"
|
assert (
|
||||||
|
isinstance(version, int) or version == "latest"
|
||||||
|
), "Only integers or the string literal `latest` is allowed as version"
|
||||||
|
|
||||||
model, actual_version = load_model(
|
model, actual_version = load_model(
|
||||||
key=key,
|
key=key,
|
||||||
|
|
@ -23,17 +25,19 @@ def use_model(
|
||||||
)
|
)
|
||||||
|
|
||||||
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||||
|
assert_function_is_not_finalised(func)
|
||||||
|
|
||||||
store = get_function_metadata_store(func)
|
store = get_function_metadata_store(func)
|
||||||
store.model_parameter_names.append(model_kwarg_name)
|
store.model_parameter_names.append(model_kwarg_name)
|
||||||
if store.version:
|
if store.model_versions:
|
||||||
store.version += "|"
|
store.model_versions += "."
|
||||||
store.version += f"{key}:{actual_version}"
|
store.model_versions += f"{key}-v{actual_version}"
|
||||||
|
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapper(*args: List[Any], **kwargs: Dict[str, Any]) -> Any:
|
def wrapper(*args: List[Any], **kwargs: Dict[str, Any]) -> Any:
|
||||||
context = TracingContext.get_current_context()
|
tracing_context = TracingContext.get_current_context()
|
||||||
if context:
|
if tracing_context:
|
||||||
context.log_model(Model(key=key, version=actual_version))
|
tracing_context.log_model(Model(key=key, version=actual_version))
|
||||||
return func(*args, **kwargs, **{model_kwarg_name: model})
|
return func(*args, **kwargs, **{model_kwarg_name: model})
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
import inspect
|
import inspect
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from great_ai.great_ai.context.get_context import get_context
|
||||||
|
|
||||||
from ..tracing import TracingContext
|
from ..tracing import TracingContext
|
||||||
|
|
||||||
|
|
||||||
def log_metric(argument_name: str, value: Any) -> None:
|
def log_metric(argument_name: str, value: Any) -> None:
|
||||||
context = TracingContext.get_current_context()
|
tracing_context = TracingContext.get_current_context()
|
||||||
caller = inspect.stack()[1].function
|
caller = inspect.stack()[1].function
|
||||||
actual_name = f"metric:{caller}:{argument_name}"
|
actual_name = f"metric:{caller}:{argument_name}"
|
||||||
if context:
|
if tracing_context:
|
||||||
context.log_value(name=actual_name, value=value)
|
tracing_context.log_value(name=actual_name, value=value)
|
||||||
|
|
||||||
|
get_context().logger.info(f"{actual_name}={value}")
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,11 @@ from functools import wraps
|
||||||
from typing import Any, Callable, Dict
|
from typing import Any, Callable, Dict
|
||||||
|
|
||||||
from ..exceptions import ArgumentValidationError
|
from ..exceptions import ArgumentValidationError
|
||||||
from ..helper import get_args, get_function_metadata_store
|
from ..helper import (
|
||||||
|
assert_function_is_not_finalised,
|
||||||
|
get_arguments,
|
||||||
|
get_function_metadata_store,
|
||||||
|
)
|
||||||
from ..tracing import TracingContext
|
from ..tracing import TracingContext
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -14,12 +18,13 @@ def parameter(
|
||||||
) -> Callable[..., Any]:
|
) -> Callable[..., Any]:
|
||||||
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||||
get_function_metadata_store(func).input_parameter_names.append(parameter_name)
|
get_function_metadata_store(func).input_parameter_names.append(parameter_name)
|
||||||
|
assert_function_is_not_finalised(func)
|
||||||
|
|
||||||
actual_name = f"arg:{func.__name__}:{parameter_name}"
|
actual_name = f"arg:{func.__name__}:{parameter_name}"
|
||||||
|
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapper(*args: Any, **kwargs: Dict[str, Any]) -> Any:
|
def wrapper(*args: Any, **kwargs: Dict[str, Any]) -> Any:
|
||||||
arguments = get_args(func, args, kwargs)
|
arguments = get_arguments(func, args, kwargs)
|
||||||
argument = arguments[parameter_name]
|
argument = arguments[parameter_name]
|
||||||
|
|
||||||
expected_type = func.__annotations__.get(parameter_name)
|
expected_type = func.__annotations__.get(parameter_name)
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ class ParallelTinyDbDriver(PersistenceDriver):
|
||||||
def save_trace(self, trace: Trace) -> str:
|
def save_trace(self, trace: Trace) -> str:
|
||||||
return self._safe_execute(lambda db: db.insert(trace.dict()))
|
return self._safe_execute(lambda db: db.insert(trace.dict()))
|
||||||
|
|
||||||
def add_evaluation(self, id: str, evaluation: Any) -> None:
|
def add_feedback(self, id: str, evaluation: Any) -> None:
|
||||||
self._safe_execute(
|
self._safe_execute(
|
||||||
lambda db: db.update(
|
lambda db: db.update(
|
||||||
fields={"evaluation": evaluation},
|
fields={"evaluation": evaluation},
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ class PersistenceDriver(ABC):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def add_evaluation(self, id: str, evaluation: Any) -> None:
|
def add_feedback(self, id: str, evaluation: Any) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|
|
||||||
|
|
@ -61,12 +61,12 @@ class TracingContext:
|
||||||
|
|
||||||
if exception is not None and type is not None:
|
if exception is not None and type is not None:
|
||||||
self.finalise(exception=exception)
|
self.finalise(exception=exception)
|
||||||
if get_context().is_production:
|
if get_context().should_log_exception_stack:
|
||||||
|
get_context().logger.exception("Could not finish operation")
|
||||||
|
else:
|
||||||
get_context().logger.error(
|
get_context().logger.error(
|
||||||
f"Could not finish operation because of {type.__name__}: {exception}"
|
f"Could not finish operation because of {type.__name__}: {exception}"
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
get_context().logger.exception("Could not finish operation")
|
|
||||||
|
|
||||||
assert self._trace is not None
|
assert self._trace is not None
|
||||||
get_context().persistence.save_trace(self._trace)
|
get_context().persistence.save_trace(self._trace)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
from .api_metadata import ApiMetadata
|
||||||
from .evaluation_feedback_request import EvaluationFeedbackRequest
|
from .evaluation_feedback_request import EvaluationFeedbackRequest
|
||||||
from .filter import Filter
|
from .filter import Filter
|
||||||
from .function_metadata import FunctionMetadata
|
from .function_metadata import FunctionMetadata
|
||||||
|
|
|
||||||
7
great_ai/src/great_ai/great_ai/views/api_metadata.py
Normal file
7
great_ai/src/great_ai/great_ai/views/api_metadata.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class ApiMetadata(BaseModel):
|
||||||
|
name: str
|
||||||
|
version: str
|
||||||
|
documentation: str
|
||||||
|
|
@ -6,4 +6,5 @@ from pydantic import BaseModel
|
||||||
class FunctionMetadata(BaseModel):
|
class FunctionMetadata(BaseModel):
|
||||||
input_parameter_names: List[str] = []
|
input_parameter_names: List[str] = []
|
||||||
model_parameter_names: List[str] = []
|
model_parameter_names: List[str] = []
|
||||||
version: str = ""
|
model_versions: str = ""
|
||||||
|
is_finalised: bool = False
|
||||||
|
|
|
||||||
|
|
@ -278,18 +278,13 @@ class LargeFile:
|
||||||
|
|
||||||
self._versions = sorted(versions, key=self._get_version_from_key)
|
self._versions = sorted(versions, key=self._get_version_from_key)
|
||||||
|
|
||||||
if self._versions:
|
|
||||||
logger.info(f"Found versions: {self.version_ids}")
|
|
||||||
else:
|
|
||||||
logger.info("No versions found")
|
|
||||||
|
|
||||||
def _fetch_versions_from_cache(self) -> List[Path]:
|
def _fetch_versions_from_cache(self) -> List[Path]:
|
||||||
logger.info(f"Fetching offline versions of {self._name}")
|
logger.debug(f"Fetching offline versions of {self._name}")
|
||||||
|
|
||||||
return list(self.cache_path.glob(f"{self._local_name}-*"))
|
return list(self.cache_path.glob(f"{self._local_name}-*"))
|
||||||
|
|
||||||
def _fetch_versions_from_s3(self) -> List[str]:
|
def _fetch_versions_from_s3(self) -> List[str]:
|
||||||
logger.info(f"Fetching online versions of {self._name}")
|
logger.debug(f"Fetching online versions of {self._name}")
|
||||||
|
|
||||||
found_objects = self._client.list_objects_v2(
|
found_objects = self._client.list_objects_v2(
|
||||||
Bucket=self.bucket_name, Prefix=self._name
|
Bucket=self.bucket_name, Prefix=self._name
|
||||||
|
|
@ -318,11 +313,15 @@ class LargeFile:
|
||||||
|
|
||||||
if self._version is None:
|
if self._version is None:
|
||||||
self._version = self.version_ids[-1]
|
self._version = self.version_ids[-1]
|
||||||
logger.info(f"Latest version of {self._name} is {self._version}")
|
logger.info(
|
||||||
|
f"Latest version of {self._name} is {self._version} "
|
||||||
|
+ f"(from versions: {', '.join((str(v) for v in self.version_ids))})"
|
||||||
|
)
|
||||||
|
|
||||||
elif self._version not in self.version_ids:
|
elif self._version not in self.version_ids:
|
||||||
raise FileNotFoundError(
|
raise FileNotFoundError(
|
||||||
f"File {self._name} not found with version {self._version}. Available versions: {self.version_ids}"
|
f"File {self._name} not found with version {self._version}. "
|
||||||
|
+ f"(from versions: {', '.join((str(v) for v in self.version_ids))})"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError("Unsupported file mode.")
|
raise ValueError("Unsupported file mode.")
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,6 @@ def parse_arguments() -> Namespace:
|
||||||
help="the name of the file containing your to-be-served function such as `main.py`\n",
|
help="the name of the file containing your to-be-served function such as `main.py`\n",
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
"--function_name",
|
|
||||||
type=str,
|
|
||||||
help="name of your inference function, defaults to the base name of the filename or the literal `app`",
|
|
||||||
default="",
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--host",
|
"--host",
|
||||||
type=str,
|
type=str,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue