update code (#9)

Co-authored-by: xuyang1 <xuyang1@microsoft.com>
This commit is contained in:
Xu Yang
2024-05-21 22:48:41 +08:00
committed by GitHub
parent 230dca2bc2
commit c6833b0858
27 changed files with 5564 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
# TODO: use pydantic for other modules in Qlib
# from pydantic_settings import BaseSettings
import os
from typing import Union
from dotenv import load_dotenv
# make sure that env variable is loaded while calling Config()
load_dotenv(verbose=True, override=True)
from pydantic_settings import BaseSettings
class FincoSettings(BaseSettings):
use_azure: bool = True
max_retry: int = 10
retry_wait_seconds: int = 1
continuous_mode: bool = False
dump_chat_cache: bool = False
use_chat_cache: bool = False
dump_embedding_cache: bool = False
use_embedding_cache: bool = False
prompt_cache_path: str = os.getcwd() + "/prompt_cache.db"
session_cache_folder_location: str = os.getcwd() + "/session_cache_folder/"
max_past_message_include: int = 10
log_llm_chat_content: bool = True
# Chat configs
chat_openai_api_key: str = ""
chat_azure_api_base: str = ""
chat_azure_api_version: str = ""
chat_model: str = ""
chat_max_tokens: int = 3000
chat_temperature: float = 0.5
chat_stream: bool = True
chat_seed: Union[int, None] = None
chat_frequency_penalty: float = 0.0
chat_presence_penalty: float = 0.0
default_system_prompt: str = "You are an AI assistant who helps to answer user's questions about finance."
# Embedding configs
embedding_openai_api_key: str = ""
embedding_azure_api_base: str = ""
embedding_azure_api_version: str = ""
embedding_model: str = ""
+183
View File
@@ -0,0 +1,183 @@
from __future__ import annotations
import copy
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any
class Feedback:
pass
class Knowledge:
pass
class QueriedKnowledge:
pass
class KnowledgeBase(ABC):
@abstractmethod
def query(
self,
) -> QueriedKnowledge | None:
raise NotImplementedError
class EvolvableSubjects:
"""The target object to be evolved"""
def clone(self) -> EvolvableSubjects:
return copy.deepcopy(self)
class QlibEvolvableSubjects(EvolvableSubjects): ...
class Evaluator(ABC):
"""Both external EvolvableSubjects and internal evovler, it is
FAQ:
- Q: If we have a external whitebox evaluator, do we need a
intenral EvolvableSubjects?
A: When the external evovler is very complex, maybe a internal LLM-based evovler
may provide more understandable feedbacks.
"""
@abstractmethod
def evaluate(self, evo: EvolvableSubjects, **kwargs: Any) -> Feedback:
raise NotImplementedError
class SelfEvaluator(Evaluator):
pass
@dataclass
class EvoStep:
"""At a specific step,
based on
- previous trace
- newly RAG kownledge `QueriedKnowledge`
the EvolvableSubjects is evolved to a new one `EvolvableSubjects`.
(optional) After evaluation, we get feedback `feedback`.
"""
evolvable_subjects: EvolvableSubjects
queried_knowledge: QueriedKnowledge | None = None
feedback: Feedback | None = None
class EvolvingStrategy(ABC):
@abstractmethod
def evolve(
self,
*evo: EvolvableSubjects,
evolving_trace: list[EvoStep] | None = None,
queried_knowledge: QueriedKnowledge | None = None,
**kwargs: Any,
) -> EvolvableSubjects:
"""The evolving trace is a list of (evolvable_subjects, feedback) ordered
according to the time.
The reason why the parameter is important for the evolving.
- evolving_trace: the historical feedback is important.
- queried_knowledge: queried knowledge
"""
class RAGStrategy(ABC):
"""Retrival Augmentation Generation Strategy"""
def __init__(self, knowledgebase: KnowledgeBase) -> None:
self.knowledgebase = knowledgebase
@abstractmethod
def query(
self,
evo: EvolvableSubjects,
evolving_trace: list[EvoStep],
**kwargs: Any,
) -> QueriedKnowledge | None:
pass
@abstractmethod
def generate_knowledge(
self,
evolving_trace: list[EvoStep],
*,
return_knowledge: bool = False,
**kwargs: Any,
) -> Knowledge | None:
"""Generating new knowledge based on the evolving trace.
- It is encouraged to query related knowledge before generating new knowledge.
RAGStrategy should maintain the new knowledge all by itself.
"""
class EvoAgent:
"""It is responsible for driving the workflow."""
evolving_trace: list[EvoStep]
def __init__(
self,
evolving_strategy: EvolvingStrategy,
rag: RAGStrategy | None = None,
) -> None:
self.evolving_trace = []
self.evolving_strategy = evolving_strategy
self.rag = rag
def step_evolving(
self,
evo: EvolvableSubjects,
eva: Evaluator | Feedback,
*,
with_knowledge: bool = False,
with_feedback: bool = True,
knowledge_self_gen: bool = False,
) -> EvolvableSubjects:
"""Common evolving mode are supported in this api .
- Interactive evolving:
- `with_feedback=True` and `eva` is a external Evaluator.
- Knowledge-driven evolving:
- `with_knowledge=True` and related knowledge are
queried based on `self.rag`
- Self-evolving: we have two ways to self-evolve.
- 1) self generating knowledge and then evolve
- `knowledge_self_gen=True` and `with_knowledge=True`
- 2) self evaluate to generate feedback and then evolve
- `with_feedback=True` and `eva` is a internal Evaluator.
"""
# knowledge self-evolving
if knowledge_self_gen and self.rag is not None:
self.rag.generate_knowledge(self.evolving_trace)
# RAG
queried_knowledge = None
if with_knowledge and self.rag is not None:
queried_knowledge = self.rag.query(evo, self.evolving_trace)
# Evolve
evo = self.evolving_strategy.evolve(
evo=evo,
evolving_trace=self.evolving_trace,
queried_knowledge=queried_knowledge,
)
es = EvoStep(evo, queried_knowledge)
# Evaluate
if with_feedback:
es.feedback = eva if isinstance(eva, Feedback) else eva.evaluate(evo, queried_knowledge=queried_knowledge)
# Update trace
self.evolving_trace.append(es)
return evo
+155
View File
@@ -0,0 +1,155 @@
from __future__ import annotations
from contextlib import contextmanager
from typing import TYPE_CHECKING, Generator, Sequence
from loguru import logger
if TYPE_CHECKING:
from loguru import Logger
class LogColors:
"""
ANSI color codes for use in console output.
"""
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
MAGENTA = "\033[95m"
CYAN = "\033[96m"
WHITE = "\033[97m"
GRAY = "\033[90m"
BLACK = "\033[30m"
BOLD = "\033[1m"
ITALIC = "\033[3m"
END = "\033[0m"
@classmethod
def get_all_colors(cls: type[LogColors]) -> list:
names = dir(cls)
names = [name for name in names if not name.startswith("__") and not callable(getattr(cls, name))]
return [getattr(cls, name) for name in names]
def render(self, text: str, color: str = "", style: str = "") -> str:
"""
render text by input color and style.
It's not recommend that input text is already rendered.
"""
# This method is called too frequently, which is not good.
colors = self.get_all_colors()
# Perhaps color and font should be distinguished here.
if color and color in colors:
# Changes to accommodate ruff checks.
# Original code:
# raise ValueError(f"color should be in: {colors} but now is: {color}")
# Description of the problem:
# TRY003 Avoid specifying long messages outside the exception class
# EM102 Exception must not use an f-string literal, assign to variable first
# References:
# https://docs.astral.sh/ruff/rules/raise-vanilla-args/
# https://docs.astral.sh/ruff/rules/f-string-in-exception/
error_message = f"color should be in: {colors} but now is: {color}"
raise ValueError(error_message)
if style and style in colors:
# Changes to accommodate ruff checks.
# Original code:
# raise ValueError(f"style should be in: {colors} but now is: {style}")
# Description of the problem:
# TRY003 Avoid specifying long messages outside the exception class
# EM102 Exception must not use an f-string literal, assign to variable first
# References:
# https://docs.astral.sh/ruff/rules/raise-vanilla-args/
# https://docs.astral.sh/ruff/rules/f-string-in-exception/
error_message = f"style should be in: {colors} but now is: {style}"
raise ValueError(error_message)
text = f"{color}{text}{self.END}"
return f"{style}{text}{self.END}"
class FinCoLog:
# logger.add(loguru_handler, level="INFO") # you can add use storage as a loguru handler
def __init__(self) -> None:
self.logger: Logger = logger
def info(self, *args: Sequence, plain: bool = False, title: str = "Info") -> None:
if plain:
return self.plain_info(*args)
for arg in args:
# Changes to accommodate ruff checks.
# Original code:
# self.logger.info(f"{LogColors.WHITE}{arg}{LogColors.END}")
# Description of the problem:
# G004 Logging statement uses f-string
# References:
# https://docs.astral.sh/ruff/rules/logging-f-string/
info = f"{LogColors.WHITE}{arg}{LogColors.END}"
self.logger.info(info)
return None
def __getstate__(self) -> dict:
return {}
# Changes to accommodate ruff checks.
# Original code: def __setstate__(self, _: str) -> None:
# Description of the problem:
# PLE0302 The special method `__setstate__` expects 2 parameters, 1 was given
# References: https://docs.astral.sh/ruff/rules/unexpected-special-method-signature/
def __setstate__(self, _: str) -> None:
self.logger = logger
def plain_info(self, *args: Sequence) -> None:
for arg in args:
# Changes to accommodate ruff checks.
# Original code:
# self.logger.info(
# f"""
# {LogColors.YELLOW}{LogColors.BOLD}
# Info:{LogColors.END}{LogColors.WHITE}{arg}{LogColors.END}
# """,
# )
# Description of the problem:
# G004 Logging statement uses f-string
# References:
# https://docs.astral.sh/ruff/rules/logging-f-string/
info = f"""
{LogColors.YELLOW}{LogColors.BOLD}
Info:{LogColors.END}{LogColors.WHITE}{arg}{LogColors.END}
"""
self.logger.info(info)
def warning(self, *args: Sequence) -> None:
for arg in args:
# Changes to accommodate ruff checks.
# Original code:
# self.logger.warning(
# f"{LogColors.BLUE}{LogColors.BOLD}Warning:{LogColors.END}{arg}",
# )
# Description of the problem:
# G004 Logging statement uses f-string
# References:
# https://docs.astral.sh/ruff/rules/logging-f-string/
info = f"{LogColors.BLUE}{LogColors.BOLD}Warning:{LogColors.END}{arg}"
self.logger.warning(info)
def error(self, *args: Sequence) -> None:
for arg in args:
# Changes to accommodate ruff checks.
# Original code:
# self.logger.error(
# f"{LogColors.RED}{LogColors.BOLD}Error:{LogColors.END}{arg}",
# )
# Description of the problem:
# G004 Logging statement uses f-string
# References:
# https://docs.astral.sh/ruff/rules/logging-f-string/
info = f"{LogColors.RED}{LogColors.BOLD}Error:{LogColors.END}{arg}"
self.logger.error(info)
+165
View File
@@ -0,0 +1,165 @@
from __future__ import annotations
import json
import os
import random
import string
import uuid
from pathlib import Path
import importlib
from typing import Any
import yaml
from fuzzywuzzy import fuzz
import multiprocessing as mp
from collections.abc import Callable
class FincoException(Exception):
pass
class SingletonMeta(type):
_instance = None
def __call__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super(SingletonMeta, cls).__call__(*args, **kwargs)
return cls._instance
class SingletonBaseClass(metaclass=SingletonMeta):
"""
Because we try to support defining Singleton with `class A(SingletonBaseClass)` instead of `A(metaclass=SingletonMeta)`
This class becomes necessary
"""
# TODO: Add move this class to Qlib's general utils.
def parse_json(response):
try:
return json.loads(response)
except json.decoder.JSONDecodeError:
pass
raise Exception(f"Failed to parse response: {response}, please report it or help us to fix it.")
def similarity(text1, text2):
text1 = text1 if isinstance(text1, str) else ""
text2 = text2 if isinstance(text2, str) else ""
# Maybe we can use other similarity algorithm such as tfidf
return fuzz.ratio(text1, text2)
def random_string(length=10):
letters = string.ascii_letters + string.digits
return "".join(random.choice(letters) for i in range(length))
def remove_uncommon_keys(new_dict, org_dict):
keys_to_remove = []
for key in new_dict:
if key not in org_dict:
keys_to_remove.append(key)
elif isinstance(new_dict[key], dict) and isinstance(org_dict[key], dict):
remove_uncommon_keys(new_dict[key], org_dict[key])
elif isinstance(new_dict[key], dict) and isinstance(org_dict[key], str):
new_dict[key] = org_dict[key]
for key in keys_to_remove:
del new_dict[key]
def crawl_the_folder(folder_path: Path):
yaml_files = []
for root, _, files in os.walk(folder_path.as_posix()):
for file in files:
if file.endswith(".yaml") or file.endswith(".yml"):
yaml_file_path = Path(os.path.join(root, file)).relative_to(folder_path)
yaml_files.append(yaml_file_path.as_posix())
return sorted(yaml_files)
def compare_yaml(file1, file2):
with open(file1, "r") as stream:
data1 = yaml.safe_load(stream)
with open(file2, "r") as stream:
data2 = yaml.safe_load(stream)
return data1 == data2
def remove_keys(valid_keys, ori_dict):
for key in list(ori_dict.keys()):
if key not in valid_keys:
ori_dict.pop(key)
return ori_dict
class YamlConfigCache(SingletonBaseClass):
def __init__(self) -> None:
super().__init__()
self.path_to_config = dict()
def load(self, path):
with open(path, "r") as stream:
data = yaml.safe_load(stream)
self.path_to_config[path] = data
def __getitem__(self, path):
if path not in self.path_to_config:
self.load(path)
return self.path_to_config[path]
def import_class(class_path: str) -> Any:
"""
Parameters
----------
class_path : str
class path like"scripts.factor_implementation.baselines.naive.one_shot.OneshotFactorGen"
Returns
-------
class of `class_path`
"""
module_path, class_name = class_path.rsplit(".", 1)
module = importlib.import_module(module_path)
return getattr(module, class_name)
def multiprocessing_wrapper(func_calls: list[tuple[Callable, tuple]], n: int) -> list:
"""It will use multiprocessing to call the functions in func_calls with the given parameters.
The results equals to `return [f(*args) for f, args in func_calls]`
It will not call multiprocessing if `n=1`
Parameters
----------
func_calls : List[Tuple[Callable, Tuple]]
the list of functions and their parameters
n : int
the number of subprocesses
Returns
-------
list
"""
if n == 1:
return [f(*args) for f, args in func_calls]
with mp.Pool(processes=n) as pool:
results = [pool.apply_async(f, args) for f, args in func_calls]
return [result.get() for result in results]
# You can test the above function
# def f(x):
# return x**2
#
# if __name__ == "__main__":
# print(multiprocessing_wrapper([(f, (i,)) for i in range(10)], 4))